mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
feat(sse): discover Anthropic partner models on Vertex AI (#11279)
Validated on the combined 8-PR board + the branch itself: vertex-anthropic-models 4/4 (new — pushed to your branch as a fix-in-place commit per Hard Rule #8, covering the parser's global/project-scoped resource names, malformed-input handling, and the claude-* → targetFormat heuristic on vertex/vertex-partner), 88/88 across the board's focused suites, typecheck:core clean, gates within baseline. Anthropic partner models on Vertex AI now discover dynamically via the Model Garden publisher endpoint and route through the Claude translator even for models outside the static registry. Thank you @maci0!
This commit is contained in:
@@ -85,10 +85,7 @@ import {
|
||||
} from "@/lib/providerModels/modelDiscovery";
|
||||
import { buildProviderModelsUrl, getDiscoveryClientVersionOptions } from "./discoveryClientVersion";
|
||||
import { getAdobeModels } from "./adobeFireflyDiscovery";
|
||||
import {
|
||||
parseGeminiModelsList,
|
||||
type GeminiDiscoveryModel,
|
||||
} from "@/lib/providerModels/geminiModelsParser";
|
||||
import { parseGeminiModelsList } from "@/lib/providerModels/geminiModelsParser";
|
||||
import { getSyncedAvailableModels, getCustomModels } from "@/lib/db/models";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
|
||||
@@ -1860,7 +1857,7 @@ export async function GET(
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (bearerToken) headers["Authorization"] = `Bearer ${bearerToken}`;
|
||||
|
||||
const allModels: GeminiDiscoveryModel[] = [];
|
||||
const allModels: any[] = [];
|
||||
let pageUrl = queryKey ? `${baseUrl}&key=${encodeURIComponent(queryKey)}` : baseUrl;
|
||||
let pageCount = 0;
|
||||
const MAX_PAGES = 20;
|
||||
@@ -1906,6 +1903,60 @@ export async function GET(
|
||||
throw error;
|
||||
}
|
||||
|
||||
// ponytail: Anthropic partner models via Model Garden publisher endpoint (Bearer only)
|
||||
if (bearerToken) {
|
||||
const psd = asRecord(connection.providerSpecificData);
|
||||
const region =
|
||||
(typeof psd.region === "string" && psd.region.trim()) || "us-central1";
|
||||
|
||||
// Extract project_id from SA JSON for project-scoped listing (mirrors executor URL pattern).
|
||||
// Falls back to global publisher endpoint if no project available.
|
||||
let anthropicModelsUrl: string;
|
||||
let projectId: string | null = null;
|
||||
if (credential) {
|
||||
try {
|
||||
const sa = JSON.parse(credential);
|
||||
if (sa?.project_id) projectId = sa.project_id;
|
||||
} catch { /* not SA JSON, skip */ }
|
||||
}
|
||||
if (projectId) {
|
||||
anthropicModelsUrl = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/anthropic/models`;
|
||||
} else {
|
||||
anthropicModelsUrl = `https://aiplatform.googleapis.com/v1/publishers/anthropic/models`;
|
||||
}
|
||||
|
||||
try {
|
||||
const anthropicResponse = await safeOutboundFetch(anthropicModelsUrl, {
|
||||
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
|
||||
guard: getProviderOutboundGuard(),
|
||||
proxyConfig: proxy,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
});
|
||||
if (anthropicResponse.ok) {
|
||||
const anthropicData = await anthropicResponse.json();
|
||||
const { parseVertexAnthropicModels } = await import(
|
||||
"@/lib/providerModels/vertexAnthropicModelsParser"
|
||||
);
|
||||
allModels.push(...parseVertexAnthropicModels(anthropicData));
|
||||
} else {
|
||||
console.log("[models] Vertex Anthropic partner discovery failed", {
|
||||
provider,
|
||||
region,
|
||||
status: anthropicResponse.status,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("[models] Vertex Anthropic partner discovery error", {
|
||||
provider,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (allModels.length > 0) {
|
||||
return buildApiDiscoveryResponse(allModels);
|
||||
}
|
||||
|
||||
44
src/lib/providerModels/vertexAnthropicModelsParser.ts
Normal file
44
src/lib/providerModels/vertexAnthropicModelsParser.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
interface VertexPublisherModel {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
supportedActions?: string[];
|
||||
versionId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface VertexAnthropicDiscoveryModel {
|
||||
id: string;
|
||||
name: string;
|
||||
supportedEndpoints: string[];
|
||||
targetFormat: string;
|
||||
owned_by: string;
|
||||
description?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function parseVertexAnthropicModels(data: unknown): VertexAnthropicDiscoveryModel[] {
|
||||
if (!data || typeof data !== "object") return [];
|
||||
const envelope = data as { models?: unknown[] };
|
||||
const models = Array.isArray(envelope.models) ? envelope.models : [];
|
||||
|
||||
return models
|
||||
.map((m: unknown) => {
|
||||
const model = m as VertexPublisherModel;
|
||||
const rawName = typeof model.name === "string" ? model.name : "";
|
||||
// "publishers/anthropic/models/claude-sonnet-4-6" or
|
||||
// "projects/x/locations/y/publishers/anthropic/models/claude-sonnet-4-6"
|
||||
const id = rawName.replace(/^(?:projects\/[^/]+\/locations\/[^/]+\/)?publishers\/anthropic\/models\//, "") || rawName;
|
||||
if (!id) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: (typeof model.displayName === "string" && model.displayName) || id,
|
||||
supportedEndpoints: ["chat"],
|
||||
targetFormat: "claude",
|
||||
...(typeof model.description === "string" ? { description: model.description } : {}),
|
||||
owned_by: "anthropic",
|
||||
} satisfies VertexAnthropicDiscoveryModel;
|
||||
})
|
||||
.filter((m): m is VertexAnthropicDiscoveryModel => m !== null);
|
||||
}
|
||||
Reference in New Issue
Block a user