mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed Continuing the base-red drain — every one of these reproduces on the pure tip. - tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1. The diff is ADDITION-ONLY — the unorouter block from #9009; no existing provider entry changed. 3/3. - tests/unit/provider-models-route.test.ts:ff012ff420added onboardUser as a bootstrap fallback next to loadCodeAssist; the mock now excludes it from the discovery-URL ledger like it already excluded loadCodeAssist, otherwise it consumed the injected 503 and the retry assertion misfired. 59/59. - tests/unit/responses-commentary-passthrough-6199.test.ts: #8990 (c996dc93c2) deliberately preserves `tools` on the TERMINAL response.completed snapshot (Codex CLI rebuilds its tool list from it); the assertion now pins the echoed tools instead of their absence. Still stripped on created/in_progress. 7/7. - tests/unit/vision-compression-authoritative-capability-7237.test.ts:68cb678780added the 'gpt-5' fragment, so the heuristic-vs-spec DRIFT this suite documented no longer exists; the cases now guard the agreement, keep a conservative-for-unknown-ids probe, and reproduce the strip-bug shape with an explicit false instead of deriving it. 4/4. - tests/unit/provider-limits-proxy-fail-closed.test.ts + tests/unit/image-generation-route.test.ts: #9100 made the proxy reachability probe NON-BLOCKING (optimistic dispatch; the probe aborts only in-flight requests — its own t14 sibling was updated to this exact pattern). Instant mocks therefore won the race and the PROXY_UNREACHABLE 503 became unobservable (a success or a generic 502). The mocks now stay in flight (never-resolving, so the aborted continuation cannot reach the restored real fetch), and the fail-closed proof is the settled rejection itself plus zero egress AFTER the fast-fail. Production fail-closed semantics are unchanged — the proxy dispatch path still throws; only the mock timing was stale. 3/3 and 20/20. Refs #9298 * fix(guardrails): forward the router deps seam through callVisionModel tests/unit/guardrails/vision-bridge-sse-and-reasoning.test.ts was 7/7 red on any clean box (CI shard 3/4): callVisionModel() called getBestVisionModel()/ getFallbackModels() WITHOUT the routers' existing VisionBridgeRouterDeps seam, so the credential check always hit the live connections DB — no vision-capable connection meant 'No vision-capable provider connected' before the mocked fetch was ever reached, and on a dev box auto-selection could swap the fixed model under the assertions. The routers already accepted deps; only the forwarding was missing. Added the optional 5th param (backward compatible — the sole production caller, visionBridge.ts, injects its own callVisionModel and is unaffected) and the suite now pins selection with hasUsableCredentials: async () => null (indeterminate → the fixed model is honored, DB untouched). 7/7. Sibling suites re-run green: vision-bridge-callmodel 2/2, visionBridge 25/25, visionBridgeHelpers.callVisionModel 8/8, visionBridgeRouter 10/10, vision-bridge-cc-no-reroute 8/8. Refs #9298 * fix(db,combo): clear the NEW base-reds the 08-06 merge batch introduced The tip moved while the first sweep PR (#9600) was in review, and three fresh base-reds landed with it — same classes as before, all reproduced on the pure tip9995bc4893: 1. ANOTHER migration collision: #9061 shipped 134_ccr_blocks.sql onto the slot 134_proxy_logs_egress_ip.sql (#9291) has held since 08-04. getMigrationFiles() throws on collision, so every DB-touching test died at bootstrap again. Renumbered to 139 (next free slot). No retroactive guard needed this time: both statements are IF NOT EXISTS, and no DB can have applied it as 134 — the runner refused to run at all while the collision existed. 2. BROKEN IMPORT killing the combo module graph: #8894 imported preferAntigravityConnectionsWithStoredProject from ../antigravityProjectPersistence.ts — a module that exists NOWHERE in the repo (it came from an unmerged sibling branch). Anything importing quotaStrategies.ts died with ERR_MODULE_NOT_FOUND. Implemented the helper in the real persistence module (antigravityProjectPersist.ts, #8491) with the semantics the call site needs — prefer connections that already carry a stored projectId, never emptying the pool — and pointed the import there. New regression suite tests/unit/antigravity-prefer-stored-project.test.ts (5/5), including an import-graph probe that reproduces the break shape. 3. Sibling-test drift from #9106 (gemini-3.1-pro-high now user-callable): its own suites were updated but provider-models-route.test.ts was not. Expected discovery list realigned; testFrozen 1784->1787 justified in the baseline (irreducible +2 after comment compression; gate counts split-newlines). Also regenerated tests/snapshots/provider/translate-path.json — addition-only: devin-cli-agentic, raycast, regolo (today's provider merges), zero removals. image-generation-route 20/20 (was import-dead), provider-models-route 59/59, antigravity-prefer-stored-project 5/5, provider-translate-path-golden 3/3. Refs #9298 * fix(changelog): convert the #9415 fragment to the required bullet shape Another base-red from the 08-06 batch:bd4407cb64landed changelog.d/features/9415-newapi-sub2api-aggregator-balance.md as YAML frontmatter + a prose paragraph. Every other fragment in changelog.d/ is a single markdown bullet, and both consumers enforce that — scripts/check/check-changelog-integrity.mjs:97 and the release aggregator (scripts/release/aggregate-changelog.mjs:57) reject anything that does not start with '- ', so 'Merge integrity (changelog + generated skills)' was red for every PR targeting the release branch. Rewritten as a bullet with the standard issue link, preserving the feature description (aggregator gateway toggle, /api/user/self balance read, dashboard badge, quota-preflight skip, NEWAPI_AGGREGATOR_BALANCE flag default off, quotaPerUnit override). Swept the rest of changelog.d/ — this was the only malformed fragment. check:changelog-integrity OK. Refs #9298 * fix(types,docs): clear the 5 typecheck errors and the fabricated env vars on the base Third pass over the base-reds, from the 2026-08-06T22:51Z verdict on #9298 — it reported "Typecheck (core)" with only the FIRST error; there are five, all on the pure tip9995bc4893. Two are real production defects. **Real bugs** - open-sse/services/compression/engines/ccr/index.ts:295 called enforceGlobalBudget(entry.bytes) against an (owner, bytes) signature. The `bytes` argument arrived undefined, so `ccrTotalBytes + undefined` is NaN, `NaN > MAX` is false (the eviction loop exits immediately) and `NaN <= MAX` is false (the re-admit is refused). The #9061 durable tier therefore NEVER repopulated its in-memory map: every retrieve after a restart or an eviction re-read from SQLite forever, and evictions could not prefer the owning principal. Fixed and pinned by a new case in tests/unit/ccr-durable-store-9061.test.ts (11/11) — verified failing against the buggy call and passing against the fix. - open-sse/services/combo/fusionPanel.ts:54 read `step.model` after #8894 widened ComboStep with ComboProviderWildcardStep (which carries modelPattern, not model), so a wildcard step in a fusion panel pushed `undefined` onto the panel. Now resolved through getComboModelString(), which already handles every step shape and returns null for the ones without a concrete model id. **Type-only** - accountSemaphore.ts:203 — isBypassed() returns a plain boolean and cannot narrow `number | null` (an `x is null | undefined` predicate would be unsound: 0 bypasses too). Added resolveActiveCap(), the narrowing companion isBypassed is now defined in terms of; the acquire path uses the narrowed value. - comboStructure.ts:140 — same #8894 widening: `prompt` only exists on a model step, so it is now read under a kind check. - firecrawlQuotaFetcher.ts:136 — the function returns full FirecrawlQuota objects but was annotated Promise<QuotaInfo | null>, which made the custom-base literal an excess-property error. Widened to the accurate type (FirecrawlQuota extends QuotaInfo, so callers are unaffected). **Fabricated docs (the "Docs sync + fabricated-docs (strict)" HARD failure)** docs/ops/VM_DEPLOYMENT_GUIDE.md recommended OMNIROUTE_MAX_POOL_SIZE and OMNIROUTE_DB_POOL_SIZE (#9471). Neither is read anywhere in the codebase. Replaced with the two knobs that do exist and are already documented in ENVIRONMENT.md: OMNIROUTE_MEMORY_MB and OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT. typecheck:core 5 errors -> 0. check:fabricated-docs + check:env-doc-sync OK. accountSemaphore 6/6, ccr-durable-store 11/11, ccr-protocol 9/9, combo-fusion-strategy 10/10, combo-fusion-comboref 5/5, combo-fusion-warn 4/4, firecrawl-executor 7/7, executor-firecrawl-fetch 4/4. Refs #9298 * fix(tests): type the #3440 vertex helpers instead of `any` (the 3 base ESLint errors) The "ESLint errors: 3 error(s)" HARD failure in the #9298 verdict is tests/unit/vertex-functioncall-id-3440.test.ts lines 32/41/50: the three find*(result: any) walkers. `@typescript-eslint/no-explicit-any` is an ERROR in tests/ (and open-sse/) since #6218, and this file landed on 2026-08-04 without a suppressions entry, so every run of `lint:json --max-warnings 0` failed. That step prints nothing on failure, which is why the gate looked like a silent crash across the open PRs. Replaced with a GeminiRequestLike interface describing exactly what the three walkers traverse (contents[].parts[]), so the assertions keep their meaning and nothing is cast away. eslint on the file: clean. Suite: 6/6. Refs #9298 * docs(proxy): use an RFC 5737 documentation IP in the proxy examples The #9298 verdict headlines its docs failure with `L810 [stale-version] 1.2.3: const removed = await failOneproxyProxy("1.2.3.4", 8080)`. That is a false positive: check-deprecated-versions.mjs matches `/\bv?[12]\.\d+\.\d+\b/`, and the example IP literal 1.2.3.4 contains "1.2.3". Swapped both occurrences in PROXY_GUIDE.md (and its pl mirror) for 203.0.113.7, from the RFC 5737 documentation range that exists precisely for examples — it cannot collide with a version pattern and is the correct thing to print in docs regardless. Drift count 64 -> 62; no gate threshold was touched. The gate that actually FAILED under "Docs sync + fabricated-docs (strict)" was check:fabricated-docs (the invented pool env vars), fixed in the previous commit; this one removes the misleading line the verdict quotes. * test(base): allowlist probeUtils and realign the #7849 suite to the replacement bound Two more base-reds, both visible only after the migration collision stopped killing the shards. **check-db-rules — src/lib/db/probeUtils.ts not classified** #9541 added probeUtils.ts (transient-error retry for the SQLite corruption probe). It is imported ONLY by src/lib/db/core.ts, exactly like its siblings schemaColumns / optimizationSettings / providerNodeSelect, so re-exporting it through localDb.ts would push callers toward the barrel-import anti-pattern the gate exists to prevent. Added to INTENTIONALLY_INTERNAL with that rationale. check-db-rules 22/22, check:db-rules exit 0. **session-dedup-memory-7849 — pinned a mechanism that was replaced**7f36b192f0(#7855 follow-up) swapped the shared "suffix work budget" for the MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and deleted both the budget and its SUFFIX_WORK_BUDGET_WARNING string. It updated session-dedup.test.ts but not this sibling, so 3 of its 4 cases asserted a warning that can no longer be emitted. Realigned to the contract that actually survives — which is the invariant #7849 was opened for, not the mechanism: - the pathological pair must stay BOUNDED (completes in <4s, body intact) — measured at ~280ms on the current guards; - it must FAIL OPEN — original body returned by identity, compressed false, stats null (the explanatory zero-savings stats belonged to the removed budget path, which skipped before producing any); - the 512 MiB child fixture must still exit 0 with the full engine chain (session-dedup, lite, rtk, headroom, caveman) — that IS the OOM guard — and session-dedup must still report its skip, now pinned by prefix since the reason string moved with the mechanism. No threshold was loosened and no case was deleted: 4/4 here, 8/8 on the sibling session-dedup.test.ts. Refs #9298 * docs(mcp): bump the tool count to 105 and realign two vitest count pins Three more base-reds from the same 08-06 batch, all count/contract drift that the merged PRs left in sibling files. **Docs Gates (fast-path) — 3 STRICT drifts** check:docs-counts measures the MCP tool set from live code: it is 105 now (#8925 added omniroute_create_combo), while README.md, AGENTS.md and docs/frameworks/MCP-SERVER.md still claimed 104. Updated all five occurrences (two of them inside SVG alt text). check:docs-all exits 0. **Vitest (fast-path) — 2 failures** - open-sse/mcp-server/__tests__/essentialTools.test.ts pinned 11 phase-1 tools; #8925 shipped omniroute_create_combo as phase 1, making it 12. Verified by enumerating MCP_ESSENTIAL_TOOLS directly. - tests/unit/autoCombo/provider-family-combos.test.ts pinned the auto/glm provider set to [auggie, glm, zai]. #8914 (Devin ACP bridge) added devin-cli-agentic, whose catalog (registry/devin/catalog.ts:90-93) advertises the glm-5-2* line — so it belongs in the family pool for exactly the reason the test's own comment gives for auggie: a no-auth backend that genuinely serves a family model is a legitimate member. Expected set updated, invariant unchanged. npm run test:vitest 36/36 files, 340/340 tests. Refs #9298 * fix(combo,usage,oauth): drain the base-reds the shard fix exposed With the migration collision and the broken import out of the way the four unit shards actually run, and a further layer of base-reds became visible on the pure tip9995bc4893. Three are production defects. **Production defects** - open-sse/services/combo/runtimeUnitCapacity.ts:58 called resolveComboTargets() WITHOUT the hidden-model snapshot, so it fell back to the default getHiddenModelsByProvider() — a fresh full key_value read PER nested combo-ref unit, on every request. #8878 threaded the snapshot through the other call sites and missed this one. Threaded it from executeRuntimeUnitCombo (and from the dispatchPrelude call site), restoring the one-snapshot-per-request invariant combo-hidden-leaf-routing.test.ts pins. 9/9. - open-sse/services/usage/firecrawl.ts silently ignored its own `apiKey` parameter:91bb6aa619moved the fetch to fetchFirecrawlQuota(connectionId, connection), which reads the key off the connection record, so any caller passing the key directly got "Firecrawl API key not available". The explicit key is now merged into the connection passed down. firecrawl-usage 8/8. - src/lib/oauth/constants/oauth.ts was missing a RAYCAST entry in PROVIDERS while src/lib/oauth/providers/index.ts registers `raycast` (#8895), so every consumer reading PROVIDERS did not know Raycast Pro exists. Also added its OAUTH_TEST_CONFIG entry (checkExpiry only — it is an `import_token` provider with refreshToken always null), which #8408's guard explicitly requires rather than grandfathering. oauth-providers-config 25/25, oauth-test-config-8408 2/2. **Count / contract drift from the same batch** - feature flags 45 -> 46, APIKEY_PROVIDERS 197 -> 198 (Raycast Pro #8895), unique MCP tools 107 -> 108. Each re-derived from the source of truth. - vi + pt-BR locales: translated the 8 keys #9415 added (providers.newApiAggregator* and providers.modelTestQuotaTooltip) instead of relaxing the parity guard. i18n-vi 5/5, i18n-pt-br 3/3. - login-bootstrap-route: #9491 added `authenticated` to the require-login payload so /login can redirect an active session; the three deepEqual bodies now carry it. 10/10. **Flaky-by-construction, made deterministic** tests/unit/chat-combo-live-test.test.ts asserted the early-keepalive frame with a 100ms mocked upstream while resolveKeepaliveThreshold() is 2000ms for openai/*. It only ever passed while unrelated handler latency happened to push the total past the threshold — incidental, not deterministic, and it stopped holding once the handler got faster. The mock now sleeps 2400ms so the slow path is guaranteed and the assertion means what it says. 5/5. typecheck:core exit 0. check:file-size (base-relative) OK. Refs #9298 * test(base): run the orphaned #8890 suite and realign three mechanism pins **check:test-discovery — a suite that had NEVER executed** #8890 landed open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts into a directory no runner collects (only one explicit file from that folder is in vitest.mcp.config.ts), so it ran zero times since it merged. Wired it into the runner AND into check-test-discovery.mjs's mirrored collector list, which the gate keeps in sync deliberately. It passes 4/4 now that it actually runs — test:vitest goes 36 -> 37 files, 340 -> 344 tests. **check-db-rules-classification** — 37 -> 38 audited modules, adding probeUtils alongside the INTENTIONALLY_INTERNAL entry from the previous commit. **ratelimit-reservoir-refresh** — #9604 (rolling RPM leases) DELETED Bottleneck's fixed-window reservoir, so currentReservoir() is null and the poll for `reservoir === 2` could never settle. It updated several sibling suites but not this one. The pin on the removed mechanism is gone; what remains is the invariant the original Bottleneck heartbeat bug actually broke and that #9529 opened this test for — after a header-learned updateSettings() the limiter must keep admitting work, proven by racing a post-exhaustion request against a 5s timer. 1/1. **translator-openai-to-gemini** — #9568 (c9a3361e5a) made buildChangedToolNameMap emit IDENTITY entries too, because Gemini lowercases tool names in functionCall responses and the response translator needs a key to map them back. Any request carrying tools therefore carries `_toolNameMap` in the Antigravity envelope now. Expected key list updated and the map's contents asserted explicitly rather than left implicit. 45/45. Refs #9298 * fix(db): restore node-backed synced catalogs and realign the #8944 context hints **Production regression from #9294 (d69f521491)** lookupModelMeta moved from getSyncedAvailableModels(providerId) to getActiveSyncedCatalog(providerId). The new reader unions models only from rows in `provider_connections` with isActive = 1 — but a provider NODE lives in `provider_nodes` and NEVER has a connections row, so filtering by active connection ids silently dropped every node's synced catalog. The consequence was not just a missing list: lookupModelMeta reads that catalog for RUNTIME METADATA, so for openai-compatible nodes it took out - `supportedThinkingEfforts`, which is what splitSyncedEffortSuffix needs — so `<prefix>/<model>-high` stopped resolving to the base id and the effort was never derived (#7694), and - `contextWindow` / `maxInputTokens`, used by the combo context-window filter. getActiveSyncedCatalog now falls back to the provider-wide key_value set — the exact pre-#9294 source — when no active connection carries a catalog, and marks that fallback explicitly NON-authoritative. #9294's live-catalog gating is about what an active connection actually serves, so a node-backed catalog informs metadata while never being able to reject a model as unavailable. `available` therefore stays fail-open for nodes, as it was before. sync-reasoning-supported-efforts-7694 23/23 (was 21/2). live-model-catalog-reconciliation-8926 11/11 and combo-provider-wildcard 23/23 confirm #9294's own coverage is untouched. **#8944 sibling-test drift**714a315a1a("Treat context metadata as a routing hint") deliberately turned the context-window check from a HARD filter into an ordering hint: a catalog-too-small target is demoted, not removed, because a stale catalog entry must never delete the only target that could accept the request at runtime. The PR updated one case in this suite and left three asserting the old drop behaviour. Realigned to the new contract — the too-small target must lose the ordering to the fitting one while remaining present — and renamed them from "still rejects"/"still dropped" to "is demoted"/"ordered last" so the names stop describing the removed behaviour. 14/14. **file-size** tests/unit/translator-openai-to-gemini.test.ts testFrozen 1616 -> 1619: the frozen value sat exactly at the base size, so the 3 lines the previous commit's _toolNameMap alignment needs could not fit. Justified in the baseline. typecheck:core exit 0. Refs #9298 * chore(stryker): register the two covering suites missing from tap.testFiles check:mutation-test-coverage flags any unit test that covers a mutated module but is absent from stryker.conf.json tap.testFiles — without the entry its mutant kills do not count toward the module's score. - tests/unit/antigravity-prefer-stored-project.test.ts covers open-sse/services/combo/quotaStrategies.ts (added earlier in this PR). - tests/unit/executor-devin-cli-agentic-acp.test.ts covers src/sse/services/auth.ts — pre-existing drift, same gate, same fix. Inserted in alphabetical position only; the rest of the file is byte-identical (it is not prettier-formatted upstream and reformatting it is out of scope here). Refs #9298 * fix(db): drop the never-wired getSessionModelUsageCounts (knip regression) The dead-code ratchet only ran once the earlier Fast Quality Gates steps stopped failing, and it lands at 228 vs baseline 227. The extra symbol is src/lib/db/contextHandoffs.ts::getSessionModelUsageCounts, added by #8894 "for least-used strategy" and never wired: the least-used branch in applyStrategyOrdering.ts uses the pre-existing sortTargetsByUsage(), and the helper has no caller in src/, open-sse/ or tests/. It is the same incomplete-PR shape as that PR's import of a module which does not exist in the repo (fixed earlier in this branch). Removed rather than baselined — bumping the ratchet would loosen the gate, and removal is exactly the remedy the gate prescribes. Same treatment the Dario installer's never-wired uninstall() got in #9600. The implementation is recoverable froma598fbb090whenever someone actually wires a session-aware least-used strategy. check:dead-code 228 -> 227 (baseline untouched). check:db-rules exit 0. context-handoff 13/13, db-context-handoffs 7/7, service-context-handoff 11/11. Refs #9298 * fix(security): embed the Raycast signature secret via resolvePublicCred (HR#11) The secret-scan ratchet only ran once the earlier Fast Quality Gates steps stopped failing, and it lands at 1 finding vs baseline 0. The finding is open-sse/services/raycast.ts:19 — RAYCAST_DEFAULT_SIG_SECRET, a 64-hex request-signature secret that #8895 committed as a bare string literal. It is genuinely public (community-extracted from the Raycast macOS client; the SAME value ships to every install, it is not a per-user credential), which is exactly the category Hard Rule #11 governs: public upstream credentials MUST go through resolvePublicCred() (open-sse/utils/publicCreds.ts), never a literal — see docs/security/PUBLIC_CREDS.md. So the fix is the mandated pattern, not a .gitleaks.toml allowlist entry: added `raycast_sig_secret` to EMBEDDED_DEFAULTS as the XOR-masked byte sequence and resolved it with the existing RAYCAST_SIG_SECRET env override. The providerSpecificData.sigSecret override is untouched. Verified the decoded value is byte-identical to the literal it replaces. check:secrets secretFindings 1 -> 0. check:public-creds exit 0. publicCreds 12/12, raycast-auth 6/6, raycast-local-extract 1/1, trae-publiccred 3/3. typecheck:core exit 0. Refs #9298 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
1787 lines
61 KiB
TypeScript
1787 lines
61 KiB
TypeScript
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-provider-model-routes-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const modelsDb = await import("../../src/lib/db/models.ts");
|
|
const providerModelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
|
|
const antigravityVersion = await import("../../open-sse/services/antigravityVersion.ts");
|
|
const providerRegistry = await import("../../open-sse/config/providerRegistry.ts");
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
const originalAllowPrivateProviderUrls = process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS;
|
|
|
|
async function resetStorage() {
|
|
globalThis.fetch = originalFetch;
|
|
if (originalAllowPrivateProviderUrls === undefined) {
|
|
delete process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS;
|
|
} else {
|
|
process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS = originalAllowPrivateProviderUrls;
|
|
}
|
|
antigravityVersion.clearAntigravityVersionCaches();
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
async function seedConnection(provider, overrides = {}) {
|
|
return providersDb.createProviderConnection({
|
|
provider,
|
|
authType: overrides.authType || "apikey",
|
|
name: overrides.name || `${provider}-${Math.random().toString(16).slice(2, 8)}`,
|
|
apiKey: overrides.apiKey,
|
|
accessToken: overrides.accessToken,
|
|
projectId: overrides.projectId,
|
|
isActive: overrides.isActive ?? true,
|
|
testStatus: overrides.testStatus || "active",
|
|
providerSpecificData: overrides.providerSpecificData || {},
|
|
});
|
|
}
|
|
|
|
async function callRoute(connectionId, search = "") {
|
|
return providerModelsRoute.GET(
|
|
new Request(`http://localhost/api/providers/${connectionId}/models${search}`),
|
|
{ params: { id: connectionId } }
|
|
);
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
globalThis.fetch = originalFetch;
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
test("provider models route returns a static local catalog for non-LLM search/agent providers (#5569/#5571/#5573/#5575)", async () => {
|
|
const cases = [
|
|
{ provider: "jules", expectId: "jules" },
|
|
{ provider: "linkup-search", expectId: "standard" },
|
|
{ provider: "ollama-search", expectId: "web_search" },
|
|
{ provider: "searchapi-search", expectId: "google" },
|
|
];
|
|
for (const { provider, expectId } of cases) {
|
|
const connection = await seedConnection(provider, { apiKey: `${provider}-key` });
|
|
const response = await callRoute(connection.id);
|
|
// RED before the fix: these had no static catalog → 400 "does not support models listing".
|
|
assert.equal(response.status, 200, `${provider} should not 400 on model import`);
|
|
const body = await response.json();
|
|
assert.equal(body.source, "local_catalog", `${provider} should serve a local catalog`);
|
|
const ids = (body.models || []).map((m) => m.id);
|
|
assert.ok(
|
|
ids.includes(expectId),
|
|
`${provider} should list "${expectId}"; got: ${ids.join(", ")}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("provider models route fetches the live AI/ML API catalog from the auth-free /models endpoint (#5570)", async () => {
|
|
const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" });
|
|
let calledUrl = "";
|
|
globalThis.fetch = async (url) => {
|
|
calledUrl = String(url);
|
|
return Response.json([
|
|
{ id: "openai/gpt-5.5", type: "chat-completion", info: { name: "GPT-5.5" } },
|
|
{ id: "zhipu/glm-5.2", type: "chat-completion", info: { name: "GLM 5.2" } },
|
|
{ id: "flux/flux-pro", type: "image", info: { name: "FLUX Pro" } },
|
|
]);
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
// RED before the fix: aimlapi had no PROVIDER_MODELS_CONFIG entry → stale
|
|
// 6-model local seed (source "local_catalog"), live endpoint never called.
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.equal(calledUrl, "https://api.aimlapi.com/models");
|
|
const ids = body.models.map((m: any) => m.id);
|
|
assert.ok(ids.includes("openai/gpt-5.5") && ids.includes("zhipu/glm-5.2"));
|
|
assert.ok(!ids.includes("flux/flux-pro"), "non-chat model types are filtered out");
|
|
});
|
|
|
|
test("provider models route falls back to the local AI/ML API catalog when the live fetch fails (#5570)", async () => {
|
|
const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" });
|
|
globalThis.fetch = async () => new Response("upstream down", { status: 500 });
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.ok(body.models.length > 0);
|
|
});
|
|
|
|
test("provider models route returns 404 for unknown connections", async () => {
|
|
const response = await callRoute("missing-connection");
|
|
|
|
assert.equal(response.status, 404);
|
|
assert.deepEqual(await response.json(), { error: "Connection not found" });
|
|
});
|
|
|
|
test("provider models route rejects connections with an empty provider id", async () => {
|
|
const connection = await seedConnection("openai", {
|
|
apiKey: "sk-openai",
|
|
});
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare("UPDATE provider_connections SET provider = '' WHERE id = ?").run(connection.id);
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.deepEqual(await response.json(), { error: "Invalid connection provider" });
|
|
});
|
|
|
|
test("provider models route rejects OpenAI-compatible providers without a base URL", async () => {
|
|
const connection = await seedConnection("openai-compatible-demo", {
|
|
apiKey: "sk-openai-compatible",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.deepEqual(await response.json(), {
|
|
error: "No base URL configured for OpenAI compatible provider",
|
|
});
|
|
});
|
|
|
|
// #6939: model-list discovery must match the test-connection guard tier (local-first ON by
|
|
// default allows LAN/loopback hosts) — see provider-models-route-lan-guard.test.ts for the
|
|
// disabled-default (still-blocked) counterpart.
|
|
test("provider models route allows private/LAN OpenAI-compatible base URLs under the local-first default (#6939)", async () => {
|
|
delete process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS;
|
|
delete process.env.OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS;
|
|
|
|
const connection = await seedConnection("openai-compatible-private", {
|
|
apiKey: "sk-openai-compatible",
|
|
providerSpecificData: {
|
|
baseUrl: "http://127.0.0.1:11434/v1",
|
|
},
|
|
});
|
|
|
|
let called = false;
|
|
globalThis.fetch = async () => {
|
|
called = true;
|
|
return Response.json({ data: [] });
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(called, true);
|
|
});
|
|
|
|
test("provider models route returns auth failures from OpenAI-compatible upstreams", async () => {
|
|
const connection = await seedConnection("openai-compatible-auth", {
|
|
apiKey: "sk-openai-compatible",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/v1/chat/completions",
|
|
},
|
|
});
|
|
const seenUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
return new Response("unauthorized", { status: 401 });
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 401);
|
|
assert.deepEqual(await response.json(), { error: "Auth failed: 401" });
|
|
assert.equal(seenUrls.length, 1);
|
|
});
|
|
|
|
test("provider models route falls back after OpenAI-compatible endpoint probes all fail", async () => {
|
|
const connection = await seedConnection("openai-compatible-fallback", {
|
|
apiKey: "sk-openai-compatible",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/v1",
|
|
},
|
|
});
|
|
const seenUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
return new Response("bad gateway", { status: 502 });
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "openai-compatible-fallback");
|
|
assert.ok(Array.isArray(body.models));
|
|
assert.ok(seenUrls.length >= 2);
|
|
});
|
|
|
|
test("provider models route retries transient OpenAI-compatible probe failures before succeeding", async () => {
|
|
const connection = await seedConnection("openai-compatible-retry", {
|
|
apiKey: "sk-openai-compatible",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/v1",
|
|
},
|
|
});
|
|
const seenUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
if (seenUrls.length === 1) {
|
|
throw new Error("temporary upstream failure");
|
|
}
|
|
|
|
return Response.json({
|
|
data: [{ id: "demo-model", name: "Demo Model" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(seenUrls, [
|
|
"https://proxy.example.com/v1/models",
|
|
"https://proxy.example.com/v1/models",
|
|
]);
|
|
assert.deepEqual(body.models, [{ id: "demo-model", name: "Demo Model" }]);
|
|
});
|
|
|
|
test("provider models route discovers SiliconFlow models from configured China base URL", async () => {
|
|
const connection = await seedConnection("siliconflow", {
|
|
apiKey: "sf-cn-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.siliconflow.cn/v1",
|
|
},
|
|
});
|
|
const seenRequests: Array<{
|
|
url: string;
|
|
method: string | undefined;
|
|
authorization: string | null;
|
|
}> = [];
|
|
|
|
globalThis.fetch = async (url, init) => {
|
|
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
|
seenRequests.push({
|
|
url: String(url),
|
|
method: init?.method,
|
|
authorization: headers.get("authorization"),
|
|
});
|
|
|
|
return Response.json({
|
|
data: [{ id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", name: "Qwen3 Coder" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id, "?refresh=true");
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "siliconflow");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(seenRequests, [
|
|
{
|
|
url: "https://api.siliconflow.cn/v1/models",
|
|
method: "GET",
|
|
authorization: "Bearer sf-cn-key",
|
|
},
|
|
]);
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
name: "Qwen3 Coder",
|
|
owned_by: "siliconflow",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route handles local hostnames named 'v1' correctly", async () => {
|
|
const connection = await seedConnection("openai-compatible-local-v1", {
|
|
apiKey: "sk-local",
|
|
providerSpecificData: {
|
|
baseUrl: "http://v1/chat/completions",
|
|
},
|
|
});
|
|
const seenUrls: string[] = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
return Response.json({
|
|
data: [{ id: "local-v1-model", name: "Local v1 Model" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(seenUrls, ["http://v1/v1/models"]);
|
|
});
|
|
|
|
test("provider models route correctly strips standard /v1 paths", async () => {
|
|
const connection = await seedConnection("openai-compatible-standard-v1", {
|
|
apiKey: "sk-standard",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
},
|
|
});
|
|
const seenUrls: string[] = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
return Response.json({
|
|
data: [{ id: "standard-model", name: "Standard Model" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(seenUrls, ["https://api.openai.com/v1/models"]);
|
|
});
|
|
|
|
test("provider models route strips /v1 when it precedes /chat/completions (#5899 no double /v1)", async () => {
|
|
// Regression for #5899 (Api Airforce): a baseUrl of the form
|
|
// "https://api.airforce/v1/chat/completions" must probe ".../v1/models" — NOT
|
|
// ".../v1/v1/models". The old `else if` strip chain only removed
|
|
// "/chat/completions", leaving a trailing "/v1" that the endpoint builder then
|
|
// doubled, producing a 308 redirect that aborted discovery.
|
|
const connection = await seedConnection("openai-compatible-airforce-v1", {
|
|
apiKey: "sk-airforce",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.airforce/v1/chat/completions",
|
|
},
|
|
});
|
|
const seenUrls: string[] = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
return Response.json({
|
|
data: [{ id: "airforce-model", name: "Airforce Model" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
// First probed endpoint must have a single /v1 — no ".../v1/v1/models".
|
|
assert.equal(seenUrls[0], "https://api.airforce/v1/models");
|
|
assert.ok(
|
|
!seenUrls.some((u) => u.includes("/v1/v1/")),
|
|
`no endpoint should contain a doubled /v1: ${JSON.stringify(seenUrls)}`
|
|
);
|
|
});
|
|
|
|
test("provider models route continues probing past a REDIRECT_BLOCKED endpoint (#5899)", async () => {
|
|
// Regression for #5899: a REDIRECT_BLOCKED error on one candidate endpoint must
|
|
// not abort the whole probe loop — discovery should fall through to the next
|
|
// endpoint instead of surfacing an empty catalog.
|
|
const connection = await seedConnection("openai-compatible-redirect-v1", {
|
|
apiKey: "sk-redirect",
|
|
providerSpecificData: {
|
|
baseUrl: "https://redirect.example",
|
|
},
|
|
});
|
|
const seenUrls: string[] = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const u = String(url);
|
|
seenUrls.push(u);
|
|
// First candidate ".../v1/models" answers with a real 308 redirect →
|
|
// safeOutboundFetch throws a SafeOutboundFetchError(REDIRECT_BLOCKED). The old
|
|
// code re-threw on it (status 503) and aborted the loop; the fix `continue`s.
|
|
if (u === "https://redirect.example/v1/models") {
|
|
return new Response(null, {
|
|
status: 308,
|
|
headers: { location: "https://redirect.example/models" },
|
|
});
|
|
}
|
|
return Response.json({
|
|
data: [{ id: "redirect-model", name: "Redirect Model" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
// Without the REDIRECT_BLOCKED `continue`, discovery aborted and fell back to a
|
|
// non-api catalog. The fix lets it reach the next endpoint and return live models.
|
|
assert.equal(body.source, "api");
|
|
assert.ok(
|
|
seenUrls.length >= 2,
|
|
`expected the loop to continue past REDIRECT_BLOCKED: ${JSON.stringify(seenUrls)}`
|
|
);
|
|
});
|
|
|
|
test("provider models route returns static catalog entries for providers with hardcoded models", async () => {
|
|
const connection = await seedConnection("bailian-coding-plan", {
|
|
apiKey: "bailian-key",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "bailian-coding-plan");
|
|
assert.equal(body.models.length, providerRegistry.REGISTRY["bailian-coding-plan"].models?.length);
|
|
assert.deepEqual(
|
|
body.models.map((model) => model.id),
|
|
providerRegistry.REGISTRY["bailian-coding-plan"].models?.map((model) => model.id)
|
|
);
|
|
});
|
|
|
|
test("provider models route returns AWS Polly speech engines from the audio registry", async () => {
|
|
const connection = await seedConnection("aws-polly", {
|
|
apiKey: "aws-secret-key",
|
|
providerSpecificData: {
|
|
accessKeyId: "AKIA_TEST",
|
|
region: "us-east-1",
|
|
},
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "aws-polly");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.deepEqual(
|
|
body.models.map((model) => model.id),
|
|
["standard", "neural", "long-form", "generative"]
|
|
);
|
|
});
|
|
|
|
test("provider models route returns the local catalog for GitLab Duo fallback models", async () => {
|
|
const connection = await seedConnection("gitlab", {
|
|
apiKey: "glpat-test",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "gitlab");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.deepEqual(body.models, [
|
|
{ id: "gitlab-duo-code-suggestions", name: "GitLab Duo Code Suggestions" },
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers local OpenAI-style models without requiring an API key", async () => {
|
|
process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS = "true";
|
|
|
|
const lmStudioConnection = await seedConnection("lm-studio", {
|
|
providerSpecificData: {
|
|
baseUrl: "http://localhost:1234/v1",
|
|
},
|
|
});
|
|
const lemonadeConnection = await seedConnection("lemonade", {
|
|
providerSpecificData: {
|
|
baseUrl: "http://localhost:13305/api/v1",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const target = String(url);
|
|
assert.equal(init.headers.Authorization, undefined);
|
|
if (target === "http://localhost:1234/v1/models") {
|
|
return Response.json({
|
|
data: [{ id: "local-model", name: "Local Model" }],
|
|
});
|
|
}
|
|
if (target === "http://localhost:13305/api/v1/models") {
|
|
return Response.json({
|
|
data: [{ id: "Llama-3.2-1B-Instruct-Hybrid", name: "Lemonade Llama" }],
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch: ${target}`);
|
|
};
|
|
|
|
const lmStudioResponse = await callRoute(lmStudioConnection.id);
|
|
const lmStudioBody = (await lmStudioResponse.json()) as any;
|
|
const lemonadeResponse = await callRoute(lemonadeConnection.id);
|
|
const lemonadeBody = (await lemonadeResponse.json()) as any;
|
|
|
|
assert.equal(lmStudioResponse.status, 200);
|
|
assert.equal(lmStudioBody.provider, "lm-studio");
|
|
assert.equal(lmStudioBody.source, "api");
|
|
assert.deepEqual(lmStudioBody.models, [{ id: "local-model", name: "Local Model" }]);
|
|
|
|
assert.equal(lemonadeResponse.status, 200);
|
|
assert.equal(lemonadeBody.provider, "lemonade");
|
|
assert.equal(lemonadeBody.source, "api");
|
|
assert.deepEqual(lemonadeBody.models, [
|
|
{ id: "Llama-3.2-1B-Instruct-Hybrid", name: "Lemonade Llama" },
|
|
]);
|
|
});
|
|
|
|
test("provider models route returns the local catalog for built-in image providers", async () => {
|
|
const connection = await seedConnection("topaz", {
|
|
apiKey: "topaz-key",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "topaz");
|
|
assert.ok(Array.isArray(body.models));
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "topaz-enhance",
|
|
name: "topaz-enhance",
|
|
apiFormat: "images",
|
|
supportedEndpoints: ["images"],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route prefers the remote OpenRouter /models API over static image models", async () => {
|
|
const connection = await seedConnection("openrouter", {
|
|
apiKey: "openrouter-key",
|
|
});
|
|
const seenUrls = [];
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
seenUrls.push(String(url));
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer openrouter-key");
|
|
return Response.json({
|
|
data: [{ id: "openai/gpt-4.1", name: "GPT-4.1 via OpenRouter" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id, "?refresh=true");
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(seenUrls, ["https://openrouter.ai/api/v1/models"]);
|
|
// #6976 — OpenRouter's live /v1/models never lists embeddings/rerank (they live
|
|
// on dedicated endpoints), so the curated specialty catalog is folded into the
|
|
// live-discovery response additively; static IMAGE models stay excluded
|
|
// (hasChatRegistry is true for openrouter — see staticModels.ts).
|
|
const ids = body.models.map((m: { id: string }) => m.id);
|
|
assert.ok(ids.includes("openai/gpt-4.1"), "live-fetched chat model is preserved");
|
|
assert.ok(ids.includes("baai/bge-m3"), "curated embedding is merged in");
|
|
assert.ok(ids.includes("cohere/rerank-v3.5"), "curated rerank is merged in");
|
|
assert.ok(
|
|
!ids.some((id: string) => id.includes("gpt-5.4-image")),
|
|
"static image models stay excluded from the chat+specialty catalog"
|
|
);
|
|
});
|
|
|
|
test("provider models route returns the local catalog for embedding and rerank providers", async () => {
|
|
const voyage = await seedConnection("voyage-ai", {
|
|
apiKey: "voyage-key",
|
|
});
|
|
const jina = await seedConnection("jina-ai", {
|
|
apiKey: "jina-key",
|
|
});
|
|
|
|
const [voyageResponse, jinaResponse] = await Promise.all([
|
|
callRoute(voyage.id),
|
|
callRoute(jina.id),
|
|
]);
|
|
const voyageBody = (await voyageResponse.json()) as any;
|
|
const jinaBody = (await jinaResponse.json()) as any;
|
|
|
|
assert.equal(voyageResponse.status, 200);
|
|
assert.equal(voyageBody.provider, "voyage-ai");
|
|
assert.equal(voyageBody.source, "local_catalog");
|
|
assert.ok(voyageBody.models.some((model) => model.id === "voyage-4-large"));
|
|
assert.ok(voyageBody.models.some((model) => model.id === "voyage-code-3"));
|
|
assert.ok(voyageBody.models.some((model) => model.id === "voyage-4-lite"));
|
|
|
|
assert.equal(jinaResponse.status, 200);
|
|
assert.equal(jinaBody.provider, "jina-ai");
|
|
assert.equal(jinaBody.source, "local_catalog");
|
|
assert.ok(
|
|
jinaBody.models.some(
|
|
(model) =>
|
|
model.id === "jina-embeddings-v5-text-small" &&
|
|
model.apiFormat === "embeddings" &&
|
|
model.supportedEndpoints?.includes("embeddings")
|
|
)
|
|
);
|
|
assert.ok(
|
|
jinaBody.models.some(
|
|
(model) =>
|
|
model.id === "jina-reranker-v3" &&
|
|
model.apiFormat === "rerank" &&
|
|
model.supportedEndpoints?.includes("rerank")
|
|
)
|
|
);
|
|
assert.ok(jinaBody.models.some((model) => model.id === "jina-reranker-m0"));
|
|
});
|
|
|
|
test("provider models route flags intentional local-catalog-only providers so model-sync imports them (#5460/#5465)", async () => {
|
|
// reka + voyage-ai never do a remote /models fetch — their local catalog is
|
|
// the intended source, so the response must carry `intentional: true` for the
|
|
// sync route to import instead of 502-ing ("local catalog fallback not synced").
|
|
const reka = await seedConnection("reka", { apiKey: "reka-key" });
|
|
const voyage = await seedConnection("voyage-ai", { apiKey: "voyage-key" });
|
|
|
|
const [rekaBody, voyageBody] = await Promise.all([
|
|
callRoute(reka.id).then((r) => r.json() as any),
|
|
callRoute(voyage.id).then((r) => r.json() as any),
|
|
]);
|
|
|
|
assert.equal(rekaBody.source, "local_catalog");
|
|
assert.equal(rekaBody.intentional, true, "reka local catalog must be flagged intentional");
|
|
assert.equal(voyageBody.source, "local_catalog");
|
|
assert.equal(voyageBody.intentional, true, "voyage-ai local catalog must be flagged intentional");
|
|
});
|
|
|
|
test("provider models route does NOT flag a degraded remote-fetch fallback as intentional (#5460/#5465)", async () => {
|
|
// aimlapi normally discovers remotely; when the live fetch fails it falls back
|
|
// to the local catalog — that IS degraded and must NOT be flagged intentional,
|
|
// so model-sync still surfaces the failure (502) for it.
|
|
const connection = await seedConnection("aimlapi", { apiKey: "aiml-key" });
|
|
globalThis.fetch = async () => new Response("upstream down", { status: 500 });
|
|
|
|
const body = (await (await callRoute(connection.id)).json()) as any;
|
|
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.notEqual(body.intentional, true, "degraded fallback must not be flagged intentional");
|
|
});
|
|
|
|
test("provider models route returns the local catalog for Runway video models", async () => {
|
|
const connection = await seedConnection("runwayml", {
|
|
apiKey: "runway-key",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "runwayml");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.ok(body.models.some((model) => model.id === "gen4.5"));
|
|
assert.ok(body.models.some((model) => model.id === "veo3.1"));
|
|
assert.ok(body.models.some((model) => model.id === "gen3a_turbo"));
|
|
});
|
|
|
|
test("provider models route returns the updated local catalog for GitHub Copilot", async () => {
|
|
const connection = await seedConnection("github", {
|
|
authType: "oauth",
|
|
apiKey: null,
|
|
accessToken: "github-access",
|
|
providerSpecificData: {
|
|
copilotToken: "copilot-token",
|
|
},
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "github");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.ok(body.models.some((model) => model.id === "gpt-5.4"));
|
|
assert.ok(body.models.some((model) => model.id === "gpt-5.3-codex"));
|
|
assert.ok(body.models.some((model) => model.id === "claude-opus-4.7"));
|
|
assert.equal(
|
|
body.models.some((model) => model.id === "gpt-5.1"),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("provider models route returns the expanded local catalog for Kiro", async () => {
|
|
const connection = await seedConnection("kiro", {
|
|
authType: "oauth",
|
|
apiKey: null,
|
|
accessToken: "kiro-access",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "kiro");
|
|
assert.equal(body.source, "local_catalog");
|
|
const kiroIds = new Set(body.models.map((model) => model.id)); // #6170: real upstream lineup
|
|
assert.ok(
|
|
kiroIds.has("claude-sonnet-5") &&
|
|
kiroIds.has("claude-sonnet-4.5") &&
|
|
kiroIds.has("claude-haiku-4.5")
|
|
);
|
|
assert.equal(kiroIds.has("claude-opus-4.7") || kiroIds.has("claude-sonnet-4.6"), false); // fabricated ids removed
|
|
});
|
|
|
|
test("provider models route returns the local catalog for new built-in chat-openai-compat providers", async () => {
|
|
const connection = await seedConnection("deepinfra", {
|
|
apiKey: "deepinfra-key",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "deepinfra");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /local catalog/i);
|
|
assert.ok(Array.isArray(body.models));
|
|
assert.ok(body.models.length > 0);
|
|
assert.ok(body.models.some((model) => model.id === "openai/gpt-oss-120b"));
|
|
});
|
|
|
|
test("provider models route merges Upstage chat and embedding catalogs", async () => {
|
|
const connection = await seedConnection("upstage", {
|
|
apiKey: "upstage-key",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
const modelIds = body.models.map((model) => model.id);
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "upstage");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.ok(modelIds.includes("solar-pro3"));
|
|
assert.ok(modelIds.includes("solar-mini"));
|
|
assert.ok(modelIds.includes("embedding-query"));
|
|
assert.ok(modelIds.includes("embedding-passage"));
|
|
assert.equal(modelIds.includes("document-parse"), false);
|
|
});
|
|
|
|
test("provider models route caches discovered opencode-go models per connection", async () => {
|
|
const connection = await seedConnection("opencode-go", {
|
|
apiKey: "opencode-go-key",
|
|
});
|
|
let fetchCalls = 0;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
fetchCalls += 1;
|
|
assert.equal(String(url), "https://opencode.ai/zen/go/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer opencode-go-key");
|
|
return Response.json({
|
|
data: [{ id: "glm-5.1", name: "GLM 5.1" }],
|
|
});
|
|
};
|
|
|
|
const firstResponse = await callRoute(connection.id);
|
|
const firstBody = (await firstResponse.json()) as any;
|
|
const cachedModels = await modelsDb.getSyncedAvailableModelsForConnection(
|
|
"opencode-go",
|
|
connection.id
|
|
);
|
|
|
|
assert.equal(firstResponse.status, 200);
|
|
assert.equal(firstBody.source, "api");
|
|
assert.deepEqual(firstBody.models, [{ id: "glm-5.1", name: "GLM 5.1", owned_by: "opencode-go" }]);
|
|
assert.deepEqual(cachedModels, [{ id: "glm-5.1", name: "GLM 5.1", source: "imported" }]);
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("cached route should not hit upstream");
|
|
};
|
|
|
|
const cachedResponse = await callRoute(connection.id);
|
|
const cachedBody = (await cachedResponse.json()) as any;
|
|
|
|
assert.equal(cachedResponse.status, 200);
|
|
assert.equal(cachedBody.source, "cache");
|
|
assert.deepEqual(cachedBody.models, [{ id: "glm-5.1", name: "GLM 5.1", source: "imported" }]);
|
|
assert.equal(fetchCalls, 1);
|
|
});
|
|
|
|
test("provider models route falls back to cached models when a refresh fails", async () => {
|
|
const connection = await seedConnection("opencode-go", {
|
|
apiKey: "opencode-go-key",
|
|
});
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [
|
|
{ id: "cached-go", name: "Cached Go", source: "imported" },
|
|
]);
|
|
let fetchCalls = 0;
|
|
|
|
globalThis.fetch = async () => {
|
|
fetchCalls += 1;
|
|
return new Response("upstream unavailable", { status: 503 });
|
|
};
|
|
|
|
const response = await callRoute(connection.id, "?refresh=true");
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "cache");
|
|
assert.match(body.warning, /cached catalog/i);
|
|
assert.deepEqual(body.models, [{ id: "cached-go", name: "Cached Go", source: "imported" }]);
|
|
// T39 multi-endpoint discovery probes `${base}/v1/models` then `${base}/models`
|
|
// before giving up; both 503 here, so it makes 2 attempts and then falls back to cache.
|
|
assert.equal(fetchCalls, 2);
|
|
});
|
|
|
|
test("provider models route clears cached discovery when a refresh returns no remote models", async () => {
|
|
const connection = await seedConnection("opencode-go", {
|
|
apiKey: "opencode-go-key",
|
|
});
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", connection.id, [
|
|
{ id: "cached-go", name: "Cached Go", source: "imported" },
|
|
]);
|
|
|
|
globalThis.fetch = async () => {
|
|
return Response.json({ data: [] });
|
|
};
|
|
|
|
const response = await callRoute(connection.id, "?refresh=true");
|
|
const body = (await response.json()) as any;
|
|
const cachedModels = await modelsDb.getSyncedAvailableModelsForConnection(
|
|
"opencode-go",
|
|
connection.id
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /no remote models discovered/i);
|
|
assert.ok(body.models.every((model) => model.id !== "cached-go"));
|
|
assert.deepEqual(cachedModels, []);
|
|
});
|
|
|
|
test("provider models route honors autoFetchModels=false and skips remote discovery", async () => {
|
|
const connection = await seedConnection("opencode-go", {
|
|
apiKey: "opencode-go-key",
|
|
providerSpecificData: {
|
|
autoFetchModels: false,
|
|
},
|
|
});
|
|
let called = false;
|
|
|
|
globalThis.fetch = async () => {
|
|
called = true;
|
|
return Response.json({
|
|
data: [{ id: "glm-5.1", name: "GLM 5.1" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /auto-fetch disabled/i);
|
|
assert.equal(called, false);
|
|
assert.ok(body.models.some((model) => model.id === "glm-5"));
|
|
});
|
|
|
|
test("provider models route uses synced models as the authoritative local catalog (#3148)", async () => {
|
|
// A connection that resolves to the local catalog (auto-fetch off, no remote
|
|
// discovery). Once a sync has populated the synced-models table for this
|
|
// provider, the route must surface the synced list — even on a connection
|
|
// that never ran the sync itself — instead of the static catalog.
|
|
const connection = await seedConnection("opencode-go", {
|
|
apiKey: "opencode-go-key",
|
|
providerSpecificData: {
|
|
autoFetchModels: false,
|
|
},
|
|
});
|
|
|
|
await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", "synced-conn", [
|
|
{ id: "synced-only-model", name: "Synced Only Model" },
|
|
]);
|
|
|
|
let called = false;
|
|
globalThis.fetch = async () => {
|
|
called = true;
|
|
return Response.json({ data: [] });
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.equal(called, false);
|
|
// Synced models become the catalog…
|
|
assert.ok(
|
|
body.models.some((model) => model.id === "synced-only-model"),
|
|
"synced model should be present in the local catalog"
|
|
);
|
|
// …and the static catalog entries are no longer surfaced for this provider.
|
|
assert.equal(
|
|
body.models.some((model) => model.id === "glm-5"),
|
|
false,
|
|
"static catalog should be superseded by the synced list"
|
|
);
|
|
});
|
|
|
|
test("provider models route retries Antigravity discovery endpoints before returning remote models", async () => {
|
|
const connection = await seedConnection("antigravity", {
|
|
authType: "oauth",
|
|
accessToken: "ag-access",
|
|
apiKey: null,
|
|
});
|
|
const seenUrls: string[] = [];
|
|
antigravityVersion.seedAntigravityIdeVersionCache("1.22.2");
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const urlString = String(url);
|
|
// After PR #2219, the discovery flow calls loadCodeAssist first as a project
|
|
// bootstrap; treat all bootstrap calls as non-fatal failures so the test
|
|
// exercises the discovery retry path.
|
|
// onboardUser is a bootstrap hop too (ff012ff420) — else it eats the single 503 below.
|
|
if (urlString.includes(":loadCodeAssist") || urlString.includes(":onboardUser")) {
|
|
return new Response("nope", { status: 503 });
|
|
}
|
|
seenUrls.push(urlString);
|
|
if (seenUrls.length === 1) {
|
|
return new Response("unavailable", { status: 503 });
|
|
}
|
|
|
|
assert.equal(init.method, "POST");
|
|
assert.equal(init.headers.Authorization, "Bearer ag-access");
|
|
assert.match(init.headers["User-Agent"], /^antigravity\/ide\/1\.22\.2 /);
|
|
assert.equal(init.headers["x-goog-api-client"], undefined);
|
|
// Use a model id that is in the current user-callable Antigravity allowlist, otherwise
|
|
// filterUserCallableAntigravityModels() drops it and discovery silently yields 0 models
|
|
// → the route falls back to local_catalog instead of returning the remote (api) list.
|
|
return Response.json({
|
|
models: [
|
|
{ id: "gemini-3.1-pro-high", displayName: "Gemini 3.1 Pro (High)" },
|
|
{ id: "gemini-pro-agent", displayName: "Gemini 3.1 Pro (High)" },
|
|
{ id: "gemini-3.6-flash-high", displayName: "upstream-3.6-high" },
|
|
{ id: "gemini-3.6-flash-medium", displayName: "upstream-3.6-medium" },
|
|
{ id: "gemini-3.6-flash-low", displayName: "upstream-3.6-low" },
|
|
{ id: "gemini-3.5-flash-extra-low", displayName: "upstream-low" },
|
|
{ id: "gemini-3.5-flash-low", displayName: "upstream-medium" },
|
|
{ id: "gemini-3-flash-agent", displayName: "upstream-high" },
|
|
{ id: "gemini-3.5-flash-high", displayName: "retired-friendly-high" },
|
|
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
// After PR #2219, the route tries `:fetchAvailableModels` URLs before
|
|
// `:models` URLs. The test mock returns 503 on the first call and success
|
|
// on the second, so only the first two `:fetchAvailableModels` URLs are
|
|
// hit — `:models` URLs are never reached. Assert on the actual discovery
|
|
// sequence the route follows.
|
|
const discoveryUrls = seenUrls.filter(
|
|
(url) => url.includes("/v1internal:fetchAvailableModels") || url.includes("/v1internal:models")
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(discoveryUrls, [
|
|
"https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
|
|
"https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
|
|
]);
|
|
assert.deepEqual(body.models, [
|
|
// #9106: both alias ids are user-callable now, so the upstream echo survives the filter.
|
|
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro (High)" },
|
|
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
|
|
{ id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)" },
|
|
{ id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)" },
|
|
{ id: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash (Low)" },
|
|
{ id: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash (Low)" },
|
|
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)" },
|
|
{ id: "gemini-3-flash-agent", name: "Gemini 3.5 Flash (High)" },
|
|
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers newly announced agy models without exposing internal models", async () => {
|
|
const connection = await seedConnection("agy", { authType: "oauth", accessToken: "agy-access" });
|
|
antigravityVersion.seedAntigravityIdeVersionCache("1.22.2");
|
|
antigravityVersion.seedAntigravityCliVersionCache("1.22.2");
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url).includes("/v1internal:loadCodeAssist")) {
|
|
return new Response("nope", { status: 503 });
|
|
}
|
|
return Response.json({
|
|
models: {
|
|
"gemini-new-live-tier": { displayName: "Gemini New Live Tier" },
|
|
tab_flash_lite_preview: { displayName: "Tab Flash Lite" },
|
|
"internal-eval-model": { displayName: "Internal Eval", isInternal: true },
|
|
},
|
|
});
|
|
};
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as {
|
|
source: string;
|
|
models: Array<{ id: string; name: string }>;
|
|
};
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [{ id: "gemini-new-live-tier", name: "Gemini New Live Tier" }]);
|
|
});
|
|
|
|
test("provider models route falls back through all Antigravity discovery endpoints when needed", async () => {
|
|
const connection = await seedConnection("antigravity", {
|
|
authType: "oauth",
|
|
accessToken: "ag-access",
|
|
apiKey: null,
|
|
});
|
|
const seenUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
seenUrls.push(String(url));
|
|
return new Response("down", { status: 502 });
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
const discoveryUrls = seenUrls.filter((url) => url.includes("/v1internal:models"));
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /local catalog/i);
|
|
assert.deepEqual(discoveryUrls, [
|
|
"https://daily-cloudcode-pa.googleapis.com/v1internal:models",
|
|
"https://cloudcode-pa.googleapis.com/v1internal:models",
|
|
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models",
|
|
]);
|
|
assert.equal(
|
|
body.models.some((model) => model.id === "gemini-3.1-pro-high"),
|
|
false
|
|
);
|
|
assert.ok(body.models.some((model) => model.id === "gemini-pro-agent"));
|
|
assert.equal(
|
|
body.models.some((model) => model.id === "gemini-3-pro-preview"),
|
|
false
|
|
);
|
|
assert.equal(
|
|
body.models.some((model) => model.id === "gemini-2.5-computer-use-preview-10-2025"),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("provider models route filters hidden models from the static Claude catalog when requested", async () => {
|
|
const connection = await seedConnection("claude", {
|
|
authType: "oauth",
|
|
accessToken: "claude-access",
|
|
apiKey: null,
|
|
});
|
|
modelsDb.mergeModelCompatOverride("claude", "claude-sonnet-4-6", { isHidden: true });
|
|
|
|
const response = await callRoute(connection.id, "?excludeHidden=true");
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "claude");
|
|
assert.ok(body.models.some((model) => model.id === "claude-opus-4-7"));
|
|
assert.equal(
|
|
body.models.some((model) => model.id === "claude-sonnet-4-6"),
|
|
false
|
|
);
|
|
assert.ok(body.models.some((model) => model.id === "claude-opus-4-6"));
|
|
});
|
|
|
|
test("provider models route rejects Anthropic-compatible providers without a base URL", async () => {
|
|
const connection = await seedConnection("anthropic-compatible-demo", {
|
|
apiKey: "sk-anthropic-compatible",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.deepEqual(await response.json(), {
|
|
error: "No base URL configured for Anthropic compatible provider",
|
|
});
|
|
});
|
|
|
|
test("provider models route trims Anthropic-compatible message URLs and filters hidden upstream models", async () => {
|
|
const connection = await seedConnection("anthropic-compatible-demo", {
|
|
apiKey: "sk-anthropic-compatible",
|
|
accessToken: "anthropic-access",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/v1/messages",
|
|
},
|
|
});
|
|
modelsDb.mergeModelCompatOverride("anthropic-compatible-demo", "hidden-model", {
|
|
isHidden: true,
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://proxy.example.com/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers["Content-Type"], "application/json");
|
|
assert.equal(init.headers["x-api-key"], "sk-anthropic-compatible");
|
|
assert.equal(init.headers.Authorization, "Bearer anthropic-access");
|
|
assert.equal(init.headers["anthropic-version"], "2023-06-01");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{ id: "visible-model", name: "Visible Model" },
|
|
{ id: "hidden-model", name: "Hidden Model" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id, "?excludeHidden=true");
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(body.models, [{ id: "visible-model", name: "Visible Model" }]);
|
|
});
|
|
|
|
test("provider models route forwards Anthropic-compatible upstream failures", async () => {
|
|
const connection = await seedConnection("anthropic-compatible-demo", {
|
|
apiKey: "sk-anthropic-compatible",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com/v1/messages",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async () => new Response("upstream unavailable", { status: 502 });
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 502);
|
|
assert.deepEqual(await response.json(), {
|
|
error: "Failed to fetch models: 502",
|
|
});
|
|
});
|
|
|
|
test("provider models route paginates generic providers and filters hidden models when requested", async () => {
|
|
const connection = await seedConnection("gemini", {
|
|
apiKey: "gm-key",
|
|
});
|
|
modelsDb.mergeModelCompatOverride("gemini", "gemini-hidden", { isHidden: true });
|
|
const seenUrls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const currentUrl = String(url);
|
|
seenUrls.push(currentUrl);
|
|
if (!currentUrl.includes("pageToken=")) {
|
|
assert.match(currentUrl, /key=gm-key/);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
name: "models/gemini-visible",
|
|
displayName: "Gemini Visible",
|
|
supportedGenerationMethods: ["generateContent"],
|
|
},
|
|
{
|
|
name: "models/gemini-hidden",
|
|
displayName: "Gemini Hidden",
|
|
supportedGenerationMethods: ["generateContent"],
|
|
},
|
|
],
|
|
nextPageToken: "page-2",
|
|
});
|
|
}
|
|
|
|
assert.match(currentUrl, /pageToken=page-2/);
|
|
assert.match(currentUrl, /key=gm-key/);
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
name: "models/text-embedding-004",
|
|
displayName: "Text Embedding 004",
|
|
supportedGenerationMethods: ["embedContent"],
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id, "?excludeHidden=true");
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(body.models.map((model) => model.id).sort(), [
|
|
"gemini-visible",
|
|
"text-embedding-004",
|
|
]);
|
|
assert.equal(seenUrls.length, 2);
|
|
});
|
|
|
|
test("provider models route stops pagination when the upstream repeats the next page token", async () => {
|
|
const connection = await seedConnection("gemini", {
|
|
apiKey: "gm-key",
|
|
});
|
|
let calls = 0;
|
|
|
|
globalThis.fetch = async () => {
|
|
calls += 1;
|
|
return Response.json({
|
|
models: [
|
|
{
|
|
name: `models/gemini-page-${calls}`,
|
|
displayName: `Gemini Page ${calls}`,
|
|
supportedGenerationMethods: ["generateContent"],
|
|
},
|
|
],
|
|
nextPageToken: "duplicate-token",
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(
|
|
body.models.map((model) => model.id),
|
|
["gemini-page-1", "gemini-page-2"]
|
|
);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test("provider models route forwards upstream status codes for generic provider model fetch failures", async () => {
|
|
const connection = await seedConnection("groq", {
|
|
apiKey: "groq-models-token",
|
|
});
|
|
|
|
globalThis.fetch = async () => new Response("upstream unavailable", { status: 503 });
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /local catalog/i);
|
|
assert.ok(Array.isArray(body.models));
|
|
assert.ok(body.models.length > 0);
|
|
});
|
|
|
|
test("provider models route returns 500 when fetching models throws unexpectedly", async () => {
|
|
const connection = await seedConnection("groq", {
|
|
apiKey: "groq-models-token",
|
|
});
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("socket closed");
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /local catalog/i);
|
|
});
|
|
|
|
test("provider models route rejects generic providers without any configured token", async () => {
|
|
const connection = await seedConnection("groq", {
|
|
apiKey: null,
|
|
accessToken: null,
|
|
});
|
|
let called = false;
|
|
|
|
globalThis.fetch = async () => {
|
|
called = true;
|
|
return Response.json({ data: [] });
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.match(body.warning, /local catalog/i);
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
test("provider models route discovers active DataRobot gateway models from the catalog endpoint", async () => {
|
|
const connection = await seedConnection("datarobot", {
|
|
apiKey: "dr-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://app.datarobot.com",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://app.datarobot.com/genai/llmgw/catalog/");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer dr-key");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{ model: "azure/gpt-5-mini-2025-08-07", isActive: true },
|
|
{ model: "azure/gpt-4o-mini", label: "Azure GPT-4o Mini", isActive: true },
|
|
{ model: "anthropic/claude-sonnet-4-6", isActive: false },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "datarobot");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "azure/gpt-5-mini-2025-08-07",
|
|
name: "azure/gpt-5-mini-2025-08-07",
|
|
owned_by: "datarobot",
|
|
},
|
|
{
|
|
id: "azure/gpt-4o-mini",
|
|
name: "Azure GPT-4o Mini",
|
|
owned_by: "datarobot",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers Clarifai OpenAI-compatible models with Key auth", async () => {
|
|
const connection = await seedConnection("clarifai", {
|
|
apiKey: "clarifai-pat",
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://api.clarifai.com/v2/ext/openai/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Key clarifai-pat");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{
|
|
id: "openai/chat-completion/models/gpt-oss-120b",
|
|
display_name: "GPT-OSS 120B",
|
|
},
|
|
{ id: "anthropic/completion/models/claude-sonnet-4" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "clarifai");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "openai/chat-completion/models/gpt-oss-120b",
|
|
name: "GPT-OSS 120B",
|
|
owned_by: "clarifai",
|
|
},
|
|
{
|
|
id: "anthropic/completion/models/claude-sonnet-4",
|
|
name: "anthropic/completion/models/claude-sonnet-4",
|
|
owned_by: "clarifai",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers Azure AI Foundry deployments through the v1 models endpoint", async () => {
|
|
const connection = await seedConnection("azure-ai", {
|
|
apiKey: "azure-ai-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://my-foundry.services.ai.azure.com",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://my-foundry.services.ai.azure.com/openai/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers["api-key"], "azure-ai-key");
|
|
|
|
return Response.json({
|
|
data: [{ id: "DeepSeek-V3.1", display_name: "DeepSeek V3.1" }, { name: "Claude-Opus-4.6" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "azure-ai");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{ id: "DeepSeek-V3.1", name: "DeepSeek V3.1", owned_by: "azure-ai" },
|
|
{ id: "Claude-Opus-4.6", name: "Claude-Opus-4.6", owned_by: "azure-ai" },
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers Azure OpenAI deployments from the resource endpoint", async () => {
|
|
const connection = await seedConnection("azure-openai", {
|
|
apiKey: "azure-openai-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://my-resource.openai.azure.com/openai",
|
|
apiVersion: "2024-12-01-preview",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(
|
|
String(url),
|
|
"https://my-resource.openai.azure.com/openai/deployments?api-version=2024-12-01-preview"
|
|
);
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers["api-key"], "azure-openai-key");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{ id: "gpt4o-prod", model: "gpt-4o", display_name: "GPT-4o Production" },
|
|
{ id: "o3-mini-staging", model: "o3-mini" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "azure-openai");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{ id: "gpt4o-prod", name: "GPT-4o Production", owned_by: "azure-openai" },
|
|
{ id: "o3-mini-staging", name: "o3-mini-staging", owned_by: "azure-openai" },
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers native Bedrock foundation models and inference profiles", async () => {
|
|
const connection = await seedConnection("bedrock", {
|
|
apiKey: "bedrock-key",
|
|
providerSpecificData: {
|
|
region: "eu-west-2",
|
|
},
|
|
});
|
|
const seenUrls: string[] = [];
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
const target = String(url);
|
|
seenUrls.push(target);
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer bedrock-key");
|
|
|
|
if (
|
|
target === "https://bedrock.eu-west-2.amazonaws.com/foundation-models?byOutputModality=TEXT"
|
|
) {
|
|
return Response.json({
|
|
modelSummaries: [
|
|
{
|
|
modelId: "anthropic.claude-sonnet-4-6",
|
|
modelName: "Claude Sonnet 4.6",
|
|
providerName: "Anthropic",
|
|
inputModalities: ["TEXT", "IMAGE"],
|
|
outputModalities: ["TEXT"],
|
|
responseStreamingSupported: true,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
if (
|
|
target ===
|
|
"https://bedrock.eu-west-2.amazonaws.com/inference-profiles?maxResults=100&typeEquals=SYSTEM_DEFINED"
|
|
) {
|
|
return Response.json({
|
|
inferenceProfileSummaries: [
|
|
{
|
|
inferenceProfileId: "eu.anthropic.claude-sonnet-4-6",
|
|
inferenceProfileName: "EU Claude Sonnet 4.6",
|
|
models: [
|
|
{
|
|
modelArn: "arn:aws:bedrock:eu-west-2::foundation-model/anthropic.claude-sonnet-4-6",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
throw new Error("unexpected fetch: " + target);
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "bedrock");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(seenUrls, [
|
|
"https://bedrock.eu-west-2.amazonaws.com/foundation-models?byOutputModality=TEXT",
|
|
"https://bedrock.eu-west-2.amazonaws.com/inference-profiles?maxResults=100&typeEquals=SYSTEM_DEFINED",
|
|
]);
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "anthropic.claude-sonnet-4-6",
|
|
name: "Claude Sonnet 4.6",
|
|
owned_by: "Anthropic",
|
|
source: "foundation",
|
|
supportsStreaming: true,
|
|
supportsVision: true,
|
|
inputTokenLimit: 1000000,
|
|
outputTokenLimit: 64000,
|
|
},
|
|
{
|
|
id: "eu.anthropic.claude-sonnet-4-6",
|
|
name: "EU Claude Sonnet 4.6",
|
|
owned_by: "bedrock",
|
|
source: "inference_profile",
|
|
supportsStreaming: true,
|
|
inputTokenLimit: 1000000,
|
|
outputTokenLimit: 64000,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers watsonx gateway models from the v1 models endpoint", async () => {
|
|
const connection = await seedConnection("watsonx", {
|
|
apiKey: "watsonx-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://ca-tor.ml.cloud.ibm.com",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://ca-tor.ml.cloud.ibm.com/ml/gateway/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer watsonx-key");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{ id: "ibm/granite-3-3-8b-instruct", display_name: "Granite 3.3 8B Instruct" },
|
|
{ model: "openai/gpt-4o", provider: "openai" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "watsonx");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "ibm/granite-3-3-8b-instruct",
|
|
name: "Granite 3.3 8B Instruct",
|
|
owned_by: "watsonx",
|
|
},
|
|
{
|
|
id: "openai/gpt-4o",
|
|
name: "openai/gpt-4o",
|
|
owned_by: "openai",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers OCI OpenAI-compatible models and forwards the project header", async () => {
|
|
const connection = await seedConnection("oci", {
|
|
apiKey: "oci-key",
|
|
projectId: "ocid1.generativeaiproject.oc1.us-chicago-1.demo",
|
|
providerSpecificData: {
|
|
baseUrl: "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(
|
|
String(url),
|
|
"https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1/models"
|
|
);
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer oci-key");
|
|
assert.equal(init.headers["OpenAI-Project"], "ocid1.generativeaiproject.oc1.us-chicago-1.demo");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{ id: "openai.gpt-oss-20b", display_name: "OpenAI GPT-OSS 20B" },
|
|
{ id: "google.gemini-2.5-pro" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "oci");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "openai.gpt-oss-20b",
|
|
name: "OpenAI GPT-OSS 20B",
|
|
owned_by: "oci",
|
|
},
|
|
{
|
|
id: "google.gemini-2.5-pro",
|
|
name: "google.gemini-2.5-pro",
|
|
owned_by: "oci",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route discovers Modal models from the configured OpenAI-compatible /v1 endpoint", async () => {
|
|
const connection = await seedConnection("modal", {
|
|
apiKey: "modal-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://alice--demo.modal.run/v1",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://alice--demo.modal.run/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer modal-key");
|
|
|
|
return Response.json({
|
|
data: [
|
|
{ id: "Qwen/Qwen3-4B-Thinking-2507-FP8", display_name: "Qwen3 4B Thinking FP8" },
|
|
{ id: "google/gemma-4-26B-A4B-it" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "modal");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "Qwen/Qwen3-4B-Thinking-2507-FP8",
|
|
name: "Qwen3 4B Thinking FP8",
|
|
owned_by: "modal",
|
|
},
|
|
{
|
|
id: "google/gemma-4-26B-A4B-it",
|
|
name: "google/gemma-4-26B-A4B-it",
|
|
owned_by: "modal",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route always returns the Reka preset catalog", async () => {
|
|
const connection = await seedConnection("reka", {
|
|
apiKey: "reka-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.reka.ai/v1",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("Reka models endpoint should not be probed");
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "reka");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.deepEqual(
|
|
body.models.map((model) => model.id),
|
|
["reka-flash-3", "reka-flash", "reka-edge-2603"]
|
|
);
|
|
});
|
|
|
|
test("provider models route returns Reka local catalog without an API key", async () => {
|
|
const connection = await seedConnection("reka", {
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.reka.ai/v1",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("Reka models endpoint should not be probed without a token");
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "reka");
|
|
assert.equal(body.source, "local_catalog");
|
|
assert.deepEqual(
|
|
body.models.map((model) => model.id),
|
|
["reka-flash-3", "reka-flash", "reka-edge-2603"]
|
|
);
|
|
});
|
|
|
|
test("provider models route discovers SAP models from AI_API_URL derived from deploymentUrl", async () => {
|
|
const connection = await seedConnection("sap", {
|
|
apiKey: "sap-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://sap.example.com/v2/lm/deployments/demo-deployment",
|
|
resourceGroup: "shared",
|
|
},
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://sap.example.com/v2/lm/scenarios/foundation-models/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers.Authorization, "Bearer sap-key");
|
|
assert.equal(init.headers["AI-Resource-Group"], "shared");
|
|
|
|
return Response.json({
|
|
resources: [
|
|
{ model: "gpt-4o", displayName: "GPT-4o", provider: "OpenAI" },
|
|
{ model: "mistralai--mistral-medium-instruct", provider: "Mistral AI" },
|
|
],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "sap");
|
|
assert.equal(body.source, "api");
|
|
assert.deepEqual(body.models, [
|
|
{
|
|
id: "gpt-4o",
|
|
name: "GPT-4o",
|
|
owned_by: "OpenAI",
|
|
},
|
|
{
|
|
id: "mistralai--mistral-medium-instruct",
|
|
name: "mistralai--mistral-medium-instruct",
|
|
owned_by: "Mistral AI",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("provider models route rejects unsupported providers without a models config", async () => {
|
|
const connection = await seedConnection("unsupported-provider", {
|
|
apiKey: "sk-unsupported",
|
|
});
|
|
|
|
const response = await callRoute(connection.id);
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.deepEqual(await response.json(), {
|
|
error: "Provider unsupported-provider does not support models listing",
|
|
});
|
|
});
|
|
|
|
test("provider models route uses provider-specific auth headers for Kimi Coding", async () => {
|
|
const connection = await seedConnection("kimi-coding", {
|
|
apiKey: "kimi-coding-key",
|
|
});
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(String(url), "https://api.kimi.com/coding/v1/models");
|
|
assert.equal(init.method, "GET");
|
|
assert.equal(init.headers["x-api-key"], "kimi-coding-key");
|
|
assert.equal(init.headers.Authorization, undefined);
|
|
|
|
return Response.json({
|
|
data: [{ id: "kimi-k2.5", display_name: "Kimi K2.5" }],
|
|
});
|
|
};
|
|
|
|
const response = await callRoute(connection.id);
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(body.provider, "kimi-coding");
|
|
assert.deepEqual(
|
|
body.models.map(({ id, name }) => ({ id, name })),
|
|
[{ id: "kimi-k2.5", name: "Kimi K2.5" }]
|
|
);
|
|
});
|