mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
Compare commits
4 Commits
maint/cher
...
maint/cher
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcbaa436ba | ||
|
|
abcf6f8128 | ||
|
|
154f773a28 | ||
|
|
be583b5392 |
26
.gitignore
vendored
26
.gitignore
vendored
@@ -72,6 +72,7 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.devin-bridge.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
@@ -171,7 +172,6 @@ config/quality/test-impact-map.json
|
||||
# GitNexus local index
|
||||
.gitnexus
|
||||
.worktrees
|
||||
bin/omniroute.mjs
|
||||
|
||||
# Consistent with .dockerignore / .npmignore
|
||||
.omc/
|
||||
@@ -201,12 +201,17 @@ scripts/i18n/_pending-keys.json
|
||||
.codegraph/
|
||||
|
||||
# Fumadocs generated source
|
||||
.source/
|
||||
/.source/
|
||||
|
||||
# Temporary local worktrees used to build unpublished npm tarballs
|
||||
/.deploy-build-*/
|
||||
|
||||
# AI agent local settings and configs
|
||||
.agents/
|
||||
.antigravitycli/
|
||||
.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
|
||||
|
||||
# PR Reviews and local feedback files
|
||||
pr_reviews*.json
|
||||
@@ -233,7 +238,10 @@ omniroute.md
|
||||
|
||||
# mise configuration
|
||||
mise.toml
|
||||
_artifacts/ # release-green artifacts
|
||||
# release-green artifacts (.gitignore has no inline comments — a trailing
|
||||
# `# ...` becomes part of the pattern, so it must sit on its own line).
|
||||
# Already covered by /_*/ above; kept explicit for discoverability.
|
||||
_artifacts/
|
||||
.claude-flow/
|
||||
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
@@ -243,6 +251,8 @@ _artifacts/ # release-green artifacts
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
.artifacts/
|
||||
# Isolated Devin bridge workspaces, evidence, and test databases
|
||||
.sandbox/
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
@@ -250,8 +260,12 @@ tests/homolog/.auth/
|
||||
tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
docker-compose.yml.bak
|
||||
.playwright-cli/
|
||||
# Playwright screenshot/log output. Today every artifact happens to land inside
|
||||
# output/**/.playwright-cli/ (covered above), but anything written directly to
|
||||
# output/ would otherwise show up as untracked.
|
||||
/output/
|
||||
|
||||
# _tasks e um repo git SEPARADO (ver AGENTS.md). A linha _tasks/ (com barra) NAO
|
||||
# ignora um SYMLINK chamado _tasks; /_tasks (ancorado) cobre arquivo/symlink/dir na raiz
|
||||
# e impede que um git add -A recapture o symlink (incidente 2026-08-08).
|
||||
# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um
|
||||
# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08).
|
||||
/_tasks
|
||||
|
||||
@@ -408,13 +408,12 @@ export class CliproxyapiExecutor extends BaseExecutor {
|
||||
|
||||
input.log?.info?.("CPA", `CLIProxyAPI → ${url} (model: ${input.model}, shape: ${shape})`);
|
||||
|
||||
// _toolNameMap and _namespaceToolIdentityMap are in-memory channels to
|
||||
// chatCore for response-side tool name restoration; never send them over
|
||||
// the wire.
|
||||
// _toolNameMap is an in-memory channel to chatCore for response-side
|
||||
// tool name restoration; never send it over the wire.
|
||||
const wireBody =
|
||||
transformedBody && typeof transformedBody === "object"
|
||||
? JSON.stringify(transformedBody, (key, value) =>
|
||||
key === "_toolNameMap" || key === "_namespaceToolIdentityMap" ? undefined : value
|
||||
key === "_toolNameMap" ? undefined : value
|
||||
)
|
||||
: JSON.stringify(transformedBody);
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ import { stageTrace } from "./chatCore/stageTrace.ts";
|
||||
import { attachCompressionUsageReceiptAfterAnalytics as attachCompressionUsageReceiptAfterAnalyticsFor } from "./chatCore/compressionUsageReceipt.ts";
|
||||
import { prepareUpstreamBody } from "./chatCore/upstreamBody.ts";
|
||||
import { getQuotaScopeLabelForProvider } from "../services/antigravityQuotaFamily.ts";
|
||||
|
||||
import {
|
||||
getCallLogPipelineCaptureStreamChunks,
|
||||
getCallLogPipelineMaxSizeBytes,
|
||||
@@ -366,7 +367,9 @@ import {
|
||||
isTpmExhausted,
|
||||
isRpmExhausted,
|
||||
} from "../services/geminiRateLimitTracker.ts";
|
||||
|
||||
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
|
||||
/**
|
||||
* Core chat handler - shared between SSE and Worker
|
||||
* Returns { success, response, status, error } for caller to handle fallback
|
||||
@@ -386,8 +389,10 @@ import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
* @param {boolean} options.isCombo - Whether this request is from a combo
|
||||
* @param {string} options.connectionId - Connection ID for settings lookup
|
||||
*/
|
||||
|
||||
// extractSystemRoleMessages extracted to chatCore/claudeSystemRole.ts (#3501); re-exported above so
|
||||
// existing importers (e.g. tests/unit/system-role-extraction.test.ts) keep resolving it from here.
|
||||
|
||||
export async function handleChatCore({
|
||||
body,
|
||||
modelInfo,
|
||||
@@ -423,6 +428,7 @@ export async function handleChatCore({
|
||||
/* fail open */
|
||||
}
|
||||
}
|
||||
|
||||
// Per-request model-routing metadata (first extracted slice of the request-setup phase).
|
||||
const { apiFormat, customModelTargetFormat, requestedModel } = resolveChatCoreRequestSetup(
|
||||
modelInfo,
|
||||
@@ -436,6 +442,7 @@ export async function handleChatCore({
|
||||
// (not Math.random) purely to satisfy CodeQL js/insecure-randomness — this id
|
||||
// is a log-correlation token, not a security secret.
|
||||
const traceId = globalThis.crypto.randomUUID().slice(0, 6);
|
||||
|
||||
// Emit request.started event for real-time dashboard
|
||||
setImmediate(() => {
|
||||
emit("request.started", {
|
||||
@@ -519,6 +526,7 @@ export async function handleChatCore({
|
||||
`long-running goal mode enabled: readinessMax=${agentGoalPolicy.readinessMaxTimeoutMs}ms streamRecovery=${agentGoalPolicy.streamRecoveryEnabled}`
|
||||
);
|
||||
}
|
||||
|
||||
let effectiveServiceTier: EffectiveServiceTier = "standard";
|
||||
// Codex service-tier resolvers extracted to chatCore/serviceTier.ts (#3501); bind the per-request
|
||||
// provider/credentials once and delegate so the existing call sites stay byte-identical.
|
||||
@@ -547,6 +555,7 @@ export async function handleChatCore({
|
||||
})
|
||||
).catch(() => {});
|
||||
};
|
||||
|
||||
// Key-health updater extracted to chatCore/keyHealth.ts (#3501); bind the per-request log once
|
||||
// and delegate so the existing call sites stay byte-identical.
|
||||
const recordKeyHealthStatus = (
|
||||
@@ -554,9 +563,11 @@ export async function handleChatCore({
|
||||
creds: Record<string, unknown> | null | undefined,
|
||||
transport?: string
|
||||
): void => recordKeyHealthStatusFor(status, creds, log, transport);
|
||||
|
||||
const persistCodexQuotaState = async (headers: Record<string, string> | null, status = 0) => {
|
||||
const currentConnectionId = getCurrentConnectionId();
|
||||
if (provider !== "codex" || !currentConnectionId || !headers) return;
|
||||
|
||||
try {
|
||||
const existingProviderData =
|
||||
credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object"
|
||||
@@ -571,23 +582,28 @@ export async function handleChatCore({
|
||||
status,
|
||||
});
|
||||
if (!built) return;
|
||||
|
||||
if (built.exhaustionLog) {
|
||||
log?.debug?.("CODEX", built.exhaustionLog);
|
||||
}
|
||||
|
||||
// Invalidate the preflight cache for this connection so the next
|
||||
// isModelAvailable check fetches fresh quota data.
|
||||
if (status === 429) {
|
||||
invalidateCodexQuotaCache(currentConnectionId);
|
||||
}
|
||||
|
||||
await updateProviderConnection(currentConnectionId, {
|
||||
providerSpecificData: built.nextProviderData,
|
||||
});
|
||||
|
||||
credentials.providerSpecificData = built.nextProviderData;
|
||||
} catch (err) {
|
||||
const errMessage = err instanceof Error ? err.message : String(err);
|
||||
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Phase 9.2: Idempotency check ──
|
||||
// Resolve the idempotency key once here and reuse it at the Phase 9.2 save site below,
|
||||
// rather than re-deriving it. (#3821-review LEDGER-6)
|
||||
@@ -606,11 +622,13 @@ export async function handleChatCore({
|
||||
if (idempotencyHit) {
|
||||
return idempotencyHit;
|
||||
}
|
||||
|
||||
// T07: Inject connectionId into credentials so executors can rotate API keys
|
||||
// using providerSpecificData.extraApiKeys (API Key Round-Robin feature)
|
||||
if (connectionId && credentials && !credentials.connectionId) {
|
||||
credentials.connectionId = connectionId;
|
||||
}
|
||||
|
||||
// Endpoint/format resolution extracted to chatCore/requestFormat.ts (#3501); pure derivation
|
||||
// from the inbound request, destructured so every downstream use stays byte-identical.
|
||||
const {
|
||||
@@ -2246,19 +2264,8 @@ export async function handleChatCore({
|
||||
// the latter is a Kiro/Claude passthrough alias channel with string values,
|
||||
// while namespace identities carry `{namespace, name}` for the #7936 response
|
||||
// seam. Extract first because Kiro merge may reuse `_toolNameMap` below.
|
||||
//
|
||||
// #9780 — prefer the dedicated channel: on a pivot the openai->claude/gemini
|
||||
// step publishes its own alias map on `_toolNameMap`, so that property alone
|
||||
// yields aliases here. The `_toolNameMap` read stays as the fallback for the
|
||||
// non-pivot producers (executors/base.ts, cliproxyapi.ts, antigravity).
|
||||
const namespaceIdentityMap = translatedBody._namespaceToolIdentityMap;
|
||||
const requestToolIdentityMap =
|
||||
namespaceIdentityMap instanceof Map
|
||||
? namespaceIdentityMap
|
||||
: translatedBody._toolNameMap instanceof Map
|
||||
? translatedBody._toolNameMap
|
||||
: null;
|
||||
delete translatedBody._namespaceToolIdentityMap;
|
||||
translatedBody._toolNameMap instanceof Map ? translatedBody._toolNameMap : null;
|
||||
delete translatedBody._toolNameMap;
|
||||
|
||||
// Kiro: sanitize tool schemas before dispatch. Kiro returns 400 "Improperly
|
||||
@@ -5018,6 +5025,7 @@ export async function handleChatCore({
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) {
|
||||
if (!expiresAt) return false;
|
||||
const expiresAtMs = new Date(expiresAt).getTime();
|
||||
|
||||
@@ -352,27 +352,7 @@ export function translateRequest(
|
||||
...(hasProvider ? { _provider: provider } : {}),
|
||||
}
|
||||
: credentials;
|
||||
// #9780 — carry the Responses namespace identity map across the pivot.
|
||||
// Target translators return a brand-new object (buildKiroPayload et
|
||||
// al.), dropping the non-enumerable property step 1 attached; the
|
||||
// #7936 seam then gets null and namespace sub-tool calls come back
|
||||
// flattened, which Codex rejects with `unsupported call: <name>`.
|
||||
const identityMap = (result as Record<string, unknown>)._namespaceToolIdentityMap;
|
||||
const translated = fromOpenAI(model, result, stream, translationCredentials);
|
||||
if (
|
||||
identityMap instanceof Map &&
|
||||
translated &&
|
||||
typeof translated === "object" &&
|
||||
!((translated as Record<string, unknown>)._namespaceToolIdentityMap instanceof Map)
|
||||
) {
|
||||
Object.defineProperty(translated, "_namespaceToolIdentityMap", {
|
||||
value: identityMap,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
result = translated;
|
||||
result = fromOpenAI(model, result, stream, translationCredentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,19 +752,8 @@ export function openaiResponsesToOpenAIRequest(
|
||||
delete result.prompt_cache_retention;
|
||||
|
||||
if (namespaceToolIdentityMap.size > 0) {
|
||||
// chatCore extracts and deletes these transient side channels before dispatch.
|
||||
// chatCore extracts and deletes this transient side channel before dispatch.
|
||||
// Non-enumerability keeps internal request metadata off the upstream wire.
|
||||
//
|
||||
// Two properties on purpose (#9780): `_toolNameMap` is also the alias
|
||||
// channel for openai-to-claude/gemini, which overwrite it on a pivot, so
|
||||
// the identity map needs a name of its own. `_toolNameMap` stays populated
|
||||
// for the existing consumers (executors/base.ts, cliproxyapi, antigravity).
|
||||
Object.defineProperty(result, "_namespaceToolIdentityMap", {
|
||||
value: namespaceToolIdentityMap,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(result, "_toolNameMap", {
|
||||
value: namespaceToolIdentityMap,
|
||||
enumerable: false,
|
||||
|
||||
385
package-lock.json
generated
385
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.8.49",
|
||||
"version": "3.8.50",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "3.8.49",
|
||||
"version": "3.8.50",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -79,7 +79,7 @@
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tsx": "^4.23.0",
|
||||
"undici": "^8.3.0",
|
||||
"undici": "^8.10.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
"uuid": "^14.0.0",
|
||||
"ws": "^8.18.0",
|
||||
@@ -132,6 +132,7 @@
|
||||
"lint-staged": "^17.0.8",
|
||||
"lockfile-lint": "^5.0.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"opencode-ai": "1.18.8",
|
||||
"playwright-ctrf-json-reporter": "^0.0.29",
|
||||
"prettier": "^3.8.3",
|
||||
"promptfoo": "^0.121.18",
|
||||
@@ -151,7 +152,7 @@
|
||||
"@atjsh/llmlingua-2": "2.0.3",
|
||||
"@huggingface/transformers": "3.5.2",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"better-sqlite3": "^13.0.1",
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"js-tiktoken": "^1.0.20",
|
||||
"keytar": "^7.9.0",
|
||||
"tls-client-node": "^0.2.0",
|
||||
@@ -3692,9 +3693,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3711,9 +3709,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3730,9 +3725,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3749,9 +3741,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3768,9 +3757,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3787,9 +3773,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3806,9 +3789,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3825,9 +3805,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3844,9 +3821,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3869,9 +3843,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3894,9 +3865,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3919,9 +3887,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3944,9 +3909,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3969,9 +3931,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3994,9 +3953,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4019,9 +3975,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5393,9 +5346,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5412,9 +5362,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5431,9 +5378,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5450,9 +5394,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10670,9 +10611,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10690,9 +10628,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10710,9 +10645,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10730,9 +10662,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12779,9 +12708,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -12795,9 +12721,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -12811,9 +12734,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -12827,9 +12747,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -12843,9 +12760,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -12859,9 +12773,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -13687,11 +13598,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
@@ -13756,10 +13670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.1.tgz",
|
||||
"integrity": "sha512-LYpmOXdkpQYf4wmlxkdzW01XGlOXNIbjLg45yNkh0FQ4814VbK9PdOFmhZpYbej+EZtR/i3FDdhEG98HqZdgnA==",
|
||||
"hasInstallScript": true,
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.2.tgz",
|
||||
"integrity": "sha512-jW6oufeDhXZaiX9Lw5A+oerVClx4iFrI6uDj1zu7SqUAjak9vbJvA0NEcKLNxHiQHb6kYCoFzzXYV0YOauhV3g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -14041,14 +13954,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -24477,6 +24392,25 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/libxmljs2/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/libxmljs2/node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libxmljs2/node_modules/cacache": {
|
||||
"version": "19.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
|
||||
@@ -26949,6 +26883,24 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimatch/node_modules/brace-expansion": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -28787,6 +28739,205 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/opencode-ai": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.18.8.tgz",
|
||||
"integrity": "sha512-eZvYK0rIc/NUDQ+s3LsO9gyUU3MswsbNOLZz06iPwVhbg/2jF6bkTaroBgiIdFWKwUn5sj+kSMc4TBYxFkMrNQ==",
|
||||
"cpu": [
|
||||
"arm64",
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"opencode": "bin/opencode.exe"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"opencode-darwin-arm64": "1.18.8",
|
||||
"opencode-darwin-x64": "1.18.8",
|
||||
"opencode-darwin-x64-baseline": "1.18.8",
|
||||
"opencode-linux-arm64": "1.18.8",
|
||||
"opencode-linux-arm64-musl": "1.18.8",
|
||||
"opencode-linux-x64": "1.18.8",
|
||||
"opencode-linux-x64-baseline": "1.18.8",
|
||||
"opencode-linux-x64-baseline-musl": "1.18.8",
|
||||
"opencode-linux-x64-musl": "1.18.8",
|
||||
"opencode-windows-arm64": "1.18.8",
|
||||
"opencode-windows-x64": "1.18.8",
|
||||
"opencode-windows-x64-baseline": "1.18.8"
|
||||
}
|
||||
},
|
||||
"node_modules/opencode-darwin-arm64": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.8.tgz",
|
||||
"integrity": "sha512-ZZCIEgTvHxOHk52Aeqhq59t/R0aqs29bPIgu45XE4rkgjmn/XCkTWalCPtyzJHipdcEbq/g0lqsE1OlJV0oNbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-darwin-x64": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.8.tgz",
|
||||
"integrity": "sha512-2EXRMJbRKnFPWI9oDU9tb7jDGmKiPmfjCLtwJMe3EF57h5wfcdEH9sP25bR3Og5NbE2M+PtMcJm0jMeHn2XoLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-darwin-x64-baseline": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.18.8.tgz",
|
||||
"integrity": "sha512-eLXa2tK9LRuZ5e20QG2k4dmWAA5xnLgJ1afRTSD0/ybE6CAeK02i8vFCnFFDaxuBo+gnq+yqO8AkqvN1m64V/Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-linux-arm64": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.8.tgz",
|
||||
"integrity": "sha512-7kj3c9JEdryHgK+o8zE/N9KzTOdbiDn6KpY8dl+hM9n5Cnmxezx4IAlgJeC9QxpIx8Omop6CYuZ+17KfrKdKLw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-linux-arm64-musl": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.18.8.tgz",
|
||||
"integrity": "sha512-tww5TF/LIOv/GoTNyzGYgqDRhbJrhoMu8R+p5yD/SpnXPg3rcfYREw2wRy9yikyPU9sAQksuIIteTsyGerPjlA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-linux-x64": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.8.tgz",
|
||||
"integrity": "sha512-Sm4fbQ9BdLI6hgN6FYYX8Nql+Sqe/2EKHJu3iWg0UYs93AXN4ROi0rvOmRbMk+ycYgOchb0hL6Ti2opxLx17sg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-linux-x64-baseline": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.18.8.tgz",
|
||||
"integrity": "sha512-egeEF4tk1rK9flIQjjeSVB9cR/X3zUti0pNAHW6ROJkNkj72z2C2FmjK1hZbfjtteCueMXPLptS23JROHGWL1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-linux-x64-baseline-musl": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.18.8.tgz",
|
||||
"integrity": "sha512-S+438BXs48gLeXX/ya4TSNytDy9mliU3sOAf6j9rfFjzGiF/S08LedemSAnHkr0riBtamik1aRPSmTjhQ0dOBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-linux-x64-musl": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.18.8.tgz",
|
||||
"integrity": "sha512-c+E4Zsp0DYVcuqcDtgxw/4YcFLrVYWdGBR8x4CzpW48ga3RshaH+BlmUiy+GY0yr1x6UR+e2V3w4uzvzm/L9UQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-windows-arm64": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.18.8.tgz",
|
||||
"integrity": "sha512-7NjdtEIiX28kmsKD9jHbFG4bbwBB5T4dAe2UwdnOqCBb2cl+ETV5eO6kbdC/xWxrgOghgZM1Wtw791T5pQPyag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-windows-x64": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.18.8.tgz",
|
||||
"integrity": "sha512-G+NEgEMvu/dEYshH5IaqHVTmsHVuGdORBvVmgphFiknT7q/NXPuoZCMtMIdfNlEFbu54BlzRDdJCR3Mqe98gUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/opencode-windows-x64-baseline": {
|
||||
"version": "1.18.8",
|
||||
"resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.8.tgz",
|
||||
"integrity": "sha512-IGbjFyWoSN9rdGUJX7TWkQ1Yl673Q3dDna54b5NtqeRcZ839p+Z47zzM5m883HKAxrdKCC6Z22HYuDcXLV0laA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
@@ -32023,6 +32174,13 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rimraf/node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
@@ -34999,9 +35157,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
|
||||
"integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
|
||||
"integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
@@ -36792,12 +36950,7 @@
|
||||
},
|
||||
"open-sse": {
|
||||
"name": "@omniroute/open-sse",
|
||||
"version": "3.8.49",
|
||||
"dependencies": {
|
||||
"@toon-format/toon": "^4.1.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smol-toml": "1.7.1"
|
||||
}
|
||||
"version": "3.8.50"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
package.json
58
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.8.49",
|
||||
"description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"version": "3.8.50",
|
||||
"description": "Unified AI router with 290 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"omniroute": "bin/omniroute.mjs",
|
||||
@@ -23,6 +23,7 @@
|
||||
".env.example",
|
||||
"scripts/build/postinstall.mjs",
|
||||
"scripts/build/fixTlsClientNodeBinary.mjs",
|
||||
"scripts/build/fixPlaywrightAndroid.mjs",
|
||||
"bin/cli/runtime/",
|
||||
"scripts/postinstall.mjs",
|
||||
"scripts/build/postinstallSupport.mjs",
|
||||
@@ -33,11 +34,15 @@
|
||||
"scripts/dev/tls-options.mjs",
|
||||
"scripts/check/check-supported-node-runtime.ts",
|
||||
"scripts/dev/sync-env.mjs",
|
||||
"scripts/build/native-binary-compat.mjs",
|
||||
"scripts/build/assembleStandalone.mjs",
|
||||
"scripts/build/backendOnlyPages.mjs",
|
||||
"scripts/build/build-next-isolated.mjs",
|
||||
"scripts/build/build-tproxy-native.mjs",
|
||||
"scripts/build/native-binary-compat.mjs",
|
||||
"scripts/build/runtime-env.mjs",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"!**/node_modules/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.tsx",
|
||||
@@ -110,6 +115,8 @@
|
||||
"test:unit:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial",
|
||||
"test:unit:ci:shard": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=$TEST_SHARD \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=4096 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=$TEST_SHARD \"tests/unit/serial/**/*.test.ts\"",
|
||||
"test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-isolation=none \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial",
|
||||
"test:scoped": "bash scripts/quality/test-scoped.sh",
|
||||
"test:scoped:staged": "bash scripts/quality/test-scoped.sh --staged",
|
||||
"test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"",
|
||||
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=1/2 \"tests/unit/serial/**/*.test.ts\"",
|
||||
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=2/2 \"tests/unit/serial/**/*.test.ts\"",
|
||||
@@ -143,6 +150,7 @@
|
||||
"check:node-runtime": "node --import tsx scripts/check/check-supported-node-runtime.ts",
|
||||
"check:pack-artifact": "node --import tsx scripts/build/validate-pack-artifact.ts",
|
||||
"check:pack-boot": "node scripts/check/check-pack-boot.mjs",
|
||||
"check:install-upgrade": "node scripts/check/check-install-upgrade.mjs",
|
||||
"check:pack-policy": "node --import tsx scripts/build/validate-pack-artifact.ts --policy-only",
|
||||
"check:cli-i18n": "node scripts/check/check-cli-i18n.mjs",
|
||||
"check:openapi-coverage": "node scripts/check/check-openapi-coverage.mjs",
|
||||
@@ -161,6 +169,7 @@
|
||||
"check:test-masking": "node scripts/check/check-test-masking.mjs",
|
||||
"check:test-runner-api": "node scripts/check/check-test-runner-api.mjs",
|
||||
"check:changelog-integrity": "node scripts/check/check-changelog-integrity.mjs",
|
||||
"sweep:stale-fragments": "node scripts/release/sweep-stale-fragments.mjs",
|
||||
"changelog:aggregate": "node scripts/release/aggregate-changelog.mjs",
|
||||
"check:agent-skills-sync": "node --import tsx/esm scripts/skills/generate-agent-skills.mjs",
|
||||
"check:build-scope": "node scripts/check/check-build-scope.mjs",
|
||||
@@ -184,6 +193,7 @@
|
||||
"check:bundle-size": "node scripts/check/check-bundle-size.mjs",
|
||||
"check:circular-deps": "node scripts/check/check-circular-deps.mjs",
|
||||
"check:mutation-ratchet": "node scripts/check/check-mutation-ratchet.mjs",
|
||||
"check:rtl-ratchet": "node scripts/check/check-rtl-ratchet.mjs",
|
||||
"check:licenses": "node scripts/check/check-licenses.mjs",
|
||||
"check:pr-evidence": "node scripts/check/check-pr-evidence.mjs",
|
||||
"check:vuln-ratchet": "node scripts/check/check-vuln-ratchet.mjs",
|
||||
@@ -200,9 +210,11 @@
|
||||
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
|
||||
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
||||
"check:dashboard-typecheck": "node scripts/check/check-dashboard-typecheck.mjs",
|
||||
"check:open-sse-typecheck": "node scripts/check/check-open-sse-typecheck.mjs",
|
||||
"backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts",
|
||||
"env:sync": "node scripts/dev/sync-env.mjs",
|
||||
"test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"",
|
||||
"test:integration:ci": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=$TEST_SHARD tests/integration/*.test.ts \"tests/integration/combo-matrix/*.test.ts\"",
|
||||
"test:combo:matrix": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/integration/combo-matrix/*.test.ts\"",
|
||||
"test:combo:live": "cross-env RUN_COMBO_LIVE=1 DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/integration/combo-live/*.live.test.ts\"",
|
||||
"test:combo:live:vps": "node scripts/test/combo-live-vps.mjs",
|
||||
@@ -232,6 +244,7 @@
|
||||
"prepare": "husky",
|
||||
"system-info": "node scripts/dev/system-info.mjs",
|
||||
"build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs",
|
||||
"postbuild": "node scripts/build/colocate-standalone.mjs",
|
||||
"release:contributors": "node scripts/release/gen-contributors.mjs",
|
||||
"release:uncovered": "node scripts/release/list-uncovered-commits.mjs",
|
||||
"test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial",
|
||||
@@ -304,7 +317,7 @@
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tsx": "^4.23.0",
|
||||
"undici": "^8.3.0",
|
||||
"undici": "^8.10.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
"uuid": "^14.0.0",
|
||||
"ws": "^8.18.0",
|
||||
@@ -317,7 +330,7 @@
|
||||
"@atjsh/llmlingua-2": "2.0.3",
|
||||
"@huggingface/transformers": "3.5.2",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"better-sqlite3": "^13.0.1",
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"js-tiktoken": "^1.0.20",
|
||||
"keytar": "^7.9.0",
|
||||
"tls-client-node": "^0.2.0",
|
||||
@@ -363,6 +376,7 @@
|
||||
"lint-staged": "^17.0.8",
|
||||
"lockfile-lint": "^5.0.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"opencode-ai": "1.18.8",
|
||||
"playwright-ctrf-json-reporter": "^0.0.29",
|
||||
"prettier": "^3.8.3",
|
||||
"promptfoo": "^0.121.18",
|
||||
@@ -394,6 +408,15 @@
|
||||
"sharp"
|
||||
]
|
||||
},
|
||||
"allowScripts": {
|
||||
"better-sqlite3": true,
|
||||
"esbuild": true,
|
||||
"@swc/core": true,
|
||||
"@parcel/watcher": true,
|
||||
"keytar": true,
|
||||
"protobufjs": true,
|
||||
"unrs-resolver": true
|
||||
},
|
||||
"overrides": {
|
||||
"fast-xml-parser": "^5.10.1",
|
||||
"sharp": "^0.35.0",
|
||||
@@ -424,27 +447,14 @@
|
||||
"adm-zip": "^0.6.0",
|
||||
"promptfoo": {
|
||||
"js-yaml": "^5.2.2",
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"undici": "^7.29.0"
|
||||
},
|
||||
"socket.io-parser": "^4.2.7",
|
||||
"tar": "^7.5.21",
|
||||
"brace-expansion": "^5.0.9",
|
||||
"minimatch": {
|
||||
"brace-expansion": "^1.1.18"
|
||||
},
|
||||
"libxmljs2": {
|
||||
"minimatch": {
|
||||
"brace-expansion": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"minimatch": {
|
||||
"brace-expansion": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"nanoid": "^3.3.17",
|
||||
"@eslint/eslintrc": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
@@ -454,9 +464,11 @@
|
||||
"xmlbuilder2": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"nanoid": "^3.3.17",
|
||||
"monaco-editor": {
|
||||
"dompurify": "^3.4.13"
|
||||
},
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"js-yaml": "^4.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ import { getResource404Bypass } from "./requestResourceHealth";
|
||||
import * as log from "../utils/logger";
|
||||
import { fisherYatesShuffle, getNextFromDeckSync } from "@/shared/utils/shuffleDeck";
|
||||
import { readHeaderValue, type AuthRequestHeaders } from "./headerReader.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
interface RecoverableConnectionState {
|
||||
connectionId: string;
|
||||
@@ -94,7 +93,6 @@ interface RecoverableConnectionState {
|
||||
lastErrorType?: string | null;
|
||||
lastErrorSource?: string | null;
|
||||
}
|
||||
|
||||
interface CredentialSelectionOptions {
|
||||
allowSuppressedConnections?: boolean;
|
||||
allowRateLimitedConnections?: boolean;
|
||||
@@ -104,14 +102,12 @@ interface CredentialSelectionOptions {
|
||||
sessionKey?: string | null;
|
||||
sessionAffinityTtlMs?: number | null;
|
||||
}
|
||||
|
||||
interface CooldownInspectionState {
|
||||
connection: ProviderConnectionView;
|
||||
connectionCooldownMs: number | null;
|
||||
codexScopeCooldownMs: number | null;
|
||||
retryableModelCooldownMs: number | null;
|
||||
}
|
||||
|
||||
const MIN_QUOTA_THRESHOLD_PERCENT = 1;
|
||||
const MAX_QUOTA_THRESHOLD_PERCENT = 100;
|
||||
const NON_RETRYABLE_MODEL_LOCKOUT_REASONS = new Set(["not_found", "not_found_local"]);
|
||||
@@ -119,25 +115,20 @@ const NON_RETRYABLE_MODEL_LOCKOUT_REASONS = new Set(["not_found", "not_found_loc
|
||||
// this base. Real upstream Retry-After hints still win — they flow through
|
||||
// `exactCooldownMs` (usedUpstreamRetryHint), not this base. (#5222)
|
||||
const ANTIGRAVITY_FAMILY_INFERRED_BASE_COOLDOWN_MS = 30_000;
|
||||
|
||||
function asRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
}
|
||||
|
||||
function toStringOrNull(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function toNullableNumber(value: unknown): number | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
const parsed = toNumber(value, Number.NaN);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function toBooleanOrDefault(value: unknown, fallback: boolean): boolean {
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeSessionKey(value: unknown, prefix: string): string | null {
|
||||
if (typeof value !== "string" || value.trim().length === 0) return null;
|
||||
const trimmed = value.trim();
|
||||
@@ -146,7 +137,6 @@ function normalizeSessionKey(value: unknown, prefix: string): string | null {
|
||||
}
|
||||
return `${prefix}:sha256:${createHash("sha256").update(trimmed).digest("hex")}`;
|
||||
}
|
||||
|
||||
function extractTextForSessionHash(value: unknown): string | null {
|
||||
if (typeof value === "string") return value;
|
||||
if (Array.isArray(value)) {
|
||||
@@ -164,7 +154,6 @@ function extractTextForSessionHash(value: unknown): string | null {
|
||||
if (value && typeof value === "object") return JSON.stringify(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFirstInputText(body: unknown): string | null {
|
||||
const record = asRecord(body);
|
||||
if (record.input !== undefined) {
|
||||
@@ -189,7 +178,6 @@ function getFirstInputText(body: unknown): string | null {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractSessionAffinityKey(
|
||||
body: unknown,
|
||||
headers?: Headers | { get?: (name: string) => string | null } | null
|
||||
@@ -216,7 +204,6 @@ export function extractSessionAffinityKey(
|
||||
if (!inputText || inputText.trim().length === 0) return null;
|
||||
return `input:sha256:${createHash("sha256").update(inputText.slice(0, 4096)).digest("hex")}`;
|
||||
}
|
||||
|
||||
function getCodexLimitPolicy(providerSpecificData: JsonRecord): {
|
||||
use5h: boolean;
|
||||
useWeekly: boolean;
|
||||
@@ -227,13 +214,11 @@ function getCodexLimitPolicy(providerSpecificData: JsonRecord): {
|
||||
useWeekly: toBooleanOrDefault(policy.useWeekly, true),
|
||||
};
|
||||
}
|
||||
|
||||
interface QuotaLimitPolicy {
|
||||
enabled: boolean;
|
||||
thresholdPercent: number;
|
||||
windows: string[];
|
||||
}
|
||||
|
||||
interface QuotaCacheView {
|
||||
quotas?: Record<
|
||||
string,
|
||||
@@ -243,7 +228,6 @@ interface QuotaCacheView {
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
function normalizeQuotaThreshold(
|
||||
value: unknown,
|
||||
fallback = DEFAULT_QUOTA_THRESHOLD_PERCENT
|
||||
@@ -251,17 +235,14 @@ function normalizeQuotaThreshold(
|
||||
const parsed = toNumber(value, fallback);
|
||||
return Math.min(MAX_QUOTA_THRESHOLD_PERCENT, Math.max(MIN_QUOTA_THRESHOLD_PERCENT, parsed));
|
||||
}
|
||||
|
||||
function normalizeWindowName(windowName: unknown): string | null {
|
||||
if (typeof windowName !== "string") return null;
|
||||
const normalized = windowName.trim().toLowerCase();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function uniqueWindows(windows: string[]): string[] {
|
||||
return [...new Set(windows)];
|
||||
}
|
||||
|
||||
function normalizeCodexWindowName(windowName: unknown): string | null {
|
||||
if (typeof windowName !== "string") return null;
|
||||
const normalized = windowName.trim().toLowerCase();
|
||||
@@ -273,7 +254,6 @@ function normalizeCodexWindowName(windowName: unknown): string | null {
|
||||
}
|
||||
return toCodexBaseQuotaWindowName(normalized);
|
||||
}
|
||||
|
||||
function applyCodexWindowPolicy(rawWindows: string[], providerSpecificData: JsonRecord): string[] {
|
||||
const codexPolicy = getCodexLimitPolicy(providerSpecificData);
|
||||
const normalizedRaw = rawWindows.map(normalizeCodexWindowName).filter(Boolean) as string[];
|
||||
@@ -291,7 +271,6 @@ function applyCodexWindowPolicy(rawWindows: string[], providerSpecificData: Json
|
||||
|
||||
return uniqueWindows(windows);
|
||||
}
|
||||
|
||||
function getCodexScopeRateLimitedUntil(
|
||||
providerSpecificData: JsonRecord,
|
||||
model: string | null
|
||||
@@ -302,7 +281,6 @@ function getCodexScopeRateLimitedUntil(
|
||||
const value = scopeMap[scope];
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function isCodexScopeUnavailable(
|
||||
connection: ProviderConnectionView,
|
||||
model: string | null
|
||||
@@ -311,7 +289,6 @@ function isCodexScopeUnavailable(
|
||||
if (!until) return false;
|
||||
return new Date(until).getTime() > Date.now();
|
||||
}
|
||||
|
||||
function getEarliestCodexScopeRateLimitedUntil(
|
||||
connections: ProviderConnectionView[],
|
||||
model: string | null
|
||||
@@ -332,11 +309,9 @@ function getEarliestCodexScopeRateLimitedUntil(
|
||||
|
||||
return earliest;
|
||||
}
|
||||
|
||||
function normalizeStatus(value: string | null): string {
|
||||
return (value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isTerminalConnectionStatus(connection: ProviderConnectionView): boolean {
|
||||
const status = normalizeStatus(connection.testStatus);
|
||||
return status === "credits_exhausted" || status === "banned" || status === "expired";
|
||||
@@ -354,7 +329,6 @@ function isRecoverableCookieAuth401(
|
||||
resolveProviderId(provider) in WEB_COOKIE_PROVIDERS
|
||||
);
|
||||
}
|
||||
|
||||
function resolveTerminalConnectionStatus(
|
||||
status: number,
|
||||
result: { permanent?: boolean; creditsExhausted?: boolean },
|
||||
@@ -381,7 +355,6 @@ function resolveTerminalConnectionStatus(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveQuotaLimitPolicy(
|
||||
provider: string,
|
||||
providerSpecificData: JsonRecord
|
||||
@@ -407,7 +380,6 @@ export function resolveQuotaLimitPolicy(
|
||||
windows,
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateQuotaLimitPolicy(
|
||||
provider: string,
|
||||
connection: ProviderConnectionView,
|
||||
@@ -440,7 +412,6 @@ export function evaluateQuotaLimitPolicy(
|
||||
resetAt: getEarliestFutureDate(resetCandidates),
|
||||
};
|
||||
}
|
||||
|
||||
function parseFutureDateMs(value: string | null): number | null {
|
||||
if (!value) return null;
|
||||
// Tolerate numeric-epoch strings (e.g. "1781696905131.0") as well as ISO
|
||||
@@ -449,7 +420,6 @@ function parseFutureDateMs(value: string | null): number | null {
|
||||
if (!Number.isFinite(ms) || ms <= Date.now()) return null;
|
||||
return ms;
|
||||
}
|
||||
|
||||
function getEarliestFutureDate(candidates: Array<string | null>): string | null {
|
||||
return (
|
||||
candidates
|
||||
@@ -461,31 +431,26 @@ function getEarliestFutureDate(candidates: Array<string | null>): string | null
|
||||
.sort((a, b) => (a.ms as number) - (b.ms as number))[0]?.raw || null
|
||||
);
|
||||
}
|
||||
|
||||
function getCachedQuotaResetAt(connectionId: string): string | null {
|
||||
const entry = getQuotaCache(connectionId);
|
||||
if (!entry?.quotas) return null;
|
||||
return getEarliestFutureDate(Object.values(entry.quotas).map((quota) => quota.resetAt));
|
||||
}
|
||||
|
||||
function isRetryableModelLockoutReason(reason: unknown): boolean {
|
||||
return typeof reason === "string" && reason.length > 0
|
||||
? !NON_RETRYABLE_MODEL_LOCKOUT_REASONS.has(reason)
|
||||
: false;
|
||||
}
|
||||
|
||||
function pushClampedPercentage(percentages: number[], value: number): void {
|
||||
if (Number.isFinite(value)) {
|
||||
percentages.push(Math.max(0, Math.min(100, value)));
|
||||
}
|
||||
}
|
||||
|
||||
function isResetAtInPast(resetAt: string | null): boolean {
|
||||
if (!resetAt) return false;
|
||||
const resetMs = new Date(resetAt).getTime();
|
||||
return Number.isFinite(resetMs) && resetMs <= Date.now();
|
||||
}
|
||||
|
||||
function collectPolicyQuotaHeadroomPercentages(
|
||||
provider: string,
|
||||
connection: ProviderConnectionView,
|
||||
@@ -508,7 +473,6 @@ function collectPolicyQuotaHeadroomPercentages(
|
||||
|
||||
return percentages;
|
||||
}
|
||||
|
||||
function collectCachedQuotaHeadroomPercentages(
|
||||
provider: string,
|
||||
connection: ProviderConnectionView,
|
||||
@@ -528,7 +492,6 @@ function collectCachedQuotaHeadroomPercentages(
|
||||
|
||||
return percentages;
|
||||
}
|
||||
|
||||
function getConnectionQuotaHeadroomPercent(
|
||||
provider: string,
|
||||
connection: ProviderConnectionView,
|
||||
@@ -548,7 +511,6 @@ function getConnectionQuotaHeadroomPercent(
|
||||
|
||||
return percentages.length > 0 ? Math.min(...percentages) : null;
|
||||
}
|
||||
|
||||
function getConnectionErrorPenalty(connection: ProviderConnectionView): number {
|
||||
const errorType = normalizeStatus(connection.lastErrorType);
|
||||
const errorSource = normalizeStatus(connection.lastErrorSource);
|
||||
@@ -572,7 +534,6 @@ function getConnectionErrorPenalty(connection: ProviderConnectionView): number {
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
function getConnectionRecencyPenalty(connection: ProviderConnectionView): number {
|
||||
if (!connection.lastUsedAt) return 0;
|
||||
const ageMs = Date.now() - new Date(connection.lastUsedAt).getTime();
|
||||
@@ -582,7 +543,6 @@ function getConnectionRecencyPenalty(connection: ProviderConnectionView): number
|
||||
if (ageMs < 5 * 60_000) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getP2CConnectionScore(
|
||||
provider: string,
|
||||
connection: ProviderConnectionView,
|
||||
@@ -628,7 +588,6 @@ function getP2CConnectionScore(
|
||||
|
||||
return { score, quotaHeadroomPercent };
|
||||
}
|
||||
|
||||
function compareP2CConnections(
|
||||
provider: string,
|
||||
a: ProviderConnectionView,
|
||||
@@ -662,12 +621,10 @@ function compareP2CConnections(
|
||||
* exclude it (#3061), otherwise it gets re-selected forever.
|
||||
*/
|
||||
const SYNTHETIC_NOAUTH_CONNECTION_ID = "noauth";
|
||||
|
||||
type AnonymousFallbackProviderDefinition = {
|
||||
anonymousFallback?: boolean;
|
||||
noAuth?: boolean;
|
||||
};
|
||||
|
||||
function buildSyntheticNoAuthCredentials(providerSpecificData: JsonRecord = {}): {
|
||||
apiKey: null;
|
||||
accessToken: null;
|
||||
@@ -756,7 +713,6 @@ async function loadNoAuthProviderSpecificData(providerId: string): Promise<JsonR
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function providerCanUseSyntheticNoAuthFallback(providerId: string): boolean {
|
||||
const providerDef = getProviderById(providerId) as
|
||||
AnonymousFallbackProviderDefinition | undefined;
|
||||
@@ -772,7 +728,6 @@ function providerCanUseSyntheticNoAuthFallback(providerId: string): boolean {
|
||||
webCookieProviderDef?.noAuth === true
|
||||
);
|
||||
}
|
||||
|
||||
async function maybeSyntheticNoAuthFallback(
|
||||
providerId: string,
|
||||
excludedConnectionIds: Set<string>,
|
||||
@@ -790,7 +745,6 @@ async function maybeSyntheticNoAuthFallback(
|
||||
const providerSpecificData = await loadNoAuthProviderSpecificData(providerId);
|
||||
return buildSyntheticNoAuthCredentials(providerSpecificData);
|
||||
}
|
||||
|
||||
function normalizeExcludedConnectionIds(
|
||||
excludeConnectionId: string | null,
|
||||
extraExcludedConnectionIds: string[] | null | undefined
|
||||
@@ -811,7 +765,6 @@ function normalizeExcludedConnectionIds(
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function formatConnectionPrefixesForLog(ids: Iterable<string>, max = 6): string {
|
||||
const prefixes = Array.from(ids)
|
||||
.filter((id) => typeof id === "string" && id.length > 0)
|
||||
@@ -819,7 +772,6 @@ function formatConnectionPrefixesForLog(ids: Iterable<string>, max = 6): string
|
||||
.map((id) => `${id.slice(0, 8)}...`);
|
||||
return prefixes.length > 0 ? prefixes.join(",") : "none";
|
||||
}
|
||||
|
||||
function buildQuotaPreflightRateLimitedResult(
|
||||
provider: string,
|
||||
blockedByPreflight: Array<{
|
||||
@@ -850,12 +802,10 @@ function buildQuotaPreflightRateLimitedResult(
|
||||
lastErrorCode: 429,
|
||||
};
|
||||
}
|
||||
|
||||
function quotaPreflightUnavailableUntil(resetAt?: string | null): string {
|
||||
const resetMs = parseFutureDateMs(resetAt ?? null);
|
||||
return new Date(resetMs ?? Date.now() + 5 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
async function markQuotaPreflightAccountUnavailable(
|
||||
provider: string,
|
||||
connectionId: string,
|
||||
@@ -884,14 +834,12 @@ async function markQuotaPreflightAccountUnavailable(
|
||||
// Provider-scoped mutexes prevent race conditions during account selection without
|
||||
// serializing unrelated providers behind a single global lock.
|
||||
const selectionMutexes = new Map<string, Promise<void>>();
|
||||
|
||||
function getSelectionMutexKey(provider: string, options: CredentialSelectionOptions): string {
|
||||
return [
|
||||
resolveProviderId(provider) || provider,
|
||||
options.forcedConnectionId ? `forced:${options.forcedConnectionId}` : "pool",
|
||||
].join(":");
|
||||
}
|
||||
|
||||
function createSelectionLock(key: string) {
|
||||
const currentMutex = selectionMutexes.get(key) ?? Promise.resolve();
|
||||
let resolveMutex: (() => void) | undefined;
|
||||
@@ -923,7 +871,6 @@ export { fisherYatesShuffle, getNextFromDeckSync as getNextFromDeck };
|
||||
// Re-export readHeaderValue and AuthRequestHeaders from headerReader.ts for
|
||||
// backwards compat with existing imports (e.g. googApiKeyAuth.ts).
|
||||
export { readHeaderValue, type AuthRequestHeaders } from "./headerReader.ts";
|
||||
|
||||
const PROVIDER_SEARCH_PAIRS: string[][] = [
|
||||
["nvidia", "nvidia_nim"],
|
||||
["kimi-coding", "kimi-coding-apikey"],
|
||||
@@ -1703,7 +1650,6 @@ export async function getProviderCredentials(
|
||||
selectionLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProviderCredentialsWithQuotaPreflight(
|
||||
provider: string,
|
||||
excludeConnectionId: string | null = null,
|
||||
@@ -2005,16 +1951,17 @@ export async function markAccountUnavailable(
|
||||
const disableCooling = connProviderSpecificData.disableCooling === true;
|
||||
|
||||
const isPerModelQuotaProvider = hasPerModelQuota(provider, model, connectionPassthroughModels);
|
||||
const isNvidiaModelGone = provider === "nvidia" && status === 410;
|
||||
const modelLockoutOptions = { maxCooldownMs: effectiveProviderProfile?.maxCooldownMs };
|
||||
if (
|
||||
isPerModelQuotaProvider &&
|
||||
provider &&
|
||||
provider !== "codex" &&
|
||||
model &&
|
||||
(status === 404 || status === 429 || status >= 500)
|
||||
(status === 404 || isNvidiaModelGone || status === 429 || status >= 500)
|
||||
) {
|
||||
const reason =
|
||||
status === 404
|
||||
status === 404 || isNvidiaModelGone
|
||||
? "not_found"
|
||||
: status === 429 && fallbackResult.reason === RateLimitReason.QUOTA_EXHAUSTED
|
||||
? "quota_exhausted"
|
||||
@@ -2046,7 +1993,10 @@ export async function markAccountUnavailable(
|
||||
? "model"
|
||||
: getQuotaScopeLabelForProvider(provider, model);
|
||||
const antigravityFamilyInferredBaseCooldownMs =
|
||||
!usesExactAntigravityLock && provider === "antigravity" && quotaScope === "family" && status === 429
|
||||
!usesExactAntigravityLock &&
|
||||
provider === "antigravity" &&
|
||||
quotaScope === "family" &&
|
||||
status === 429
|
||||
? ANTIGRAVITY_FAMILY_INFERRED_BASE_COOLDOWN_MS
|
||||
: null;
|
||||
const lockout = recordModelLockoutFailure(
|
||||
@@ -2055,7 +2005,7 @@ export async function markAccountUnavailable(
|
||||
model,
|
||||
reason,
|
||||
status,
|
||||
status === 404
|
||||
status === 404 || isNvidiaModelGone
|
||||
? (effectiveProviderProfile?.baseCooldownMs ?? COOLDOWN_MS.notFoundLocal)
|
||||
: (antigravityFamilyInferredBaseCooldownMs ??
|
||||
fallbackResult.baseCooldownMs ??
|
||||
@@ -2352,7 +2302,6 @@ export interface RecoveredStateExpectation {
|
||||
lastErrorAt: string | null;
|
||||
rateLimitedUntil: string | null;
|
||||
}
|
||||
|
||||
export async function clearRecoveredProviderState(
|
||||
credentials: Partial<RecoverableConnectionState> | null,
|
||||
expectedState?: RecoveredStateExpectation
|
||||
@@ -2373,12 +2322,10 @@ export async function clearRecoveredProviderState(
|
||||
await clearAccountError(credentials.connectionId, credentials);
|
||||
return { applied: true };
|
||||
}
|
||||
|
||||
type AuthRequestLike = {
|
||||
headers?: AuthRequestHeaders | null;
|
||||
url?: string | null;
|
||||
};
|
||||
|
||||
function readNonEmptyUrlToken(request: AuthRequestLike): string | null {
|
||||
if (typeof request?.url !== "string" || request.url.trim().length === 0) return null;
|
||||
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
"tests/unit/no-memory-header.test.ts",
|
||||
"tests/unit/noauth-autocombo-lockout-7623.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",
|
||||
"tests/unit/nvidia-quota-phase1.test.ts",
|
||||
"tests/unit/oauth-providers-config.test.ts",
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
// #9780 — the Responses namespace identity map must survive the hub-and-spoke
|
||||
// pivot in translator/index.ts. Step 1 flattens namespace sub-tools (#8295) and
|
||||
// records `{namespace, name}`; step 2 returns a new object and used to drop it,
|
||||
// leaving the #7936 seam with null and Codex rejecting `unsupported call`.
|
||||
// A naive copy-through is not an option: openai-to-claude/gemini publish their
|
||||
// own alias map on `_toolNameMap`, hence the dedicated channel asserted here.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
await import("../../open-sse/translator/bootstrap.ts");
|
||||
const { translateRequest, initState } = await import("../../open-sse/translator/index.ts");
|
||||
const { openaiToOpenAIResponsesResponse } = await import(
|
||||
"../../open-sse/translator/response/openai-responses.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
type NamespaceIdentity = { namespace: string; name: string };
|
||||
|
||||
const NAMESPACE_REQUEST = {
|
||||
model: "any-model",
|
||||
instructions: "coding agent",
|
||||
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "go" }] }],
|
||||
tools: [
|
||||
{
|
||||
type: "namespace",
|
||||
name: "functions",
|
||||
tools: [
|
||||
{
|
||||
name: "exec",
|
||||
description: "Run a shell command",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { cmd: { type: "string" } },
|
||||
required: ["cmd"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function pivot(targetFormat: string): Record<string, unknown> {
|
||||
return translateRequest(
|
||||
"openai-responses",
|
||||
targetFormat,
|
||||
"any-model",
|
||||
structuredClone(NAMESPACE_REQUEST),
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function identityOf(body: Record<string, unknown>) {
|
||||
const map = body._namespaceToolIdentityMap;
|
||||
assert.ok(map instanceof Map, "expected a _namespaceToolIdentityMap after the pivot");
|
||||
return map as Map<string, NamespaceIdentity>;
|
||||
}
|
||||
|
||||
test("#9780: namespace identity survives the openai-responses -> kiro pivot", () => {
|
||||
const identity = identityOf(pivot("kiro"));
|
||||
|
||||
assert.equal(identity.size, 1);
|
||||
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
|
||||
});
|
||||
|
||||
test("#9780: namespace identity survives the openai-responses -> cursor pivot", () => {
|
||||
const identity = identityOf(pivot("cursor"));
|
||||
|
||||
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
|
||||
});
|
||||
|
||||
// Regression guard: these two appeared to "keep" a map before the fix, but it
|
||||
// was the alias map.
|
||||
for (const target of ["claude", "gemini"]) {
|
||||
test(`#9780: ${target} pivot keeps its alias map AND the namespace identity`, () => {
|
||||
const body = pivot(target);
|
||||
const identity = identityOf(body);
|
||||
|
||||
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
|
||||
|
||||
// The alias channel must be untouched: string values, not identities.
|
||||
const aliases = body._toolNameMap;
|
||||
assert.ok(aliases instanceof Map, `${target} must still publish its alias map`);
|
||||
for (const value of (aliases as Map<string, unknown>).values()) {
|
||||
assert.equal(typeof value, "string", `${target} alias values must stay strings`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Same-format requests are never flattened, so an absent map is correct here.
|
||||
test("#9780: same-format openai-responses request is not flattened at all", () => {
|
||||
const body = pivot("openai-responses");
|
||||
const tools = body.tools as Array<Record<string, unknown>>;
|
||||
|
||||
assert.equal(tools[0].type, "namespace");
|
||||
assert.equal((tools[0].tools as Array<{ name: string }>)[0].name, "exec");
|
||||
assert.equal(body._namespaceToolIdentityMap, undefined);
|
||||
});
|
||||
|
||||
test("#9780: the identity channel is non-enumerable and never serializes", () => {
|
||||
const body = pivot("kiro");
|
||||
|
||||
assert.ok(body._namespaceToolIdentityMap instanceof Map);
|
||||
assert.equal(
|
||||
Object.prototype.propertyIsEnumerable.call(body, "_namespaceToolIdentityMap"),
|
||||
false
|
||||
);
|
||||
assert.equal("_namespaceToolIdentityMap" in JSON.parse(JSON.stringify(body)), false);
|
||||
});
|
||||
|
||||
// End-to-end: request pivot + response seam, i.e. what the Codex adjudicator
|
||||
// actually receives. Before the fix every target emitted `functions__exec` with
|
||||
// no namespace, which is the reported `unsupported call`.
|
||||
for (const target of ["kiro", "cursor", "claude", "gemini"]) {
|
||||
test(`#9780: ${target} round-trip returns the declared name and its namespace`, () => {
|
||||
const body = pivot(target);
|
||||
const state = initState(FORMATS.OPENAI_RESPONSES) as Record<string, unknown>;
|
||||
state.requestToolIdentityMap = body._namespaceToolIdentityMap;
|
||||
|
||||
// The upstream echoes the flattened wire name (#8295).
|
||||
const events = openaiToOpenAIResponsesResponse(
|
||||
{
|
||||
id: "chatcmpl-9780",
|
||||
model: "any-model",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "call_9780",
|
||||
type: "function",
|
||||
function: { name: "functions__exec", arguments: '{"cmd":"git status"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||
},
|
||||
state
|
||||
) as Array<{ event: string; data: { item?: NamespaceIdentity } }>;
|
||||
|
||||
const added = events.find((e) => e.event === "response.output_item.added")?.data.item;
|
||||
assert.ok(added, "expected response.output_item.added");
|
||||
assert.deepEqual(
|
||||
{ name: added.name, namespace: added.namespace },
|
||||
{ name: "exec", namespace: "functions" }
|
||||
);
|
||||
});
|
||||
}
|
||||
196
tests/unit/nvidia-410-model-scope.test.ts
Normal file
196
tests/unit/nvidia-410-model-scope.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
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-nvidia-410-model-scope-"));
|
||||
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "nvidia-410-model-scope-test-secret";
|
||||
|
||||
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 fallback = await import("../../open-sse/services/accountFallback.ts");
|
||||
|
||||
const DEAD_MODEL = "deepseek-ai/deepseek-v4-pro";
|
||||
const HEALTHY_MODEL = "z-ai/glm-5.2";
|
||||
|
||||
const GONE_BODY = JSON.stringify({
|
||||
type: "about:blank",
|
||||
title: "Gone",
|
||||
status: 410,
|
||||
detail:
|
||||
"The model 'deepseek-ai/deepseek-v4-pro' has reached its end of life " +
|
||||
"and is no longer available.",
|
||||
});
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function seedNvidiaConnection() {
|
||||
return providersDb.createProviderConnection({
|
||||
provider: "nvidia",
|
||||
authType: "apikey",
|
||||
name: "nvidia-410-model-scope",
|
||||
apiKey: "sk-nvidia-410-model-scope",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("NVIDIA 410 Gone stays model-scoped and leaves the connection usable", async () => {
|
||||
const connection = await seedNvidiaConnection();
|
||||
|
||||
assert.equal(
|
||||
fallback.hasPerModelQuota("nvidia", DEAD_MODEL),
|
||||
true,
|
||||
"NVIDIA must use per-model failure scoping"
|
||||
);
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
connection.id,
|
||||
410,
|
||||
GONE_BODY,
|
||||
"nvidia",
|
||||
DEAD_MODEL
|
||||
);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
|
||||
const after = await providersDb.getProviderConnectionById(connection.id);
|
||||
|
||||
assert.equal(
|
||||
after?.rateLimitedUntil ?? null,
|
||||
null,
|
||||
"410 for one retired NVIDIA model must not apply a connection-wide cooldown"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
after?.testStatus,
|
||||
"active",
|
||||
"410 for one retired NVIDIA model must leave the NVIDIA connection active"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
fallback.isModelLocked("nvidia", connection.id, DEAD_MODEL),
|
||||
true,
|
||||
"the retired model itself should be locked"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
fallback.isModelLocked("nvidia", connection.id, HEALTHY_MODEL),
|
||||
false,
|
||||
"a healthy sibling NVIDIA model must remain unlocked"
|
||||
);
|
||||
|
||||
const healthyCredentials = await auth.getProviderCredentials("nvidia", null, null, HEALTHY_MODEL);
|
||||
|
||||
assert.equal(
|
||||
healthyCredentials?.connectionId,
|
||||
connection.id,
|
||||
"the same NVIDIA connection must remain selectable for healthy sibling models"
|
||||
);
|
||||
});
|
||||
|
||||
test("non-per-model provider keeps 410 connection-scoped", async () => {
|
||||
assert.equal(
|
||||
fallback.hasPerModelQuota("openai", DEAD_MODEL),
|
||||
false,
|
||||
"plain OpenAI API-key connections are not per-model quota providers"
|
||||
);
|
||||
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "openai-410-connection-scope",
|
||||
apiKey: "sk-openai-410-connection-scope",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
connection.id,
|
||||
410,
|
||||
"Gone",
|
||||
"openai",
|
||||
DEAD_MODEL
|
||||
);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
|
||||
const after = await providersDb.getProviderConnectionById(connection.id);
|
||||
|
||||
assert.ok(
|
||||
after?.rateLimitedUntil,
|
||||
"non-per-model providers should retain the existing connection-level 410 behavior"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
after?.testStatus,
|
||||
"unavailable",
|
||||
"410 model scoping must not be applied globally to every provider"
|
||||
);
|
||||
});
|
||||
|
||||
test("other per-model providers retain existing 410 connection scope", async () => {
|
||||
assert.equal(
|
||||
fallback.hasPerModelQuota("gemini", DEAD_MODEL),
|
||||
true,
|
||||
"Gemini provides a non-NVIDIA per-model control case"
|
||||
);
|
||||
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "gemini",
|
||||
authType: "apikey",
|
||||
name: "gemini-410-control",
|
||||
apiKey: "sk-gemini-410-control",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
|
||||
const result = await auth.markAccountUnavailable(
|
||||
connection.id,
|
||||
410,
|
||||
"Gone",
|
||||
"gemini",
|
||||
DEAD_MODEL
|
||||
);
|
||||
|
||||
assert.equal(result.shouldFallback, true);
|
||||
|
||||
const after = await providersDb.getProviderConnectionById(connection.id);
|
||||
|
||||
assert.ok(
|
||||
after?.rateLimitedUntil,
|
||||
"410 must remain connection-scoped for per-model providers without an explicit 410 contract"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
after?.testStatus,
|
||||
"unavailable",
|
||||
"the NVIDIA-specific 410 fix must not change other provider semantics"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
fallback.isModelLocked("gemini", connection.id, DEAD_MODEL),
|
||||
false,
|
||||
"a generic per-model provider must not inherit NVIDIA's 410 model lock"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user