Files
OmniRoute/tests/unit/model-capability-resolution-snapshot-9199.test.ts
Diego Rodrigues de Sa e Souza f6ccd3cf9f fix(quality): green release/v3.8.50 base-reds round 2 (#9985) (#10131)
* fix(quality): green release/v3.8.50 base-reds round 2 — gateways/conol/deepai corruption, migrations, docs, ratchets, dashboard-typecheck

Base-red fix for issue #9985 after the 2026-08-11 merge storm (99 PRs).

Real defects fixed:
- gateways.ts: close regolo entry (was swallowing naga-ac + chatanywhere from #9421), drop stale duplicate chatanywhere entry (#9594)
- conol-web + deepai registry: correct ../shared import depth + deepai executor:default
- modelSelectModalHelpers: close isProviderModelHidden (#9011)
- driverFactory.test.ts: restore eaten test-closing brace (#9173)
- usageTracking: remove duplicate cache_* props
- modelCapability{Overrides,ResolutionSnapshot,Capabilities}: max_token -> max_output_tokens (#9199 vs #8908) + test align
- videoGeneration: drop duplicate handleFalVideoGeneration import (mediaGeneration/fal canonical, #9982)
- responseSanitizer: cast input_tokens_details before .cached_tokens access
- EditConnectionModal: missing alibaba code fields, hoist validationPsd, providerPageHelpers Badge variant union
- FreeBudgetCard: t() -> labels.noApiKey
- peerRouting + cliRuntime: ProcessEnv typing
- image-combo.test.ts: type any -> unknown
- fal.test.ts: moved to tests/unit/services (collected path) 14 tests green
- remove duplicate 143_job_registry.sql (146 canonical), KNOWN_GAPS fix

Docs/ratchets (owner-authorized rebaselines, annotated):
- CHANGELOG 3.8.50 living section restored + 42 i18n mirrors
- MCP-SERVER.md 104->105 tools + i18n
- ENVIRONMENT.md/.env.example: ADOBE_FIREFLY_CHROME_HEADED + DEBUG_CLAUDE_NONSTREAM
- fabricated-docs allowlist: TELEGRAM proposal env vars
- file-size: 5 grown files + proxyFetch 1207->1220
- dead-code 230->248, codeql 2->9 (drift from merged PRs, not this PR)
- untrack _tasks symlink; agent-skills-sync --apply (config-codex-cli)

* fix(changelog): reformat two feature fragments to the bullet convention (#9239, #9490)

* fix(quality): prune stale ESLint suppressions (base-red)

* fix(quality): resolve open-sse type errors + catalog/build regressions (base-red round 3)

Storm-merge splices repaired in the base-fix PR #10131:
- doctor.ts: AppConfig missing brokerSocketPath
- conol-web.ts: Buffer not assignable to BodyInit (Uint8Array)
- tinycms.ts: TinyCmsExecutor.execute return matches BaseExecutor (response/url/transformedBody)
- tinycmsSigner.ts: encodeInto never-narrowing guard + dead wasm URL fallback (Turbopack)
- virtualFactory.ts: options slot for resolutionSnapshot
- bottleneckPatch.ts: insufficient-overlap casts (as unknown as)
- imageCombo.ts: narrow handleImageGeneration union result
- browser-worker.ts: AppConfig + turn.capabilities splice
- conolDiscovery.ts: getProviderOutboundGuard from Policy module
- catalog.ts: drop removed SWR hooks (getCatalogStaleWhileRevalidateMs + accessors), CatalogCachePolicy -> inline settings, resolve 4-arg call
- catalogCache.ts: remove dead inFlight/promise refs
- chat.ts: add isProviderBreakerFailureStatus import
- model-catalog-cache-swr-8728.test.ts: align to #9199 new API (policy injection removed)

* fix(quality): align UI test fixtures to current component contracts (base-red vitest)

- setup-wizard: provide required serverState prop (component gained it in a merged PR)
- grok-device-oauth-modal: next-intl stub resolves grok flow keys to EN labels
- provider-quota-widget: label now inline (PR #8916 removed AutoRefreshButtonLabel extraction) — test the widget
- use-provider-connections-cursor-refresh + phase1f: match /api/providers?provider=<id> query form; hoist heavy dynamic imports to module scope (timeout flake)
- home-topology: mock next/navigation useRouter (component added node-click navigation)
- cooling/lobe/AutoComboCatalog: raise cold-import describe timeouts to 30-60s
- request-logger-*: align to current detail-view contract

* fix(search): guard params.token undefined in serper headers (typecheck base-red)

* fix(search): guard token headers + non-null providerConfig (typecheck base-red)

* fix(changelog): restore base CHANGELOGs eaten by merge auto-resolve (43 files)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: backryun <bakryun0718@proton.me>
2026-08-12 16:09:34 -03:00

531 lines
19 KiB
TypeScript

/**
* #9199 — build-local ModelCapabilityResolutionSnapshot parity.
*
* Snapshot-backed resolution must match ordinary on-demand resolvers for the
* same pure chain (synced + max_token override + context override + alias
* fallback + missing data). The snapshot must not flip models.dev's module-
* global all-row cache.
*/
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-cap-snapshot-9199-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "cap-snapshot-9199-secret";
// Isolate getTokenLimit from host env CONTEXT_LENGTH_* overrides.
const originalContextLengthEnv = new Map(
Object.entries(process.env).filter(([key]) => key.startsWith("CONTEXT_LENGTH_"))
);
for (const key of originalContextLengthEnv.keys()) delete process.env[key];
const core = await import("../../src/lib/db/core.ts");
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
const capabilityOverrides = await import("../../src/lib/db/modelCapabilityOverrides.ts");
const contextOverrides = await import("../../src/lib/db/modelContextOverrides.ts");
const modelCapabilities = await import("../../src/lib/modelCapabilities.ts");
const contextManager = await import("../../open-sse/services/contextManager.ts");
const model = await import("../../open-sse/services/model.ts");
const { MODEL_SPECS } = await import("../../src/shared/constants/modelSpecs.ts");
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
test.after(() => {
core.resetDbInstance();
for (const key of Object.keys(process.env)) {
if (key.startsWith("CONTEXT_LENGTH_")) delete process.env[key];
}
for (const [key, value] of originalContextLengthEnv) process.env[key] = value;
try {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
});
function seedFixture() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
core.getDbInstance();
modelsDevSync.saveModelsDevCapabilities({
// Direct provider/model hit used by ordinary + snapshot paths.
"parity-provider": {
"parity-model": {
tool_call: true,
reasoning: true,
attachment: false,
structured_output: null,
temperature: true,
modalities_input: '["text"]',
modalities_output: '["text"]',
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: 111111,
limit_input: 100000,
limit_output: 2222,
interleaved_field: null,
},
},
// Alias-side storage only — canonical `opencode` must still resolve via fallback.
"opencode-zen": {
"zen-only-model": {
tool_call: true,
reasoning: false,
attachment: false,
structured_output: null,
temperature: true,
modalities_input: '["text"]',
modalities_output: '["text"]',
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: 333333,
limit_input: 300000,
limit_output: 4444,
interleaved_field: null,
},
},
});
assert.equal(
capabilityOverrides.setModelCapabilityOverride(
"parity-provider/parity-model",
"max_output_tokens",
99999
),
true
);
// Malformed capability override row must be filtered from list/bulk maps.
core
.getDbInstance()
.prepare(
"INSERT OR REPLACE INTO model_capability_overrides " +
"(provider, model_id, override_key, override_value, refreshed_at) " +
"VALUES (?, ?, ?, ?, datetime('now'))"
)
.run("parity-provider", "parity-model-bad", "max_output_tokens", "not-a-number");
// Collision pairs that a delimiter-composed key would merge: (a, b\0c) vs (a\0b, c).
assert.equal(
capabilityOverrides.setModelCapabilityOverride("a/b\u0000c", "max_output_tokens", 10101),
true
);
assert.equal(
capabilityOverrides.setModelCapabilityOverride("a\u0000b/c", "max_output_tokens", 20202),
true
);
assert.equal(contextOverrides.setModelContextOverride("a", "b\u0000c", 30303, "manual"), true);
assert.equal(contextOverrides.setModelContextOverride("a\u0000b", "c", 40404, "manual"), true);
// Real provider-model alias from open-sse/services/model.ts PROVIDER_MODEL_ALIASES.
// Override is stored only under the raw alias id, not the canonical model.
const aliasCanonical = model.resolveCanonicalProviderModel("github", "claude-4.5-opus");
assert.equal(aliasCanonical.provider, "github");
assert.equal(aliasCanonical.model, "claude-opus-4-5-20251101");
assert.notEqual(aliasCanonical.model, "claude-4.5-opus");
assert.equal(
capabilityOverrides.setModelCapabilityOverride("github/claude-4.5-opus", "max_output_tokens", 77777),
true
);
assert.equal(
capabilityOverrides.getModelCapabilityOverride(
"github",
"claude-opus-4-5-20251101",
"max_output_tokens"
),
null,
"override must exist only under the raw alias model id"
);
assert.equal(
contextOverrides.setModelContextOverride("parity-provider", "parity-model", 555555, "manual"),
true
);
}
function insertRawCapability(provider: string, modelId: string): void {
modelsDevSync.ensureCapabilitiesTable();
core
.getDbInstance()
.prepare(
"INSERT OR REPLACE INTO model_capabilities " +
"(provider, model_id, tool_call, reasoning, attachment, structured_output, temperature, " +
"modalities_input, modalities_output, knowledge_cutoff, release_date, last_updated, " +
"status, family, open_weights, limit_context, limit_input, limit_output, interleaved_field, last_synced) " +
"VALUES (?, ?, 1, 1, 0, NULL, 1, '[]', '[]', NULL, NULL, NULL, NULL, NULL, NULL, 111111, 100000, 2222, NULL, datetime('now'))"
)
.run(provider, modelId);
}
function pickParityFields(
resolved: ReturnType<typeof modelCapabilities.getResolvedModelCapabilities>
) {
return {
provider: resolved.provider,
model: resolved.model,
rawModel: resolved.rawModel,
toolCalling: resolved.toolCalling,
reasoning: resolved.reasoning,
supportsThinking: resolved.supportsThinking,
supportsTools: resolved.supportsTools,
supportsVision: resolved.supportsVision,
contextWindow: resolved.contextWindow,
maxInputTokens: resolved.maxInputTokens,
maxOutputTokens: resolved.maxOutputTokens,
attachment: resolved.attachment,
modalitiesInput: resolved.modalitiesInput,
modalitiesOutput: resolved.modalitiesOutput,
};
}
function assertOrdinarySnapshotParity(
provider: string,
modelId: string,
snapshot: modelCapabilities.ModelCapabilityResolutionSnapshot,
label: string
) {
const ordinaryCaps = modelCapabilities.getResolvedModelCapabilities({
provider,
model: modelId,
});
const snapshotCaps = modelCapabilities.getResolvedModelCapabilities(
{ provider, model: modelId },
snapshot
);
assert.deepEqual(
pickParityFields(snapshotCaps),
pickParityFields(ordinaryCaps),
`${label}: getResolvedModelCapabilities parity`
);
const ordinaryLimit = contextManager.getTokenLimit(provider, modelId);
const snapshotLimit = contextManager.getTokenLimit(provider, modelId, snapshot);
assert.equal(
snapshotLimit,
ordinaryLimit,
`${label}: getTokenLimit parity (got ${snapshotLimit}, want ${ordinaryLimit})`
);
const ordinaryContext = modelCapabilities.getModelContextLimit(provider, modelId);
const snapshotContext = modelCapabilities.getModelContextLimit(provider, modelId, snapshot);
assert.equal(snapshotContext, ordinaryContext, `${label}: getModelContextLimit parity`);
}
test("#9199 bulk capability rows treat prototype-shaped keys as data", () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
core.getDbInstance();
insertRawCapability("__proto__", "polluted-model");
insertRawCapability("prototype-provider", "__proto__");
assert.equal(Object.hasOwn(Object.prototype, "polluted-model"), false);
const ordinary = modelsDevSync.getSyncedCapability("__proto__", "polluted-model");
assert.equal(ordinary?.limit_context, 111111, "ordinary point lookup must support the row");
assert.equal(Object.hasOwn(Object.prototype, "polluted-model"), false);
try {
const bulk = modelsDevSync.loadAllSyncedCapabilitiesUncached();
assert.equal(Object.hasOwn(bulk, "__proto__"), true);
assert.equal(bulk["__proto__"]?.["polluted-model"]?.limit_output, 2222);
assert.equal(Object.hasOwn(bulk["prototype-provider"], "__proto__"), true);
assert.equal(bulk["prototype-provider"]?.["__proto__"]?.limit_context, 111111);
assert.equal(Object.hasOwn(Object.prototype, "polluted-model"), false);
} finally {
delete (Object.prototype as Record<string, unknown>)["polluted-model"];
}
});
test("#9199 uncached bulk load does not mutate models.dev all-row cache", () => {
seedFixture();
// clearModelsDevCapabilities leaves cachedCapabilitiesLoadedAll=true with {}.
modelsDevSync.clearModelsDevCapabilities();
assert.equal(modelsDevSync.getSyncedCapability("parity-provider", "parity-model"), null);
// Insert behind the module-global cache via raw SQL.
const db = core.getDbInstance();
insertRawCapability("parity-provider", "parity-model");
// Ordinary path still sees the empty module cache.
assert.equal(modelsDevSync.getSyncedCapability("parity-provider", "parity-model"), null);
const bulk = modelsDevSync.loadAllSyncedCapabilitiesUncached();
assert.equal(bulk["parity-provider"]?.["parity-model"]?.limit_context, 111111);
// Uncached bulk must not publish into the module-global cache.
assert.equal(
modelsDevSync.getSyncedCapability("parity-provider", "parity-model"),
null,
"loadAllSyncedCapabilitiesUncached must not flip cachedCapabilitiesLoadedAll data"
);
const originalPrepare = db.prepare;
const callPrepare = originalPrepare.bind(db);
const counts = { synced: 0, capabilityOverrides: 0, contextOverrides: 0 };
(db as unknown as { prepare: typeof db.prepare }).prepare = ((sql: string) => {
const normalized = String(sql).replace(/\s+/g, " ").trim();
if (normalized.includes("FROM model_capabilities")) counts.synced++;
if (normalized.includes("FROM model_capability_overrides")) counts.capabilityOverrides++;
if (normalized.includes("FROM model_context_overrides")) counts.contextOverrides++;
return callPrepare(sql);
}) as typeof db.prepare;
try {
const snapshot = modelCapabilities.createModelCapabilityResolutionSnapshot();
assert.equal(snapshot.synced["parity-provider"]?.["parity-model"]?.limit_output, 2222);
} finally {
(db as unknown as { prepare: typeof db.prepare }).prepare = originalPrepare;
}
assert.deepEqual(counts, { synced: 1, capabilityOverrides: 1, contextOverrides: 1 });
assert.equal(
modelsDevSync.getSyncedCapability("parity-provider", "parity-model"),
null,
"snapshot creation must remain build-local"
);
});
test("#9199 nested override maps keep delimiter-colliding pairs distinct", () => {
seedFixture();
const snapshot = modelCapabilities.createModelCapabilityResolutionSnapshot();
// Delimiter-composed keys would merge these pairs; nested maps must not.
assert.equal(snapshot.maxTokenOverrides.get("a")?.get("b\u0000c"), 10101);
assert.equal(snapshot.maxTokenOverrides.get("a\u0000b")?.get("c"), 20202);
assert.notEqual(
snapshot.maxTokenOverrides.get("a")?.get("b\u0000c"),
snapshot.maxTokenOverrides.get("a\u0000b")?.get("c")
);
assert.equal(snapshot.contextOverrides.get("a")?.get("b\u0000c"), 30303);
assert.equal(snapshot.contextOverrides.get("a\u0000b")?.get("c"), 40404);
assert.notEqual(
snapshot.contextOverrides.get("a")?.get("b\u0000c"),
snapshot.contextOverrides.get("a\u0000b")?.get("c")
);
assert.equal(
capabilityOverrides.getModelCapabilityOverride(
"a",
"b\u0000c",
"max_output_tokens",
snapshot.maxTokenOverrides
),
10101
);
assert.equal(
capabilityOverrides.getModelCapabilityOverride(
"a\u0000b",
"c",
"max_output_tokens",
snapshot.maxTokenOverrides
),
20202
);
assert.equal(
contextOverrides.getModelContextOverride("a", "b\u0000c", snapshot.contextOverrides),
30303
);
assert.equal(
contextOverrides.getModelContextOverride("a\u0000b", "c", snapshot.contextOverrides),
40404
);
assertOrdinarySnapshotParity("a", "b\u0000c", snapshot, "collision pair (a, b\\0c)");
assertOrdinarySnapshotParity("a\u0000b", "c", snapshot, "collision pair (a\\0b, c)");
assert.equal(
modelCapabilities.getResolvedModelCapabilities({ provider: "a", model: "b\u0000c" }, snapshot)
.maxOutputTokens,
10101
);
assert.equal(
modelCapabilities.getResolvedModelCapabilities({ provider: "a\u0000b", model: "c" }, snapshot)
.maxOutputTokens,
20202
);
assert.equal(modelCapabilities.getModelContextLimit("a", "b\u0000c", snapshot), 30303);
assert.equal(modelCapabilities.getModelContextLimit("a\u0000b", "c", snapshot), 40404);
});
test("#9199 snapshot-backed resolution matches ordinary resolvers across resolution paths", () => {
seedFixture();
const snapshot = modelCapabilities.createModelCapabilityResolutionSnapshot();
// Static MODEL_SPECS pin (not seeded in models.dev / overrides).
const staticSpec = MODEL_SPECS["gpt-4o-mini"];
assert.ok(staticSpec, "gpt-4o-mini must remain a real MODEL_SPECS entry");
assert.equal(typeof staticSpec.maxOutputTokens, "number");
assert.equal(typeof staticSpec.contextWindow, "number");
assert.equal(
MODEL_SPECS["glm-5-turbo"],
undefined,
"glm-5-turbo must stay absent from MODEL_SPECS so its case exercises PROVIDER_MODELS"
);
// Provider registry defaultContextLength path: no per-model catalog hit.
const defaultContextProvider = "gitlab-duo";
const defaultContextModel = "no-catalog-model-9199";
assert.equal(
REGISTRY[defaultContextProvider]?.defaultContextLength,
128000,
"gitlab-duo must keep a real provider-registry defaultContextLength"
);
assert.equal(
modelCapabilities.getModelContextLimit(defaultContextProvider, defaultContextModel),
null,
"default-context path requires no resolved per-model contextWindow"
);
// Model-name heuristic path: unknown provider with a claude-shaped model id.
const heuristicProvider = "unknown-provider-9199";
const heuristicModel = "custom-claude-lab";
assert.equal(REGISTRY[heuristicProvider], undefined);
assert.equal(
modelCapabilities.getModelContextLimit(heuristicProvider, heuristicModel),
null,
"heuristic path requires no resolved per-model contextWindow"
);
const cases: Array<{ provider: string; model: string; label: string }> = [
{ provider: "parity-provider", model: "parity-model", label: "direct synced + both overrides" },
{ provider: "opencode", model: "zen-only-model", label: "alias fallback to opencode-zen" },
{
provider: "github",
model: "claude-4.5-opus",
label: "canonical model alias with rawModel-only max_token override",
},
{
provider: "glm",
model: "glm-5-turbo",
label: "registry-backed model (PROVIDER_MODELS context/output limits)",
},
{
provider: "missing-provider",
model: "gpt-4o-mini",
label: "static MODEL_SPECS-backed model",
},
{
provider: defaultContextProvider,
model: defaultContextModel,
label: "provider registry defaultContextLength path",
},
{
provider: heuristicProvider,
model: heuristicModel,
label: "model-name heuristic/default path",
},
{ provider: "missing-provider", model: "missing-model", label: "missing synced + overrides" },
];
for (const entry of cases) {
assertOrdinarySnapshotParity(entry.provider, entry.model, snapshot, entry.label);
}
// Explicit override/precedence pins so the test is not only structural.
assert.equal(
modelCapabilities.getResolvedModelCapabilities(
{ provider: "parity-provider", model: "parity-model" },
snapshot
).maxOutputTokens,
99999,
"max_token override must win over synced limit_output"
);
assert.equal(
modelCapabilities.getModelContextLimit("parity-provider", "parity-model", snapshot),
555555,
"context override must win over synced limit_context / resolved contextWindow"
);
assert.equal(
modelCapabilities.getResolvedModelCapabilities(
{ provider: "opencode", model: "zen-only-model" },
snapshot
).contextWindow,
333333,
"canonical opencode must resolve capabilities stored under opencode-zen"
);
const aliasResolved = modelCapabilities.getResolvedModelCapabilities(
{ provider: "github", model: "claude-4.5-opus" },
snapshot
);
assert.equal(aliasResolved.rawModel, "claude-4.5-opus");
assert.equal(aliasResolved.model, "claude-opus-4-5-20251101");
assert.equal(
aliasResolved.maxOutputTokens,
77777,
"max_token override stored only under rawModel must still apply after alias resolution"
);
const canonicalOnly = modelCapabilities.getResolvedModelCapabilities(
{ provider: "github", model: "claude-opus-4-5-20251101" },
snapshot
);
assert.equal(canonicalOnly.rawModel, "claude-opus-4-5-20251101");
assert.equal(canonicalOnly.model, "claude-opus-4-5-20251101");
assert.notEqual(
canonicalOnly.maxOutputTokens,
77777,
"canonical id alone must not invent the rawModel-only max_token override"
);
// Registry-backed pins: glm-5-turbo is absent from MODEL_SPECS.
const registryResolved = modelCapabilities.getResolvedModelCapabilities(
{ provider: "glm", model: "glm-5-turbo" },
snapshot
);
assert.equal(registryResolved.contextWindow, 200000);
assert.equal(registryResolved.maxOutputTokens, 131072);
// Static MODEL_SPECS pin without a provider registry hit for this missing provider.
assert.equal(
modelCapabilities.getResolvedModelCapabilities(
{ provider: "missing-provider", model: "gpt-4o-mini" },
snapshot
).maxOutputTokens,
staticSpec.maxOutputTokens
);
assert.equal(
modelCapabilities.getResolvedModelCapabilities(
{ provider: "missing-provider", model: "gpt-4o-mini" },
snapshot
).contextWindow,
staticSpec.contextWindow
);
// Provider defaultContextLength path (getTokenLimit step 3).
assert.equal(
contextManager.getTokenLimit(defaultContextProvider, defaultContextModel, snapshot),
128000
);
// Model-name heuristic path (getTokenLimit step 4 → DEFAULT_LIMITS.claude).
assert.equal(contextManager.getTokenLimit(heuristicProvider, heuristicModel, snapshot), 200000);
// Missing path remains a pure default/fallback, not a catalog hit.
assert.equal(
modelCapabilities.getResolvedModelCapabilities(
{ provider: "missing-provider", model: "missing-model" },
snapshot
).contextWindow,
null
);
assert.equal(contextManager.getTokenLimit("missing-provider", "missing-model", snapshot), 128000);
assert.equal(
snapshot.maxTokenOverrides.get("parity-provider")?.has("parity-model-bad") ?? false,
false,
"malformed max_token rows must be filtered from the bulk map"
);
});