mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Compare commits
8 Commits
feat/i18n-
...
b87fd70572
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b87fd70572 | ||
|
|
4053e2314a | ||
|
|
cc63ac9f53 | ||
|
|
c447be4329 | ||
|
|
d06d3fb67d | ||
|
|
278640b438 | ||
|
|
ec0be07682 | ||
|
|
b0a0bcc727 |
1
changelog.d/fixes/7587-vscode-models-missing-openai.md
Normal file
1
changelog.d/fixes/7587-vscode-models-missing-openai.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): expose Responses-API-format (OpenAI/Codex) chat models on every VS Code Ollama-compatible listing route, not just `/models` (#7587)
|
||||
@@ -0,0 +1 @@
|
||||
- chore(ci): resync the stale `no-explicit-any` suppression count for `tests/unit/proxy-registry.test.ts` (frozen at 55, actual 54 since #8447) — ESLint was failing the whole run with "There are suppressions left that do not occur anymore", turning `npm run lint` red on `release/v3.8.49` for every PR branched off it
|
||||
@@ -1937,7 +1937,7 @@
|
||||
},
|
||||
"tests/unit/proxy-registry.test.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 55
|
||||
"count": 54
|
||||
}
|
||||
},
|
||||
"tests/unit/proxy-resolution-status-filter.test.ts": {
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1054,
|
||||
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 948,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1990,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
|
||||
"src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 903,
|
||||
@@ -324,7 +324,7 @@
|
||||
"open-sse/executors/huggingchat.ts": 813,
|
||||
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
|
||||
"src/lib/providers/validation/webProvidersA.ts": 809,
|
||||
"src/lib/tokenHealthCheck.ts": 841,
|
||||
"src/lib/tokenHealthCheck.ts": 843,
|
||||
"_rebaseline_2026_07_09_6587_kiro_api_key_auth": "PR #6587 (@strangersp) own growth for Kiro long-lived API-key auth, merged onto v3.8.47 tip: openai-to-kiro.ts 890->912 (+22, auth-header selection for API-key-vs-OAuth-token connections), providerLimits.ts 998->1000 (+2, API-key auth-type branch), translator-openai-to-kiro.test.ts 1234->1257 (+23), providers-page-utils.test.ts 1109->1107 (net -2 after merging with parallel release drift; connectionMatchesProviderCard api_key coverage added), provider-validation-specialty.test.ts 2856->2980 (+124 net after merge with parallel release drift; this PR also removed the file's `@typescript-eslint/no-explicit-any` eslint-suppression entry by fixing all `any` usages, adding typed replacements). Cohesive additive feature growth, well tested; not extractable without splitting the existing chokepoints mid-merge.",
|
||||
"_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
"_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \"./db/autoCandidateOverrides\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.",
|
||||
@@ -470,5 +470,6 @@
|
||||
"_rebaseline_2026_07_23_v3849_merge_train_15": "Own-growth do merge-train de 15 PRs (2026-07-23), medido na tip combinada, release pura abaixo do baseline (auth.ts 2448, muse-spark 1393, translator-test 1523). auth.ts 2462->2475 (#8321 cookie-auth 401 cooldown-em-vez-de-terminal + #8324 noauth opencode-zen via proxy — wiring de classificação no chokepoint getProviderCredentials/markAccountUnavailable, não extraível), muse-spark-web.ts 1394->1396 (#8298 sanitizeErrorMessage runtime repairs isolados do #8177), tests/unit/translator-openai-to-gemini.test.ts 1553->1616 (#8312 cobertura do cap de thinking budget no path budget_tokens explícito). Owner-approved. Frozen; shrink estrutural em #3501.",
|
||||
"_rebaseline_2026_07_22_providerLimits_webcookie_chain": "providerLimits.ts 1003->1005: own-growth from web-cookie provider usage-fetcher entries (#7994/#8006/#8027 chain) landing after the prior rebaseline.",
|
||||
"_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size.",
|
||||
"_rebaseline_2026_07_25_v3849_basered_filesize": "Base-red unblock (2026-07-25): check:file-size was failing on release/v3.8.49 at its own HEAD (36f8fd10), so the quality.yml fast-gates job was red for EVERY PR->release regardless of content — growth inherited from already-merged PRs, with no offending PR branch left to fix (same situation as _rebaseline_2026_07_02_5798_release_green). Prod frozen raised to the current base values: src/lib/tokenHealthCheck.ts 832->841, src/sse/handlers/chat.ts 1865->1866, src/sse/services/auth.ts 2475->2486, open-sse/services/accountFallback.ts 1941->1966, open-sse/services/combo.ts 3630->3642. accountFallback.ts was first frozen here at 1960 (the base value at 36f8fd10) and re-measured to 1966 at base tip 1cafd328c a few hours later — the same inherited drift this entry exists for, since check:file-size does not run on the PR->release fast path and so accrues unmeasured between release rebaselines. These files remain frozen and cannot grow further; any in-flight PR that adds lines to them (e.g. #8482 touches accountFallback.ts and combo.ts) bumps its own entry as usual. The release captain rebaseline-at-release supersedes this note."
|
||||
"_rebaseline_2026_07_25_v3849_basered_filesize": "Base-red unblock (2026-07-25): check:file-size was failing on release/v3.8.49 at its own HEAD (36f8fd10), so the quality.yml fast-gates job was red for EVERY PR->release regardless of content — growth inherited from already-merged PRs, with no offending PR branch left to fix (same situation as _rebaseline_2026_07_02_5798_release_green). Prod frozen raised to the current base values: src/lib/tokenHealthCheck.ts 832->841, src/sse/handlers/chat.ts 1865->1866, src/sse/services/auth.ts 2475->2486, open-sse/services/accountFallback.ts 1941->1966, open-sse/services/combo.ts 3630->3642. accountFallback.ts was first frozen here at 1960 (the base value at 36f8fd10) and re-measured to 1966 at base tip 1cafd328c a few hours later — the same inherited drift this entry exists for, since check:file-size does not run on the PR->release fast path and so accrues unmeasured between release rebaselines. These files remain frozen and cannot grow further; any in-flight PR that adds lines to them (e.g. #8482 touches accountFallback.ts and combo.ts) bumps its own entry as usual. The release captain rebaseline-at-release supersedes this note.",
|
||||
"_rebaseline_2026_07_25_v3849_basered_filesize_2": "Base-red unblock (2026-07-25, second pass): after _rebaseline_2026_07_25_v3849_basered_filesize (measured at 36f8fd10) two more already-merged PRs grew frozen files on release/v3.8.49, so check:file-size — and with it the whole Fast Quality Gates job — is red for EVERY PR->release again, with no offending PR branch left to fix. src/lib/tokenHealthCheck.ts 841->843 (#8426 4528fc455, excludes local CLI providers from expiration) and src/app/(dashboard)/dashboard/providers/page.tsx 1927->1990 (#8349 58ab8b1d2, scroll-position restore on provider-card back-navigation). Trust-but-verify: both values measured on the pristine release tip 30709255 with no working-tree changes. Same situation and remedy as _rebaseline_2026_07_02_5798_release_green. Structural reduction of providers/page.tsx stays tracked separately — it is a 1990-line page, not something to extract inside a base-repair PR."
|
||||
}
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -44,7 +44,7 @@
|
||||
"ink-text-input": "^6.0.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"jose": "^6.2.3",
|
||||
"js-yaml": "^5.0.0",
|
||||
"js-yaml": "^5.2.2",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lowdb": "^7.0.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/bun": "*",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -23683,9 +23683,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz",
|
||||
"integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -27778,9 +27778,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -30080,9 +30080,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"version": "8.5.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -30099,7 +30099,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
||||
12
package.json
12
package.json
@@ -265,7 +265,7 @@
|
||||
"ink-text-input": "^6.0.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"jose": "^6.2.3",
|
||||
"js-yaml": "^5.0.0",
|
||||
"js-yaml": "^5.2.2",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lowdb": "^7.0.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
@@ -394,7 +394,7 @@
|
||||
"dompurify": "^3.4.12",
|
||||
"fast-xml-parser": "^5.10.1",
|
||||
"sharp": "^0.35.0",
|
||||
"postcss": "^8.5.14",
|
||||
"postcss": "^8.5.18",
|
||||
"ip-address": "10.2.0",
|
||||
"qs": "^6.15.2",
|
||||
"uuid": "^14.0.0",
|
||||
@@ -418,6 +418,12 @@
|
||||
"concurrently": {
|
||||
"shell-quote": "^1.9.0"
|
||||
},
|
||||
"adm-zip": "^0.6.0"
|
||||
"adm-zip": "^0.6.0",
|
||||
"promptfoo": {
|
||||
"js-yaml": "^5.2.2",
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"js-yaml": "^4.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
|
||||
"comboForecast", // intentionally-internal: src/lib/usage/comboForecast.ts
|
||||
"commandCodeAuth", // intentionally-internal: 5 API routes em /api/providers/command-code/auth/*
|
||||
"compression", // intentionally-internal: 2 API routes (settings/compression, context/rtk/config)
|
||||
"compressionDetailNormalizers", // db-internal: importado só por db/compression.ts (normalizeSessionDedupConfig/normalizeCcrConfig/buildDetailConfigDefaults/applyDetailConfigUpdate — normalizadores do detail-config split do compression.ts, #8404)
|
||||
"vacuumScheduler", // intentionally-internal: src/instrumentation-node.ts (dynamic import, lifecycle wiring per Rule #2)
|
||||
"detailedLogs", // intentionally-internal: 3 callers (callLogs.ts, logs/detail route, embeddings handler)
|
||||
"discovery", // DEAD?: 0 importers na auditoria de 2026-06-11; lib/discovery/index.ts não usa db/discovery
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstModelCandidates, getFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
import { isUsableChatModel } from "@/app/api/v1/vscode/[token]/usableChatModel";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
@@ -33,35 +34,6 @@ type OpenAiCatalogModel = {
|
||||
supported_endpoints?: string[];
|
||||
};
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCatalogModelId(model: OpenAiCatalogModel) {
|
||||
return model.id || model.name || model.root || "unknown";
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/[token]/tokenizedRequest";
|
||||
import { isUsableChatModel } from "@/app/api/v1/vscode/[token]/usableChatModel";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
@@ -64,35 +65,6 @@ async function selectPreferredModels(models: OpenAiCatalogModel[]) {
|
||||
return codexModels.length > 0 ? codexModels : models;
|
||||
}
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getOllamaModelFamily(model: OpenAiCatalogModel, canonicalFamily?: string | null) {
|
||||
const rawModelId = getModelName(model).trim();
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
parseVscodeServiceTierVariantModelId,
|
||||
} from "@/app/api/v1/vscode/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstPublishedModelId } from "@/app/api/v1/vscode/[token]/familyFirstModelIds";
|
||||
import { isUsableChatModel } from "@/app/api/v1/vscode/[token]/usableChatModel";
|
||||
|
||||
type CatalogModelEntry = {
|
||||
id?: string;
|
||||
@@ -72,12 +73,6 @@ type EnrichModelForVscodeOptions = {
|
||||
preserveNativeId?: boolean;
|
||||
};
|
||||
|
||||
const TEXT_GENERATION_API_FORMATS = new Set([
|
||||
"chat-completions",
|
||||
"responses",
|
||||
"openai-responses",
|
||||
]);
|
||||
|
||||
function usesResponsesApi(model: CatalogModelEntry) {
|
||||
return (
|
||||
model.api_format === "responses" ||
|
||||
@@ -86,41 +81,6 @@ function usesResponsesApi(model: CatalogModelEntry) {
|
||||
);
|
||||
}
|
||||
|
||||
function excludesChatAndResponsesEndpoints(model: CatalogModelEntry) {
|
||||
return (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat") &&
|
||||
!model.supported_endpoints.includes("responses")
|
||||
);
|
||||
}
|
||||
|
||||
function excludesTextOutputModality(model: CatalogModelEntry) {
|
||||
return (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
);
|
||||
}
|
||||
|
||||
function isUsableChatModel(model: CatalogModelEntry) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
if (
|
||||
typeof model.api_format === "string" &&
|
||||
!TEXT_GENERATION_API_FORMATS.has(model.api_format)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (excludesChatAndResponsesEndpoints(model)) return false;
|
||||
if (excludesTextOutputModality(model)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getModelImportReasoningEffortValues(model: VscodeCatalogModel, reasoningEffortValues: string[]) {
|
||||
const providerId =
|
||||
(model.owned_by || "").trim() ||
|
||||
|
||||
63
src/app/api/v1/vscode/[token]/usableChatModel.ts
Normal file
63
src/app/api/v1/vscode/[token]/usableChatModel.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// Shared "is this catalog model usable as a VS Code chat model" filter.
|
||||
//
|
||||
// Historically this predicate was copy-pasted independently into every VS
|
||||
// Code listing route (models, api/tags, api/show — both the token-prefixed
|
||||
// and `raw` token variants). PR #7012 widened only the `models/route.ts`
|
||||
// copy to accept Responses-API-format models (api_format "responses" /
|
||||
// "openai-responses", or supported_endpoints containing "responses")
|
||||
// alongside plain "chat-completions" models — the other 4 copies were left
|
||||
// on the old, stricter filter, silently hiding OpenAI/Codex "responses"
|
||||
// models from every Ollama-compatible listing endpoint (#7587).
|
||||
//
|
||||
// Centralizing the predicate here means a future widening only has to
|
||||
// happen once.
|
||||
|
||||
export type UsableChatModelCandidate = {
|
||||
owned_by?: string;
|
||||
parent?: string | null;
|
||||
type?: string;
|
||||
api_format?: string;
|
||||
supported_endpoints?: string[];
|
||||
output_modalities?: string[];
|
||||
};
|
||||
|
||||
export const TEXT_GENERATION_API_FORMATS = new Set([
|
||||
"chat-completions",
|
||||
"responses",
|
||||
"openai-responses",
|
||||
]);
|
||||
|
||||
function excludesChatAndResponsesEndpoints(model: UsableChatModelCandidate) {
|
||||
return (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat") &&
|
||||
!model.supported_endpoints.includes("responses")
|
||||
);
|
||||
}
|
||||
|
||||
function excludesTextOutputModality(model: UsableChatModelCandidate) {
|
||||
return (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
);
|
||||
}
|
||||
|
||||
export function isUsableChatModel(model: UsableChatModelCandidate) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
if (
|
||||
typeof model.api_format === "string" &&
|
||||
!TEXT_GENERATION_API_FORMATS.has(model.api_format)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (excludesChatAndResponsesEndpoints(model)) return false;
|
||||
if (excludesTextOutputModality(model)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { parseVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { getFamilyFirstModelCandidates } from "@/app/api/v1/vscode/raw/[token]/familyFirstModelIds";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
import { isUsableChatModel } from "@/app/api/v1/vscode/[token]/usableChatModel";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
@@ -31,35 +32,6 @@ type OpenAiCatalogModel = {
|
||||
supported_endpoints?: string[];
|
||||
};
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCatalogModelId(model: OpenAiCatalogModel) {
|
||||
return model.id || model.name || model.root || "unknown";
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/app/api/v1/vscode/raw/[token]/reasoningMetadata";
|
||||
import { parseVscodeServiceTierVariantModelId } from "@/app/api/v1/vscode/raw/[token]/serviceTierVariants";
|
||||
import { withPathTokenApiKey } from "@/app/api/v1/vscode/raw/[token]/tokenizedRequest";
|
||||
import { isUsableChatModel } from "@/app/api/v1/vscode/[token]/usableChatModel";
|
||||
|
||||
type OpenAiCatalogModel = {
|
||||
id?: string;
|
||||
@@ -59,35 +60,6 @@ async function selectPreferredModels(models: OpenAiCatalogModel[]) {
|
||||
return codexModels.length > 0 ? codexModels : models;
|
||||
}
|
||||
|
||||
function isUsableChatModel(model: OpenAiCatalogModel) {
|
||||
if (typeof model.owned_by === "string" && model.owned_by.trim().toLowerCase() === "combo") {
|
||||
return false;
|
||||
}
|
||||
if (typeof model.parent === "string" && model.parent.length > 0) return false;
|
||||
if (typeof model.type === "string" && model.type !== "chat") return false;
|
||||
|
||||
const apiFormat = typeof model.api_format === "string" ? model.api_format : "chat-completions";
|
||||
if (apiFormat !== "chat-completions") return false;
|
||||
|
||||
if (
|
||||
Array.isArray(model.supported_endpoints) &&
|
||||
model.supported_endpoints.length > 0 &&
|
||||
!model.supported_endpoints.includes("chat")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(model.output_modalities) &&
|
||||
model.output_modalities.length > 0 &&
|
||||
!model.output_modalities.includes("text")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getOllamaModelFamily(model: OpenAiCatalogModel, canonicalFamily?: string | null) {
|
||||
const rawModelId = getModelName(model).trim();
|
||||
const tierParsedModel = parseVscodeServiceTierVariantModelId(rawModelId);
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
"tests/unit/8247-accountfallback-model-unhealthy.test.ts",
|
||||
"tests/unit/8248-accountfallback-nvidia-degraded.test.ts",
|
||||
"tests/unit/8332-combo-vision-fallback.test.ts",
|
||||
"tests/unit/8376-econnrefused-breaker.test.ts",
|
||||
"tests/unit/8396-cooldown-429-cap.test.ts",
|
||||
"tests/unit/account-fallback-anthropic-quota.test.ts",
|
||||
"tests/unit/account-fallback-lockout-eviction.test.ts",
|
||||
"tests/unit/account-fallback-retry-after-json.test.ts",
|
||||
@@ -251,6 +253,7 @@
|
||||
"tests/unit/rate-limit-enhanced.test.ts",
|
||||
"tests/unit/rate-limit-manager.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
"tests/unit/repro-7503-no-choices.test.ts",
|
||||
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
||||
"tests/unit/responses-handler.test.ts",
|
||||
"tests/unit/rotation-config-omniroute.test.ts",
|
||||
|
||||
@@ -121,7 +121,7 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
|
||||
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
|
||||
});
|
||||
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 36 audited modules", () => {
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 37 audited modules", () => {
|
||||
const expected = [
|
||||
"_rowTypes",
|
||||
"accessTokens",
|
||||
@@ -133,6 +133,7 @@ test("INTENTIONALLY_INTERNAL contains the expected 36 audited modules", () => {
|
||||
"comboForecast",
|
||||
"commandCodeAuth",
|
||||
"compression",
|
||||
"compressionDetailNormalizers",
|
||||
"detailedLogs",
|
||||
"discovery",
|
||||
"domainState",
|
||||
|
||||
@@ -98,13 +98,21 @@ test("502 transient: exponential backoff doubles until the configured max backof
|
||||
assert.equal(result.newBackoffLevel, level + 1);
|
||||
assert.equal(result.reason, RateLimitReason.SERVER_ERROR);
|
||||
}
|
||||
// #8396: the scaled cooldown is now clamped by capScaledCooldownMs
|
||||
// (open-sse/services/accountFallback/cooldownCap.ts). With no provider the
|
||||
// ceiling is BACKOFF_CONFIG.max, so the doubling stops there instead of
|
||||
// running on to transientInitial * 32.
|
||||
assert.ok(
|
||||
COOLDOWN_MS.transientInitial * 32 > BACKOFF_CONFIG.max,
|
||||
"precondition: the 6th step must exceed the cap, or this test proves nothing"
|
||||
);
|
||||
assert.deepEqual(cooldowns, [
|
||||
COOLDOWN_MS.transientInitial,
|
||||
COOLDOWN_MS.transientInitial * 2,
|
||||
COOLDOWN_MS.transientInitial * 4,
|
||||
COOLDOWN_MS.transientInitial * 8,
|
||||
COOLDOWN_MS.transientInitial * 16,
|
||||
COOLDOWN_MS.transientInitial * 32,
|
||||
BACKOFF_CONFIG.max,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -237,8 +245,14 @@ test("subscription quota uses long cooldown when upstream retry hints are disabl
|
||||
test("high transient backoff levels clamp to the configured maxBackoffSteps", () => {
|
||||
const result = checkFallbackError(502, "", BACKOFF_CONFIG.maxLevel + 5, null, null);
|
||||
assert.equal(result.newBackoffLevel, BACKOFF_CONFIG.maxLevel);
|
||||
// #8396: the level still clamps at maxLevel, but the resulting duration is
|
||||
// additionally capped — unclamped this would be ~45.5h, which is the blackout
|
||||
// that PR removed.
|
||||
assert.equal(
|
||||
result.cooldownMs,
|
||||
COOLDOWN_MS.transientInitial * Math.pow(2, BACKOFF_CONFIG.maxLevel)
|
||||
Math.min(
|
||||
COOLDOWN_MS.transientInitial * Math.pow(2, BACKOFF_CONFIG.maxLevel),
|
||||
BACKOFF_CONFIG.max
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,9 +39,15 @@ test("API profile has shorter transient cooldown", () => {
|
||||
test("Exponential backoff clamps to the configured maxBackoffLevel", () => {
|
||||
const result = checkFallbackError(502, "", 20, null, null);
|
||||
assert.equal(result.newBackoffLevel, BACKOFF_CONFIG.maxLevel);
|
||||
// #8396: the level still clamps at maxLevel, but capScaledCooldownMs also
|
||||
// bounds the duration — with no provider profile the ceiling is
|
||||
// BACKOFF_CONFIG.max rather than the unbounded baseCooldownMs * 2^level.
|
||||
assert.equal(
|
||||
result.cooldownMs,
|
||||
COOLDOWN_MS.transientInitial * Math.pow(2, BACKOFF_CONFIG.maxLevel)
|
||||
Math.min(
|
||||
COOLDOWN_MS.transientInitial * Math.pow(2, BACKOFF_CONFIG.maxLevel),
|
||||
BACKOFF_CONFIG.max
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
153
tests/unit/vscode-token-routes-responses-listing.test.ts
Normal file
153
tests/unit/vscode-token-routes-responses-listing.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
// #7587: PR #7012 widened only the `/models` route's isUsableChatModel copy to
|
||||
// accept Responses-API-format models (e.g. Codex-discovery-synced GPT models with
|
||||
// apiFormat "responses"); the other 4 duplicated copies (tags/show, token + raw)
|
||||
// still rejected anything that wasn't literally "chat-completions", silently
|
||||
// dropping every OpenAI/Codex "responses" model from the Ollama-compatible
|
||||
// listing endpoints VS Code's "Ollama" provider import flow actually uses.
|
||||
//
|
||||
// Split into its own file (rather than appended to vscode-token-routes.test.ts)
|
||||
// because that file is frozen by check-file-size.mjs's testFrozen baseline.
|
||||
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-vscode-responses-listing-")
|
||||
);
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "vscode-responses-listing-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const vscodeModelsRoute = await import("../../src/app/api/v1/vscode/[token]/models/route.ts");
|
||||
const vscodeTagsRoute = await import("../../src/app/api/v1/vscode/[token]/api/tags/route.ts");
|
||||
const vscodeShowRoute = await import("../../src/app/api/v1/vscode/[token]/api/show/route.ts");
|
||||
const vscodeRawTagsRoute =
|
||||
await import("../../src/app/api/v1/vscode/raw/[token]/api/tags/route.ts");
|
||||
const vscodeRawShowRoute =
|
||||
await import("../../src/app/api/v1/vscode/raw/[token]/api/show/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("vscode Ollama-compatible tags/show routes (token + raw) expose Codex-discovered responses-format GPT models", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: true,
|
||||
password: "hashed-password",
|
||||
requireAuthForModels: true,
|
||||
});
|
||||
|
||||
// Note: the id deliberately avoids the "gpt-5.4" family — it collides with
|
||||
// CODEX_DISCOVERY_EXCLUDED_ID_PREFIXES (src/shared/services/codexDiscoveryPolicy.ts)
|
||||
// and would be dropped from the catalog before ever reaching the listing filter
|
||||
// this test targets.
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "apikey",
|
||||
name: "codex-vscode-responses-listing",
|
||||
apiKey: "sk-test",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("codex", connection.id, [
|
||||
{
|
||||
id: "gpt-6.9-responses-probe",
|
||||
name: "GPT 6.9 Responses Probe",
|
||||
apiFormat: "responses",
|
||||
supportedEndpoints: ["responses"],
|
||||
inputTokenLimit: 400000,
|
||||
outputTokenLimit: 128000,
|
||||
},
|
||||
]);
|
||||
const key = await apiKeysDb.createApiKey(
|
||||
"vscode-responses-listing",
|
||||
"machine-vscode-responses-listing"
|
||||
);
|
||||
|
||||
const modelsResponse = await vscodeModelsRoute.GET(
|
||||
new Request(`http://localhost/api/v1/vscode/${encodeURIComponent(key.key)}/models`)
|
||||
);
|
||||
const modelsBody = (await modelsResponse.json()) as {
|
||||
data?: Array<{ id?: string; owned_by?: string; api_format?: string }>;
|
||||
};
|
||||
const responsesModel = (modelsBody.data || []).find(
|
||||
(model) => model.owned_by === "codex" && model.api_format === "responses"
|
||||
);
|
||||
assert.ok(
|
||||
responsesModel,
|
||||
"precondition failed: expected the responses-format GPT model on /models (PR #7012 fix)"
|
||||
);
|
||||
|
||||
const tagsResponse = await vscodeTagsRoute.GET(
|
||||
new Request(`http://localhost/api/v1/vscode/${encodeURIComponent(key.key)}/api/tags`)
|
||||
);
|
||||
const tagsBody = (await tagsResponse.json()) as { models?: Array<{ name?: string }> };
|
||||
const tagNames = new Set((tagsBody.models || []).map((model) => model.name));
|
||||
assert.ok(
|
||||
tagNames.has(responsesModel!.id),
|
||||
`expected /api/tags (Ollama flow) to also expose the responses-format GPT model, got: ${JSON.stringify(
|
||||
Array.from(tagNames)
|
||||
)}`
|
||||
);
|
||||
|
||||
const rawTagsResponse = await vscodeRawTagsRoute.GET(
|
||||
new Request(`http://localhost/api/v1/vscode/raw/${encodeURIComponent(key.key)}/api/tags`)
|
||||
);
|
||||
const rawTagsBody = (await rawTagsResponse.json()) as { models?: Array<{ name?: string }> };
|
||||
const rawTagNames: string[] = (rawTagsBody.models || [])
|
||||
.map((model) => model.name)
|
||||
.filter((name): name is string => typeof name === "string");
|
||||
const rawResponsesModelName = rawTagNames.find((name) =>
|
||||
name.includes("gpt-6.9-responses-probe")
|
||||
);
|
||||
assert.ok(
|
||||
rawResponsesModelName,
|
||||
`expected raw /api/tags to also expose the responses-format GPT model, got: ${JSON.stringify(
|
||||
rawTagNames
|
||||
)}`
|
||||
);
|
||||
|
||||
const showResponse = await vscodeShowRoute.POST(
|
||||
new Request(`http://localhost/api/v1/vscode/${encodeURIComponent(key.key)}/api/show`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: responsesModel!.id }),
|
||||
})
|
||||
);
|
||||
assert.equal(
|
||||
showResponse.status,
|
||||
200,
|
||||
"expected /api/show to find the responses-format GPT model by its tag name"
|
||||
);
|
||||
|
||||
const rawShowResponse = await vscodeRawShowRoute.POST(
|
||||
new Request(`http://localhost/api/v1/vscode/raw/${encodeURIComponent(key.key)}/api/show`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: rawResponsesModelName }),
|
||||
})
|
||||
);
|
||||
assert.equal(
|
||||
rawShowResponse.status,
|
||||
200,
|
||||
"expected raw /api/show to find the responses-format GPT model by its tag name"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user