fix(providers): suppress retired Copilot Gemini models (#9103)

Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates: typecheck/complexity/cognitive/changelog/vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-222248-suite.log
This commit is contained in:
backryun
2026-08-06 10:30:01 +09:00
committed by GitHub
parent a174ea6d33
commit 2ea22bb71d
5 changed files with 231 additions and 106 deletions

View File

@@ -4,10 +4,17 @@
* models/; this file re-exports their public APIs for backward compatibility.
*/
import { isRetiredGitHubCopilotModelId } from "@omniroute/open-sse/config/providers/registry/github/retiredModels.ts";
import { getDbInstance } from "./core";
import { backupDbFile } from "./backup";
import { getProviderConnectionsCount } from "./providers";
import { type JsonRecord, asRecord, toNonEmptyString, getKeyValue } from "./models/shared";
import { type JsonRecord, getKeyValue } from "./models/shared";
import {
normalizeSyncedAvailableModels,
type SyncedAvailableModel,
type SyncedAvailableModelInput,
} from "./models/synced";
import {
readCompatList,
writeCompatList,
@@ -40,6 +47,7 @@ export {
deleteModelAliasesForProvider,
} from "./models/aliases";
export { getMitmAlias, setMitmAliasAll } from "./models/mitmAlias";
export type { SyncedAvailableModel } from "./models/synced";
// ──────────────── Custom Models ────────────────
@@ -318,104 +326,6 @@ export async function removeCustomModel(providerId: string, modelId: string) {
// Each connection stores its own model list. Reads union across all connections
// for a provider. Deleting a connection removes only its models.
export interface SyncedAvailableModel {
id: string;
name: string;
source: "imported";
apiFormat?: string;
targetFormat?: string;
upstreamProtocol?: string;
supportedEndpoints?: string[];
supportedThinkingEfforts?: string[];
defaultThinkingEffort?: string;
inputTokenLimit?: number;
outputTokenLimit?: number;
description?: string;
supportsThinking?: boolean;
alwaysThinking?: boolean;
supportsTools?: boolean;
supportsVideo?: boolean;
// #4264: image-input capability captured at sync time (e.g. OpenRouter
// `architecture.input_modalities`/`modality`) so the catalog can surface vision.
supportsVision?: boolean;
}
type SyncedAvailableModelInput = Omit<SyncedAvailableModel, "source"> & {
source?: string;
};
function normalizeSyncedAvailableModel(model: unknown): SyncedAvailableModel | null {
const record = asRecord(model);
const id =
toNonEmptyString(record.id) || toNonEmptyString(record.name) || toNonEmptyString(record.model);
if (!id) return null;
const name =
toNonEmptyString(record.name) ||
toNonEmptyString(record.displayName) ||
toNonEmptyString(record.model) ||
id;
const supportedEndpoints = Array.isArray(record.supportedEndpoints)
? Array.from(
new Set(
record.supportedEndpoints
.map((endpoint) => toNonEmptyString(endpoint))
.filter((endpoint): endpoint is string => Boolean(endpoint))
)
).sort()
: undefined;
return {
id,
name,
source: "imported",
...(toNonEmptyString(record.apiFormat)
? { apiFormat: toNonEmptyString(record.apiFormat)! }
: {}),
...(toNonEmptyString(record.targetFormat)
? { targetFormat: toNonEmptyString(record.targetFormat)! }
: {}),
...(toNonEmptyString(record.upstreamProtocol)
? { upstreamProtocol: toNonEmptyString(record.upstreamProtocol)! }
: {}),
...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}),
...(Array.isArray(record.supportedThinkingEfforts)
? {
supportedThinkingEfforts: record.supportedThinkingEfforts.filter(
(effort): effort is string => typeof effort === "string" && effort.length > 0
),
}
: {}),
...(toNonEmptyString(record.defaultThinkingEffort)
? { defaultThinkingEffort: toNonEmptyString(record.defaultThinkingEffort)! }
: {}),
...(typeof record.inputTokenLimit === "number"
? { inputTokenLimit: record.inputTokenLimit }
: {}),
...(typeof record.outputTokenLimit === "number"
? { outputTokenLimit: record.outputTokenLimit }
: {}),
...(typeof record.description === "string" ? { description: record.description } : {}),
...(typeof record.supportsThinking === "boolean"
? { supportsThinking: record.supportsThinking }
: {}),
...(record.alwaysThinking === true ? { alwaysThinking: true } : {}),
...(typeof record.supportsTools === "boolean" ? { supportsTools: record.supportsTools } : {}),
...(typeof record.supportsVideo === "boolean" ? { supportsVideo: record.supportsVideo } : {}),
...(record.supportsVision === true ? { supportsVision: true } : {}),
};
}
function normalizeSyncedAvailableModels(models: unknown): SyncedAvailableModel[] {
if (!Array.isArray(models)) return [];
const deduped = new Map<string, SyncedAvailableModel>();
for (const model of models) {
const normalized = normalizeSyncedAvailableModel(model);
if (normalized) deduped.set(normalized.id, normalized);
}
return Array.from(deduped.values());
}
/**
* Get synced available models for a specific provider connection.
*/
@@ -432,7 +342,7 @@ export async function getSyncedAvailableModelsForConnection(
if (!value) return [];
try {
const models = JSON.parse(value);
return normalizeSyncedAvailableModels(models);
return normalizeSyncedAvailableModels(models, providerId);
} catch {
return [];
}
@@ -454,7 +364,7 @@ export async function getSyncedAvailableModels(
for (const row of rows) {
const { key, value } = getKeyValue(row);
if (!key || value === null) continue;
const models = normalizeSyncedAvailableModels(JSON.parse(value));
const models = normalizeSyncedAvailableModels(JSON.parse(value), providerId);
for (const m of models) {
if (m.id) map.set(m.id, m);
}
@@ -481,7 +391,7 @@ export async function getSyncedAvailableModelsByConnection(
if (!key || value === null || !key.startsWith(prefix)) continue;
try {
const connectionId = key.slice(prefix.length);
result[connectionId] = normalizeSyncedAvailableModels(JSON.parse(value));
result[connectionId] = normalizeSyncedAvailableModels(JSON.parse(value), providerId);
} catch {
// Ignore malformed legacy entries.
}
@@ -506,7 +416,7 @@ export async function getAllSyncedAvailableModels(): Promise<
if (!key || value === null) continue;
const providerId = key.split(":")[0];
if (!byProvider.has(providerId)) byProvider.set(providerId, new Map());
const models = normalizeSyncedAvailableModels(JSON.parse(value));
const models = normalizeSyncedAvailableModels(JSON.parse(value), providerId);
const map = byProvider.get(providerId)!;
for (const m of models) {
if (m.id) map.set(m.id, m);
@@ -549,7 +459,8 @@ export async function getActiveProvidersWithSyncedModel(modelId: string): Promis
return rows
.map((row) => row.provider)
.filter((provider): provider is string => typeof provider === "string" && provider.length > 0);
.filter((provider): provider is string => typeof provider === "string" && provider.length > 0)
.filter((provider) => !isRetiredGitHubCopilotModelId(provider, modelId));
}
/**
@@ -570,7 +481,7 @@ export async function replaceSyncedAvailableModelsForConnection(
// the synced store so they remain listed-but-hidden across re-syncs instead of
// churning back on through the managed-alias path ("Auto Sync Enabling all
// Models"). See getModelIsDeleted for the legacy-row caveat.
const normalizedModels = normalizeSyncedAvailableModels(models).filter(
const normalizedModels = normalizeSyncedAvailableModels(models, providerId).filter(
(m) => !getModelIsDeleted(providerId, m.id)
);
if (normalizedModels.length === 0) {
@@ -617,7 +528,7 @@ export async function removeSyncedAvailableModel(
continue;
}
const models = normalizeSyncedAvailableModels(parsedModels);
const models = normalizeSyncedAvailableModels(parsedModels, providerId);
const filtered = models.filter((m) => m.id !== modelId);
if (filtered.length !== models.length) {
removedAny = true;

106
src/lib/db/models/synced.ts Normal file
View File

@@ -0,0 +1,106 @@
import { isRetiredGitHubCopilotModelId } from "@omniroute/open-sse/config/providers/registry/github/retiredModels.ts";
import { asRecord, toNonEmptyString } from "./shared";
export interface SyncedAvailableModel {
id: string;
name: string;
source: "imported";
apiFormat?: string;
targetFormat?: string;
upstreamProtocol?: string;
supportedEndpoints?: string[];
supportedThinkingEfforts?: string[];
defaultThinkingEffort?: string;
inputTokenLimit?: number;
outputTokenLimit?: number;
description?: string;
supportsThinking?: boolean;
alwaysThinking?: boolean;
supportsTools?: boolean;
supportsVideo?: boolean;
// #4264: image-input capability captured at sync time (e.g. OpenRouter
// `architecture.input_modalities`/`modality`) so the catalog can surface vision.
supportsVision?: boolean;
}
export type SyncedAvailableModelInput = Omit<SyncedAvailableModel, "source"> & {
source?: string;
};
function normalizeSyncedAvailableModel(model: unknown): SyncedAvailableModel | null {
const record = asRecord(model);
const id =
toNonEmptyString(record.id) || toNonEmptyString(record.name) || toNonEmptyString(record.model);
if (!id) return null;
const name =
toNonEmptyString(record.name) ||
toNonEmptyString(record.displayName) ||
toNonEmptyString(record.model) ||
id;
const supportedEndpoints = Array.isArray(record.supportedEndpoints)
? Array.from(
new Set(
record.supportedEndpoints
.map((endpoint) => toNonEmptyString(endpoint))
.filter((endpoint): endpoint is string => Boolean(endpoint))
)
).sort()
: undefined;
return {
id,
name,
source: "imported",
...(toNonEmptyString(record.apiFormat)
? { apiFormat: toNonEmptyString(record.apiFormat)! }
: {}),
...(toNonEmptyString(record.targetFormat)
? { targetFormat: toNonEmptyString(record.targetFormat)! }
: {}),
...(toNonEmptyString(record.upstreamProtocol)
? { upstreamProtocol: toNonEmptyString(record.upstreamProtocol)! }
: {}),
...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}),
...(Array.isArray(record.supportedThinkingEfforts)
? {
supportedThinkingEfforts: record.supportedThinkingEfforts.filter(
(effort): effort is string => typeof effort === "string" && effort.length > 0
),
}
: {}),
...(toNonEmptyString(record.defaultThinkingEffort)
? { defaultThinkingEffort: toNonEmptyString(record.defaultThinkingEffort)! }
: {}),
...(typeof record.inputTokenLimit === "number"
? { inputTokenLimit: record.inputTokenLimit }
: {}),
...(typeof record.outputTokenLimit === "number"
? { outputTokenLimit: record.outputTokenLimit }
: {}),
...(typeof record.description === "string" ? { description: record.description } : {}),
...(typeof record.supportsThinking === "boolean"
? { supportsThinking: record.supportsThinking }
: {}),
...(record.alwaysThinking === true ? { alwaysThinking: true } : {}),
...(typeof record.supportsTools === "boolean" ? { supportsTools: record.supportsTools } : {}),
...(typeof record.supportsVideo === "boolean" ? { supportsVideo: record.supportsVideo } : {}),
...(record.supportsVision === true ? { supportsVision: true } : {}),
};
}
export function normalizeSyncedAvailableModels(
models: unknown,
providerId?: string
): SyncedAvailableModel[] {
if (!Array.isArray(models)) return [];
const deduped = new Map<string, SyncedAvailableModel>();
for (const model of models) {
const normalized = normalizeSyncedAvailableModel(model);
if (normalized && !isRetiredGitHubCopilotModelId(providerId, normalized.id)) {
deduped.set(normalized.id, normalized);
}
}
return Array.from(deduped.values());
}