mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 03:42:21 +03:00
feat(models): live account catalog for Claude, Codex, Copilot, AGY (#12866)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. O ponto que sustenta a PR é o `models.dev` virar overlay de preço em vez de fonte de catálogo. Um catálogo estático que sobrevive à conta já ter listado ids mais novos é o tipo de defeito que só aparece quando o modelo novo é justamente o que se quer usar. Nota de integração: `activeSyncedCatalog.ts` colidiu com o #12934 (união dos `customModels` do picker no catálogo de despacho). Como você extraiu o bloco original para `loadConnectionCatalog`, os dois se compõem: a união dos irmãos agy/antigravity primeiro, o `unionCustomModels` por cima. Revalidei com `custom-models-live-catalog-12597`, `live-model-catalog-reconciliation-8926`, `sync-models-degraded-cached-catalog-9683`, `models-dev-catalog-read-gate`, `discovery-class`, `reactive-model-sync` e `l1-oauth-autosync-default` juntos — 48/48 — mais typecheck:core limpo. Sobre o `autoSync` padrão em Claude/Codex/Copilot com scheduler de 6h: passei isso pelo dono antes de mergear e a decisão foi manter como está.
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
} from "@omniroute/open-sse/config/grokBuild.ts";
|
||||
import { getAntigravityContentHeaders } from "@omniroute/open-sse/services/antigravityHeaders.ts";
|
||||
import { parseGeminiModelsList } from "@/lib/providerModels/geminiModelsParser";
|
||||
import { buildClaudeModelsHeaders } from "@/lib/providerModels/claudeModelsHeaders";
|
||||
import {
|
||||
CLINE_MODELS_ENDPOINT,
|
||||
CLINEPASS_MODELS_ENDPOINT,
|
||||
@@ -103,10 +104,12 @@ export function parsePerplexitySonarModels(data: any): any[] {
|
||||
(model: any) => typeof model?.id === "string" && /^sonar(-|$)/.test(model.id)
|
||||
);
|
||||
}
|
||||
type ProviderModelsHeaderContext = {
|
||||
export type ProviderModelsHeaderContext = {
|
||||
authType?: string;
|
||||
providerSpecificData?: unknown;
|
||||
email?: string | null;
|
||||
accessToken?: string | null;
|
||||
apiKey?: string | null;
|
||||
};
|
||||
|
||||
export type ProviderModelsConfigEntry = {
|
||||
@@ -124,6 +127,20 @@ export type ProviderModelsConfigEntry = {
|
||||
parseResponse: (data: any) => any;
|
||||
};
|
||||
|
||||
export function assembleProviderModelsHeaders(
|
||||
config: ProviderModelsConfigEntry,
|
||||
token: string,
|
||||
context?: ProviderModelsHeaderContext,
|
||||
): Record<string, string> {
|
||||
const headers = config.buildHeaders
|
||||
? config.buildHeaders(token, context)
|
||||
: { ...config.headers };
|
||||
if (!config.buildHeaders && config.authHeader && !config.authQuery) {
|
||||
headers[config.authHeader] = (config.authPrefix || "") + token;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
const DASHSCOPE_TEXT_MODELS_CONFIG: ProviderModelsConfigEntry = {
|
||||
url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models",
|
||||
method: "GET",
|
||||
@@ -387,10 +404,14 @@ export const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> =
|
||||
url: "https://api.anthropic.com/v1/models",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Anthropic-Version": "2023-06-01",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
authHeader: "x-api-key",
|
||||
buildHeaders: (_token, context) =>
|
||||
buildClaudeModelsHeaders({
|
||||
accessToken: context?.accessToken,
|
||||
apiKey: context?.apiKey,
|
||||
}),
|
||||
parseResponse: (data) => data.data || [],
|
||||
},
|
||||
gemini: {
|
||||
|
||||
@@ -92,6 +92,7 @@ import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLease
|
||||
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
|
||||
import { fetchCursorAvailableModels } from "@/lib/providerModels/cursorAvailableModels";
|
||||
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
|
||||
import { resolveCopilotDiscoveryToken } from "@/lib/providerModels/copilotDiscoveryToken";
|
||||
import {
|
||||
type JsonRecord,
|
||||
asRecord,
|
||||
@@ -117,6 +118,7 @@ import { isNamedOpenAIStyleProvider } from "./discovery/providerSets";
|
||||
import { buildStaleEncryptionKeyResponse } from "./staleEncryptionGuard";
|
||||
import {
|
||||
type ProviderModelsConfigEntry,
|
||||
assembleProviderModelsHeaders,
|
||||
PROVIDER_MODELS_CONFIG,
|
||||
} from "./discovery/providerModelsConfig";
|
||||
import {
|
||||
@@ -1336,14 +1338,6 @@ export async function GET(
|
||||
return buildApiDiscoveryResponse(normalizeSapModelsResponse(await response.json()));
|
||||
}
|
||||
|
||||
if (provider === "claude") {
|
||||
return buildResponse({
|
||||
provider,
|
||||
connectionId,
|
||||
models: getStaticModelsForProvider("claude") || [],
|
||||
});
|
||||
}
|
||||
|
||||
if (provider === "cursor") {
|
||||
const cachedResponse = maybeReturnCachedDiscovery();
|
||||
if (cachedResponse) return cachedResponse;
|
||||
@@ -1650,8 +1644,10 @@ export async function GET(
|
||||
// the exchanged token; only DISCOVERY needs the raw token.) This mirrors the
|
||||
// Copilot CLI + Hermes "de-gate model discovery" fix. Exchanged token stays
|
||||
// as a fallback for connections that only captured that.
|
||||
const copilotToken =
|
||||
toNonEmptyString(accessToken) || toNonEmptyString(psd.copilotToken) || null;
|
||||
const copilotToken = resolveCopilotDiscoveryToken({
|
||||
accessToken,
|
||||
copilotToken: psd.copilotToken,
|
||||
});
|
||||
|
||||
const discovery = await fetchGitHubCopilotModels({
|
||||
token: copilotToken,
|
||||
@@ -1697,8 +1693,10 @@ export async function GET(
|
||||
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
|
||||
|
||||
const psd = asRecord(connection.providerSpecificData);
|
||||
const copilotToken =
|
||||
toNonEmptyString(psd.copilotToken) || toNonEmptyString(accessToken) || null;
|
||||
const copilotToken = resolveCopilotDiscoveryToken({
|
||||
accessToken,
|
||||
copilotToken: psd.copilotToken,
|
||||
});
|
||||
// endpoints.api serves the real chat model catalog; endpoints.proxy only
|
||||
// has NES/autocomplete models. Prefer the api host, fall back to proxy for
|
||||
// legacy connections that predate copilotApiUrl capture.
|
||||
@@ -2157,10 +2155,13 @@ export async function GET(
|
||||
}
|
||||
|
||||
if (githubCatalogModels && githubCatalogModels.length > 0) {
|
||||
return buildApiDiscoveryResponse(
|
||||
finalizeCodexCatalog(githubCatalogModels),
|
||||
"Codex live catalog unavailable — using GitHub model catalog"
|
||||
);
|
||||
return buildResponse({
|
||||
provider,
|
||||
connectionId,
|
||||
models: finalizeCodexCatalog(githubCatalogModels),
|
||||
source: "github_catalog",
|
||||
warning: "Codex live catalog unavailable — using GitHub model catalog",
|
||||
});
|
||||
}
|
||||
|
||||
if (cachedDiscoveryModels.length > 0) {
|
||||
@@ -2292,12 +2293,8 @@ export async function GET(
|
||||
}
|
||||
|
||||
// Build headers
|
||||
const headers = config.buildHeaders
|
||||
? config.buildHeaders(token, connection)
|
||||
: { ...config.headers };
|
||||
if (!config.buildHeaders && config.authHeader && !config.authQuery) {
|
||||
headers[config.authHeader] = (config.authPrefix || "") + token;
|
||||
}
|
||||
const headerContext = { ...connection, accessToken, apiKey };
|
||||
const headers = assembleProviderModelsHeaders(config, token, headerContext);
|
||||
|
||||
// Make request (with pagination for providers that use nextPageToken, e.g. Gemini)
|
||||
const fetchOptions: any = {
|
||||
|
||||
@@ -48,6 +48,23 @@ export function isDegradedCachedCatalog(modelsData: {
|
||||
return typeof modelsData?.warning === "string" && modelsData.warning.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex live-empty GET returns `{ source: "github_catalog", warning: "…" }`
|
||||
* via `buildResponse`. That is a display fallback of public models.json, not
|
||||
* an authoritative discovery. Same discriminator as cache: source match + a
|
||||
* non-empty warning. Without this, Import/Sync and boot ModelSync would persist
|
||||
* the public catalog as the synced source of truth (spec 3.7).
|
||||
*/
|
||||
export function isDegradedGithubCatalog(modelsData: {
|
||||
source?: unknown;
|
||||
warning?: unknown;
|
||||
}): boolean {
|
||||
const source =
|
||||
typeof modelsData?.source === "string" ? modelsData.source.trim().toLowerCase() : "";
|
||||
if (source !== "github_catalog") return false;
|
||||
return typeof modelsData?.warning === "string" && modelsData.warning.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Either degraded shape. Model-sync must refuse to treat these as a successful
|
||||
* discovery: persisting them would silently pin a stale catalog and hide the
|
||||
@@ -58,5 +75,9 @@ export function isDegradedDiscovery(modelsData: {
|
||||
intentional?: unknown;
|
||||
warning?: unknown;
|
||||
}): boolean {
|
||||
return isDegradedLocalCatalog(modelsData) || isDegradedCachedCatalog(modelsData);
|
||||
return (
|
||||
isDegradedLocalCatalog(modelsData) ||
|
||||
isDegradedCachedCatalog(modelsData) ||
|
||||
isDegradedGithubCatalog(modelsData)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user