Compare commits

..

2 Commits

99 changed files with 329 additions and 3358 deletions

View File

@@ -1 +0,0 @@
- **feat(providers):** let operators declare per-provider error rules through `settings.providerErrorRules` instead of patching the catalog — an operator-supplied rule for a provider is consulted before the built-in `providerRuleRegistry`, receives the raw error text, and has its declared scope/cooldown/reason actually honored end to end, for any provider (declaring the rule is the opt-in — no extra allowlist entry needed). Matches are plain case-insensitive substrings (never RegExp) and bounded to 50 rules to keep the hot path safe ([#11104](https://github.com/diegosouzapw/OmniRoute/pull/11104))

View File

@@ -1 +0,0 @@
- fix(providers): filter Perplexity model import to the Sonar family so Agent-API catalog ids stop surfacing as routable chat models (#11060)

View File

@@ -1 +0,0 @@
- **fix(providers):** Reject silent validation degradation on provider connection patch — unknown `rateLimitOverrides` keys (e.g. a typo'd `tpm`) and empty/non-numeric values now return `400` with the rejected key list instead of being silently dropped ([#11101](https://github.com/diegosouzapw/OmniRoute/pull/11101))

View File

@@ -1 +0,0 @@
- **Autopilot suggestion counter:** the combo health autopilot summary now reports `suggestionCount` (the real number of suggested actions across all issues) instead of conflating it with link counts, while keeping `actionableCount` as a deprecated alias for backward compatibility. The `run_combo_test` action now links to the dashboard with the combo id (`/dashboard/combos?test=<comboId>`) rather than the read-only API route, so operators can actually trigger a test from the UI ([#11102](https://github.com/diegosouzapw/OmniRoute/pull/11102)).

View File

@@ -1 +0,0 @@
- **Config audit persistence:** persist the configuration audit trail to SQLite (`config_audit_log`) instead of an in-memory buffer capped at 1000 volatile entries, and bound its growth with `cleanupConfigAudit()` driven by the `retention.configAudit` setting (default 30 days), wired into `runAutoCleanup` ([#11103](https://github.com/diegosouzapw/OmniRoute/pull/11103)).

View File

@@ -1 +0,0 @@
- fix(sse): resume mid-stream recovery after a _completed_ tool call — `finish_reason: "tool_calls"` is now tracked per-call instead of as a general terminal marker, so truncation of trailing prose after a fully-delivered tool call is recoverable while in-flight calls stay blocked ([#11109](https://github.com/diegosouzapw/OmniRoute/pull/11109))

View File

@@ -1 +0,0 @@
- **fix(providers):** `reasoning_effort` now learns the accepted values from a provider's own 400/422 response and clamps to the highest one instead of forwarding an unsupported `xhigh`/`max` (or a hardcoded `"high"` fallback) — fixes custom OpenAI-compatible connections and registered providers with no reasoning metadata ([#11116](https://github.com/diegosouzapw/OmniRoute/pull/11116)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(sse):** parallel `function_call` items in a Responses API stream (e.g. several tool calls dispatched in the same turn) now each get a stable, distinct `index`/`id` when translated to Chat Completions streaming deltas, instead of colliding on index 0 and tripping strict stream parsers with `Expected 'id' to be a string.` ([#11144](https://github.com/diegosouzapw/OmniRoute/pull/11144))

View File

@@ -1 +0,0 @@
- fix(i18n): complete Vietnamese translations for recently added UI strings (#9985)

View File

@@ -1 +0,0 @@
- fix(quality): register `tests/unit/authz/oauth-autoimport-local-only.test.ts` in stryker `tap.testFiles` (residual of #11053)

View File

@@ -1 +0,0 @@
- fix(i18n): translate the 14 `providers.harImport*` keys into Vietnamese (parity gap left by #11069)

View File

@@ -448,14 +448,14 @@ classification rules pick the fallback `reason` and lock `scope`
Classification rules only see full error **text** (needed to match body
markers like `额度不足`) for providers listed in the `FULL_TEXT_RULE_PROVIDERS`
allowlist in `providerErrorRules.ts` — currently only `"agentrouter"`. For
every other **built-in catalog** provider, `checkFallbackError` hands
`getProviderErrorRuleMatch` only the structured error (`{code, type}`), which
is enough for header/status/code-based rules but blind to body-text markers.
The helper `resolveRuleMatchBody()` performs this selection: full error text
for allowlisted providers, the structured error otherwise. Adding a
**built-in** provider to `FULL_TEXT_RULE_PROVIDERS` is an explicit per-provider
opt-in — it exists so that the default path for every provider not on the
list stays byte-for-byte unchanged.
every other provider, `checkFallbackError` hands `getProviderErrorRuleMatch`
only the structured error (`{code, type}`), which is enough for
header/status/code-based rules but blind to body-text markers. The helper
`resolveRuleMatchBody()` performs this selection: full error text for
allowlisted providers, the structured error otherwise. Adding a provider to
`FULL_TEXT_RULE_PROVIDERS` is an explicit per-provider opt-in — it exists so
that the default path for every provider not on the list stays
byte-for-byte unchanged.
A rule's `scope` (`model` / `provider` / `connection`) is a separate opt-in
from `FULL_TEXT_RULE_PROVIDERS`: `checkFallbackError` only surfaces it as
@@ -466,31 +466,6 @@ honorsRuleLockScope()` — today only `"agentrouter"`). See "Restated quota
errors" above for what a `scope: "connection"` match actually does once a
provider is on that allowlist.
**#11104 — operator-declared rules bypass both allowlists.** An operator can
declare a per-provider rule at runtime via `settings.providerErrorRules`
(`open-sse/config/providerErrorRules.ts::setOperatorProviderErrorRules`)
without editing this file. Gating an operator rule behind
`FULL_TEXT_RULE_PROVIDERS`/`HONORS_RULE_LOCK_SCOPE_PROVIDERS` — allowlists
meant to protect the **default** behavior of built-in catalog rules — would
make the settings mechanism inert for every provider except the ones already
listed there, since declaring the rule is already the operator's explicit
opt-in. `resolveRuleMatchBody()` and `honorsRuleLockScope()` both check
`hasOperatorRuleForProvider()` first: a provider with an operator rule gets
the raw error text and has its declared `scope` honored, regardless of
whether it also appears in either allowlist.
**Known gap — `providerRuleRegistry` is never consulted for HTTP 400.**
`checkFallbackError`'s `BAD_REQUEST` branch classifies status 400 entirely
through its own pattern arrays (`MODEL_ACCESS_DENIED_PATTERNS`,
`CONTEXT_OVERFLOW_PATTERNS`, etc. in `accountFallback.ts`) and returns before
the `configuredRule`/`getProviderErrorRuleMatch` branch above it is reached.
A built-in catalog rule (or an operator rule) with `status: 400` is
syntactically valid but will never fire. No existing rule targets 400 today,
so nothing in production is affected — but a future 400 rule needs this
branch touched first, which is a larger change than adding a rule (it
reclassifies 400 for every provider already relying on the pattern-array
behavior) and is out of scope for a single-provider rule addition.
### Adding a new quota-misstating gateway
1. Register one rule array in `statusRestatementRegistry`

View File

@@ -120,6 +120,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
| `firecrawl` | caution | Cloud API ToS has no explicit personal-proxy prohibition found, but the open-source self-hosted version is AGPL-3.0 (re… |
| `gemini` | caution | ToS explicitly states the free tier is for "developers building with Google AI models for professional or business purp… |
| `groq` | caution | Services Agreement §6.3 prohibits reselling, sublicensing, or distributing API access; §3.2 bars reselling/leasing acco… |
| `hackclub` | caution | Service is explicitly scoped to Hack Club teen members building projects/learning; no public ToS found explicitly permi… |
| `huggingchat` | caution | Hugging Face ToS does not explicitly ban personal self-hosted proxies, but supplemental terms (referenced but not fully… |
| `huggingface` | caution | ToS grants a limited license to access/use the service; the document does not explicitly permit or forbid a single-user… |
| `hyperbolic` | caution | ToS grants API access "solely for your own personal or internal business purposes" and explicitly prohibits licensing, … |
@@ -221,6 +222,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
| `duckduckgo-web` | keyless | — | — | avoid | 6 |
| `freemodel-dev` | keyless | — | — | unknown | 4 |
| `friendliai` | keyless | — | — | avoid | 2 |
| `hackclub` | keyless | — | — | caution | 3 |
| `iflytek` | keyless | — | — | avoid | 1 |
| `inference-net` | keyless | — | — | caution | 3 |
| `liquid` | keyless | — | — | unknown | 1 |
@@ -278,6 +280,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
- **`gitlawb`** — The shipped freeNote "Free tier available" is effectively stale. The original free MiMo access was removed in May 2026; the only remaining "free" option is a temporary promotional model (Nemotron 3 U…
- **`gitlawb-gmi`** — Partially still accurate — free tier exists but is now narrowed to a single model (Nemotron 3 Ultra) after MiMo free access was revoked in late May 2026. The shipped note "Free tier available" unders…
- **`groq`** — The shipped freeNote "30 RPM / 14.4K RPD" is accurate only for llama-3.1-8b-instant. Most other models (including llama-3.3-70b-versatile) have a much lower 1K RPD cap. The note omits model-specific …
- **`hackclub`** — The "30+ models" count appears accurate and still matches. The core offering remains free for Hack Club members. No evidence of tightening — still "$0 ALWAYS FREE" per the homepage. The freeNote omit…
- **`huggingchat`** — The shipped freeNote ("Free LLM chat — no subscription required. Rate limits apply.") is partially accurate but significantly understates the restrictions. The free tier now operates on a hard $0.10/…
- **`huggingface`** — Significantly tightened. The shipped freeNote ("Free Inference API for thousands of models") implied unlimited/generous free access, but as of mid-2025 the free tier is capped at $0.10/month in recur…
- **`hyperbolic`** — Our shipped freeNote says "$1-5 trial credits on signup" — the $1 trial credit portion is accurate, but the "$5" figure refers to the minimum deposit required to unlock GPU rental (not free credits g…

View File

@@ -213,6 +213,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `glm-cn` | `glmcn` | GLM Coding (China) | API key | [link](https://open.bigmodel.cn) | — |
| `glmt` | `glmt` | GLM Thinking | API key | [link](https://open.bigmodel.cn) | — |
| `groq` | `groq` | Groq | API key | [link](https://groq.com) | Free tier: 30 RPM / 14.4K RPD — no credit card |
| `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. |
| `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api |
| `hcnsec` | `hcnsec` | Huancheng Public API | API key | [link](https://api.hcnsec.cn) | Get API key at api.hcnsec.cn |
| `helixmind` | `helixmind` | HelixMind | API key, aggregator | [link](https://helixmind.online) | Previously circulated 3 RPM/50 RPD and no-card claims were not confirmed during the 2026-08-02 audit; current quota and billing require account verification. |

View File

@@ -171,7 +171,6 @@ export const HTTP_STATUS = {
FORBIDDEN: 403,
NOT_FOUND: 404,
NOT_ACCEPTABLE: 406,
UNPROCESSABLE_ENTITY: 422,
REQUEST_TIMEOUT: 408,
GONE: 410,
RATE_LIMITED: 429,
@@ -264,17 +263,11 @@ export const PROVIDER_PROFILES = {
circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS", 30000),
// Provider-level circuit breaker (entire provider cooldown after repeated failures)
providerFailureThreshold: envInt("OMNIROUTE_PROVIDER_BREAKER_API_KEY_FAILURE_THRESHOLD", 15), // Scaled for 500+ connections (was 5)
providerFailureWindowMs: envInt(
"OMNIROUTE_PROVIDER_BREAKER_API_KEY_FAILURE_WINDOW_MS",
1800000
), // 30min window (was 20min)
providerFailureWindowMs: envInt("OMNIROUTE_PROVIDER_BREAKER_API_KEY_FAILURE_WINDOW_MS", 1800000), // 30min window (was 20min)
providerCooldownMs: envInt("OMNIROUTE_PROVIDER_BREAKER_API_KEY_COOLDOWN_MS", 600000), // 10min cooldown when threshold reached
degradationThreshold: envInt("OMNIROUTE_PROVIDER_BREAKER_API_KEY_DEGRADATION_THRESHOLD", 7),
maxBackoffMultiplier: envInt("OMNIROUTE_PROVIDER_BREAKER_API_KEY_MAX_BACKOFF_MULTIPLIER", 4),
backoffEscalationCount: envInt(
"OMNIROUTE_PROVIDER_BREAKER_API_KEY_BACKOFF_ESCALATION_COUNT",
3
),
backoffEscalationCount: envInt("OMNIROUTE_PROVIDER_BREAKER_API_KEY_BACKOFF_ESCALATION_COUNT", 3),
},
// Local providers (localhost inference backends like Ollama, LM Studio, oMLX).
// Not yet wired into getProviderProfile() — will be used when local provider_nodes

View File

@@ -194,6 +194,9 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "groq", modelId: "openai/gpt-oss-120b", displayName: "GPT-OSS 120B", monthlyTokens: 15000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "groq", tos: "caution", hardStopGuaranteed: true },
{ provider: "groq", modelId: "openai/gpt-oss-20b", displayName: "GPT-OSS 20B", monthlyTokens: 15000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "groq", tos: "caution", hardStopGuaranteed: true },
{ provider: "groq", modelId: "qwen/qwen3-32b", displayName: "Qwen3 32B", monthlyTokens: 15000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "groq", tos: "caution", hardStopGuaranteed: true },
{ provider: "hackclub", modelId: "meta-llama/llama-3.3-70b-instruct", displayName: "Llama 3.3 70B", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "hackclub", tos: "caution" },
{ provider: "hackclub", modelId: "mistralai/mistral-7b-instruct", displayName: "Mistral 7B", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "hackclub", tos: "caution" },
{ provider: "hackclub", modelId: "deepseek-ai/deepseek-coder-33b", displayName: "DeepSeek Coder 33B", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "hackclub", tos: "caution" },
{ provider: "huggingchat", modelId: "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT", displayName: "ERNIE 4.5 VL 424B A47B Base PT", monthlyTokens: 500000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "huggingchat", tos: "caution" },
{ provider: "huggingchat", modelId: "CohereLabs/c4ai-command-r7b-12-2024", displayName: "Command R7B 12-2024", monthlyTokens: 500000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "huggingchat", tos: "caution" },
{ provider: "huggingchat", modelId: "CohereLabs/command-a-reasoning-08-2025", displayName: "Command A Reasoning 08-2025", monthlyTokens: 500000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "huggingchat", tos: "caution" },

View File

@@ -30,63 +30,21 @@ export type ProviderErrorRule = {
export type ProviderErrorRuleMatch = {
reason: ConfiguredErrorReason;
/**
* Intended lock scope. #10334: for a BUILT-IN catalog rule, this field is
* CONSUMED end-to-end only for providers in `HONORS_RULE_LOCK_SCOPE_PROVIDERS`
* (agentrouter-exclusive today, gated by `honorsRuleLockScope()`) — for those,
* `checkFallbackError` surfaces it as `ruleScope` on its return value for the
* persistence layer to honor instead of re-deriving scope from
* `hasPerModelQuota()`. For every other built-in-rule provider it remains
* INFORMATIONAL. #11104: an OPERATOR-declared rule (`OperatorProviderErrorRule`)
* is exempt from this allowlist — `honorsRuleLockScope()` always returns true
* when the provider has one, since the operator already opted in by declaring
* the rule. Widening `HONORS_RULE_LOCK_SCOPE_PROVIDERS` itself (for a new
* built-in catalog rule) is tracked as a follow-up — see
* `docs/architecture/RESILIENCE_GUIDE.md` §7.
* Intended lock scope. #10334: this field is CONSUMED end-to-end only for
* providers in `HONORS_RULE_LOCK_SCOPE_PROVIDERS` (agentrouter-exclusive
* today, gated by `honorsRuleLockScope()`) — for those, `checkFallbackError`
* surfaces it as `ruleScope` on its return value for the persistence layer
* to honor instead of re-deriving scope from `hasPerModelQuota()`. For
* every other provider it remains INFORMATIONAL: `getProviderErrorRuleMatch`
* callers still read only `reason`/`cooldownMs`, and the actual lock scope
* is decided independently by each call site. Widening the allowlist is
* tracked as a follow-up — see `docs/architecture/RESILIENCE_GUIDE.md` §7.
*/
scope: "model" | "provider" | "connection";
/** Optional explicit cooldown; falls back to the existing per-reason defaults. */
cooldownMs?: number;
};
/**
* Operator-declared per-provider error rule (settings-driven).
*
* Mirrors the catalog `ProviderErrorRule` but is data-only so an operator can
* add a scope/cooldown/reason override for a provider without editing this
* file. `match` is a plain case-insensitive SUBSTRING of the error body — never
* a RegExp — so an operator-supplied pattern can never introduce a ReDoS on the
* error-classification hot path. Bounded to <= 50 rules total by the settings
* schema. An operator rule is consulted BEFORE the built-in `providerRuleRegistry`
* and wins on the first status+substring match for a provider.
*/
export type OperatorProviderErrorRule = {
status: number;
match: string;
scope: "model" | "provider" | "connection";
reason?: ConfiguredErrorReason;
cooldownMs?: number;
};
let operatorProviderErrorRules: Record<string, OperatorProviderErrorRule[]> = {};
/**
* Inject operator-declared rules. Called from the runtime-settings applier
* (`applyRuntimeSettings`) once at boot and on every settings update, with the
* value validated by the settings schema. Pass `undefined`/empty/null to clear.
* Provider keys are lowercased so lookups are case-insensitive.
*/
export function setOperatorProviderErrorRules(
rules: Record<string, OperatorProviderErrorRule[]> | undefined | null
): void {
operatorProviderErrorRules = {};
if (!rules) return;
for (const [provider, list] of Object.entries(rules)) {
if (Array.isArray(list) && list.length > 0) {
operatorProviderErrorRules[provider.toLowerCase()] = list;
}
}
}
// ─── Opencode ───────────────────────────────────────────────────────────────────
// Opencode Go uses an account-wide quota. The body usually says "rate limit
// reached" but the presence of `x-ratelimit-remaining-requests: 0` is the
@@ -314,21 +272,11 @@ export const providerRuleRegistry = new Map<string, ProviderErrorRule[]>([
* FULL_TEXT_RULE_PROVIDERS: that set controls what body a rule matches against
* (input), this one controls whether the matched scope changes caller behavior
* (output). A provider could need one without the other.
*
* Providers with an operator-declared rule (`setOperatorProviderErrorRules`)
* are honored too, without being added here: the allowlist exists to gate
* BUILT-IN catalog rules, which change default behavior for every operator
* running that provider — an operator rule is already an explicit, per-operator
* opt-in, so gating it a second time behind this list would make the settings
* mechanism (#11104) silently inert for every provider except the ones listed
* below. See `hasOperatorRuleForProvider`.
*/
const HONORS_RULE_LOCK_SCOPE_PROVIDERS = new Set(["agentrouter"]);
export function honorsRuleLockScope(provider: string | null | undefined): boolean {
if (!provider) return false;
const key = provider.toLowerCase();
return HONORS_RULE_LOCK_SCOPE_PROVIDERS.has(key) || hasOperatorRuleForProvider(key);
return !!provider && HONORS_RULE_LOCK_SCOPE_PROVIDERS.has(provider.toLowerCase());
}
/**
@@ -362,51 +310,28 @@ export function egressBucketedLockProviders(): string[] {
}
/**
* Providers whose BUILT-IN catalog rules match on the FULL upstream error
* text. checkFallbackError's rule lookup normally passes only the structured
* Providers whose rules match on the FULL upstream error text.
* checkFallbackError's rule lookup normally passes only the structured
* error ({code, type} — message stripped by the combo callers), which is
* enough for header/status/code rules but blind to body-text markers like
* agentrouter's "额度不足". Providers in this set get the raw error text as
* the match body instead. EXCLUSIVE allowlist by owner decision (2026-08-13):
* adding a provider here is an explicit opt-in — the default path for every
* other provider must remain byte-for-byte unchanged.
*
* Operator-declared rules bypass this allowlist entirely (see
* `hasOperatorRuleForProvider`): the operator's `match` is a literal substring
* of the error body by construction, so a rule that never sees body text could
* never match anything, defeating the point of declaring it.
*/
const FULL_TEXT_RULE_PROVIDERS = new Set(["agentrouter"]);
/**
* True when an operator has declared at least one rule for this provider via
* `settings.providerErrorRules` (injected through `setOperatorProviderErrorRules`).
* Presence of the rule IS the opt-in — no separate allowlist to maintain, and
* no widening decision needed as new operators configure new providers.
*/
export function hasOperatorRuleForProvider(provider: string | null | undefined): boolean {
if (!provider) return false;
const rules = operatorProviderErrorRules[provider.toLowerCase()];
return !!rules && rules.length > 0;
}
/**
* Resolve the body handed to getProviderErrorRuleMatch inside
* checkFallbackError: full error text for FULL_TEXT_RULE_PROVIDERS or any
* provider with an operator-declared rule, the structured error for everyone
* else.
* checkFallbackError: full error text for FULL_TEXT_RULE_PROVIDERS,
* the structured error for everyone else.
*/
export function resolveRuleMatchBody(
provider: string | null | undefined,
structuredError: unknown,
errorText: string | null | undefined
): unknown {
if (
provider &&
(FULL_TEXT_RULE_PROVIDERS.has(provider.toLowerCase()) ||
hasOperatorRuleForProvider(provider)) &&
errorText
) {
if (provider && FULL_TEXT_RULE_PROVIDERS.has(provider.toLowerCase()) && errorText) {
return errorText;
}
return structuredError ?? null;
@@ -421,32 +346,10 @@ export function getProviderErrorRuleMatch(
provider: string | null | undefined,
status: number,
headers: Headers | Record<string, string> | null | undefined,
body?: unknown,
operatorRules?: Record<string, OperatorProviderErrorRule[]>
body?: unknown
): ProviderErrorRuleMatch | null {
if (!provider) return null;
const key = provider.toLowerCase();
// Operator-declared rules win first: an operator can override any catalog
// rule for a provider without editing this file. `operatorRules` is the
// injected source (tests / direct callers); when omitted we fall back to the
// settings-backed cache populated by `setOperatorProviderErrorRules`.
const opRules = (operatorRules ?? operatorProviderErrorRules)?.[key];
if (opRules && opRules.length > 0) {
const text = typeof body === "string" ? body : JSON.stringify(body ?? "");
const lowered = text.toLowerCase();
for (const r of opRules) {
if (r.status === status && lowered.includes(r.match.toLowerCase())) {
return {
reason: r.reason ?? "quota_exhausted",
scope: r.scope,
cooldownMs: r.cooldownMs,
};
}
}
}
const rules = providerRuleRegistry.get(key);
const rules = providerRuleRegistry.get(provider.toLowerCase());
if (!rules) return null;
// Normalize headers: accept either a `Headers` object (from `fetch()`) or
// a plain record. Provider rules access headers via plain object indexing.

View File

@@ -10,7 +10,6 @@ export {
} from "./providers/registry/alibaba/index.ts";
export { REGISTRY } from "./providers/index.ts";
import { REGISTRY } from "./providers/index.ts";
import { isPrivateHost } from "@/shared/network/outboundUrlGuard";
import {
RegistryModel,
REASONING_UNSUPPORTED,
@@ -133,8 +132,11 @@ export function isLocalProvider(baseUrl?: string | null): boolean {
try {
const url = new URL(baseUrl);
const hostname = url.hostname;
if (!hostname) return false;
return LOCAL_HOSTNAMES.has(hostname) || isPrivateHost(hostname);
// Strictly matching 172.16.0.0/12 (Docker/local) and explicitly blocking ::1 per SSRF hardening
return (
LOCAL_HOSTNAMES.has(hostname) ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname)
);
} catch {
return false;
}

View File

@@ -70,6 +70,7 @@ import { togetherProvider } from "./registry/together/index.ts";
import { cohereProvider } from "./registry/cohere/index.ts";
import { cursorProvider, cursor_apiProvider } from "./registry/cursor/index.ts";
import { volcengineProvider } from "./registry/volcengine/index.ts";
import { hackclubProvider } from "./registry/hackclub/index.ts";
import { freetheaiProvider } from "./registry/freetheai/index.ts";
import { g4f_groqProvider } from "./registry/g4f-groq/index.ts";
import { g4f_geminiProvider } from "./registry/g4f-gemini/index.ts";
@@ -335,6 +336,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
cursor: cursorProvider,
"cursor-api": cursor_apiProvider,
volcengine: volcengineProvider,
hackclub: hackclubProvider,
freetheai: freetheaiProvider,
"g4f-groq": g4f_groqProvider,
"g4f-gemini": g4f_geminiProvider,

View File

@@ -27,7 +27,7 @@ export const clineProvider: RegistryEntry = {
// the official free bucket and text-output models advertised as zero-cost.
models: [
{
id: "z-ai/glm-5.2",
id: "zai/glm-5.2",
name: "GLM 5.2",
toolCalling: true,
supportsReasoning: true,

View File

@@ -0,0 +1,19 @@
import type { RegistryEntry } from "../../shared.ts";
export const hackclubProvider: RegistryEntry = {
id: "hackclub",
alias: "hc",
format: "openai",
executor: "default",
baseUrl: "https://ai.hackclub.com/proxy/v1/chat/completions",
modelsUrl: "https://ai.hackclub.com/proxy/v1/models",
authType: "optional",
authHeader: "bearer",
passthroughModels: true,
defaultContextLength: 128000,
models: [
{ id: "meta-llama/llama-3.3-70b-instruct", name: "Llama 3.3 70B" },
{ id: "mistralai/mistral-7b-instruct", name: "Mistral 7B" },
{ id: "deepseek-ai/deepseek-coder-33b", name: "DeepSeek Coder 33B" },
],
};

View File

@@ -14,6 +14,8 @@ export const opencode_goProvider: RegistryEntry = {
authPrefix: "Bearer",
defaultContextLength: 200000,
models: [
...OPENCODE_ZEN_GO_SHARED_MODELS,
// Port from decolua/9router 8efacc11: align with official Go endpoints —
// glm-5.2 is now advertised and Kimi chat traffic must route through
// `kimi-k2.7-code` (the live API rejects the plain `kimi-k2.7` alias for
@@ -24,10 +26,6 @@ export const opencode_goProvider: RegistryEntry = {
{ id: "glm-5.2", name: "GLM-5.2", supportsReasoning: true },
{ id: "glm-5.2-high", name: "GLM-5.2 (high effort)", supportsReasoning: true },
{ id: "glm-5.2-max", name: "GLM-5.2 (max effort)", supportsReasoning: true },
...OPENCODE_ZEN_GO_SHARED_MODELS,
// models[0] (glm-5.2) is the dashboard default (LlmChatCard/ProviderTestSlideOver take models[0]).
{ id: "glm-5.1", name: "GLM-5.1" },
{ id: "glm-5", name: "GLM-5" },
// kimi-k2.7-code declared identically on opencode-zen — see OPENCODE_ZEN_GO_SHARED_MODELS.

View File

@@ -16,6 +16,8 @@ export const opencode_zenProvider: RegistryEntry = {
// from the live API response so new models work without a code deploy.
passthroughModels: true,
models: [
...OPENCODE_ZEN_GO_SHARED_MODELS,
// ── Chat / Coding ──────────────────────────────────────────
// #2900: big-pickle's upstream runs DeepSeek thinking mode — declare the
// interleaved reasoning_content contract so follow-up/tool-use turns replay
@@ -26,10 +28,6 @@ export const opencode_zenProvider: RegistryEntry = {
supportsReasoning: true,
interleavedField: "reasoning_content",
},
...OPENCODE_ZEN_GO_SHARED_MODELS,
// models[0] (big-pickle) is the dashboard default; SHARED spread kept after it.
{ id: "gpt-5.6-sol", name: "GPT 5.6 Sol" },
{ id: "gpt-5.6-terra", name: "GPT 5.6 Terra" },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna" },
@@ -69,8 +67,6 @@ export const opencode_zenProvider: RegistryEntry = {
supportsReasoning: true,
targetFormat: "openai-responses",
},
// Explicit wire-format overlay of the base opencode provider's muse-spark entry
// (targetFormat: openai-responses). Keep in sync with base on catalog syncs.
{
id: "muse-spark-1.2-contributor-free",
name: "Muse Spark 1.2 Contributor Free",

View File

@@ -10,8 +10,6 @@
* perplexity-search reuses credentials from the "perplexity" chat provider.
*/
import { isProviderBlockedByIdOrAlias } from "@/shared/utils/noAuthProviders";
export interface SearchProviderConfig {
id: string;
name: string;
@@ -396,18 +394,16 @@ export function supportsSearchType(
/**
* Get all search providers as a flat list
*/
export function getAllSearchProviders(blockedProviders: string[] = []): Array<{
export function getAllSearchProviders(): Array<{
id: string;
name: string;
searchTypes: string[];
}> {
return Object.values(SEARCH_PROVIDERS)
.filter((p) => !p.disabled && !isProviderBlockedByIdOrAlias(p.id, blockedProviders))
.map((p) => ({
id: p.id,
name: p.name,
searchTypes: p.searchTypes,
}));
return Object.values(SEARCH_PROVIDERS).map((p) => ({
id: p.id,
name: p.name,
searchTypes: p.searchTypes,
}));
}
/**

View File

@@ -20,10 +20,6 @@ import {
recordLearnedThinkingCap,
parseThinkingBudgetMax,
} from "../services/learnedThinkingCaps.ts";
import {
recordLearnedReasoningEffort,
parseReasoningEffortEnum,
} from "../services/learnedReasoningEffortCaps.ts";
import {
getParamFilterConfig,
addParamToBlocklist,
@@ -830,9 +826,6 @@ export class BaseExecutor {
// loop. The learned cap is also recorded process-wide via
// recordLearnedThinkingCap so future requests skip the 400 entirely.
let thinkingBudgetClampedMax: number | null = null;
// Set by the reasoning_effort 4xx clamp-and-retry below — guards the same
// "fires at most once per URL" invariant as thinkingBudgetClampedMax above.
let reasoningEffortClamped = false;
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const requestCredentials = withForcedResponsesUpstream(
@@ -1536,49 +1529,6 @@ export class BaseExecutor {
}
}
// Reasoning-effort enum 4xx clamp-and-retry (any provider/model without a
// declared reasoning_effort capability — custom OpenAI-compatible
// connections, or a registered provider the registry hasn't caught up
// with). Mirrors the thinking_budget clamp-and-retry above: parse the
// upstream-advertised accepted values, record them process-wide (so
// FUTURE requests clamp proactively via sanitizeReasoningEffortForProvider
// → getLearnedReasoningEffort), clamp the live transformedBody by
// re-running the sanitizer, and retry the same URL once.
if (
(response.status === HTTP_STATUS.BAD_REQUEST ||
response.status === HTTP_STATUS.UNPROCESSABLE_ENTITY) &&
!reasoningEffortClamped &&
transformedBody &&
typeof transformedBody === "object"
) {
const errText = await response
.clone()
.text()
.catch(() => "");
const acceptedValues = parseReasoningEffortEnum(errText);
if (acceptedValues) {
reasoningEffortClamped = true;
const learned = recordLearnedReasoningEffort(this.provider, model, acceptedValues);
if (learned) {
transformedBody = sanitizeReasoningEffortForProvider(
transformedBody,
this.provider,
model,
log
);
let retryBody = JSON.stringify(transformedBody);
if (usesClaudeCodeProtocol || this.provider === "claude") {
retryBody = await signRequestBody(retryBody);
}
log?.info?.(
"REASONING_SANITIZE",
`Upstream ${response.status} rejected reasoning_effort on ${url} — clamped to ${learned} and retrying (learned for ${this.provider}/${model})`
);
response = await fetchWithStartTimeout(url, { ...fetchOptions, body: retryBody });
}
}
}
// Generic reactive 400 field-downgrade; each field is stripped at most once.
if (
response.status === HTTP_STATUS.BAD_REQUEST &&

View File

@@ -8,10 +8,6 @@ import {
getProviderModel,
getProviderModels,
} from "../../config/providerModels.ts";
import {
getLearnedReasoningEffort,
REASONING_EFFORT_ORDER,
} from "../../services/learnedReasoningEffortCaps.ts";
/**
* Sanitize reasoning_effort for providers that don't accept all values.
@@ -342,24 +338,10 @@ export function sanitizeReasoningEffortForProvider(
const supportsXHigh = supportsXHighEffort(provider, modelStr);
const supportsMax = supportsMaxEffortForProvider(provider, modelStr);
// Highest value we've actually seen this provider+model accept in a real
// upstream 4xx (learnedReasoningEffortCaps.ts) — takes priority over the
// static registry (which defaults to "supports everything" when there's no
// entry, e.g. custom OpenAI-compatible connections) and over the hardcoded
// "high" fallback below (which isn't always valid either).
const learnedCap = getLearnedReasoningEffort(provider, modelStr);
const learnedRank = learnedCap ? REASONING_EFFORT_ORDER.indexOf(learnedCap) : -1;
// ── xhigh handling ──────────────────────────────────────────────────────
// xhigh is OmniRoute-internal. Map it to the best effort the model accepts.
if (effortStr === "xhigh") {
if (learnedCap && learnedRank < REASONING_EFFORT_ORDER.indexOf("xhigh")) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: clamped reasoning_effort xhigh → ${learnedCap} (learned)`
);
return writeEffortValue(b, learnedCap, c);
}
if (supportsXHigh) return body; // model accepts xhigh natively
if (supportsMax) {
log?.info?.(
@@ -384,13 +366,6 @@ export function sanitizeReasoningEffortForProvider(
// upstream, and if it 400s the user gets a clear signal. This prevents
// new models from being unusable for weeks until they're whitelisted (#8057).
if (effortStr === "max") {
if (learnedCap && learnedRank < REASONING_EFFORT_ORDER.indexOf("max")) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: clamped reasoning_effort max → ${learnedCap} (learned)`
);
return writeEffortValue(b, learnedCap, c);
}
if (supportsMax) return body; // explicitly known to accept max
// A model that explicitly advertises its accepted tiers is safe to normalize.

View File

@@ -199,8 +199,6 @@ export async function handleRerank({
return_documents,
credentials,
connectionId = null,
apiKeyId = null,
apiKeyName = null,
}) {
const startTime = Date.now();
if (!model) return errorResponse(400, "model is required");
@@ -269,23 +267,10 @@ export async function handleRerank({
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
const errorMessage =
errData.message || errData.error?.message || `Provider returned HTTP ${res.status}`;
saveCallLog({
method: "POST",
path: "/v1/rerank",
status: res.status,
model: `${providerId}/${modelId}`,
provider: providerId,
connectionId: connectionId || undefined,
duration: Date.now() - startTime,
requestBody,
responseBody: errData,
error: errorMessage,
apiKeyId: apiKeyId || undefined,
apiKeyName: apiKeyName || undefined,
}).catch(() => {});
return errorResponse(res.status, errorMessage);
return errorResponse(
res.status,
errData.message || errData.error?.message || `Provider returned HTTP ${res.status}`
);
}
const data = await res.json();
@@ -304,13 +289,10 @@ export async function handleRerank({
status: 200,
model: `${providerId}/${modelId}`,
provider: providerId,
connectionId: connectionId || undefined,
duration: Date.now() - startTime,
tokens: { prompt_tokens: 0, completion_tokens: 0 },
requestBody,
responseBody: result,
apiKeyId: apiKeyId || undefined,
apiKeyName: apiKeyName || undefined,
responseBody: { results_count: Array.isArray(result?.results) ? result.results.length : 0 },
connectionId,
}).catch(() => {});
const headers = new Headers({ ...CORS_HEADERS, "Content-Type": "application/json" });

View File

@@ -21,7 +21,7 @@ import {
honorsRuleLockScope,
} from "../config/providerErrorRules.ts";
import * as rot from "./rotationConfig.ts";
import { getPassthroughProviders, getProviderCategory, isLocalProvider } from "../config/providerRegistry.ts";
import { getPassthroughProviders, getProviderCategory } from "../config/providerRegistry.ts";
import {
DEFAULT_RESILIENCE_SETTINGS,
resolveResilienceSettings,
@@ -37,7 +37,7 @@ import {
type FailureKind,
} from "../../src/shared/utils/classify429";
import { recordProviderSuccess as resetCooldownFailureCount } from "./providerCooldownTracker.ts";
import { resolveProviderId, isLocalProvider as isLocalProviderId, isSelfHostedChatProvider } from "../../src/shared/constants/providers";
import { resolveProviderId } from "../../src/shared/constants/providers";
import { resolveUseUpstream429BreakerHints } from "../../src/shared/utils/providerHints";
import { getCodexModelScope } from "../config/codexQuotaScopes.ts";
import { getQuotaScopedModelForProvider } from "./antigravityQuotaFamily.ts";
@@ -791,14 +791,12 @@ export function hasPerModelQuota(
return connectionPassthroughModels;
}
if (!provider) return false;
const canonicalId = resolveProviderId(provider);
if (getCanonicalLockProvider(canonicalId) === "antigravity") return true;
if (getCanonicalLockProvider(canonicalId) === "codex") return true;
if (canonicalId === "gemini" || canonicalId === "github") return true;
if (canonicalId === "antigravity" || canonicalId === "agy") return true;
if (getPassthroughProviders().has(canonicalId)) return true;
if (isCompatibleProvider(canonicalId)) return true;
if (isLocalProviderId(canonicalId) || isSelfHostedChatProvider(canonicalId)) return true;
if (getCanonicalLockProvider(provider) === "antigravity") return true;
if (getCanonicalLockProvider(provider) === "codex") return true;
if (provider === "gemini" || provider === "github") return true;
if (provider === "antigravity" || provider === "agy") return true;
if (getPassthroughProviders().has(provider)) return true;
if (isCompatibleProvider(provider)) return true;
return false;
}

View File

@@ -1,126 +0,0 @@
/**
* Learned Reasoning-Effort Caps — reactive capability memory for providers/models
* OmniRoute has no static registry entry for (custom OpenAI-compatible connections,
* or any registered provider whose registry entry carries no reasoning metadata).
*
* Same shape as `learnedThinkingCaps.ts` (thinking_budget), generalized from a
* numeric budget to an ordinal reasoning_effort scale: on a 4xx whose body
* enumerates the accepted values, `base.ts`'s executor calls
* `recordLearnedReasoningEffort`, which stores the highest recognized value in a
* module-level Map keyed "provider:model" (lowercased). Subsequent requests for
* the same provider+model read the cap via `getLearnedReasoningEffort` (consulted
* by `sanitizeReasoningEffortForProvider` in `executors/base/reasoningEffort.ts`)
* so the 4xx→retry round-trip is paid at most once per process per provider+model.
*
* In-memory only (same operator-accepted tradeoff as the thinking-budget cache):
* restart resets, the first request after a restart may re-learn at the cost of
* one upstream 4xx.
*/
export const REASONING_EFFORT_ORDER: readonly string[] = [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
];
// key: `${provider}:${model}` lowercased → highest value known to be accepted.
const learnedCaps = new Map<string, string>();
function buildKey(provider: string | null | undefined, model: string | null | undefined): string {
const p = typeof provider === "string" ? provider.trim().toLowerCase() : "";
const m = typeof model === "string" ? model.trim().toLowerCase() : "";
if (!p || !m) return "";
return `${p}:${m}`;
}
function rankOf(value: string): number {
return REASONING_EFFORT_ORDER.indexOf(value);
}
/**
* Return the learned cap for provider+model, or null when nothing has been
* learned yet (no upstream 4xx recorded). Keyed case-insensitively.
*/
export function getLearnedReasoningEffort(
provider: string | null | undefined,
model: string | null | undefined
): string | null {
const key = buildKey(provider, model);
if (!key) return null;
return learnedCaps.get(key) ?? null;
}
/**
* Record that `acceptedValues` is the enum the upstream advertised for
* provider+model, and store the highest recognized value as the learned cap.
* Returns the stored value, or null when `acceptedValues` contained no token
* from `REASONING_EFFORT_ORDER` (nothing usable to learn) or the key is unusable.
*
* Always monotonically decreases: if a cap already stored ranks lower than the
* newly computed highest, the stored (lower) value wins and is returned
* unchanged. This keeps a later, laxer-looking response (or a race between
* concurrent requests) from ratcheting the cap back up.
*/
export function recordLearnedReasoningEffort(
provider: string | null | undefined,
model: string | null | undefined,
acceptedValues: string[]
): string | null {
const key = buildKey(provider, model);
if (!key) return null;
let best: string | null = null;
let bestRank = -1;
for (const raw of acceptedValues) {
const rank = rankOf(raw);
if (rank > bestRank) {
bestRank = rank;
best = raw;
}
}
if (best === null) return null;
const existing = learnedCaps.get(key);
if (existing !== undefined && rankOf(existing) <= bestRank) {
return existing; // already learned an equal-or-lower cap; keep it
}
learnedCaps.set(key, best);
return best;
}
// Matches both prose shapes observed: OVH's `@ai-sdk/openai-compatible`
// deserializer ("expected one of `a`, `b`") and a generic vendor prose form
// ("Supported types are a, b, and c").
const LIST_INTRO = /(?:expected one of|supported (?:types|values) are)[:\s]*([^.]+)/i;
/**
* Extract the upstream-advertised accepted reasoning_effort values from a 4xx
* error body. Returns only tokens present in REASONING_EFFORT_ORDER (unknown
* tokens are dropped defensively) in the order they appeared, or null when the
* text names no recognized enum member.
*/
export function parseReasoningEffortEnum(errText: unknown): string[] | null {
if (typeof errText !== "string" || !errText) return null;
const match = LIST_INTRO.exec(errText);
if (!match) return null;
const tokens = match[1]
.split(/,|\band\b|&/i)
.map((t) =>
t
.replace(/`/g, "")
.replace(/\([^)]*\)/g, "")
.trim()
.toLowerCase()
)
.filter((t) => t.length > 0 && REASONING_EFFORT_ORDER.includes(t));
return tokens.length > 0 ? tokens : null;
}
/** Test-only: clear the learned-cap Map between tests. */
export function __test_resetLearnedReasoningEffortCaps(): void {
learnedCaps.clear();
}

View File

@@ -1,6 +1,5 @@
import { REGISTRY } from "../config/providerRegistry.ts";
import type { ReasoningTransport } from "../config/providerRegistry.ts";
import { isValidResponsesItemId } from "./responsesItemId.ts";
type JsonRecord = Record<string, unknown>;
@@ -280,27 +279,13 @@ function sanitizeResponsesInput(
if (!hasPlaintext && !hasOpaque && (!hasDisplaySummary(next) || stripOrphanedSummaries)) {
continue;
}
// `id` is only worth keeping on an opaque item with a valid string value —
// non-opaque items don't replay their id, and a malformed value (e.g. `null`,
// observed on opencode/zen) must not survive either way (#11108).
if (!hasOpaque || !isValidResponsesItemId(next.id)) delete next.id;
// Some upstreams (e.g. opencode/zen) omit `summary` entirely on opaque
// reasoning items instead of sending an empty array. Replaying that shape
// verbatim trips strict Responses-API validators that require the field
// to be present on every `input[]` item of type `reasoning` (#11108).
// Plaintext-only items intentionally have no `summary` key and must stay
// untouched.
if (hasOpaque && next.summary === undefined) next.summary = [];
if (!hasOpaque && typeof next.id === "string") delete next.id;
filtered.push(next);
continue;
}
const cloned = { ...record };
// Strip `id` whenever present, valid or not: these items don't need a
// replayed server id, and a malformed one (e.g. `null`, same opencode/zen
// omission pattern as the reasoning branch above) must not survive either
// (#11108).
if (cloned.id !== undefined) delete cloned.id;
if (typeof cloned.id === "string") delete cloned.id;
filtered.push(cloned);
}
return filtered;

View File

@@ -1,5 +1,3 @@
import { isValidResponsesItemId } from "./responsesItemId.ts";
type JsonRecord = Record<string, unknown>;
type SanitizeResponsesInputOptions = {
dropInternalAssistantMessages?: boolean;
@@ -42,12 +40,7 @@ function sanitizeFunctionName(name: string): string {
}
function sanitizeInputItemId(record: JsonRecord): JsonRecord {
if (record.id === undefined) return record;
if (!isValidResponsesItemId(record.id)) {
const next = { ...record };
delete next.id;
return next;
}
if (typeof record.id !== "string") return record;
const type = typeof record.type === "string" ? record.type : "";
const expectedPrefix = SERVER_ITEM_ID_PREFIX_BY_TYPE[type];

View File

@@ -1,7 +0,0 @@
// Shared by reasoningInputPolicy.ts and responsesInputSanitizer.ts: both strip a
// Responses-API `input[]` item's `id` field when it isn't a valid string before
// replay, so a malformed value (e.g. `null`, observed on opencode/zen) never
// survives to trip a strict upstream with "Expected 'id' to be a string." (#11108).
export function isValidResponsesItemId(id: unknown): id is string {
return typeof id === "string";
}

View File

@@ -185,21 +185,7 @@ export interface OpenAiSseScan {
text: string;
/** True if any `choices[].delta.tool_calls` appeared — NEVER continue those. */
sawToolCall: boolean;
/**
* True only when `tool_calls` appeared in this scan AND its own
* `finish_reason: "tool_calls"` has NOT also appeared in the same scan — i.e. the
* call is still being streamed (arguments may be mid-flight). Once
* `finish_reason: "tool_calls"` closes it, the call is complete, not in flight: the
* client has the full arguments and a truncation past this point only drops
* trailing prose, which continuation can safely recover.
*/
sawToolCallInFlight: boolean;
/**
* True if a terminal marker for the OVERALL stream appeared: `[DONE]`, or a
* `finish_reason` other than `"tool_calls"`. A `finish_reason: "tool_calls"` ends
* that one choice but is not terminal for continuation purposes — the model turn
* (and the client-visible SSE) is still eligible to be resumed past it.
*/
/** True if a terminal marker (`[DONE]` or a non-null `finish_reason`) appeared. */
terminal: boolean;
/** True if at least one OpenAI-shaped `choices[].delta` was parsed (format gate). */
parsedOpenAi: boolean;
@@ -213,11 +199,10 @@ export interface OpenAiSseScan {
export function scanOpenAiSseText(sse: string): OpenAiSseScan {
let text = "";
let sawToolCall = false;
let toolCallFinished = false;
let terminal = false;
let parsedOpenAi = false;
if (typeof sse !== "string" || sse.length === 0) {
return { text, sawToolCall, sawToolCallInFlight: false, terminal, parsedOpenAi };
return { text, sawToolCall, terminal, parsedOpenAi };
}
for (const line of sse.split("\n")) {
const trimmed = line.trimStart();
@@ -246,17 +231,10 @@ export function scanOpenAiSseText(sse: string): OpenAiSseScan {
if (Array.isArray(toolCalls) && toolCalls.length > 0) sawToolCall = true;
}
const finishReason = (choice as { finish_reason?: unknown })?.finish_reason;
if (finishReason === "tool_calls") {
// Ends this one choice, but the overall stream/turn stays continuable —
// never counts as the general terminal marker (see OpenAiSseScan.terminal).
toolCallFinished = true;
} else if (finishReason != null) {
terminal = true;
}
if (finishReason != null) terminal = true;
}
}
const sawToolCallInFlight = sawToolCall && !toolCallFinished;
return { text, sawToolCall, sawToolCallInFlight, terminal, parsedOpenAi };
return { text, sawToolCall, terminal, parsedOpenAi };
}
export interface ContinuableBody {
@@ -391,7 +369,7 @@ export function createRecoverableStream(
let emittedTail = ""; // raw SSE not yet scanned (awaiting an event boundary)
let emittedText = ""; // assistant text already delivered to the client
let emittedTerminal = false;
let emittedToolCallInFlight = false;
let emittedToolCall = false;
let emittedParsedOpenAi = false;
// Enqueue to the client and, when continuation is enabled, fold the chunk into the
@@ -410,7 +388,7 @@ export function createRecoverableStream(
const scan = scanOpenAiSseText(complete);
emittedText += scan.text;
if (scan.terminal) emittedTerminal = true;
if (scan.sawToolCallInFlight) emittedToolCallInFlight = true;
if (scan.sawToolCall) emittedToolCall = true;
if (scan.parsedOpenAi) emittedParsedOpenAi = true;
};
@@ -424,7 +402,7 @@ export function createRecoverableStream(
continueEnabled &&
continuations < maxContinuations &&
emittedParsedOpenAi &&
!emittedToolCallInFlight &&
!emittedToolCall &&
!emittedTerminal &&
emittedText.length > 0;

View File

@@ -201,14 +201,6 @@ export function openaiToOpenAIResponsesRequest(
input.push({
type: "reasoning",
content: [{ type: "reasoning_text", text: reasoning }],
// Strict Responses-API upstreams (e.g. opencode/zen) require `summary`
// on every `input[]` item of type "reasoning", plaintext or opaque —
// omitting it rejects the request with `input[N] missing required
// field summary`. This item is always freshly built from a chat
// client's plaintext reasoning, so there is no source summary to
// preserve; default to an empty array like the replay sanitizer does
// for opaque items in reasoningInputPolicy.ts (#11108).
summary: [],
});
}

View File

@@ -866,25 +866,21 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
function openaiResponsesToOpenAIResponseStream(chunk, state) {
if (!chunk) {
// Iterate every still-open call needing schema-aware normalization, not just a
// single one — multiple parallel calls can each be pending here if the stream
// ends before their output_item.done arrives.
const pendingNormalized: Array<{ index: number; argsStr: string }> = [];
if (state.toolCallByCallId instanceof Map) {
for (const entry of state.toolCallByCallId.values()) {
if (entry.needsNormalization && entry.argsBuffer) {
const toolSchema = state.toolSchemas?.get(entry.name);
const argsToEmit = stripEmptyOptionalToolArgs(entry.argsBuffer, entry.name, toolSchema);
pendingNormalized.push({
index: entry.index,
argsStr: typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit ?? {}),
});
entry.argsBuffer = "";
entry.needsNormalization = false;
}
}
}
if (pendingNormalized.length > 0) {
if (
state.currentToolCallNeedsNormalization &&
state.currentToolCallArgsBuffer &&
state.currentToolCallName
) {
const toolSchema = state.toolSchemas?.get(state.currentToolCallName);
const argsToEmit = stripEmptyOptionalToolArgs(
state.currentToolCallArgsBuffer,
state.currentToolCallName,
toolSchema
);
const argsStr =
typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit ?? {});
state.currentToolCallArgsBuffer = "";
state.currentToolCallNeedsNormalization = false;
state.finishReasonSent = true;
state.finishReason = "tool_calls";
const common = {
@@ -893,21 +889,24 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
created: state.created,
model: state.model || "gpt-4",
};
const chunks: Record<string, unknown>[] = pendingNormalized.map(({ index, argsStr }) => ({
...common,
choices: [
{
index: 0,
delta: { tool_calls: [{ index, function: { arguments: argsStr } }] },
finish_reason: null,
},
],
}));
chunks.push({
...common,
choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
});
return chunks;
return [
{
...common,
choices: [
{
index: 0,
delta: {
tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsStr } }],
},
finish_reason: null,
},
],
},
{
...common,
choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
},
];
}
// Flush: send final chunk with finish_reason
if (!state.finishReasonSent && state.started) {
@@ -953,23 +952,7 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
state.chatId = `chatcmpl-${Date.now()}`;
state.created = Math.floor(Date.now() / 1000);
state.toolCallIndex = 0;
// Kept for computeFinishReason (synthesizeCompletedToolCalls.ts) compatibility —
// that snapshot path mutates it directly and expects it to exist. In a turn with
// multiple parallel calls this only ever reflects the LAST one opened/closed, so
// it must never be used to identify a specific call — only as the "is at least
// one tool call in flight this turn" signal computeFinishReason needs, which
// toolCallIndex > 0 already covers on its own once any call has been added.
state.currentToolCallId = null;
// Per-call state keyed by call_id (replaces the old singular
// currentToolCallId/ArgsBuffer/Name/NeedsNormalization/Deferred fields, which
// assumed only one function_call could ever be in flight at a time).
state.toolCallByCallId = new Map();
// response.function_call_arguments.delta carries `item_id`/`output_index`, not
// `call_id` — resolve either one back to the call_id key used by
// toolCallByCallId (two independent reverse maps, since some upstreams omit
// item_id on delta events but still send output_index).
state.toolCallItemToCallId = new Map();
state.toolCallOutputIndexToCallId = new Map();
}
// Text content delta
@@ -1000,48 +983,22 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
// Function call started
if (eventType === "response.output_item.added" && data.item?.type === "function_call") {
const item = data.item;
const callId = item.call_id || fallbackToolCallId();
// Kept for computeFinishReason (synthesizeCompletedToolCalls.ts) compatibility.
state.currentToolCallId = callId;
const toolName = normalizeToolName(item.name);
// Assign this call's index NOW, at .added, not at .done — two calls opened before
// either closes (a genuine parallel dispatch) must never share an index. Deferred
// (still-nameless) calls are the one exception: they don't claim an index until
// .done resolves a real name, so a call that never gets one never burns a slot
// another call could have used.
let index: number | null = null;
if (toolName) {
index = state.toolCallIndex ?? 0;
state.toolCallIndex = index + 1;
}
if (!(state.toolCallByCallId instanceof Map)) state.toolCallByCallId = new Map();
state.toolCallByCallId.set(callId, {
index,
name: toolName,
argsBuffer: "",
deferred: !toolName,
needsNormalization: toolName === "Agent",
});
if (!(state.toolCallItemToCallId instanceof Map)) state.toolCallItemToCallId = new Map();
if (item.id) state.toolCallItemToCallId.set(item.id, callId);
// `output_index` is a top-level field on every Responses API streamed event
// (response.output_item.added/.done AND function_call_arguments.delta alike) —
// an identifier independent of item_id, for upstreams that omit item_id on delta
// events.
if (!(state.toolCallOutputIndexToCallId instanceof Map)) {
state.toolCallOutputIndexToCallId = new Map();
}
if (data.output_index != null) state.toolCallOutputIndexToCallId.set(data.output_index, callId);
state.currentToolCallId = item.call_id || fallbackToolCallId();
state.currentToolCallArgsBuffer = ""; // reset per-call arg buffer
state.currentToolCallDeferred = false;
// Track this call_id so response.completed doesn't synthesize a duplicate
if (!state.toolCallIdsSeen) state.toolCallIdsSeen = new Set();
state.toolCallIdsSeen.add(callId);
if (state.currentToolCallId) state.toolCallIdsSeen.add(state.currentToolCallId);
const toolName = normalizeToolName(item.name);
state.currentToolName = toolName; // track for schema lookup at done time
state.currentToolCallName = toolName;
state.currentToolCallNeedsNormalization = toolName === "Agent";
if (!toolName) {
// Some Responses providers briefly emit placeholder/empty tool names.
// Defer emission until output_item.done in case the final name is populated there.
state.currentToolCallDeferred = true;
return null;
}
@@ -1056,8 +1013,8 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
delta: {
tool_calls: [
{
index,
id: callId,
index: state.toolCallIndex,
id: state.currentToolCallId,
type: "function",
function: {
name: toolName,
@@ -1080,26 +1037,11 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
const argsDelta = data.delta || "";
if (!argsDelta) return null;
// Resolve which in-flight call this delta belongs to. Try item_id first (the
// field the Responses API documents for this event), then output_index (also a
// top-level field on this event, and independent of item_id — covers upstreams
// that omit item_id on delta events but still send output_index). Only once both
// identifying fields are absent/unresolved do we fall back to guessing (the
// single open call, or the most recently opened one as a last resort).
const map = state.toolCallByCallId instanceof Map ? state.toolCallByCallId : null;
let callId = data.item_id ? state.toolCallItemToCallId?.get(data.item_id) : undefined;
if (!callId && data.output_index != null) {
callId = state.toolCallOutputIndexToCallId?.get(data.output_index);
}
if (!callId && map) {
callId = map.size === 1 ? [...map.keys()][0] : state.currentToolCallId;
}
const entry = callId ? map?.get(callId) : undefined;
if (!entry) return null;
state.currentToolCallArgsBuffer = (state.currentToolCallArgsBuffer || "") + argsDelta;
if (state.currentToolCallDeferred || state.currentToolCallNeedsNormalization) return null;
// #9168: buffer arguments until output_item.done for schema-aware null normalization
// Previously emitted raw null values for optional enum fields (e.g. isolation: null).
entry.argsBuffer = (entry.argsBuffer || "") + argsDelta;
return null;
}
@@ -1119,30 +1061,13 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
// carry the complete arguments only in output_item.done (no preceding delta events).
if (eventType === "response.output_item.done" && data.item?.type === "function_call") {
const item = data.item;
const map = state.toolCallByCallId instanceof Map ? state.toolCallByCallId : null;
let callId = item.call_id;
if (!callId && item.id) callId = state.toolCallItemToCallId?.get(item.id);
if (!callId) callId = state.currentToolCallId || fallbackToolCallId();
const trackedEntry = callId ? map?.get(callId) : undefined;
// Some upstreams (e.g. Codex) send the complete payload only in output_item.done,
// with no preceding output_item.added at all — there is no tracked entry to read an
// index from.
const entry = trackedEntry || { index: null, argsBuffer: "", deferred: false };
const buffered = entry.argsBuffer || "";
const buffered = state.currentToolCallArgsBuffer || "";
const currentIndex = state.toolCallIndex; // capture before increment
const callId = item.call_id || state.currentToolCallId || fallbackToolCallId();
const toolName = normalizeToolName(item.name);
// Claim (and advance) this call's index now if it wasn't assigned at .added — either
// a deferred call whose name has just now resolved, or a Codex-style done-only
// payload that never had an .added at all. A deferred call whose name is STILL empty
// never claims an index (nothing was ever emitted for it either way).
if (entry.index == null && toolName) {
entry.index = state.toolCallIndex ?? 0;
state.toolCallIndex = entry.index + 1;
}
const currentIndex = entry.index;
const toolSchema = state.toolSchemas?.get(toolName);
const shouldNormalizeArguments = toolName === "Agent";
state.currentToolCallNeedsNormalization = shouldNormalizeArguments;
if (toolName && state.toolCalls instanceof Map) {
const completedArguments =
@@ -1152,9 +1077,6 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
toolName,
toolSchema
);
// Keyed by index, not insertion order — readers that need call order for
// parallel calls closed out of order should sort by this key rather than
// relying on Map iteration order.
state.toolCalls.set(currentIndex, {
id: callId,
index: currentIndex,
@@ -1173,17 +1095,17 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
if (!state.toolCallIdsSeen) state.toolCallIdsSeen = new Set();
if (callId) state.toolCallIdsSeen.add(callId);
// This call is fully closed — remove it from the in-flight map (bounds the map
// to genuinely in-flight calls, and keeps the single-open-call fallback in the
// function_call_arguments.delta handler correct for whichever call opens next).
if (map && callId) map.delete(callId);
if (state.currentToolCallId === callId) state.currentToolCallId = null;
if (state.currentToolCallDeferred) {
state.currentToolCallDeferred = false;
state.currentToolCallArgsBuffer = "";
state.currentToolCallId = null;
if (entry.deferred) {
if (!toolName) {
return null;
}
state.toolCallIndex++;
const terminalArguments =
typeof item.arguments === "string"
? item.arguments.length > 0
@@ -1226,7 +1148,12 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
};
}
const needsNormalization = shouldNormalizeArguments;
state.toolCallIndex++;
state.currentToolCallArgsBuffer = ""; // reset for next tool call
state.currentToolCallId = null;
const needsNormalization = state.currentToolCallNeedsNormalization === true;
state.currentToolCallNeedsNormalization = false;
state.currentToolCallName = "";
// Nullable omission sentinels must be normalized before any argument bytes reach the client.
// Other tool calls retain immediate argument streaming.

View File

@@ -337,7 +337,6 @@ function createOpenAIReducer(fallbackModel?: string | null): SummaryReducer {
// same-name tool_calls) into its own separate tool_calls entries.
const finalToolCalls: ToolCall[] = [];
let nextIndex = 0;
// Normalize tool_call indexes to contiguous 0-based (OpenAI contract).
for (const tc of mergedToolCalls) {
const splitArgs = splitConcatenatedToolCallArguments(tc.function.arguments);
if (!splitArgs) {

View File

@@ -291,9 +291,7 @@ function ComboAutopilotPanel({ report }: { report: ComboAutopilotReport }) {
icon="monitor_heart"
label={t("comboHealthIssues")}
value={report.summary.issueCount.toLocaleString()}
subValue={t("comboHealthActionable", {
count: report.summary.suggestionCount ?? report.summary.actionableCount ?? 0,
})}
subValue={t("comboHealthActionable", { count: report.summary.actionableCount })}
/>
<MetricBlock
icon="error"

View File

@@ -87,22 +87,6 @@ export function parseAlibabaModelStudioModelsForConnection(
export function parseQwenCloudTextModels(data: any): any[] {
return parseCuratedDashscopeModels(data, QWEN_CLOUD_TEXT_MODELS, QWEN_CLOUD_TEXT_MODEL_IDS);
}
// Perplexity's /v1/models lists the Agent API catalog (vendor-prefixed ids like
// "anthropic/claude-fable-5"), but chat requests always go to the classic
// /chat/completions endpoint, which only accepts the Sonar family. Filter
// discovery to Sonar-family ids so agent-style ids never surface as routable
// chat models (#11060). Bounded pattern — no ReDoS-prone quantifiers.
export function parsePerplexitySonarModels(data: any): any[] {
const models = Array.isArray(data?.data)
? data.data
: Array.isArray(data?.models)
? data.models
: [];
return models.filter(
(model: any) => typeof model?.id === "string" && /^sonar(-|$)/.test(model.id)
);
}
type ProviderModelsHeaderContext = {
authType?: string;
providerSpecificData?: unknown;
@@ -675,17 +659,6 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
headers: { Accept: "application/json" },
parseResponse: parseClinepassRecommendedModels,
},
// Perplexity's /v1/models lists the Agent API catalog (vendor-prefixed agent
// ids), but chat only accepts the Sonar family on /chat/completions. Import
// must keep Sonar-family ids only (#11060).
perplexity: {
url: "https://api.perplexity.ai/v1/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: parsePerplexitySonarModels,
},
cohere: {
url: "https://api.cohere.com/v2/models",
method: "GET",

View File

@@ -121,17 +121,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
const { id } = await params;
const validation = validateBody(updateProviderConnectionSchema, rawBody);
if (isValidationFailure(validation)) {
// never drop an operator's intent silently. Surface the rejected
// keys (field paths and unrecognized-key names) alongside the existing
// error envelope so clients and the UI can tell exactly what was refused.
const rejected = [
...validation.error.details.map((d) => d.field).filter(Boolean),
...validation.error.details.flatMap((d) => d.keys ?? []),
];
return NextResponse.json(
{ error: { ...validation.error, rejected } },
{ status: 400 }
);
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const body = validation.data;
const {

View File

@@ -3,7 +3,7 @@ export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
import { getProviderConnections } from "@/lib/db/providers";
import { getProviderConnections } from "@/lib/localDb";
import { getProviderNodes } from "@/models";
import { matchesSearch } from "@/shared/utils/turkishText";
@@ -27,66 +27,6 @@ function rowPriority(row: any): number {
return 2;
}
/**
* Applies the active filter predicates to a single merged call-log row.
*
* `getCallLogs()` already filters the persisted DB rows server-side, but the
* in-memory entries (active/pending + recently-completed) are merged in by
* `buildCallLogListRows()` and would otherwise bypass every filter except
* `correlationId`. Running the same predicates over the merged rows closes that
* gap. It is idempotent for DB rows (they already satisfy the predicate) while
* correctly excluding in-memory rows that do not match.
*/
export function rowMatchesFilter(row: any, filter: Record<string, any>): boolean {
if (!filter) return true;
if (filter.status === "error") {
if (!(Number(row?.status) >= 400 || Boolean(row?.error))) return false;
} else if (filter.status === "ok") {
if (!(Number(row?.status) >= 200 && Number(row?.status) < 300)) return false;
} else if (typeof filter.status === "number" || (typeof filter.status === "string" && !isNaN(Number(filter.status)))) {
if (Number(row?.status) !== Number(filter.status)) return false;
}
if (filter.model && !matchesSearch(row?.model || "", String(filter.model))) {
return false;
}
if (filter.provider && !matchesSearch(row?.provider || "", String(filter.provider))) {
return false;
}
if (filter.account && !matchesSearch(row?.account || "", String(filter.account))) {
return false;
}
if (filter.apiKey && !matchesSearch(row?.apiKeyName || "", String(filter.apiKey))) {
return false;
}
if (filter.combo && !matchesSearch(row?.comboName || "", String(filter.combo))) {
return false;
}
if (filter.correlationId && !matchesSearch(row?.correlationId || "", String(filter.correlationId))) {
return false;
}
if (filter.search) {
const term = String(filter.search);
const haystack = [
row?.model,
row?.provider,
row?.providerDisplay,
row?.account,
row?.apiKeyName,
row?.comboName,
row?.correlationId,
row?.error,
row?.path,
]
.filter(Boolean)
.join(" ");
if (!matchesSearch(haystack, term)) return false;
}
return true;
}
export function buildCallLogListRows({
logs,
connections,
@@ -234,8 +174,15 @@ export async function GET(request: Request) {
completedDetails: getCompletedDetails().values(),
});
const filtered = rows.filter((r: any) => rowMatchesFilter(r, filter));
return NextResponse.json(filtered);
// When correlationId filter is set, also filter in-memory entries
// (active + completed) that don't match — getCallLogs already filters
// the DB rows but activeEntries/completedEntries bypass it.
if (filter.correlationId) {
const cid = filter.correlationId;
return NextResponse.json(rows.filter((r: any) => matchesSearch(r.correlationId || "", cid)));
}
return NextResponse.json(rows);
} catch (error) {
console.error("[API ERROR] /api/usage/call-logs failed:", error);
return NextResponse.json({ error: "Failed to fetch call logs" }, { status: 500 });

View File

@@ -10,15 +10,11 @@ import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
import { v1RerankSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { getCachedProviderNodes } from "@/lib/db/readCache";
import { getCachedProviderNodes } from "@/lib/localDb";
import {
isAllRateLimitedCredentials,
rateLimitedProviderResponse,
} from "@/app/api/v1/_shared/rateLimit";
import { saveCallLog } from "@/lib/usageDb";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { generateRequestId } from "@/shared/utils/requestId";
import { CORS_HEADERS } from "@omniroute/open-sse/utils/cors.ts";
/**
* Handle CORS preflight
@@ -125,8 +121,6 @@ async function postHandler(request, context) {
return_documents: body.return_documents,
credentials,
connectionId: (credentials as { connectionId?: string } | null)?.connectionId || null,
apiKeyId: policy.apiKeyInfo?.id || null,
apiKeyName: policy.apiKeyInfo?.name || null,
});
if (response?.ok) {
await clearRecoveredProviderState(credentials);
@@ -154,9 +148,8 @@ async function postHandler(request, context) {
}
const token = credentials?.apiKey || credentials?.accessToken;
const startTime = Date.now();
try {
let res = await fetch(localProvider.baseUrl, {
const res = await fetch(localProvider.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -171,110 +164,19 @@ async function postHandler(request, context) {
}),
});
// Some local providers (e.g. Infinity, TEI) mount at /rerank rather than /v1/rerank
if (res.status === 404 && localProvider.baseUrl.endsWith("/v1/rerank")) {
const fallbackUrl = localProvider.baseUrl.replace(/\/v1\/rerank$/, "/rerank");
try {
const fallbackRes = await fetch(fallbackUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
model: localModel,
query: body.query,
documents: body.documents,
top_n: body.top_n || body.documents.length,
return_documents: body.return_documents !== false,
}),
});
if (fallbackRes.ok || fallbackRes.status !== 404) {
res = fallbackRes;
}
} catch {
// retain original 404 response if fallback fetch fails
}
}
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
const errorMessage =
errData.message || errData.detail || `Provider returned HTTP ${res.status}`;
saveCallLog({
method: "POST",
path: "/v1/rerank",
status: res.status,
model: body.model,
provider: prefix,
connectionId:
(credentials as { connectionId?: string } | null)?.connectionId || undefined,
duration: Date.now() - startTime,
requestBody: {
model: body.model,
query: body.query,
documents: body.documents,
top_n: body.top_n,
return_documents: body.return_documents,
},
responseBody: errData,
error: errorMessage,
apiKeyId: policy.apiKeyInfo?.id || undefined,
apiKeyName: policy.apiKeyInfo?.name || undefined,
}).catch(() => {});
return errorResponse(res.status, errorMessage);
return errorResponse(
res.status,
errData.message || errData.detail || `Provider returned HTTP ${res.status}`
);
}
const data = await res.json();
const latencyMs = Date.now() - startTime;
saveCallLog({
method: "POST",
path: "/v1/rerank",
status: 200,
model: body.model,
provider: prefix,
connectionId:
(credentials as { connectionId?: string } | null)?.connectionId || undefined,
duration: latencyMs,
tokens: { prompt_tokens: 0, completion_tokens: 0 },
requestBody: {
model: body.model,
query: body.query,
documents: body.documents,
top_n: body.top_n,
return_documents: body.return_documents,
},
responseBody: data,
apiKeyId: policy.apiKeyInfo?.id || undefined,
apiKeyName: policy.apiKeyInfo?.name || undefined,
}).catch(() => {});
const headers = new Headers({ ...CORS_HEADERS, "Content-Type": "application/json" });
attachOmniRouteMetaHeaders(headers, {
provider: prefix,
model: localModel,
costUsd: 0,
latencyMs,
requestId: generateRequestId(),
});
return new Response(JSON.stringify(data), {
status: 200,
headers,
return Response.json(data, {
headers: {},
});
} catch (err: any) {
saveCallLog({
method: "POST",
path: "/v1/rerank",
status: 500,
model: body.model,
provider: prefix,
connectionId:
(credentials as { connectionId?: string } | null)?.connectionId || undefined,
duration: Date.now() - startTime,
error: err.message,
apiKeyId: policy.apiKeyInfo?.id || undefined,
apiKeyName: policy.apiKeyInfo?.name || undefined,
}).catch(() => {});
return errorResponse(500, `Rerank request failed: ${err.message}`);
}
}

View File

@@ -58,7 +58,9 @@ export async function OPTIONS() {
export async function GET() {
const settings = await getSettings().catch(() => ({} as any));
const blockedProviders = settings?.blockedProviders || [];
const providers = getAllSearchProviders(blockedProviders);
const providers = getAllSearchProviders().filter(
(p) => !isProviderBlockedByIdOrAlias(p.id, blockedProviders)
);
const timestamp = Math.floor(Date.now() / 1000);
const data = providers.map((p) => ({

View File

@@ -13,8 +13,6 @@
* - Optional human notes
*/
import { getDbInstance } from "../lib/db/core";
/** Types of configuration entities that can be audited */
export type AuditTarget = "provider" | "combo" | "policy" | "connection" | "settings";
@@ -74,8 +72,10 @@ export interface ConfigSnapshot {
data: Record<string, unknown>;
}
// ── SQLite-backed store ───────────────────────────────────────────────────────
// ── In-memory store ──────────────────────────────────────────────────────────
// In production, persist to SQLite alongside other domain state.
let auditLog: ConfigAuditEntry[] = [];
let idCounter = 0;
function generateId(): string {
@@ -85,40 +85,6 @@ function generateId(): string {
return `audit-${ts}-${seq}`;
}
function db() {
return getDbInstance();
}
interface ConfigAuditRow {
id: string;
timestamp: string;
action: string;
target: string;
target_id: string;
target_name: string;
before_json: string | null;
after_json: string | null;
diff_json: string;
source: string;
note: string | null;
}
function rowToEntry(row: ConfigAuditRow): ConfigAuditEntry {
return {
id: row.id,
timestamp: row.timestamp,
action: row.action as AuditAction,
target: row.target as AuditTarget,
targetId: row.target_id,
targetName: row.target_name,
before: row.before_json === null ? null : (JSON.parse(row.before_json) as Record<string, unknown> | null),
after: row.after_json === null ? null : (JSON.parse(row.after_json) as Record<string, unknown> | null),
source: row.source as AuditSource,
diff: JSON.parse(row.diff_json) as ConfigDiff,
note: row.note,
};
}
/**
* Compute a structured diff between two configuration states.
*/
@@ -193,24 +159,12 @@ export function recordChange(
note: note ?? null,
};
db().prepare(
`INSERT INTO config_audit_log
(id, timestamp, action, target, target_id, target_name, before_json, after_json, diff_json, source, note)
VALUES
(@id, @timestamp, @action, @target, @targetId, @targetName, @beforeJson, @afterJson, @diffJson, @source, @note)`
).run({
id: entry.id,
timestamp: entry.timestamp,
action: entry.action,
target: entry.target,
targetId: entry.targetId,
targetName: entry.targetName,
beforeJson: before === null ? null : JSON.stringify(before),
afterJson: after === null ? null : JSON.stringify(after),
diffJson: JSON.stringify(entry.diff),
source: entry.source,
note: entry.note,
});
auditLog.push(entry);
// Keep log bounded (max 1000 entries in memory)
if (auditLog.length > 1000) {
auditLog = auditLog.slice(-1000);
}
return entry;
}
@@ -227,57 +181,42 @@ export function getAuditLog(options?: {
limit?: number;
offset?: number;
}): { entries: ConfigAuditEntry[]; total: number } {
const where: string[] = [];
const params: Record<string, unknown> = {};
let filtered = auditLog;
if (options?.target) {
where.push("target = @target");
params.target = options.target;
filtered = filtered.filter((e) => e.target === options.target);
}
if (options?.targetId) {
where.push("target_id = @targetId");
params.targetId = options.targetId;
filtered = filtered.filter((e) => e.targetId === options.targetId);
}
if (options?.action) {
where.push("action = @action");
params.action = options.action;
filtered = filtered.filter((e) => e.action === options.action);
}
if (options?.source) {
where.push("source = @source");
params.source = options.source;
filtered = filtered.filter((e) => e.source === options.source);
}
if (options?.since) {
where.push("timestamp >= @since");
params.since = options.since;
filtered = filtered.filter((e) => e.timestamp >= options.since!);
}
const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
const total = filtered.length;
const totalRow = db()
.prepare(`SELECT COUNT(*) AS c FROM config_audit_log ${whereSql}`)
.get(params) as { c: number };
const total = totalRow.c;
// Sort newest first
filtered = [...filtered].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
// Paginate
const offset = options?.offset ?? 0;
const limit = options?.limit ?? 50;
filtered = filtered.slice(offset, offset + limit);
const rows = db()
.prepare(
`SELECT * FROM config_audit_log ${whereSql} ORDER BY datetime(timestamp) DESC, id DESC LIMIT @limit OFFSET @offset`
)
.all({ ...params, limit, offset }) as ConfigAuditRow[];
return { entries: rows.map(rowToEntry), total };
return { entries: filtered, total };
}
/**
* Get a specific audit entry by ID.
*/
export function getAuditEntry(id: string): ConfigAuditEntry | null {
const row = db()
.prepare("SELECT * FROM config_audit_log WHERE id = @id")
.get({ id }) as ConfigAuditRow | undefined;
return row ? rowToEntry(row) : null;
return auditLog.find((e) => e.id === id) ?? null;
}
/**
@@ -321,23 +260,19 @@ export function getAuditSummary(): {
const byAction: Record<string, number> = {};
const bySource: Record<string, number> = {};
const rows = db()
.prepare("SELECT * FROM config_audit_log ORDER BY datetime(timestamp) DESC, id DESC")
.all() as ConfigAuditRow[];
for (const row of rows) {
byTarget[row.target] = (byTarget[row.target] || 0) + 1;
byAction[row.action] = (byAction[row.action] || 0) + 1;
bySource[row.source] = (bySource[row.source] || 0) + 1;
for (const entry of auditLog) {
byTarget[entry.target] = (byTarget[entry.target] || 0) + 1;
byAction[entry.action] = (byAction[entry.action] || 0) + 1;
bySource[entry.source] = (bySource[entry.source] || 0) + 1;
}
return {
totalEntries: rows.length,
totalEntries: auditLog.length,
byTarget,
byAction,
bySource,
oldestEntry: rows.length > 0 ? rows[rows.length - 1].timestamp : null,
newestEntry: rows.length > 0 ? rows[0].timestamp : null,
oldestEntry: auditLog.length > 0 ? auditLog[0].timestamp : null,
newestEntry: auditLog.length > 0 ? auditLog[auditLog.length - 1].timestamp : null,
};
}
@@ -345,6 +280,6 @@ export function getAuditSummary(): {
* Reset the audit log. Useful for testing.
*/
export function resetAuditLog(): void {
db().prepare("DELETE FROM config_audit_log").run();
auditLog = [];
idCounter = 0;
}

View File

@@ -5692,8 +5692,8 @@
"aggregatorsGateways": "Aggregators Gateways",
"enterpriseCloud": "Enterprise & Cloud",
"apiFormatLabel": "Api Format Label",
"apiKeyOptionalHint": "Leave empty if your local setup or provider does not require authentication.",
"apiKeyOptionalLabel": "API Key (optional)",
"apiKeyOptionalHint": "Api Key Optional Hint",
"apiKeyOptionalLabel": "Api Key Optional Label",
"apiRegionChina": "Api Region China",
"apiRegionHint": "Api Region Hint",
"apiRegionInternational": "Api Region International",

View File

@@ -6269,20 +6269,6 @@
"webSessionGuideStep3": "Sao chép thông tin xác thực được yêu cầu từ tên miền riêng của nhà cung cấp. Đối với cookie, chỉ sao chép giá trị tiêu đề Cookie và bỏ qua Cookie:.",
"webSessionGuideStep3Manual": "Cách thủ công: mở công cụ dành cho nhà phát triển của trình duyệt (F12 → Network), tải lại trang, mở một yêu cầu đã xác thực và sao chép giá trị tiêu đề Cookie trong Request Headers — bỏ tiền tố Cookie:.",
"webSessionGuideStep4": "Dán vào đây và kiểm tra kết nối. Nếu nó ngừng hoạt động, hãy đăng nhập lại và thay thế bằng một giá trị mới.",
"harImportButtonLabel": "Nhập tệp .har",
"harImportButtonBusy": "Đang nhập…",
"harImportButtonHint": "Xuất từ thẻ Network của DevTools sau khi gửi ít nhất một tin nhắn chat.",
"harImportStatusValid": "Đã nhập — hợp lệ trong ~{minutes} phút.",
"harImportStatusExpiringSoon": "Đã nhập — chỉ còn hợp lệ ~{minutes} phút nữa.",
"harImportStatusExpired": "Đã nhập, nhưng token này đã hết hạn ({minutes} phút trước) — hãy xuất một HAR mới.",
"harImportStatusUnknownExpiry": "Đã nhập. Không đọc được thời hạn.",
"harImportErrorNotJson": "Tệp đó không phải JSON hợp lệ — có đúng là bản xuất .har không?",
"harImportErrorNoEntries": "HAR này không có mục network nào được ghi lại.",
"harImportErrorNoChathubUrl": "Không tìm thấy kết nối Copilot chat trong HAR này. Hãy gửi ít nhất một tin nhắn chat trong m365.cloud.microsoft trước khi xuất.",
"harImportErrorUnparsableUrl": "Tìm thấy kết nối chat, nhưng không đọc được URL của nó.",
"harImportErrorMissingFields": "Tìm thấy kết nối chat, nhưng token bị thiếu trong đó.",
"harImportErrorReadFailed": "Không đọc được tệp đó.",
"harImportErrorUnknown": "Không trích xuất được thông tin xác thực từ tệp HAR đó.",
"webSessionSecurityHint": "Hãy coi đây như mật khẩu: nó có thể truy cập vào tài khoản web đã đăng nhập của bạn cho đến khi hết hạn hoặc bị thu hồi.",
"webNoAuthGuideTitle": "Không yêu cầu thông tin xác thực",
"webNoAuthGuideBody": "{provider} không cần khóa API hoặc cookie. Lưu kết nối để sử dụng endpoint web miễn phí của nó.",
@@ -12221,7 +12207,7 @@
},
"omni-webhooks": {
"name": "Webhook",
"description": "Đăng ký, liệt kê, kiểm thử và xoá các endpoint webhook. Cấu hình đăng ký sự kiện (request.completed, request.failed, quota.exceeded, v.v.) và quản lý thử lại giao hàng."
"description": "__MISSING__:Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries."
},
"omni-mcp": {
"name": "Máy chủ MCP",

View File

@@ -1,9 +1,5 @@
import { clearHealthCheckLogCache } from "@/lib/tokenHealthCheck";
import { setCustomBannedSignals } from "@omniroute/open-sse/services/accountFallback.ts";
import {
setOperatorProviderErrorRules,
type OperatorProviderErrorRule,
} from "@omniroute/open-sse/config/providerErrorRules.ts";
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
type JsonRecord = Record<string, unknown>;
@@ -50,7 +46,6 @@ interface RuntimeSettingsSnapshot {
systemTransforms: unknown;
authzBypass: AuthzBypassSnapshot;
customBannedSignals: string[];
providerErrorRules: Record<string, OperatorProviderErrorRule[]> | null;
}
// Default bypass policy: kill-switch on, `/api/mcp/` bypassable. Mirrors the
@@ -77,7 +72,6 @@ const DEFAULT_RUNTIME_SETTINGS_SNAPSHOT: RuntimeSettingsSnapshot = {
systemTransforms: null,
authzBypass: DEFAULT_AUTHZ_BYPASS_SNAPSHOT,
customBannedSignals: [],
providerErrorRules: null,
};
let lastAppliedSnapshot: RuntimeSettingsSnapshot | null = null;
@@ -144,34 +138,6 @@ function normalizeStringArray(value: unknown): string[] {
);
}
/**
* Defensive shape-check of operator-declared error rules pulled from settings.
* The settings schema already validates this on write; this guard prevents a
* malformed stored value (or an unexpected shape) from crashing the
* error-classification hot path. Returns null when the value is missing or not
* a record of non-empty rule arrays.
*/
function normalizeOperatorProviderErrorRules(
value: unknown
): Record<string, OperatorProviderErrorRule[]> | null {
if (value === null || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
const result: Record<string, OperatorProviderErrorRule[]> = {};
for (const [provider, list] of Object.entries(record)) {
if (!Array.isArray(list) || list.length === 0) continue;
const rules = list.filter(
(entry): entry is OperatorProviderErrorRule =>
!!entry &&
typeof entry === "object" &&
typeof (entry as OperatorProviderErrorRule).status === "number" &&
typeof (entry as OperatorProviderErrorRule).match === "string" &&
typeof (entry as OperatorProviderErrorRule).scope === "string"
);
if (rules.length > 0) result[provider.toLowerCase()] = rules;
}
return Object.keys(result).length > 0 ? result : null;
}
function normalizeStringRecord(value: unknown): Record<string, string> {
const record = toRecord(parseStoredJson(value, "modelAliases"));
const entries = Object.entries(record)
@@ -278,7 +244,6 @@ export function buildRuntimeSettingsSnapshot(
systemTransforms: parseStoredJson(settings.systemTransforms, "systemTransforms"),
authzBypass: normalizeAuthzBypass(settings),
customBannedSignals: normalizeStringArray(settings.customBannedSignals),
providerErrorRules: normalizeOperatorProviderErrorRules(settings.providerErrorRules),
};
}
@@ -575,13 +540,6 @@ export async function applyRuntimeSettings(
markChanged("bannedSignals");
}
if (
force ||
hasChanged(currentSnapshot.providerErrorRules, previousSnapshot.providerErrorRules)
) {
setOperatorProviderErrorRules(currentSnapshot.providerErrorRules ?? undefined);
}
lastAppliedSnapshot = currentSnapshot;
return changes;
}

View File

@@ -193,31 +193,6 @@ export async function cleanupMcpAudit(): Promise<CleanupResult> {
return result;
}
/**
* Clean up old config_audit_log based on retention settings.
*/
export async function cleanupConfigAudit(retentionDays = getRetentionSettings().configAudit): Promise<CleanupResult> {
const db = getDbInstance();
const result: CleanupResult = { deleted: 0, errors: 0 };
try {
const stmt = db.prepare(
"DELETE FROM config_audit_log WHERE datetime(timestamp) < datetime('now', '-' || ? || ' days')"
);
const runResult = stmt.run(String(retentionDays));
result.deleted = runResult.changes;
console.log(
`[Cleanup] Deleted ${result.deleted} config_audit_log older than ${retentionDays} days`
);
} catch (err: unknown) {
console.error("[Cleanup] Error cleaning config_audit_log:", err);
result.errors++;
}
return result;
}
/**
* Clean up old a2a_task_events based on retention settings.
*/
@@ -445,7 +420,6 @@ export async function runAutoCleanup(): Promise<{
usageHistory: await cleanupUsageHistory(),
compressionAnalytics: await cleanupCompressionAnalytics(),
mcpAudit: await cleanupMcpAudit(),
configAudit: await cleanupConfigAudit(),
a2aEvents: await cleanupA2aEvents(),
memoryEntries: await cleanupMemoryEntries(),
domainCostHistory: await cleanupDomainCostHistory(),

View File

@@ -46,7 +46,6 @@ const LEGACY_FLAT_KEYS: {
quotaSnapshots: ["quotaSnapshots"],
compressionAnalytics: ["compressionAnalytics"],
mcpAudit: ["mcpAudit"],
configAudit: ["configAudit"],
a2aEvents: ["a2aEvents"],
callLogs: ["callLogs"],
usageHistory: ["usageHistory"],

View File

@@ -25,7 +25,6 @@ INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSetting
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'quotaSnapshots', '90');
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'compressionAnalytics', '30');
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'mcpAudit', '30');
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'configAudit', '30');
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'a2aEvents', '30');
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'callLogs', '90');
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'usageHistory', '365');

View File

@@ -1,15 +0,0 @@
CREATE TABLE IF NOT EXISTS config_audit_log (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT NOT NULL,
target_id TEXT NOT NULL,
target_name TEXT NOT NULL,
before_json TEXT,
after_json TEXT,
diff_json TEXT NOT NULL,
source TEXT NOT NULL,
note TEXT
);
CREATE INDEX IF NOT EXISTS idx_config_audit_log_target_created ON config_audit_log(target, timestamp);
CREATE INDEX IF NOT EXISTS idx_config_audit_log_created ON config_audit_log(timestamp);

View File

@@ -1,21 +0,0 @@
-- 162_remove_hackclub_provider.sql
-- Hack Club AI provider was removed from OmniRoute at the request of Hack Club's
-- maintainers (#11118). Clean up any locally stored configuration for it.
-- Historical request and usage records are intentionally preserved under the
-- provider identity that existed when they were written.
DELETE FROM provider_connections
WHERE provider = 'hackclub';
DELETE FROM registered_keys
WHERE provider = 'hackclub';
DELETE FROM provider_key_limits
WHERE provider = 'hackclub';
DELETE FROM discovery_results
WHERE provider_id = 'hackclub';
DELETE FROM key_value
WHERE namespace = 'customModels'
AND key = 'hackclub';

View File

@@ -627,26 +627,15 @@ export async function createProviderConnection(data: JsonRecord) {
// to no-overrides) keeps the field present on the returned object so the
// UI can tell "field was read, no overrides" apart from "field absent."
if ("quotaWindowThresholds" in connection) {
const result = sanitizeQuotaWindowThresholds(connection.quotaWindowThresholds);
if (result.rejected.length > 0) {
throw new Error(
`Refusing to persist quotaWindowThresholds with rejected keys: ${result.rejected.join(", ")}`
);
}
connection.quotaWindowThresholds = result.sanitized;
connection.quotaWindowThresholds = sanitizeQuotaWindowThresholds(
connection.quotaWindowThresholds
);
}
// Same sanitization for rateLimitOverrides — keep in-memory representation
// in sync with what gets persisted. Reject (don't silently drop) invalid
// keys/values so a direct DB writer can't lose operator intent.
// in sync with what gets persisted.
if ("rateLimitOverrides" in connection) {
const result = sanitizeRateLimitOverrides(connection.rateLimitOverrides);
if (result.rejected.length > 0) {
throw new Error(
`Refusing to persist rateLimitOverrides with rejected keys: ${result.rejected.join(", ")}`
);
}
connection.rateLimitOverrides = result.sanitized;
connection.rateLimitOverrides = sanitizeRateLimitOverrides(connection.rateLimitOverrides);
}
_insertConnectionRow(db, encryptConnectionFields({ ...connection }));
@@ -860,24 +849,13 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
// Mirror the sanitization the create path applies — keep the returned
// object in lockstep with what we persist.
if ("quotaWindowThresholds" in merged) {
const result = sanitizeQuotaWindowThresholds(merged.quotaWindowThresholds);
if (result.rejected.length > 0) {
throw new Error(
`Refusing to persist quotaWindowThresholds with rejected keys: ${result.rejected.join(", ")}`
);
}
const sanitized = sanitizeQuotaWindowThresholds(merged.quotaWindowThresholds);
// For updates we always carry the key forward (even as null) so the read
// path surfaces the cleared state to callers that merged it.
merged.quotaWindowThresholds = result.sanitized;
// path surfaces the cleared state to callers that just patched it.
merged.quotaWindowThresholds = sanitized;
}
if ("rateLimitOverrides" in merged) {
const result = sanitizeRateLimitOverrides(merged.rateLimitOverrides);
if (result.rejected.length > 0) {
throw new Error(
`Refusing to persist rateLimitOverrides with rejected keys: ${result.rejected.join(", ")}`
);
}
merged.rateLimitOverrides = result.sanitized;
merged.rateLimitOverrides = sanitizeRateLimitOverrides(merged.rateLimitOverrides);
}
const existingRecord = toRecord(existing);

View File

@@ -64,37 +64,20 @@ export function normalizeBooleanColumn(value: unknown, fallback: boolean): boole
return fallback;
}
// Result of sanitizing a per-connection overrides/threshold map. `sanitized`
// is the cleaned value (or null when it collapses to nothing); `rejected`
// lists every key that was refused so callers can fail loudly
// instead of silently dropping the operator's input.
export type SanitizeResult = {
sanitized: Record<string, number> | null;
rejected: string[];
};
// Sanitize the per-connection rate limit overrides map: keep only known
// fields with valid non-negative integer values. Called once at each
// write-path boundary. Unknown keys and invalid values go into `rejected`
// rather than being dropped in silence.
export function sanitizeRateLimitOverrides(value: unknown): SanitizeResult {
if (value === null || value === undefined) return { sanitized: null, rejected: [] };
if (typeof value !== "object" || Array.isArray(value)) return { sanitized: null, rejected: [] };
// fields with valid numeric values. Called once at each write-path boundary.
export function sanitizeRateLimitOverrides(value: unknown): Record<string, number> | null {
if (value === null || value === undefined) return null;
if (typeof value !== "object" || Array.isArray(value)) return null;
const allowedKeys = new Set(["rpm", "tpm", "tpd", "minTime", "maxConcurrent"]);
const rejected: string[] = [];
const map: Record<string, number> = {};
for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
if (!allowedKeys.has(key)) {
rejected.push(key);
continue;
}
if (!allowedKeys.has(key)) continue;
if (typeof v === "number" && Number.isInteger(v) && v >= 0) {
map[key] = v;
} else {
rejected.push(key);
}
}
return { sanitized: Object.keys(map).length === 0 ? null : map, rejected };
return Object.keys(map).length === 0 ? null : map;
}
// Serialize an already-sanitized map for SQLite TEXT storage.
@@ -108,29 +91,20 @@ export function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" ? (value as JsonRecord) : {};
}
// Sanitize the per-window threshold map: keep only 0-100 integer values with
// keys no longer than 64 chars. Called once at each write-path boundary
// (createProviderConnection + updateProviderConnection) so both the in-memory
// return and the persisted row share the same shape. Serialization below
// trusts this output. Invalid keys/values go into `rejected` rather than being
// dropped in silence.
export function sanitizeQuotaWindowThresholds(value: unknown): SanitizeResult {
if (value === null || value === undefined) return { sanitized: null, rejected: [] };
if (typeof value !== "object" || Array.isArray(value)) return { sanitized: null, rejected: [] };
const rejected: string[] = [];
// Sanitize the per-window threshold map: keep only 0-100 integer values.
// Called once at each write-path boundary (createProviderConnection +
// updateProviderConnection) so both the in-memory return and the persisted
// row share the same shape. Serialization below trusts this output.
export function sanitizeQuotaWindowThresholds(value: unknown): Record<string, number> | null {
if (value === null || value === undefined) return null;
if (typeof value !== "object" || Array.isArray(value)) return null;
const map: Record<string, number> = {};
for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
if (key.length > 64) {
rejected.push(key);
continue;
}
if (typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 100) {
map[key] = v;
} else {
rejected.push(key);
}
}
return { sanitized: Object.keys(map).length === 0 ? null : map, rejected };
return Object.keys(map).length === 0 ? null : map;
}
export function toStringOrNull(value: unknown): string | null {

View File

@@ -16,7 +16,6 @@ import type {
ComboForecastMetrics,
ComboForecastResponse,
ComboForecastRiskLevel,
ProviderAutopilotReport,
ComboHealthMetrics,
ComboHealthResponse,
ComboRecord,
@@ -35,7 +34,6 @@ export interface ComboHealthAutopilotOptions {
combos?: ComboRecord[];
healthResponse?: ComboHealthResponse;
forecastResponse?: ComboForecastResponse;
providerHealthResponse?: ProviderAutopilotReport;
}
type ProviderIssueView = {
@@ -105,12 +103,7 @@ function actionSet(
case "open_combo_editor":
return action(type, "Open combo editor", target, "/dashboard/combos");
case "run_combo_test":
return action(
type,
"Run combo test",
target,
`/dashboard/combos?test=${encodeURIComponent(target.comboId)}`
);
return action(type, "Run combo test", target, "/dashboard/combos");
case "open_provider_health_autopilot":
return action(type, "Open provider autopilot", target, "/dashboard/health");
case "review_quota_limits":
@@ -454,8 +447,7 @@ export async function buildComboHealthAutopilotReport(
now: options.now,
combos: combosSnapshot,
}),
options.providerHealthResponse ??
buildProviderHealthAutopilotReport({ includeHealthy: false, includeActions: false }),
buildProviderHealthAutopilotReport({ includeHealthy: false, includeActions: false }),
]);
const forecastsByComboId = new Map(forecast.combos.map((entry) => [entry.comboId, entry]));
@@ -478,7 +470,7 @@ export async function buildComboHealthAutopilotReport(
const degradedCount = allCombos.filter((combo) => combo.state === "degraded").length;
const healthyCount = allCombos.filter((combo) => combo.state === "healthy").length;
const issueCount = allCombos.reduce((sum, combo) => sum + combo.issues.length, 0);
const suggestionCount = allCombos.reduce(
const actionableCount = allCombos.reduce(
(sum, combo) =>
sum + combo.issues.reduce((issueSum, issue) => issueSum + issue.actions.length, 0),
0
@@ -495,8 +487,7 @@ export async function buildComboHealthAutopilotReport(
degradedCount,
downCount,
issueCount,
suggestionCount,
actionableCount: suggestionCount,
actionableCount,
},
combos,
};

View File

@@ -106,6 +106,7 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
"empower",
"poe",
"chutes",
"hackclub",
"freetheai",
"g4f-groq",
"g4f-gemini",
@@ -236,7 +237,9 @@ export function isSelfHostedChatProvider(providerId: unknown): boolean {
const EXPLICIT_OPTIONAL_APIKEY_PROVIDER_IDS = new Set([
"searxng-search",
"firecrawl",
"pollinations",
"copilot-web",
"hackclub",
"g4f-groq",
"g4f-gemini",
"g4f-pollinations",

View File

@@ -84,29 +84,11 @@ export const buildOpenCodeProviderConfig = ({
};
};
export const buildOpenCodeV2ProviderConfig = (
input: OpenCodeConfigInput
): Record<string, any> => {
const v1Config = buildOpenCodeProviderConfig(input);
return {
name: v1Config.name,
package: "@opencode-ai/ai/providers/openai-compatible",
settings: {
baseURL: v1Config.options.baseURL,
apiKey: v1Config.options.apiKey,
},
models: v1Config.models,
};
};
export const buildOpenCodeConfigDocument = (input: OpenCodeConfigInput) => ({
$schema: "https://opencode.ai/config.json",
provider: {
omniroute: buildOpenCodeProviderConfig(input),
},
providers: {
omniroute: buildOpenCodeV2ProviderConfig(input),
},
});
export const mergeOpenCodeConfig = (
@@ -118,18 +100,18 @@ export const mergeOpenCodeConfig = (
? existingConfig
: {};
// Same guard as the root above, one level down. Spreading a non-object here
// does not throw, it splays the value into index keys: an existing
// `"provider": ["a", "b"]` merged to `{"0": "a", "1": "b", omniroute: ... }`
// and a string was exploded one character per key. mergeOpenCodeConfigText
// refuses the same input outright, so the two disagreed on what to do with a
// malformed config.
const existingProvider = (safeConfig as Record<string, unknown>).provider;
const safeProvider =
existingProvider && typeof existingProvider === "object" && !Array.isArray(existingProvider)
? (existingProvider as Record<string, unknown>)
: {};
const existingProviders = (safeConfig as Record<string, unknown>).providers;
const safeProviders =
existingProviders && typeof existingProviders === "object" && !Array.isArray(existingProviders)
? (existingProviders as Record<string, unknown>)
: {};
return {
...safeConfig,
$schema: safeConfig.$schema || "https://opencode.ai/config.json",
@@ -137,10 +119,6 @@ export const mergeOpenCodeConfig = (
...safeProvider,
omniroute: buildOpenCodeProviderConfig(input),
},
providers: {
...safeProviders,
omniroute: buildOpenCodeV2ProviderConfig(input),
},
};
};
@@ -149,7 +127,6 @@ export const mergeOpenCodeConfigText = (
input: OpenCodeConfigInput
) => {
const providerConfig = buildOpenCodeProviderConfig(input);
const v2ProviderConfig = buildOpenCodeV2ProviderConfig(input);
const content = typeof existingText === "string" ? existingText : "";
const trimmedContent = content.trim();
@@ -184,11 +161,6 @@ export const mergeOpenCodeConfigText = (
const providerEdits = modify(nextText, ["provider", "omniroute"], providerConfig, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});
nextText = applyEdits(nextText, providerEdits);
const v2ProviderEdits = modify(nextText, ["providers", "omniroute"], v2ProviderConfig, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});
return applyEdits(nextText, v2ProviderEdits);
return applyEdits(nextText, providerEdits);
};

View File

@@ -260,9 +260,7 @@ export interface ComboAutopilotReport {
degradedCount: number;
downCount: number;
issueCount: number;
suggestionCount: number;
/** @deprecated Use suggestionCount instead. Kept as an alias for backward compatibility; remove after 2 releases. */
actionableCount?: number;
actionableCount: number;
};
combos: ComboAutopilotCombo[];
}

View File

@@ -4,10 +4,6 @@ import { z } from "zod";
type ValidationErrorDetail = {
field: string;
message: string;
// Present for `unrecognized_keys` issues: the unknown key names that were
// refused (e.g. a typo'd override key). Surfaced by callers so clients can
// tell exactly which keys were rejected.
keys?: string[];
};
type ValidationErrorPayload = {
@@ -49,9 +45,6 @@ export function validateBody<TSchema extends z.ZodTypeAny>(
details: issues.map((e) => ({
field: e.path.join("."),
message: e.message,
...(("keys" in e && (e as { keys?: string[] }).keys)
? { keys: (e as { keys: string[] }).keys }
: {}),
})),
},
};

View File

@@ -420,25 +420,6 @@ export const providerNodeValidateSchema = z.object({
modelId: z.string().trim().max(200).optional().or(z.literal("")),
});
// rate-limit override numeric fields must reject operator intent loss.
// `z.coerce.number()` silently turns "" into 0 and "60abc" into NaN, which
// would drop or distort the value instead of rejecting it. Preprocess first so
// an empty/non-numeric string fails validation (surfaced as a 400), while still
// coercing legit numeric strings like "60".
function rateLimitOverrideNumber(max: number) {
return z.preprocess(
(raw) => {
if (typeof raw === "string") {
if (raw.trim() === "") return NaN;
const parsed = Number(raw);
return Number.isNaN(parsed) ? raw : parsed;
}
return raw;
},
z.coerce.number().int().min(0).max(max)
);
}
export const updateProviderConnectionSchema = z
.object({
name: z.string().max(200).optional(),
@@ -487,24 +468,17 @@ export const updateProviderConnectionSchema = z
projectId: z.union([z.string(), z.null()]).optional(),
// Per-connection rate limit overrides — overrides the global RequestQueueSettings
// for this connection. Set to null to clear all overrides.
// Per-connection rate limit overrides — overrides the global
// RequestQueueSettings for this connection. Set to null to clear all
// overrides. `.strict()` rejects unknown keys (e.g. a typo'd `tmp`) with a
// 400 instead of silently stripping them: the operator's intent is
// never dropped without an error. `.nullable()` (rather than a
// `z.union([z.null(), …])`) keeps the `unrecognized_keys` issue at the top
// level so the rejected key name survives into the 400 response.
rateLimitOverrides: z
.object({
rpm: rateLimitOverrideNumber(1_000_000).optional(),
tpm: rateLimitOverrideNumber(100_000_000).optional(),
tpd: rateLimitOverrideNumber(10_000_000_000).optional(),
minTime: rateLimitOverrideNumber(60_000).optional(),
maxConcurrent: rateLimitOverrideNumber(10_000).optional(),
})
.partial()
.strict()
.nullable()
.union([
z.null(),
z.object({
rpm: z.coerce.number().int().min(0).max(1_000_000).optional(),
tpm: z.coerce.number().int().min(0).max(100_000_000).optional(),
tpd: z.coerce.number().int().min(0).max(10_000_000_000).optional(),
minTime: z.coerce.number().int().min(0).max(60_000).optional(),
maxConcurrent: z.coerce.number().int().min(0).max(10_000).optional(),
}),
])
.optional(),
proxyEnabled: z.boolean().optional(),
perKeyProxyEnabled: z.boolean().optional(),

View File

@@ -259,48 +259,6 @@ export const updateSettingsSchema = z.object({
})
)
.optional(),
/**
* Operator-declared per-provider error rules. Consulted BEFORE the built-in
* `providerRuleRegistry` in open-sse/config/providerErrorRules.ts so an
* operator can add a scope/cooldown/reason override for a provider without
* editing the catalog. Matches are plain case-insensitive SUBSTRINGS of the
* error body (never RegExp) to keep the classification hot path ReDoS-safe.
* Bounded to 50 rules total so a misconfigured setting cannot blow up the
* matcher.
*/
providerErrorRules: z
.record(
z.string().trim().min(1).max(100),
z.array(
z.object({
status: z.number().int().min(100).max(599),
match: z.string().min(1).max(200),
scope: z.enum(["model", "provider", "connection"]),
reason: z
.enum([
"auth_error",
"quota_exhausted",
"rate_limit_exceeded",
"model_capacity",
"server_error",
"unknown",
])
.optional(),
cooldownMs: z.number().int().min(0).max(86_400_000).optional(),
})
)
)
.optional()
.superRefine((value, ctx) => {
if (!value) return;
const total = Object.values(value).reduce((n, rules) => n + rules.length, 0);
if (total > 50) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerErrorRules: at most 50 rules total, got ${total}`,
});
}
}),
// #6168: global session-stickiness opt-out (per-combo config overrides this).
disableSessionStickiness: z.boolean().optional(),
/** Keep eligible combo targets close to the provider-side prompt cache. */

View File

@@ -44,7 +44,6 @@ export interface DatabaseSettings {
quotaSnapshots: number;
compressionAnalytics: number;
mcpAudit: number;
configAudit: number;
a2aEvents: number;
callLogs: number;
usageHistory: number;
@@ -115,7 +114,6 @@ export const DEFAULT_DATABASE_SETTINGS: Omit<DatabaseSettings, "location" | "sta
quotaSnapshots: 7,
compressionAnalytics: 30,
mcpAudit: 30,
configAudit: 30,
a2aEvents: 30,
callLogs: 30,
usageHistory: 30,

View File

@@ -86,7 +86,6 @@
"tests/unit/auth-opencode-zen-noauth-fallback.test.ts",
"tests/unit/auth-terminal-status.test.ts",
"tests/unit/authz/discovery-routes-local-only.test.ts",
"tests/unit/authz/oauth-autoimport-local-only.test.ts",
"tests/unit/authz/route-guard-local-prefix.test.ts",
"tests/unit/authz/route-guard-skills-collect.test.ts",
"tests/unit/authz/route-guard-version-get-exemption.test.ts",
@@ -272,7 +271,6 @@
"tests/unit/model-lockout-max-cooldown.test.ts",
"tests/unit/no-memory-header.test.ts",
"tests/unit/noauth-autocombo-lockout-7623.test.ts",
"tests/unit/ollama-404-model-lockout-11071.test.ts",
"tests/unit/non-streaming-sse-terminal-typescan-4459.test.ts",
"tests/unit/nvidia-410-model-scope.test.ts",
"tests/unit/nvidia-passthrough-models-6773.test.ts",
@@ -284,7 +282,6 @@
"tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts",
"tests/unit/openapi-security-tiers.test.ts",
"tests/unit/opencode-autocombo-search-pair.test.ts",
"tests/unit/opencode-v2-config-11070.test.ts",
"tests/unit/openrouter-free-model-credits-exhausted.test.ts",
"tests/unit/openrouter-passthrough-models.test.ts",
"tests/unit/openrouter-quota-6842.test.ts",

View File

@@ -1,47 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { rowMatchesFilter } from "../../src/app/api/usage/call-logs/route.ts";
test.describe("call-logs rowMatchesFilter unit tests", () => {
const baseRow = {
id: "log-1",
status: 200,
model: "openai/gpt-4o",
provider: "openai",
providerDisplay: "OpenAI Main",
account: "Work Account",
apiKeyName: "DevKey",
comboName: "SmartRouter",
correlationId: "corr-12345",
path: "/v1/chat/completions",
error: null,
};
test("status filter matches ok, error, and explicit status codes", () => {
assert.equal(rowMatchesFilter(baseRow, { status: "ok" }), true);
assert.equal(rowMatchesFilter(baseRow, { status: "error" }), false);
assert.equal(rowMatchesFilter(baseRow, { status: 200 }), true);
assert.equal(rowMatchesFilter(baseRow, { status: 500 }), false);
const errorRow = { ...baseRow, status: 500, error: "Internal Error" };
assert.equal(rowMatchesFilter(errorRow, { status: "ok" }), false);
assert.equal(rowMatchesFilter(errorRow, { status: "error" }), true);
});
test("provider filter matches provider name and excludes mismatched in-memory rows", () => {
assert.equal(rowMatchesFilter(baseRow, { provider: "openai" }), true);
assert.equal(rowMatchesFilter(baseRow, { provider: "anthropic" }), false);
});
test("model filter matches model name and excludes mismatched in-memory rows", () => {
assert.equal(rowMatchesFilter(baseRow, { model: "gpt-4o" }), true);
assert.equal(rowMatchesFilter(baseRow, { model: "claude-3-5-sonnet" }), false);
});
test("search query matches across haystack fields", () => {
assert.equal(rowMatchesFilter(baseRow, { search: "SmartRouter" }), true);
assert.equal(rowMatchesFilter(baseRow, { search: "DevKey" }), true);
assert.equal(rowMatchesFilter(baseRow, { search: "corr-12345" }), true);
assert.equal(rowMatchesFilter(baseRow, { search: "non-existent" }), false);
});
});

View File

@@ -6,13 +6,12 @@ import path from "node:path";
// Shared across all tests — the module caches DATA_DIR / SQLITE_FILE at load time,
// so we must create the temp dir and import exactly once.
type CoreModule = typeof import("../../src/lib/db/core.ts");
let tempDir: string;
let originalDataDir: string | undefined;
let getDbInstance: CoreModule["getDbInstance"];
let resetDbInstance: CoreModule["resetDbInstance"];
let ensureDbInitialized: CoreModule["ensureDbInitialized"];
let closeDbInstance: CoreModule["closeDbInstance"];
let getDbInstance: any;
let resetDbInstance: any;
let ensureDbInitialized: any;
let closeDbInstance: any;
before(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-test-"));

View File

@@ -1,39 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { getModelsByProviderId } from "../../open-sse/config/providerModels.ts";
import { parseClineRecommendedModels } from "../../open-sse/services/clinepassModels.ts";
test("#11099: Cline provider catalog model IDs use valid modelType/model format", () => {
const models = getModelsByProviderId("cline");
assert.ok(models.length > 0, "cline provider must expose models");
for (const model of models) {
assert.match(
model.id,
/^[a-z0-9-]+-?[a-z0-9-]*\/[a-z0-9._:-]+$/i,
`Model ID '${model.id}' must follow provider/model format`
);
assert.notEqual(
model.id.split("/")[0],
"zai",
"Model ID must use 'z-ai' instead of invalid 'zai'"
);
}
});
test("#11099: parseClineRecommendedModels correctly extracts recommended/free models", () => {
const mockPayload = {
recommended: [
{ id: "moonshotai/kimi-k3", name: "kimi-k3" },
{ id: "x-ai/grok-4.5", name: "grok-4.5" },
],
free: [{ id: "deepseek/deepseek-v4-flash", name: "deepseek-v4-flash" }],
};
const parsed = parseClineRecommendedModels(mockPayload);
assert.equal(parsed.length, 3);
assert.equal(parsed[0].id, "moonshotai/kimi-k3");
assert.equal(parsed[1].id, "x-ai/grok-4.5");
assert.equal(parsed[2].id, "deepseek/deepseek-v4-flash");
});

View File

@@ -1,24 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
sanitizeRateLimitOverrides,
sanitizeQuotaWindowThresholds,
} from "@/lib/db/providers/columns";
test("sanitizeRateLimitOverrides surfaces rejected keys (blocking, not silent)", () => {
const r = sanitizeRateLimitOverrides({ rpm: 10, foo: 1, tpm: -1 });
assert.deepEqual(r.sanitized, { rpm: 10 });
assert.deepEqual(r.rejected.sort(), ["foo", "tpm"]);
});
test("sanitizeQuotaWindowThresholds surfaces key-too-long and out-of-range", () => {
const r = sanitizeQuotaWindowThresholds({ ["a".repeat(65)]: 50, win: 101 });
assert.ok(r.rejected.length >= 1);
assert.ok(r.rejected.includes("win"));
});
test("valid input yields no rejected keys", () => {
const r = sanitizeRateLimitOverrides({ rpm: 10, tpm: 20 });
assert.deepEqual(r.rejected, []);
assert.deepEqual(r.sanitized, { rpm: 10, tpm: 20 });
});

View File

@@ -1,108 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import type {
ComboForecastResponse,
ComboHealthResponse,
ProviderAutopilotReport,
} from "../../src/shared/types/utilization.ts";
import { buildComboHealthAutopilotReport } from "../../src/lib/monitoring/comboHealthAutopilot.ts";
function healthResponse(): ComboHealthResponse {
return {
timeRange: "24h",
combos: [
{
comboId: "c1",
comboName: "my-combo",
strategy: "fallback",
models: [],
targetHealth: [
{
executionKey: "e1",
stepId: "s1",
model: "m",
provider: "p",
connectionId: null,
label: null,
requests: 5,
successRate: 90,
avgLatencyMs: 100,
lastStatus: "error",
lastUsedAt: null,
quotaRemainingPct: 50,
quotaIsExhausted: false,
quotaTrend: "stable",
quotaScope: "provider",
},
],
quotaHealth: { providers: [], worstRemainingPct: 100 },
usageSkew: { modelDistribution: [], giniCoefficient: 0 },
performance: { avgLatencyMs: 100, successRate: 1.0, totalRequests: 10 },
},
],
};
}
function forecastResponse(): ComboForecastResponse {
return {
timeRange: "24h",
horizon: "30d",
asOf: new Date(0).toISOString(),
method: "linear_history",
combos: [],
};
}
function providerHealthResponse(): ProviderAutopilotReport {
return { providers: [] } as unknown as ProviderAutopilotReport;
}
function buildOptions() {
return {
range: "24h" as const,
horizon: "30d" as const,
healthResponse: healthResponse(),
forecastResponse: forecastResponse(),
providerHealthResponse: providerHealthResponse(),
};
}
describe("combo health autopilot counter", () => {
it("exposes suggestionCount and keeps actionableCount alias", async () => {
const report = await buildComboHealthAutopilotReport(buildOptions());
assert.equal(typeof report.summary.suggestionCount, "number");
assert.equal(report.summary.actionableCount, report.summary.suggestionCount);
const expected = report.combos.reduce(
(sum, combo) =>
sum + combo.issues.reduce((issueSum, issue) => issueSum + issue.actions.length, 0),
0
);
assert.equal(report.summary.suggestionCount, expected);
});
it("run_combo_test action links the dashboard with the combo id", async () => {
const report = await buildComboHealthAutopilotReport(buildOptions());
const actions = report.combos.flatMap((combo) => combo.issues.flatMap((i) => i.actions));
const runTest = actions.find((a) => a.type === "run_combo_test");
assert.ok(runTest, "run_combo_test action should exist");
assert.equal(typeof runTest.href, "string");
assert.ok(runTest.href?.includes("c1"), "href must carry the combo id");
assert.equal(
runTest.href?.includes("/api/combos/test?comboId="),
false,
"href must not target the GET-only API route (405)"
);
});
it("keeps every action in manual mode", async () => {
const report = await buildComboHealthAutopilotReport(buildOptions());
for (const combo of report.combos) {
for (const issue of combo.issues) {
for (const action of issue.actions) {
assert.equal(action.mode, "manual");
}
}
}
});
});

View File

@@ -1,124 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-config-audit-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
const cleanup = await import("../../src/lib/db/cleanup.ts");
const audit = await import("../../src/domain/configAudit.ts");
type CountRow = { c: number };
function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function countRows(): number {
const db = core.getDbInstance();
const row = db.prepare("SELECT COUNT(*) AS c FROM config_audit_log").get() as CountRow;
return row.c;
}
function insertOldRow(id: string, daysAgo: number) {
const db = core.getDbInstance();
const old = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000).toISOString();
db.prepare(
`INSERT INTO config_audit_log
(id, timestamp, action, target, target_id, target_name, before_json, after_json, diff_json, source, note)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
id,
old,
"update",
"provider",
"p1",
"P1",
null,
null,
JSON.stringify({ added: [], removed: [], changed: [], isEmpty: true }),
"api",
null
);
}
test.beforeEach(() => {
resetStorage();
});
test.after(() => {
resetStorage();
});
test("recordChange persists to SQLite, not memory", () => {
const db = core.getDbInstance();
const tableRow = db
.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='config_audit_log'")
.get() as CountRow;
assert.equal(tableRow.c, 1);
const e = audit.recordChange("update", "provider", "p1", "My Provider", { a: 1 }, { a: 2 }, "api", null);
assert.equal(countRows(), 1);
const { entries, total } = audit.getAuditLog({ target: "provider" });
assert.equal(total, 1);
assert.equal(entries[0].id, e.id);
assert.deepEqual(entries[0].diff.changed, [{ key: "a", from: 1, to: 2 }]);
});
test("pagination + filters read from SQLite", () => {
audit.recordChange("create", "combo", "c1", "C1", null, { models: ["m1"] }, "dashboard");
audit.recordChange("update", "combo", "c1", "C1", { models: ["m1"] }, { models: ["m1", "m2"] }, "api");
const { entries, total } = audit.getAuditLog({ target: "combo", limit: 1, offset: 0 });
assert.equal(total, 2);
assert.equal(entries.length, 1);
});
test("getRollbackState returns the before snapshot", () => {
const e = audit.recordChange("update", "policy", "pol1", "Pol", { x: 1 }, { x: 2 }, "api");
assert.deepEqual(audit.getRollbackState(e.id), { x: 1 });
});
test("computeDiff stays pure", () => {
const d = audit.computeDiff({ a: 1 }, { a: 2, b: 3 });
assert.deepEqual(d.added, ["b"]);
assert.deepEqual(d.changed, [{ key: "a", from: 1, to: 2 }]);
});
test("resetAuditLog clears persisted rows", () => {
audit.recordChange("update", "provider", "p1", "P1", { a: 1 }, { a: 2 }, "api");
assert.equal(countRows(), 1);
audit.resetAuditLog();
assert.equal(countRows(), 0);
});
test("cleanupConfigAudit prunes rows beyond retentionDays", async () => {
insertOldRow("audit-old", 40);
const r = await cleanup.cleanupConfigAudit(30);
assert.equal(r.deleted, 1);
assert.equal(countRows(), 0);
});
test("cleanupConfigAudit keeps recent rows within retention", async () => {
insertOldRow("audit-recent", 5);
const r = await cleanup.cleanupConfigAudit(30);
assert.equal(r.deleted, 0);
assert.equal(countRows(), 1);
});
test("runAutoCleanup includes a configAudit result", async () => {
insertOldRow("audit-old-2", 40);
const result = await cleanup.runAutoCleanup();
assert.ok(result.results.configAudit);
assert.equal(typeof result.results.configAudit.deleted, "number");
assert.equal(typeof result.results.configAudit.errors, "number");
assert.equal(result.results.configAudit.deleted, 1);
assert.equal(countRows(), 0);
});

View File

@@ -75,15 +75,10 @@ for (const modelId of HYPERAGENT_FALLBACK_MODEL_IDS) {
}
test("getTokenLimit: does not force 1M onto non-hyperagent providers serving the same model ids", () => {
// windsurf used to pin this exact id at 200000, but its built-in provider entry was
// retired (#8228 — replaced by devin-desktop). With no per-provider source left,
// #11034 resolves the effort-suffixed variant via its BASE model (`claude-opus-4.7-max`
// → `claude-opus-4.7` → canonical `claude-opus-4-7`), whose real catalog window IS 1M.
// This is a legitimate base-model resolution, not the forbidden hyperagent default leak:
// it comes from the shared model catalog, never from the hyperagent registry scope.
assert.equal(getTokenLimit("windsurf", "claude-opus-4.7-max"), 1_000_000);
// bluesminds still pins its own claude-opus-4-5 entry to 200000, and that pin must win
// over both the name heuristic and any 1M window from sibling providers/catalogs.
// windsurf declares an explicit per-model contextLength of 200000 for this exact id —
// a provider-unscoped substring match on "claude-opus-4" would have clobbered it to 1M.
assert.equal(getTokenLimit("windsurf", "claude-opus-4.7-max"), 200000);
// bluesminds likewise pins its own claude-opus-4-5 entry to 200000.
assert.equal(getTokenLimit("bluesminds", "claude-opus-4-5"), 200000);
});

View File

@@ -52,38 +52,26 @@ describe("providers/columns — normalizeBooleanColumn", () => {
});
describe("providers/columns — sanitizeRateLimitOverrides", () => {
it("returns {sanitized:null,rejected:[]} for nullish / non-object / array input", () => {
assert.deepEqual(sanitizeRateLimitOverrides(null), { sanitized: null, rejected: [] });
assert.deepEqual(sanitizeRateLimitOverrides(undefined), { sanitized: null, rejected: [] });
assert.deepEqual(sanitizeRateLimitOverrides("x"), { sanitized: null, rejected: [] });
assert.deepEqual(sanitizeRateLimitOverrides([1, 2]), { sanitized: null, rejected: [] });
it("returns null for nullish / non-object / array input", () => {
assert.equal(sanitizeRateLimitOverrides(null), null);
assert.equal(sanitizeRateLimitOverrides(undefined), null);
assert.equal(sanitizeRateLimitOverrides("x"), null);
assert.equal(sanitizeRateLimitOverrides([1, 2]), null);
});
it("keeps only allowed keys with non-negative integers, reports the rest as rejected", () => {
assert.deepEqual(sanitizeRateLimitOverrides({ rpm: 10, bogus: 5, tpm: -1 }), {
sanitized: { rpm: 10 },
rejected: ["bogus", "tpm"],
});
it("keeps only allowed keys with non-negative integers", () => {
assert.deepEqual(sanitizeRateLimitOverrides({ rpm: 10, bogus: 5, tpm: -1 }), { rpm: 10 });
});
it("returns {sanitized:null} when nothing valid remains, with rejected keys", () => {
assert.deepEqual(sanitizeRateLimitOverrides({ rpm: 1.5, nope: 3 }), {
sanitized: null,
rejected: ["rpm", "nope"],
});
it("returns null when nothing valid remains", () => {
assert.equal(sanitizeRateLimitOverrides({ rpm: 1.5, nope: 3 }), null);
});
});
describe("providers/columns — sanitizeQuotaWindowThresholds", () => {
it("keeps only 0-100 integers, reports the rest as rejected", () => {
assert.deepEqual(sanitizeQuotaWindowThresholds({ a: 50, b: 120, c: 0 }), {
sanitized: { a: 50, c: 0 },
rejected: ["b"],
});
it("keeps only 0-100 integers", () => {
assert.deepEqual(sanitizeQuotaWindowThresholds({ a: 50, b: 120, c: 0 }), { a: 50, c: 0 });
});
it("returns {sanitized:null} when nothing valid remains, with rejected keys", () => {
assert.deepEqual(sanitizeQuotaWindowThresholds({ a: 200 }), {
sanitized: null,
rejected: ["a"],
});
it("returns null when empty", () => {
assert.equal(sanitizeQuotaWindowThresholds({ a: 200 }), null);
});
});

View File

@@ -1,7 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { HTTP_STATUS } from "../../open-sse/config/constants.ts";
test("HTTP_STATUS declares UNPROCESSABLE_ENTITY as 422", () => {
assert.equal(HTTP_STATUS.UNPROCESSABLE_ENTITY, 422);
});

View File

@@ -1,38 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { isLocalProvider } from "../../open-sse/config/providerRegistry.ts";
test("isLocalProvider detects RFC1918, CGNAT/Tailscale, and mDNS private hosts", () => {
// Local / loopback
assert.equal(isLocalProvider("http://localhost:11434/v1"), true);
assert.equal(isLocalProvider("http://127.0.0.1:11434/v1"), true);
// Docker 172.16/12
assert.equal(isLocalProvider("http://172.18.0.2:11434/v1"), true);
// RFC1918 LAN hosts (Issue #11091)
assert.equal(isLocalProvider("http://192.168.1.50:11434/v1"), true);
assert.equal(isLocalProvider("http://10.0.0.5:11434/v1"), true);
// Tailscale / CGNAT (100.64/10)
assert.equal(isLocalProvider("http://100.64.1.2:11434/v1"), true);
// Link-local (169.254/16)
assert.equal(isLocalProvider("http://169.254.1.1:11434/v1"), true);
// mDNS / private suffixes
assert.equal(isLocalProvider("http://studio.local:11434/v1"), true);
assert.equal(isLocalProvider("http://mybox.internal:11434/v1"), true);
// Public hosts (should be false)
assert.equal(isLocalProvider("https://api.openai.com/v1"), false);
assert.equal(isLocalProvider("https://api.anthropic.com/v1"), false);
assert.equal(isLocalProvider("http://8.8.8.8:8080/v1"), false);
// Fails open on missing or unparseable input (Issue #11091 review finding)
assert.equal(isLocalProvider(null), false);
assert.equal(isLocalProvider(undefined), false);
assert.equal(isLocalProvider(""), false);
assert.equal(isLocalProvider("not a url"), false);
assert.equal(isLocalProvider("file:///models"), false);
});

View File

@@ -1,126 +0,0 @@
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
REASONING_EFFORT_ORDER,
parseReasoningEffortEnum,
recordLearnedReasoningEffort,
getLearnedReasoningEffort,
__test_resetLearnedReasoningEffortCaps,
} from "../../open-sse/services/learnedReasoningEffortCaps.ts";
beforeEach(() => {
__test_resetLearnedReasoningEffortCaps();
});
after(() => {
__test_resetLearnedReasoningEffortCaps();
});
// ── REASONING_EFFORT_ORDER ──────────────────────────────────────────────────
test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < max", () => {
assert.deepEqual(REASONING_EFFORT_ORDER, [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
]);
});
// ── parseReasoningEffortEnum ────────────────────────────────────────────────
test("parseReasoningEffortEnum extracts the real OVH 422 enum (backtick-quoted)", () => {
const err =
"Failed to deserialize the JSON body into the target type: reasoning_effort: " +
"unknown variant `xhigh`, expected one of `none`, `high`, `medium`, `low`, `minimal`";
assert.deepEqual(parseReasoningEffortEnum(err), ["none", "high", "medium", "low", "minimal"]);
});
test("parseReasoningEffortEnum extracts a bare comma/and-joined enum with annotations", () => {
const err =
"Unexpected reasoning effort high. Supported types are xhigh (default), medium, and low.";
assert.deepEqual(parseReasoningEffortEnum(err), ["xhigh", "medium", "low"]);
});
test("parseReasoningEffortEnum drops unrecognized tokens", () => {
const err = "expected one of `none`, `turbo`, `high`";
assert.deepEqual(parseReasoningEffortEnum(err), ["none", "high"]);
});
test("parseReasoningEffortEnum returns null for unrelated error text", () => {
assert.equal(parseReasoningEffortEnum("connection refused"), null);
assert.equal(parseReasoningEffortEnum(""), null);
assert.equal(parseReasoningEffortEnum(null), null);
assert.equal(parseReasoningEffortEnum(undefined), null);
});
test("parseReasoningEffortEnum returns null when the list has no recognized token", () => {
assert.equal(parseReasoningEffortEnum("expected one of `foo`, `bar`"), null);
});
// ── recordLearnedReasoningEffort / getLearnedReasoningEffort ───────────────
test("records the highest recognized value from the accepted list", () => {
const learned = recordLearnedReasoningEffort("ovh", "qwen3-coder-30b-a3b-instruct", [
"none",
"high",
"medium",
"low",
"minimal",
]);
assert.equal(learned, "high");
assert.equal(getLearnedReasoningEffort("ovh", "qwen3-coder-30b-a3b-instruct"), "high");
});
test("returns null and stores nothing when acceptedValues has no recognized token", () => {
const learned = recordLearnedReasoningEffort("acme", "model-x", ["foo", "bar"]);
assert.equal(learned, null);
assert.equal(getLearnedReasoningEffort("acme", "model-x"), null);
});
test("monotonic decrease: a later, higher accepted-list never ratchets the cap back up", () => {
recordLearnedReasoningEffort("acme", "model-x", ["none", "low", "medium"]);
const learned = recordLearnedReasoningEffort("acme", "model-x", [
"none",
"low",
"medium",
"high",
"xhigh",
]);
assert.equal(learned, "medium");
assert.equal(getLearnedReasoningEffort("acme", "model-x"), "medium");
});
test("a later, lower accepted-list does ratchet the cap down", () => {
recordLearnedReasoningEffort("acme", "model-x", ["none", "low", "medium", "high"]);
const learned = recordLearnedReasoningEffort("acme", "model-x", ["none", "low"]);
assert.equal(learned, "low");
assert.equal(getLearnedReasoningEffort("acme", "model-x"), "low");
});
test("getLearnedReasoningEffort returns null for unknown provider+model", () => {
assert.equal(getLearnedReasoningEffort("acme", "unknown-model"), null);
});
test("getLearnedReasoningEffort is keyed case-insensitively on provider+model", () => {
recordLearnedReasoningEffort("OVH", "Qwen3-Coder-30B", ["none", "high"]);
assert.equal(getLearnedReasoningEffort("ovh", "qwen3-coder-30b"), "high");
assert.equal(getLearnedReasoningEffort("OVH", "QWEN3-CODER-30B"), "high");
});
test("different providers for the same model id have independent caps", () => {
recordLearnedReasoningEffort("ovh", "shared-model", ["none", "high"]);
assert.equal(getLearnedReasoningEffort("openrouter", "shared-model"), null);
});
test("handles empty/null provider or model gracefully", () => {
assert.equal(getLearnedReasoningEffort("", "m"), null);
assert.equal(getLearnedReasoningEffort("p", ""), null);
assert.equal(getLearnedReasoningEffort(null, "m"), null);
assert.equal(getLearnedReasoningEffort("p", null), null);
assert.equal(recordLearnedReasoningEffort("", "m", ["high"]), null);
assert.equal(recordLearnedReasoningEffort("p", "", ["high"]), null);
});

View File

@@ -1,215 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rerank-test-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const { invalidateDbCache } = await import("../../src/lib/db/readCache.ts");
const { createProviderNode, createProviderConnection } =
await import("../../src/lib/db/providers.ts");
const { getCallLogs, getCallLogById, waitForCallLogSaves } =
await import("../../src/lib/usage/callLogs.ts");
const { POST } = await import("../../src/app/api/v1/rerank/route.ts");
interface RerankSuccessResponse {
results: Array<{ index: number; relevance_score: number }>;
}
interface CallLogRow {
id: string;
model: string;
provider: string;
status: number;
error?: string;
connectionId?: string;
}
test.describe("Local rerank provider logging and fallback", () => {
const originalFetch = globalThis.fetch;
test.after(() => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
try {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
} catch {
// ignore
}
});
test("successfully logs local rerank calls and attaches metadata headers", async () => {
const now = new Date().toISOString();
await createProviderNode({
id: "vram",
name: "vram",
type: "openai",
prefix: "vram",
baseUrl: "http://127.0.0.1:8000/v1",
createdAt: now,
updatedAt: now,
});
await createProviderConnection({
id: "conn-vram-1",
provider: "vram",
authType: "apikey",
name: "vram-local",
apiKey: "test-token",
createdAt: now,
updatedAt: now,
});
invalidateDbCache("nodes");
invalidateDbCache("connections");
globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => {
assert.equal(String(url), "http://127.0.0.1:8000/v1/rerank");
const parsedBody = JSON.parse(String(init?.body || "{}"));
assert.equal(parsedBody.model, "BAAI/bge-reranker-v2-m3");
assert.equal(parsedBody.query, "test query");
assert.deepEqual(parsedBody.documents, ["doc1", "doc2"]);
return new Response(
JSON.stringify({
results: [
{ index: 0, relevance_score: 0.95 },
{ index: 1, relevance_score: 0.2 },
],
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
};
const req = new Request("http://localhost:20128/api/v1/rerank", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "vram/BAAI/bge-reranker-v2-m3",
query: "test query",
documents: ["doc1", "doc2"],
}),
});
const res = await POST(req, {} as Record<string, unknown>);
assert.equal(res.status, 200);
assert.equal(res.headers.get("x-omniroute-provider"), "vram");
assert.equal(res.headers.get("x-omniroute-model"), "BAAI/bge-reranker-v2-m3");
const json = (await res.json()) as RerankSuccessResponse;
assert.equal(json.results.length, 2);
await waitForCallLogSaves(15000);
const logs = (await getCallLogs({ limit: 10 })) as unknown as CallLogRow[];
const logEntry = logs.find((l) => l.model === "vram/BAAI/bge-reranker-v2-m3");
assert.ok(logEntry, "Expected call log entry for local rerank");
assert.equal(logEntry.provider, "vram");
assert.equal(logEntry.status, 200);
const detail = await getCallLogById(logEntry.id);
assert.deepEqual(detail?.requestBody, {
model: "vram/BAAI/bge-reranker-v2-m3",
query: "test query",
documents: ["doc1", "doc2"],
});
assert.deepEqual(detail?.responseBody, {
results: [
{ index: 0, relevance_score: 0.95 },
{ index: 1, relevance_score: 0.2 },
],
});
});
test("falls back from /v1/rerank to /rerank when local provider returns 404", async () => {
const now = new Date().toISOString();
await createProviderNode({
id: "infinity",
name: "infinity",
type: "openai",
prefix: "infinity",
baseUrl: "http://127.0.0.1:7997",
createdAt: now,
updatedAt: now,
});
await createProviderConnection({
id: "conn-infinity-1",
provider: "infinity",
authType: "apikey",
name: "infinity-local",
apiKey: "test-token",
createdAt: now,
updatedAt: now,
});
invalidateDbCache("nodes");
invalidateDbCache("connections");
const urlsAttempted: string[] = [];
globalThis.fetch = async (url: string | URL | Request) => {
urlsAttempted.push(String(url));
if (String(url).endsWith("/v1/rerank")) {
return new Response("Not Found", { status: 404 });
}
return new Response(
JSON.stringify({
results: [{ index: 0, relevance_score: 0.99 }],
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
};
const req = new Request("http://localhost:20128/api/v1/rerank", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "infinity/bge-reranker-large",
query: "search",
documents: ["doc1"],
}),
});
const res = await POST(req, {} as Record<string, unknown>);
assert.equal(res.status, 200);
assert.deepEqual(urlsAttempted, [
"http://127.0.0.1:7997/v1/rerank",
"http://127.0.0.1:7997/rerank",
]);
});
test("records error call log when local provider returns 500", async () => {
globalThis.fetch = async () => {
return new Response(JSON.stringify({ detail: "Local backend failure" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
};
const req = new Request("http://localhost:20128/api/v1/rerank", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "vram/BAAI/bge-reranker-v2-m3",
query: "test query",
documents: ["doc1"],
}),
});
const res = await POST(req, {} as Record<string, unknown>);
assert.equal(res.status, 500);
await waitForCallLogSaves(15000);
const logs = (await getCallLogs({ limit: 10 })) as unknown as CallLogRow[];
const logEntry = logs.find(
(l) => l.model === "vram/BAAI/bge-reranker-v2-m3" && l.status === 500
);
assert.ok(logEntry, "Expected 500 call log entry for local rerank failure");
assert.equal(logEntry.provider, "vram");
assert.equal(logEntry.error, "Local backend failure");
});
});

View File

@@ -1,66 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ollama-404-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const auth = await import("../../src/sse/services/auth.ts");
const { hasPerModelQuota, isModelLocked } = await import("../../open-sse/services/accountFallback.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("hasPerModelQuota returns true for ollama-local and ollama providers", () => {
assert.equal(hasPerModelQuota("ollama-local"), true);
assert.equal(hasPerModelQuota("ollama"), true);
});
test("markAccountUnavailable locks only the missing model on a 404 from ollama-local", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "ollama-local",
authType: "none",
baseUrl: "http://127.0.0.1:11434/v1",
isActive: true,
});
const result = await auth.markAccountUnavailable(
connection.id,
404,
"model 'model-b' not found",
"ollama-local",
"model-b"
);
assert.equal(result.shouldFallback, true);
// The missing model must be locked
assert.equal(isModelLocked("ollama-local", connection.id, "model-b"), true);
// The connection in DB must remain active / not marked unavailable for sibling models
const connInDb = await providersDb.getProviderConnectionById(connection.id);
assert.notEqual(connInDb?.testStatus, "unavailable", "connection should not be marked unavailable connection-wide on a 404 model-not-found error");
// getProviderCredentials must still serve sibling models
const selectedForSibling = await auth.getProviderCredentials(
"ollama-local",
null,
null,
"model-a"
);
assert.ok(selectedForSibling && !("allExpired" in selectedForSibling), "sibling model-a must still be selected on the same connection");
});

View File

@@ -1,40 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
const opencodeConfig = await import("../../src/shared/services/opencodeConfig.ts");
test("buildOpenCodeConfigDocument includes both V1 (provider) and V2 (providers) definitions", () => {
const doc = opencodeConfig.buildOpenCodeConfigDocument({
baseUrl: "http://localhost:20128/v1",
apiKey: "{env:OMNIROUTE_API_KEY}",
models: ["auto/best-coding"],
});
assert.ok(doc.provider?.omniroute, "V1 provider.omniroute must be present");
assert.equal(doc.provider.omniroute.npm, "@ai-sdk/openai-compatible");
assert.equal(doc.provider.omniroute.options.baseURL, "http://localhost:20128/v1");
assert.ok(doc.providers?.omniroute, "V2 providers.omniroute must be present");
assert.equal(doc.providers.omniroute.package, "@opencode-ai/ai/providers/openai-compatible");
assert.equal(doc.providers.omniroute.settings.baseURL, "http://localhost:20128/v1");
assert.equal(doc.providers.omniroute.settings.apiKey, "{env:OMNIROUTE_API_KEY}");
assert.ok(doc.providers.omniroute.models["auto/best-coding"].limit, "V2 model limit must be present");
});
test("mergeOpenCodeConfig preserves existing properties and updates both provider and providers", () => {
const existing = {
$schema: "https://opencode.ai/config.json",
customField: "keep-me",
};
const merged = opencodeConfig.mergeOpenCodeConfig(existing, {
baseUrl: "http://localhost:20128/v1",
apiKey: "sk_test_key",
models: ["auto/best-coding"],
});
assert.equal(merged.customField, "keep-me");
assert.ok(merged.provider?.omniroute);
assert.ok(merged.providers?.omniroute);
assert.equal(merged.providers.omniroute.settings.apiKey, "sk_test_key");
});

View File

@@ -24,16 +24,3 @@ test("every OPENCODE_ZEN_GO_SHARED_MODELS entry is present, unmodified, exactly
test("OPENCODE_ZEN_GO_SHARED_MODELS is frozen (no accidental cross-registry mutation)", () => {
assert.ok(Object.isFrozen(OPENCODE_ZEN_GO_SHARED_MODELS));
});
test("referenced non-shared model ids remain present", () => {
const goIds = new Set(opencode_goProvider.models.map((m) => m.id));
const zenIds = new Set(opencode_zenProvider.models.map((m) => m.id));
for (const id of ["minimax-m3", "glm-5.1"]) {
assert.ok(goIds.has(id) || zenIds.has(id), `expected ${id} in go or zen`);
}
});
test("models[0] is the intended dashboard default", () => {
assert.equal(opencode_goProvider.models[0].id, "glm-5.2");
assert.equal(opencode_zenProvider.models[0].id, "big-pickle");
});

View File

@@ -1,63 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { PROVIDER_MODELS_CONFIG } from "../../src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts";
// Regression guard for #11060 — Perplexity's /v1/models endpoint lists the
// Agent API catalog (vendor-prefixed ids like "anthropic/claude-fable-5"), but
// chat requests always go to the classic /chat/completions endpoint, which only
// accepts the Sonar family. Without a PROVIDER_MODELS_CONFIG entry, generic
// model import pulled those agent-style ids into the connection's chat model
// list and every routed request failed with 400 "Invalid model". The discovery
// entry must exist and its parseResponse must keep only Sonar-family ids.
test("perplexity has a discovery entry in PROVIDER_MODELS_CONFIG", () => {
const cfg = PROVIDER_MODELS_CONFIG.perplexity;
assert.ok(cfg, "expected a perplexity entry in PROVIDER_MODELS_CONFIG");
assert.equal(cfg.method, "GET");
assert.equal(cfg.url, "https://api.perplexity.ai/v1/models");
assert.equal(typeof cfg.parseResponse, "function");
});
test("perplexity parseResponse keeps only the Sonar family (#11060)", () => {
const cfg = PROVIDER_MODELS_CONFIG.perplexity;
const models = cfg.parseResponse({
object: "list",
data: [
{ id: "anthropic/claude-fable-5", object: "model", owned_by: "anthropic" },
{ id: "sonar-pro", object: "model", owned_by: "perplexity" },
{ id: "sonar", object: "model", owned_by: "perplexity" },
],
}) as Array<{ id: string }>;
assert.deepEqual(
models.map((model) => model.id),
["sonar-pro", "sonar"]
);
});
test("perplexity parseResponse keeps every Sonar variant and drops non-Sonar ids", () => {
const cfg = PROVIDER_MODELS_CONFIG.perplexity;
const models = cfg.parseResponse({
data: [
{ id: "sonar-deep-research" },
{ id: "sonar-reasoning-pro" },
{ id: "sonar-pro" },
{ id: "sonar" },
{ id: "openai/gpt-5" },
{ id: "sonarish" },
],
}) as Array<{ id: string }>;
assert.deepEqual(
models.map((model) => model.id),
["sonar-deep-research", "sonar-reasoning-pro", "sonar-pro", "sonar"]
);
});
test("perplexity parseResponse tolerates empty and malformed payloads", () => {
const cfg = PROVIDER_MODELS_CONFIG.perplexity;
assert.deepEqual(cfg.parseResponse({ data: [] }), []);
assert.deepEqual(cfg.parseResponse(undefined), []);
assert.deepEqual(cfg.parseResponse({}), []);
});

View File

@@ -1,11 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { providerAllowsOptionalApiKey } from "../../src/shared/constants/providers.js";
test("pollinations provider requires an API key and does not allow optional API key", () => {
assert.equal(
providerAllowsOptionalApiKey("pollinations"),
false,
"pollinations must require an API key because anonymous completions are no longer supported"
);
});

View File

@@ -106,36 +106,18 @@ test("updateProviderConnection with explicit null clears the column entirely", a
assert.ok(reread.quotaWindowThresholds === null || reread.quotaWindowThresholds === undefined);
});
test("DB serializer refuses out-of-range / invalid values instead of dropping silently", async () => {
// the DB module must refuse the write (throw) rather than
// silently prune invalid keys/values on the way in, so operator intent is
// never lost without an error. The Zod schema already rejects at the API
// boundary; this is defense in depth for direct DB writers (seed/scripts).
await assert.rejects(
() =>
providersDb.createProviderConnection({
provider: "codex",
authType: "apikey",
name: "Codex Sanitize",
apiKey: "sk-san",
quotaWindowThresholds: { window5h: 95, bogus: 999, fractional: 1.5 },
}),
/rejected keys/
);
});
test("DB serializer refuses unknown rate-limit override keys instead of dropping silently", async () => {
await assert.rejects(
() =>
providersDb.createProviderConnection({
provider: "codex",
authType: "apikey",
name: "Codex Sanitize RLO",
apiKey: "sk-san-2",
rateLimitOverrides: { rpm: 10, bogus: 999, tpm: -1 },
}),
/rejected keys/
);
test("DB serializer drops out-of-range values silently", async () => {
// The DB module sanitizes the map on the way in; values outside 0-100 or
// non-integers are pruned. This is a defense in depth — the Zod schema
// already rejects them at the API boundary, but the DB shouldn't trust.
const created = await providersDb.createProviderConnection({
provider: "codex",
authType: "apikey",
name: "Codex Sanitize",
apiKey: "sk-san",
quotaWindowThresholds: { window5h: 95, bogus: 999, fractional: 1.5 },
});
assert.deepEqual(created.quotaWindowThresholds, { window5h: 95 });
});
test("updateProviderConnectionSchema accepts a valid window map", () => {

View File

@@ -1,135 +0,0 @@
import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
getProviderErrorRuleMatch,
setOperatorProviderErrorRules,
resolveRuleMatchBody,
honorsRuleLockScope,
type OperatorProviderErrorRule,
} from "../../open-sse/config/providerErrorRules.ts";
describe("operator error rules", () => {
beforeEach(() => {
// Isolate each test from the settings-backed cache.
setOperatorProviderErrorRules(undefined);
});
it("operator rule overrides the catalog registry for a provider", () => {
const op: Record<string, OperatorProviderErrorRule[]> = {
nvidia: [{ status: 404, match: "Not found for account", scope: "model", cooldownMs: 1000 }],
};
const m = getProviderErrorRuleMatch("nvidia", 404, null, "Not found for account id 123", op);
assert.ok(m, "operator rule should match");
assert.equal(m.scope, "model");
assert.equal(m.cooldownMs, 1000);
});
it("operator rule wins even when a catalog rule would also match", () => {
const op: Record<string, OperatorProviderErrorRule[]> = {
openrouter: [{ status: 402, match: "credits exhausted", scope: "model" }],
};
const m = getProviderErrorRuleMatch("openrouter", 402, null, "credits exhausted on key", op);
assert.ok(m);
// Catalog rule for openrouter/402 uses scope "connection"; the operator
// override must take precedence.
assert.equal(m.scope, "model");
});
it("operator can reclassify a 401 before the global permanent rule", () => {
const op: Record<string, OperatorProviderErrorRule[]> = {
acme: [
{ status: 401, match: "transient quota", scope: "connection", reason: "quota_exhausted" },
],
};
const m = getProviderErrorRuleMatch("acme", 401, null, "transient quota — retry shortly", op);
assert.ok(m);
assert.equal(m.scope, "connection");
assert.equal(m.reason, "quota_exhausted");
});
it("unknown provider with no operator rule returns null (no throw)", () => {
const m = getProviderErrorRuleMatch("unknown-provider", 402, null, "anything");
assert.equal(m, null);
});
it("substring match is case-insensitive", () => {
const op: Record<string, OperatorProviderErrorRule[]> = {
nvidia: [{ status: 404, match: "NOT FOUND", scope: "model" }],
};
const m = getProviderErrorRuleMatch("nvidia", 404, null, "Body says Not Found Here", op);
assert.ok(m);
assert.equal(m.scope, "model");
});
it("status must match before the substring is considered", () => {
const op: Record<string, OperatorProviderErrorRule[]> = {
nvidia: [{ status: 404, match: "not found", scope: "model" }],
};
// 500 with the same body text must NOT match a 404 rule.
const m = getProviderErrorRuleMatch("nvidia", 500, null, "not found for account", op);
assert.equal(m, null);
});
it("without an operator override the catalog registry is intact", () => {
const m = getProviderErrorRuleMatch("openrouter", 402, null, "credits exhausted on key");
assert.ok(m);
assert.equal(m.scope, "connection");
assert.equal(m.cooldownMs, 2 * 60 * 1000);
});
it("reads the settings-backed cache via setOperatorProviderErrorRules", () => {
setOperatorProviderErrorRules({
nvidia: [{ status: 404, match: "Not found", scope: "model" }],
});
const m = getProviderErrorRuleMatch("nvidia", 404, null, "Not found for account");
assert.ok(m);
assert.equal(m.scope, "model");
// Provider key lookup is case-insensitive.
const m2 = getProviderErrorRuleMatch("NVIDIA", 404, null, "Not found here");
assert.ok(m2);
assert.equal(m2.scope, "model");
});
// Regression coverage for #11104's original gap: an operator rule for any
// provider outside the built-in FULL_TEXT_RULE_PROVIDERS/
// HONORS_RULE_LOCK_SCOPE_PROVIDERS allowlists was silently text-blind (only
// {code,type} reached the matcher) and had its declared scope dropped by the
// persistence layer. Declaring an operator rule for a provider must be
// sufficient by itself — no separate allowlist entry required.
describe("operator rule bypasses the built-in allowlists", () => {
it("resolveRuleMatchBody hands the full error text once an operator rule exists for the provider", () => {
setOperatorProviderErrorRules({
acme: [{ status: 404, match: "model withdrawn", scope: "model" }],
});
const body = resolveRuleMatchBody("acme", { code: "not_found" }, "Model withdrawn upstream");
assert.equal(body, "Model withdrawn upstream");
});
it("resolveRuleMatchBody keeps returning the structured error for a provider with no operator rule", () => {
const body = resolveRuleMatchBody("acme", { code: "not_found" }, "Model withdrawn upstream");
assert.deepEqual(body, { code: "not_found" });
});
it("honorsRuleLockScope is true once an operator rule exists for the provider", () => {
assert.equal(honorsRuleLockScope("acme"), false);
setOperatorProviderErrorRules({
acme: [{ status: 404, match: "model withdrawn", scope: "model" }],
});
assert.equal(honorsRuleLockScope("acme"), true);
});
it("an operator rule for a non-allowlisted provider matches on raw body text end to end", () => {
setOperatorProviderErrorRules({
acme: [{ status: 404, match: "model withdrawn", scope: "model" }],
});
const body = resolveRuleMatchBody(
"acme",
{ code: "not_found" },
"Error: model withdrawn upstream"
);
const m = getProviderErrorRuleMatch("acme", 404, null, body);
assert.ok(m, "operator rule should match once resolveRuleMatchBody hands it the raw text");
assert.equal(m.scope, "model");
});
});
});

View File

@@ -5,19 +5,17 @@ const { createProviderSchema, providersBatchTestSchema } =
await import("../../src/shared/validation/schemas.ts");
const { providerAllowsOptionalApiKey } = await import("../../src/shared/constants/providers.ts");
// #11117: Pollinations no longer serves anonymous requests (401 without a key),
// so it left EXPLICIT_OPTIONAL_APIKEY_PROVIDER_IDS — key is now required.
test("Pollinations requires an API key", () => {
assert.equal(providerAllowsOptionalApiKey("pollinations"), false);
test("Pollinations is treated as a keyless-capable provider", () => {
assert.equal(providerAllowsOptionalApiKey("pollinations"), true);
});
test("createProviderSchema rejects Pollinations without apiKey", () => {
test("createProviderSchema allows Pollinations without apiKey", () => {
const result = createProviderSchema.safeParse({
provider: "pollinations",
name: "Pollinations",
});
assert.equal(result.success, false);
assert.equal(result.success, true);
});
test("providersBatchTestSchema accepts cloud-agent batch mode", () => {

View File

@@ -1,42 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { updateProviderConnectionSchema } from "@/shared/validation/schemas/provider";
test("PATCH rateLimitOverrides {rpm:\"60\"} coerces to a valid number", () => {
const r = updateProviderConnectionSchema.safeParse({ rateLimitOverrides: { rpm: "60" } });
assert.equal(r.success, true);
});
test("unknown key in rateLimitOverrides is rejected (no silent drop)", () => {
const r = updateProviderConnectionSchema.safeParse({ rateLimitOverrides: { rpm: 10, foo: 1 } });
assert.equal(r.success, false);
const flaggedFoo = r.error!.issues.some(
(i) => i.path.includes("foo") || (i as { keys?: string[] }).keys?.includes("foo") || i.message.includes("foo")
);
assert.ok(
flaggedFoo,
`expected an issue flagging "foo", got: ${JSON.stringify(r.error!.issues)}`
);
});
test("quotaWindowThresholds key longer than 64 chars is rejected", () => {
const r = updateProviderConnectionSchema.safeParse({
quotaWindowThresholds: { ["a".repeat(65)]: 50 },
});
assert.equal(r.success, false);
});
test("empty string rate limit value is rejected (coerce \"\"→0 trap)", () => {
const r = updateProviderConnectionSchema.safeParse({ rateLimitOverrides: { rpm: "" } });
assert.equal(r.success, false);
});
test("non-numeric rate limit value is rejected", () => {
const r = updateProviderConnectionSchema.safeParse({ rateLimitOverrides: { rpm: "60abc" } });
assert.equal(r.success, false);
});
test("quotaWindowThresholds value outside 0-100 is rejected", () => {
const r = updateProviderConnectionSchema.safeParse({ quotaWindowThresholds: { win: 101 } });
assert.equal(r.success, false);
});

View File

@@ -664,7 +664,6 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
{
type: "reasoning",
content: [{ type: "reasoning_text", text: "Cached Chat continuation reasoning" }],
summary: [],
}
);
});

View File

@@ -1,110 +0,0 @@
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { BaseExecutor } from "../../open-sse/executors/base.ts";
import {
getLearnedReasoningEffort,
recordLearnedReasoningEffort,
__test_resetLearnedReasoningEffortCaps,
} from "../../open-sse/services/learnedReasoningEffortCaps.ts";
const OVH_422_BODY = JSON.stringify({
error: {
message:
"Failed to deserialize the JSON body into the target type: reasoning_effort: " +
"unknown variant `xhigh`, expected one of `none`, `high`, `medium`, `low`, `minimal`",
},
});
// Passthrough executor: returns the body unchanged so we assert on exactly what
// base.ts sends upstream.
class SimpleExecutor extends BaseExecutor {
constructor() {
super("openai-compatible-chat-eaff6869", {
baseUrls: ["https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions"],
});
}
async transformRequest(_model: string, body: Record<string, unknown>) {
return { ...body };
}
}
beforeEach(() => {
__test_resetLearnedReasoningEffortCaps();
});
after(() => {
__test_resetLearnedReasoningEffortCaps();
});
test("422 'unknown variant xhigh, expected one of ...' clamps reasoning_effort and retries once", async () => {
const executor = new SimpleExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
const body = JSON.parse(String(init.body));
capturedBodies.push(body);
if (capturedBodies.length === 1) {
return new Response(OVH_422_BODY, {
status: 422,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
try {
const result = await executor.execute({
model: "qwen3-coder-30b-a3b-instruct",
body: { reasoning_effort: "xhigh" },
stream: false,
credentials: {},
});
assert.equal(capturedBodies.length, 2);
assert.equal(capturedBodies[0].reasoning_effort, "xhigh");
assert.equal(capturedBodies[1].reasoning_effort, "high");
assert.equal(
getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct"),
"high"
);
assert.equal(result.response.status, 200);
} finally {
globalThis.fetch = originalFetch;
}
});
test("a second request for the same provider+model sends the learned value on the first try", async () => {
const executor = new SimpleExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
const body = JSON.parse(String(init.body));
capturedBodies.push(body);
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
try {
recordLearnedReasoningEffort(
"openai-compatible-chat-eaff6869",
"qwen3-coder-30b-a3b-instruct",
["none", "high", "medium", "low", "minimal"]
);
await executor.execute({
model: "qwen3-coder-30b-a3b-instruct",
body: { reasoning_effort: "xhigh" },
stream: false,
credentials: {},
});
assert.equal(capturedBodies.length, 1);
assert.equal(capturedBodies[0].reasoning_effort, "high");
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -1,91 +0,0 @@
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { sanitizeReasoningEffortForProvider } from "../../open-sse/executors/base/reasoningEffort.ts";
import {
recordLearnedReasoningEffort,
__test_resetLearnedReasoningEffortCaps,
} from "../../open-sse/services/learnedReasoningEffortCaps.ts";
beforeEach(() => {
__test_resetLearnedReasoningEffortCaps();
});
after(() => {
__test_resetLearnedReasoningEffortCaps();
});
test("unregistered/custom provider+model: no learned cap yet sends xhigh unchanged", () => {
const body = { reasoning_effort: "xhigh" };
const result = sanitizeReasoningEffortForProvider(
body,
"openai-compatible-chat-eaff6869",
"qwen3-coder-30b-a3b-instruct"
) as { reasoning_effort: string };
assert.equal(result.reasoning_effort, "xhigh");
});
test("unregistered/custom provider+model: a learned cap clamps xhigh down to it", () => {
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct", [
"none",
"high",
"medium",
"low",
"minimal",
]);
const body = { reasoning_effort: "xhigh" };
const result = sanitizeReasoningEffortForProvider(
body,
"openai-compatible-chat-eaff6869",
"qwen3-coder-30b-a3b-instruct"
) as { reasoning_effort: string };
assert.equal(result.reasoning_effort, "high");
});
test("learned cap only clamps when the requested effort is above it", () => {
recordLearnedReasoningEffort("acme", "model-x", ["none", "low", "medium"]);
const body = { reasoning_effort: "low" };
const result = sanitizeReasoningEffortForProvider(body, "acme", "model-x") as {
reasoning_effort: string;
};
assert.equal(result.reasoning_effort, "low");
});
test("registry says supportsXHighEffort:false (and no supportsMax path) with a learned cap below 'high': uses the learned cap, not the hardcoded 'high'", () => {
// claude-haiku-4-5 is registered with supportsXHighEffort:false
// (open-sse/config/providers/registry/claude/index.ts) and its family is
// excluded from supportsClaudeMaxEffort (CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS
// in providerModels.ts), so it reaches the hardcoded-"high" line today —
// a real registry-covered case. Teach a lower cap and confirm it wins.
recordLearnedReasoningEffort("claude", "claude-haiku-4-5-20251001", ["none", "low", "medium"]);
const body = { reasoning_effort: "xhigh" };
const result = sanitizeReasoningEffortForProvider(
body,
"claude",
"claude-haiku-4-5-20251001"
) as {
reasoning_effort: string;
};
assert.equal(result.reasoning_effort, "medium");
});
test("registry says supportsXHighEffort:false with no learned cap: falls back to hardcoded 'high' (unchanged behavior)", () => {
const body = { reasoning_effort: "xhigh" };
const result = sanitizeReasoningEffortForProvider(
body,
"claude",
"claude-haiku-4-5-20251001"
) as {
reasoning_effort: string;
};
assert.equal(result.reasoning_effort, "high");
});
test("deepseek's non-ordinal max<->xhigh translation is untouched by the learned-cap catch-all", () => {
recordLearnedReasoningEffort("deepseek", "deepseek-v4", ["none", "low"]);
const body = { reasoning_effort: "xhigh" };
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4") as {
reasoning_effort: string;
};
// deepseek's special case returns early — xhigh -> max, never reaches the catch-all.
assert.equal(result.reasoning_effort, "max");
});

View File

@@ -1,145 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
const { applyReasoningInputPolicy } =
await import("../../open-sse/services/reasoningInputPolicy.ts");
test("#11108 applyReasoningInputPolicy defaults summary on a kept opaque reasoning item", () => {
const body: Record<string, unknown> = {
input: [
{
type: "reasoning",
id: "rs_example",
encrypted_content: "opaque-blob",
},
],
};
applyReasoningInputPolicy(body, "responses", {
provider: "opencode",
preserveEncryptedReasoning: true,
});
const input = body.input as Record<string, unknown>[];
assert.equal(input.length, 1);
assert.deepEqual(input[0].summary, []);
});
test("#11108 applyReasoningInputPolicy preserves an existing summary on a kept reasoning item", () => {
const body: Record<string, unknown> = {
input: [
{
type: "reasoning",
id: "rs_example",
encrypted_content: "opaque-blob",
summary: [{ type: "summary_text", text: "Planning." }],
},
],
};
applyReasoningInputPolicy(body, "responses", {
provider: "opencode",
preserveEncryptedReasoning: true,
});
const input = body.input as Record<string, unknown>[];
assert.deepEqual(input[0].summary, [{ type: "summary_text", text: "Planning." }]);
});
test("#11108 applyReasoningInputPolicy defaults summary on an opaque item surviving incompatible-drop", () => {
// Mixed item (plaintext + opaque) on an opaque-only transport is incompatible;
// dropIncompatibleResponsesReasoning() strips the plaintext content but keeps
// the opaque item alive — it must still get a default `summary`.
const body: Record<string, unknown> = {
input: [
{
type: "reasoning",
id: "rs_mixed",
content: [{ type: "reasoning_text", text: "inspect first" }],
encrypted_content: "opaque-blob",
},
],
};
const result = applyReasoningInputPolicy(body, "responses", {
provider: "codex",
onIncompatibleReasoning: "drop",
});
assert.equal(result.incompatibleReasoning, false);
const input = body.input as Record<string, unknown>[];
assert.equal(input.length, 1);
assert.equal(input[0].content, undefined);
assert.equal(input[0].encrypted_content, "opaque-blob");
assert.deepEqual(input[0].summary, []);
});
test("#11108 applyReasoningInputPolicy strips a non-string id on a kept opaque reasoning item", () => {
// Same gap class as the summary fix above: opencode/zen also omits `id`
// entirely (surfaced by the client as `id: null`) on opaque-only reasoning
// items instead of a `rs_...` string. Replaying that shape verbatim trips
// strict Responses-API validators with "Expected 'id' to be a string."
const body: Record<string, unknown> = {
input: [
{
type: "reasoning",
id: null,
encrypted_content: "opaque-blob",
},
],
};
applyReasoningInputPolicy(body, "responses", {
provider: "opencode",
preserveEncryptedReasoning: true,
});
const input = body.input as Record<string, unknown>[];
assert.equal(input.length, 1);
assert.equal("id" in input[0], false);
});
test("#11108 applyReasoningInputPolicy strips a non-string id on a non-reasoning item (function_call)", () => {
// Same gap class, generic branch: any non-"reasoning" input item (function_call,
// message, ...) only stripped `id` when it was already a valid string, so a
// malformed `id` (e.g. `null`, mirroring the opencode/zen omission pattern)
// on a function_call item survived replay untouched.
const body: Record<string, unknown> = {
input: [
{
type: "function_call",
id: null,
call_id: "call_abc",
name: "bash",
arguments: "{}",
},
],
};
applyReasoningInputPolicy(body, "responses", { provider: "opencode" });
const input = body.input as Record<string, unknown>[];
assert.equal(input.length, 1);
assert.equal("id" in input[0], false);
assert.equal(input[0].call_id, "call_abc");
});
test("#11108 applyReasoningInputPolicy preserves a valid string id on a kept opaque reasoning item", () => {
const body: Record<string, unknown> = {
input: [
{
type: "reasoning",
id: "rs_example",
encrypted_content: "opaque-blob",
},
],
};
applyReasoningInputPolicy(body, "responses", {
provider: "opencode",
preserveEncryptedReasoning: true,
});
const input = body.input as Record<string, unknown>[];
assert.equal(input[0].id, "rs_example");
});

View File

@@ -1,7 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
test("hackclub provider is removed from REGISTRY", () => {
assert.equal("hackclub" in REGISTRY, false);
});

View File

@@ -73,23 +73,6 @@ test("keeps valid server reasoning item ids", () => {
assert.equal(result[0].id, "rs_123");
});
test("strips a non-string reasoning item id instead of passing it through (#11108)", () => {
// Same gap class fixed in reasoningInputPolicy.ts: some upstreams (e.g.
// opencode/zen) send `id: null` instead of omitting it. The previous
// `typeof record.id !== "string"` guard returned the record unchanged in
// that case, letting a malformed id reach a strict Responses-API upstream.
const items = [
{
id: null,
type: "reasoning",
summary: [{ type: "summary_text", text: "cached reasoning" }],
},
];
const result = sanitizeResponsesInputItems(items) as Array<Record<string, unknown>>;
assert.equal("id" in result[0], false);
assert.equal(result[0].type, "reasoning");
});
test("normalizes user image_url content parts to input_image", () => {
const items = [
{

View File

@@ -1,294 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
const { openaiResponsesToOpenAIResponse } =
await import("../../open-sse/translator/response/openai-responses.ts");
// Issue: 2+ `function_call` items opened (response.output_item.added) before any of
// them closes (response.output_item.done) — a genuine parallel tool-call dispatch —
// causes `state.toolCallIndex` (only incremented in the `.done` handler) to stay at 0
// for every "added" header chunk. Clients that key their tool-call accumulator by
// `delta.tool_calls[].index` (e.g. opencode's github-copilot chat-language-model
// stream parser) then see the *first* `.done` argument chunk at index 1/2 with no
// prior header and no `id`, and throw "Expected 'id' to be a string."
test("Responses -> OpenAI: parallel function_call items get distinct index+id on the added header", () => {
const state = {};
const added0 = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_0", name: "task" },
},
state
);
const added1 = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_1", name: "task" },
},
state
);
const added2 = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_2", name: "task" },
},
state
);
const headers = [added0, added1, added2].map((r) => r.choices[0].delta.tool_calls[0]);
assert.deepEqual(
headers.map((h) => h.index),
[0, 1, 2],
"each parallel tool call must get its own header index, not all 0"
);
assert.deepEqual(
headers.map((h) => h.id),
["call_0", "call_1", "call_2"]
);
const done0 = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_0", name: "task", arguments: '{"i":0}' },
},
state
);
const done1 = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_1", name: "task", arguments: '{"i":1}' },
},
state
);
const done2 = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_2", name: "task", arguments: '{"i":2}' },
},
state
);
assert.deepEqual(
[done0, done1, done2].map((r) => r.choices[0].delta.tool_calls[0].index),
[0, 1, 2],
"argument chunks must reuse the SAME index assigned at .added time for each call_id"
);
});
test("Responses -> OpenAI: parallel calls closed out of order keep their own index", () => {
const state = {};
for (const callId of ["call_a", "call_b", "call_c"]) {
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: callId, name: "task" },
},
state
);
}
// Close in reverse order: c, then a, then b.
const doneC = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_c", name: "task", arguments: "{}" },
},
state
);
const doneA = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_a", name: "task", arguments: "{}" },
},
state
);
const doneB = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_b", name: "task", arguments: "{}" },
},
state
);
assert.equal(doneC.choices[0].delta.tool_calls[0].index, 2);
assert.equal(doneA.choices[0].delta.tool_calls[0].index, 0);
assert.equal(doneB.choices[0].delta.tool_calls[0].index, 1);
});
test("Responses -> OpenAI: argument deltas interleaved across 2 parallel calls do not get glued together", () => {
const state = {};
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_x", name: "Read", id: "fc_call_x" },
},
state
);
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_y", name: "Read", id: "fc_call_y" },
},
state
);
// Interleave argument deltas by item_id — x, y, x, y — before either closes.
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", item_id: "fc_call_x", delta: '{"filePath"' },
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", item_id: "fc_call_y", delta: '{"filePath"' },
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", item_id: "fc_call_x", delta: ':"/a.txt"}' },
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", item_id: "fc_call_y", delta: ':"/b.txt"}' },
state
);
const doneX = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_x", name: "Read" },
},
state
);
const doneY = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_y", name: "Read" },
},
state
);
assert.equal(doneX.choices[0].delta.tool_calls[0].function.arguments, '{"filePath":"/a.txt"}');
assert.equal(doneY.choices[0].delta.tool_calls[0].function.arguments, '{"filePath":"/b.txt"}');
});
test("Responses -> OpenAI: a deferred (nameless) call that never resolves a name never consumes an index", () => {
const state = {};
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_deferred", name: "" },
},
state
);
const done = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_deferred", name: " " },
},
state
);
assert.equal(done, null);
assert.equal(state.toolCallIndex, 0);
});
test("Responses -> OpenAI: argument deltas interleaved across 2 parallel calls resolve by output_index when the upstream omits item_id", () => {
const state = {};
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
output_index: 0,
item: { type: "function_call", call_id: "call_p", name: "Read" },
},
state
);
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
output_index: 1,
item: { type: "function_call", call_id: "call_q", name: "Read" },
},
state
);
// No item_id on any of these deltas — only output_index, which the Responses API
// guarantees on every streamed event regardless of whether item_id is also sent.
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", output_index: 0, delta: '{"filePath"' },
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", output_index: 1, delta: '{"filePath"' },
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", output_index: 0, delta: ':"/p.txt"}' },
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", output_index: 1, delta: ':"/q.txt"}' },
state
);
const doneP = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_p", name: "Read" },
},
state
);
const doneQ = openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_q", name: "Read" },
},
state
);
assert.equal(doneP.choices[0].delta.tool_calls[0].function.arguments, '{"filePath":"/p.txt"}');
assert.equal(doneQ.choices[0].delta.tool_calls[0].function.arguments, '{"filePath":"/q.txt"}');
});
test("Responses -> OpenAI: 2 parallel Agent calls still open at stream end each get their own flush chunk", () => {
const state = {};
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_agent0", name: "Agent" },
},
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", output_index: 0, delta: '{"task":"a"}' },
state
);
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.added",
item: { type: "function_call", call_id: "call_agent1", name: "Agent" },
},
state
);
openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", output_index: 1, delta: '{"task":"b"}' },
state
);
// Stream ends (chunk === null) before either call's output_item.done arrives.
const flushed = openaiResponsesToOpenAIResponse(null, state);
assert.ok(Array.isArray(flushed));
const argChunks = flushed.filter((c) => c.choices[0].delta.tool_calls);
assert.deepEqual(
argChunks.map((c) => c.choices[0].delta.tool_calls[0].index).sort(),
[0, 1],
"each still-open parallel call must get its own flush chunk, at its own index"
);
const finalChunk = flushed[flushed.length - 1];
assert.equal(finalChunk.choices[0].finish_reason, "tool_calls");
});

View File

@@ -1,11 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { getAllSearchProviders } from "../../open-sse/config/searchRegistry.ts";
test("getAllSearchProviders filters out blocked providers", () => {
const all = getAllSearchProviders();
assert.ok(all.some((p) => p.id === "serper-search"));
const filtered = getAllSearchProviders(["serper-search"]);
assert.equal(filtered.some((p) => p.id === "serper-search"), false);
});

View File

@@ -2,7 +2,6 @@ import test from "node:test";
import assert from "node:assert/strict";
const collector = await import("../../open-sse/utils/streamPayloadCollector.ts");
import { splitConcatenatedToolCallArguments } from "../../open-sse/utils/streamPayloadCollector.ts";
test("compactStructuredStreamPayload returns null for null input", () => {
assert.equal(collector.compactStructuredStreamPayload(null), null);
@@ -414,33 +413,3 @@ test("#9315: getSummary() returns undefined when no format was configured (unaff
c.push({ choices: [{ index: 0, delta: { content: "hi" } }] });
assert.equal(c.getSummary(), undefined);
});
test("splitConcatenatedToolCallArguments — two back-to-back JSON objects", () => {
const a = JSON.stringify({ tool: "x", args: "1" });
const b = JSON.stringify({ tool: "y", args: "2" });
const out = splitConcatenatedToolCallArguments(a + b);
assert.deepEqual(out, [a, b]); // >=2 valid values -> split (array of parts)
});
test("splitConcatenatedToolCallArguments — nested object + escaped quotes stay single JSON", () => {
const a = JSON.stringify({ a: 'he said "hi"', b: { c: 1 } });
const single = a; // a is ONE valid JSON object -> no split
const out = splitConcatenatedToolCallArguments(single);
assert.equal(out, null); // single valid JSON -> untouched (null)
});
test("splitConcatenatedToolCallArguments — braces/quotes inside strings exercise escaped scanner", () => {
// Two valid JSON values whose string bodies contain braces and escaped quotes.
// Concatenated they reach the inString/escaped state machine (not the JSON.parse
// fast path), so this covers the case the owner asked about.
const a = JSON.stringify({ cmd: 'echo "}{" ; x' });
const b = JSON.stringify({ cmd: "{[not json]}" });
const out = splitConcatenatedToolCallArguments(a + b);
assert.deepEqual(out, [a, b]); // >=2 valid values -> split into parts
});
test("splitConcatenatedToolCallArguments — top-level array is single value", () => {
const arr = JSON.stringify([{ tool: "x" }, { tool: "y" }]);
const out = splitConcatenatedToolCallArguments(arr);
assert.equal(out, null); // one value boundary (array) -> not split
});

View File

@@ -1,178 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
createRecoverableStream,
TruncatedStreamError,
scanOpenAiSseText,
} from "../../open-sse/services/streamRecovery.ts";
const enc = new TextEncoder();
// Deliver the SSE chunk on the first read, then error on the second read so the
// holdback window has committed (post-commit truncation) before the cut.
function makeStream(sse: string): ReadableStream<Uint8Array> {
let n = 0;
return new ReadableStream<Uint8Array>({
pull(c) {
n += 1;
if (n === 1) {
c.enqueue(enc.encode(sse));
return;
}
c.error(new TruncatedStreamError());
},
});
}
// A clock that jumps past HOLDBACK_MS on the second read so the very first pushed
// chunk commits the holdback window immediately (post-commit truncation path).
function jumpingClock(): () => number {
let t = 0;
return () => (t += 1000);
}
describe("scanOpenAiSseText: terminal vs in-flight tool call", () => {
it("tool_calls without finish_reason → inFlight true, terminal false", () => {
const sse =
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_1","function":{"name":"lookup"}}]}}]}\n\n';
const r = scanOpenAiSseText(sse);
assert.equal(r.sawToolCall, true);
assert.equal(r.sawToolCallInFlight, true);
assert.equal(r.terminal, false);
});
it("complete tool_calls + finish_reason + [DONE] → terminal true, inFlight false", () => {
const sse =
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_1","function":{"name":"lookup","arguments":"{}"}}]}}]}\n' +
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}\n' +
"data: [DONE]\n\n";
const r = scanOpenAiSseText(sse);
assert.equal(r.sawToolCall, true);
assert.equal(r.terminal, true);
assert.equal(r.sawToolCallInFlight, false);
});
it("plain text → no tool call", () => {
const sse = 'data: {"choices":[{"index":0,"delta":{"content":"hello"}}]}\n\n';
const r = scanOpenAiSseText(sse);
assert.equal(r.sawToolCall, false);
assert.equal(r.sawToolCallInFlight, false);
assert.equal(r.terminal, false);
});
it("complete tool_calls WITHOUT [DONE] → terminal false, inFlight false (the actual fix)", () => {
// This is the case the original plan promised to unblock: the tool call itself is
// done (finish_reason: "tool_calls"), but the overall stream/turn has not sent its
// own terminal marker yet — a truncation right here is recoverable.
const sse =
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_1","function":{"name":"lookup","arguments":"{}"}}]}}]}\n' +
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}\n\n';
const r = scanOpenAiSseText(sse);
assert.equal(r.sawToolCall, true);
assert.equal(r.sawToolCallInFlight, false);
assert.equal(r.terminal, false);
});
});
describe("stream recovery does not duplicate an in-flight tool call", () => {
it("truncation with an in-flight tool call → no continuation", async () => {
let continued = false;
const sse =
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"c1","function":{"name":"f"}}]}}]}\n\n';
const wrapped = createRecoverableStream(makeStream(sse), async () => null, {
finalize: () => {},
now: jumpingClock(),
continueStream: async () => {
continued = true;
return null;
},
});
const reader = wrapped.getReader();
try {
for (;;) {
const r = await reader.read();
if (r.done) break;
}
} catch {
// the in-flight tool call makes the stream close without continuing
}
assert.equal(continued, false);
});
it("truncation right after a completed tool call → continuation attempted (the real 91% gain)", async () => {
// Text was emitted, THEN the tool call completed (finish_reason: "tool_calls"), THEN
// the connection drops before a [DONE]/other terminal marker. Before this fix, the
// blunt `emittedToolCall` guard blocked recovery here even though the call itself is
// done and only trailing prose was lost — this is the exact case the plan promised
// to unblock and the pre-fix table proved was a no-op.
let continued = false;
const sse =
'data: {"choices":[{"index":0,"delta":{"content":"Let me check that. "}}]}\n' +
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"c1","function":{"name":"f","arguments":"{}"}}]}}]}\n' +
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}\n\n';
const wrapped = createRecoverableStream(makeStream(sse), async () => null, {
finalize: () => {},
now: jumpingClock(),
continueStream: async () => {
continued = true;
return null;
},
});
const reader = wrapped.getReader();
try {
for (;;) {
const r = await reader.read();
if (r.done) break;
}
} catch {
// no-op
}
assert.equal(continued, true);
});
it("truncation of plain text → continuation attempted", async () => {
let continued = false;
const sse = 'data: {"choices":[{"index":0,"delta":{"content":"hello "}}]}\n\n';
const wrapped = createRecoverableStream(makeStream(sse), async () => null, {
finalize: () => {},
now: jumpingClock(),
continueStream: async () => {
continued = true;
return null;
},
});
const reader = wrapped.getReader();
try {
for (;;) {
const r = await reader.read();
if (r.done) break;
}
} catch {
// no-op
}
assert.equal(continued, true);
});
it("naive removal of the tool-call guard would duplicate a partial tool call", () => {
// The blunt `sawToolCall` flag is true for BOTH a complete tool call and a
// partial (in-flight) one. The new `sawToolCallInFlight` flag is the only
// signal that tells them apart: a naive guard keyed on `sawToolCall` would
// block the complete call AND let the partial one through to the
// continuation, where trimContinuationOverlap (text-only) cannot de-duplicate
// the replayed tool_calls arguments.
const ssePartial =
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_1","function":{"name":"lookup","arguments":"{\\"q\\""}}]}}]}\n\n';
const sseFull =
'data: {"choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_1","function":{"name":"lookup","arguments":"{\\"q\\":\\"x\\"}"}}]}}]}\n' +
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}\n\n';
const scanPartial = scanOpenAiSseText(ssePartial);
const scanFull = scanOpenAiSseText(sseFull);
// The blunt flag cannot distinguish them.
assert.equal(scanPartial.sawToolCall, true);
assert.equal(scanFull.sawToolCall, true);
// The in-flight flag can — and that is what keeps canContinue false only for
// the partial tool call, so the continuation never replays it.
assert.equal(scanPartial.sawToolCallInFlight, true);
assert.equal(scanFull.sawToolCallInFlight, false);
});
});

View File

@@ -274,11 +274,7 @@ test("explicit custom target opt-in remains an opaque transport override", () =>
const result = applyReasoningInputPolicy(body, "responses", { preserveEncryptedReasoning: true });
assert.equal(result.incompatibleReasoning, false);
// #11108: a kept opaque item defaults `summary` when the source omitted it —
// some upstreams reject `input[]` reasoning items missing the field entirely.
assert.deepEqual(body.input, [
{ type: "reasoning", encrypted_content: "encrypted-blob", summary: [] },
]);
assert.deepEqual(body.input, [{ type: "reasoning", encrypted_content: "encrypted-blob" }]);
});
test("preserved opaque reasoning remains redacted from log copies", () => {

View File

@@ -475,7 +475,6 @@ test("Chat -> Responses defaults unannotated targets to plaintext reasoning", ()
{
type: "reasoning",
content: [{ type: "reasoning_text", text: "Inspect the repository first" }],
summary: [],
},
{
type: "function_call",
@@ -514,7 +513,6 @@ test("Chat -> DeepSeek Responses accepts the plaintext reasoning alias", () => {
assert.deepEqual(result.input[0], {
type: "reasoning",
content: [{ type: "reasoning_text", text: "Alias plaintext reasoning" }],
summary: [],
});
});

View File

@@ -1,6 +1,5 @@
import test from "node:test";
import assert from "node:assert/strict";
import { WEBHOOK_EVENT_VALUES } from "../../src/lib/webhooks/eventDescriptions.ts";
const { buildDiscordPayload } = await import("../../src/lib/webhooks/integrations/discord.ts");
@@ -15,7 +14,15 @@ test("buildDiscordPayload — request.failed produces embed with model", () => {
});
test("buildDiscordPayload — all WEBHOOK_EVENTS return object with content or embeds", () => {
const events = WEBHOOK_EVENT_VALUES;
const events = [
"request.completed",
"request.failed",
"provider.error",
"provider.recovered",
"quota.exceeded",
"combo.switched",
"test.ping",
] as const;
for (const event of events) {
const payload = buildDiscordPayload(event, {});
assert.ok(
@@ -26,7 +33,7 @@ test("buildDiscordPayload — all WEBHOOK_EVENTS return object with content or e
});
test("buildDiscordPayload — embeds have title and color fields", () => {
const payload = buildDiscordPayload("request.failed", { provider: "openai" });
const payload = buildDiscordPayload("provider.error", { provider: "openai" });
assert.ok(Array.isArray(payload.embeds) && payload.embeds.length > 0, "should have embeds");
const embed = payload.embeds![0];
assert.ok(typeof embed.title === "string" && embed.title.length > 0, "embed must have title");

View File

@@ -1,6 +1,5 @@
import test from "node:test";
import assert from "node:assert/strict";
import { WEBHOOK_EVENT_VALUES } from "../../src/lib/webhooks/eventDescriptions.ts";
const { buildSlackPayload } = await import("../../src/lib/webhooks/integrations/slack.ts");
@@ -31,8 +30,8 @@ test("buildSlackPayload — test.ping produces a ping/test message", () => {
);
});
test("buildSlackPayload — request.failed includes provider context", () => {
const payload = buildSlackPayload("request.failed", { provider: "openai" });
test("buildSlackPayload — provider.error includes provider context", () => {
const payload = buildSlackPayload("provider.error", { provider: "openai", model: "gpt-4" });
const combined = JSON.stringify(payload);
assert.ok(
combined.includes("Provider") ||
@@ -44,7 +43,15 @@ test("buildSlackPayload — request.failed includes provider context", () => {
});
test("buildSlackPayload — all WEBHOOK_EVENTS produce valid payloads with text field", () => {
const events = WEBHOOK_EVENT_VALUES;
const events = [
"request.completed",
"request.failed",
"provider.error",
"provider.recovered",
"quota.exceeded",
"combo.switched",
"test.ping",
] as const;
for (const event of events) {
const payload = buildSlackPayload(event, {});
assert.ok(

View File

@@ -1,6 +1,5 @@
import test from "node:test";
import assert from "node:assert/strict";
import { WEBHOOK_EVENT_VALUES } from "../../src/lib/webhooks/eventDescriptions.ts";
const { buildTelegramPayload, buildTelegramUrl } =
await import("../../src/lib/webhooks/integrations/telegram.ts");
@@ -78,7 +77,15 @@ test("buildTelegramPayload — chat_id matches provided value for groups", () =>
});
test("buildTelegramPayload — all WEBHOOK_EVENTS produce valid payloads with chat_id", () => {
const events = WEBHOOK_EVENT_VALUES;
const events = [
"request.completed",
"request.failed",
"provider.error",
"provider.recovered",
"quota.exceeded",
"combo.switched",
"test.ping",
] as const;
for (const event of events) {
const payload = buildTelegramPayload(event, {}, "99999");
assert.equal(payload.chat_id, "99999");

View File

@@ -30,16 +30,4 @@ describe("webhook catalogue", () => {
const { notifyWebhookEvent } = await import("../../src/lib/webhookDispatcher.ts");
assert.equal(typeof notifyWebhookEvent, "function");
});
it("every builder accepts every value in WEBHOOK_EVENT_VALUES without throwing", async () => {
const { buildDiscordPayload } = await import("../../src/lib/webhooks/integrations/discord.ts");
const { buildSlackPayload } = await import("../../src/lib/webhooks/integrations/slack.ts");
const { buildTelegramPayload } =
await import("../../src/lib/webhooks/integrations/telegram.ts");
for (const event of WEBHOOK_EVENT_VALUES) {
assert.doesNotThrow(() => buildDiscordPayload(event, {}));
assert.doesNotThrow(() => buildSlackPayload(event, {}));
assert.doesNotThrow(() => buildTelegramPayload(event, {}, "99999"));
}
});
});