fix(ai): updated google vertex model catalog

Replaced Google Vertex project discovery with the models.dev catalog so bundled model selection includes current Vertex MaaS and Gemini entries while pruning retired fallbacks.

Fixes #1456
This commit is contained in:
roboomp
2026-05-27 19:14:26 +00:00
parent 6fac33f099
commit 3ea4981eeb
10 changed files with 1023 additions and 516 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Google Vertex's bundled model list to use the authoritative models.dev catalog, including MaaS entries such as `deepseek-ai/deepseek-v3.2-maas` and removing retired Gemini 1.5 fallbacks. ([#1456](https://github.com/can1357/oh-my-pi/issues/1456))
## [15.5.7] - 2026-05-27
### Added
- `SimpleStreamOptions.openrouterVariant` (`"nitro"`, `"floor"`, `"online"`, `"exacto"`, …) — when set, appends `:<variant>` to OpenRouter model IDs at request time, leaving ids that already carry an explicit `:suffix` untouched. Plumbed through `openai-completions` and the pi-native gateway forwarder.
+15 -6
View File
@@ -354,18 +354,27 @@ async function generateModels() {
}
}
// Merge previous models.json entries as fallback for any provider/model
// not fetched dynamically. This replaces all hardcoded fallback lists —
// static-only providers (vertex, gemini-cli), auth-gated providers when
// credentials are unavailable, and ad-hoc model additions all persist
// through the existing models.json seed.
const modelsDevAuthoritativeProviders = new Set<string>();
for (const model of modelsDevModels) {
if (model.provider === "google-vertex") {
modelsDevAuthoritativeProviders.add(model.provider);
}
}
// Merge previous models.json entries as fallback for provider/model pairs not
// fetched dynamically. Providers that models.dev covers authoritatively keep
// the upstream list exactly, so retired entries from the previous snapshot do
// not reappear during regeneration.
// Discovery-only providers (local inference servers) — never bundle static models.
const discoveryOnlyProviders = new Set(["ollama", "vllm"]);
const fetchedKeys = new Set(allModels.map(model => `${model.provider}/${model.id}`));
for (const models of Object.values(prevModelsJson as Record<string, Record<string, Model>>)) {
for (const model of Object.values(models)) {
if (!fetchedKeys.has(`${model.provider}/${model.id}`) && !discoveryOnlyProviders.has(model.provider)) {
if (
!fetchedKeys.has(`${model.provider}/${model.id}`) &&
!discoveryOnlyProviders.has(model.provider) &&
!modelsDevAuthoritativeProviders.has(model.provider)
) {
allModels.push(model);
}
}
File diff suppressed because it is too large Load Diff
+2 -38
View File
@@ -2,7 +2,6 @@ import type { ModelManagerOptions } from "../model-manager";
import type { FetchImpl } from "../types";
import { fetchAntigravityDiscoveryModels } from "../utils/discovery/antigravity";
import { fetchGeminiModels } from "../utils/discovery/gemini";
import { fetchVertexOpenAIModels } from "../utils/discovery/vertex";
export interface GoogleModelManagerConfig {
apiKey?: string;
@@ -38,43 +37,8 @@ export function googleModelManagerOptions(
};
}
export function googleVertexModelManagerOptions(config?: GoogleVertexModelManagerConfig): ModelManagerOptions {
const project = resolveVertexProject(config);
const hasApiKey = (config?.apiKey ?? Bun.env.GOOGLE_CLOUD_API_KEY ?? "").trim().length > 0;
const location = resolveVertexLocation(config);
if (hasApiKey) {
return { providerId: "google-vertex" };
}
if (project && location) {
return {
providerId: "google-vertex",
staticModels: [],
fetchDynamicModels: () =>
fetchVertexOpenAIModels({
project,
location,
signal: config?.signal,
fetch: config?.fetch,
}),
};
}
// With neither ADC project+location nor API key auth configured, drop the
// bundled static catalog so stale fallbacks (e.g. `gemini-1.5-*`) cannot leak
// into `/models` alongside an authoritative cached Vertex project catalog on
// the next refresh.
return { providerId: "google-vertex", staticModels: [] };
}
function resolveVertexProject(config?: GoogleVertexModelManagerConfig): string | undefined {
const project = config?.project ?? Bun.env.GOOGLE_CLOUD_PROJECT ?? Bun.env.GCP_PROJECT ?? Bun.env.GCLOUD_PROJECT;
const trimmed = project?.trim();
return trimmed ? trimmed : undefined;
}
function resolveVertexLocation(config?: GoogleVertexModelManagerConfig): string | undefined {
const location =
config?.location ?? Bun.env.GOOGLE_VERTEX_LOCATION ?? Bun.env.GOOGLE_CLOUD_LOCATION ?? Bun.env.VERTEX_LOCATION;
const trimmed = location?.trim();
return trimmed ? trimmed : undefined;
export function googleVertexModelManagerOptions(_config?: GoogleVertexModelManagerConfig): ModelManagerOptions {
return { providerId: "google-vertex" };
}
export function googleAntigravityModelManagerOptions(
@@ -2360,6 +2360,17 @@ function anthropicMessagesDescriptor(
return simpleModelsDevDescriptor(modelsDevKey, providerId, "anthropic-messages", baseUrl, options);
}
const GOOGLE_VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com";
const GOOGLE_VERTEX_OPENAI_BASE_URL =
"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi";
function resolveGoogleVertexApi(modelId: string, raw: ModelsDevModel): { api: Api; baseUrl: string } {
if (modelId.includes("/") || raw.provider?.npm === "@ai-sdk/openai-compatible") {
return { api: "openai-completions", baseUrl: GOOGLE_VERTEX_OPENAI_BASE_URL };
}
return { api: "google-vertex", baseUrl: GOOGLE_VERTEX_BASE_URL };
}
const MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescriptor[] = [
// --- Amazon Bedrock ---
{
@@ -2515,6 +2526,13 @@ const filterActiveToolCallModels = (_id: string, m: ModelsDevModel): boolean =>
return true;
};
const MODELS_DEV_PROVIDER_DESCRIPTORS_GOOGLE_VERTEX: readonly ModelsDevProviderDescriptor[] = [
simpleModelsDevDescriptor("google-vertex", "google-vertex", "google-vertex", GOOGLE_VERTEX_BASE_URL, {
filterModel: filterActiveToolCallModels,
resolveApi: resolveGoogleVertexApi,
}),
];
const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDescriptor[] = [
// --- Cloudflare AI Gateway ---
anthropicMessagesDescriptor(
@@ -2592,6 +2610,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
/** All provider descriptors for models.dev data mapping in generate-models.ts. */
export const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [
...MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK,
...MODELS_DEV_PROVIDER_DESCRIPTORS_GOOGLE_VERTEX,
...MODELS_DEV_PROVIDER_DESCRIPTORS_CORE,
...MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS,
...MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED,
+5 -1
View File
@@ -48,7 +48,8 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
const location = resolveLocation(options);
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
const host = resolveEndpointHost(location);
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
const publisher = resolvePublisher(model.id);
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/${publisher}/models/${model.id}:streamGenerateContent?alt=sse`;
return {
params,
url,
@@ -76,6 +77,9 @@ function resolveProject(options?: GoogleVertexOptions): string {
return project;
}
function resolvePublisher(modelId: string): string {
return modelId.startsWith("claude-") ? "anthropic" : "google";
}
function resolveEndpointHost(location: string): string {
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
}
+26 -1
View File
@@ -90,11 +90,36 @@ function createVertexOpenAIFetch(options: StreamOptions | undefined): FetchImpl
const token = await getVertexAccessToken({ signal: options?.signal, fetch: baseFetch });
const headers = new Headers(init?.headers);
headers.set("Authorization", `Bearer ${token}`);
return baseFetch(input, { ...init, headers });
return baseFetch(resolveVertexOpenAIRequest(input), { ...init, headers });
};
return Object.assign(vertexFetch, baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {});
}
function resolveVertexOpenAIRequest(input: string | URL | Request): string | URL | Request {
const project = $env.GOOGLE_CLOUD_PROJECT || $env.GCP_PROJECT || $env.GCLOUD_PROJECT;
const location = $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION;
if (!project || !location) return input;
const rewriteUrl = (url: string): string => {
if (!url.includes("{project}") && !url.includes("{location}")) return url;
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
return url
.replace("https://{location}-aiplatform.googleapis.com", `https://${host}`)
.replaceAll("{project}", encodeURIComponent(project))
.replaceAll("{location}", encodeURIComponent(location));
};
if (input instanceof Request) {
const rewrittenUrl = rewriteUrl(input.url);
return rewrittenUrl === input.url ? input : new Request(rewrittenUrl, input);
}
if (input instanceof URL) {
const rewrittenUrl = rewriteUrl(input.toString());
return rewrittenUrl === input.toString() ? input : new URL(rewrittenUrl);
}
return rewriteUrl(input);
}
type KeyResolver = string | (() => string | undefined);
const serviceProviderMap: Record<string, KeyResolver> = {
-1
View File
@@ -2,4 +2,3 @@ export * from "./antigravity";
export * from "./codex";
export * from "./gemini";
export * from "./openai-compatible";
export * from "./vertex";
-210
View File
@@ -1,210 +0,0 @@
import { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "@oh-my-pi/pi-ai";
import * as z from "zod/v4";
import { getVertexAccessToken } from "../../providers/google-auth";
import type { FetchImpl, Model } from "../../types";
const API_VERSION = "v1";
const DEFAULT_PAGE_SIZE = 100;
const DEFAULT_MAX_PAGES = 25;
const vertexOpenAIModelSchema = z.object({
id: z.string().optional().catch(undefined),
name: z.string().optional().catch(undefined),
displayName: z.string().optional().catch(undefined),
});
const vertexOpenAIModelsResponseSchema = z.object({
data: z
.array(z.unknown())
.optional()
.transform(items => {
if (!items) return [];
const parsedItems: VertexOpenAIModelItem[] = [];
for (const item of items) {
const parsed = vertexOpenAIModelSchema.safeParse(item);
if (parsed.success) parsedItems.push(parsed.data);
}
return parsedItems;
}),
nextPageToken: z.string().optional().catch(undefined),
});
type VertexOpenAIModelItem = z.infer<typeof vertexOpenAIModelSchema>;
/** Configuration for Vertex AI OpenAI-compatible model discovery. */
export interface VertexDiscoveryOptions {
/** Google Cloud project ID hosting the Vertex AI endpoint. */
project: string;
/** Vertex AI location, for example `global` or `us-central1`. */
location: string;
/** Optional requested page size for model listing. */
pageSize?: number;
/** Maximum number of pages to request before stopping pagination. */
maxPages?: number;
/** Optional abort signal for HTTP requests. */
signal?: AbortSignal;
/** Optional fetch implementation override for tests. */
fetch?: FetchImpl;
}
/**
* Fetches models exposed by Vertex AI's OpenAI-compatible endpoint.
*
* Returns `null` on auth, transport, or protocol failures so callers can fall
* back to cache/static models without surfacing discovery noise at startup.
*/
export async function fetchVertexOpenAIModels(
options: VertexDiscoveryOptions,
): Promise<Model<"openai-completions">[] | null> {
const project = options.project.trim();
const location = options.location.trim();
if (!project || !location) return null;
const fetchImpl = options.fetch ?? fetch;
const baseUrl = buildVertexOpenAIBaseUrl(project, location);
const pageSize = normalizePositiveInt(options.pageSize, DEFAULT_PAGE_SIZE);
const maxPages = normalizePositiveInt(options.maxPages, DEFAULT_MAX_PAGES);
let accessToken: string;
try {
accessToken = await getVertexAccessToken({ signal: options.signal, fetch: fetchImpl });
} catch {
return null;
}
const modelsById = new Map<string, Model<"openai-completions">>();
const seenTokens = new Set<string>();
let nextPageToken: string | undefined;
for (let page = 0; page < maxPages; page += 1) {
const requestUrl = buildModelsUrl(baseUrl, pageSize, nextPageToken);
let response: Response;
try {
response = await fetchImpl(requestUrl, {
method: "GET",
headers: { Authorization: `Bearer ${accessToken}` },
signal: options.signal,
});
} catch {
return null;
}
if (!response.ok) return null;
let payload: unknown;
try {
payload = await response.json();
} catch {
return null;
}
const parsed = vertexOpenAIModelsResponseSchema.safeParse(payload);
if (!parsed.success) return null;
for (const item of parsed.data.data) {
const model = normalizeModel(item, baseUrl);
if (model) modelsById.set(model.id, model);
}
const token = normalizePageToken(parsed.data.nextPageToken);
if (!token || seenTokens.has(token)) break;
seenTokens.add(token);
nextPageToken = token;
}
return Array.from(modelsById.values()).sort((left, right) => left.id.localeCompare(right.id));
}
/** Returns the stable Vertex AI OpenAI-compatible endpoint base URL. */
export function buildVertexOpenAIBaseUrl(project: string, location: string): string {
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
return `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/endpoints/openapi`;
}
function buildModelsUrl(baseUrl: string, pageSize: number, pageToken?: string): URL {
const url = new URL(`${baseUrl}/models`);
url.searchParams.set("pageSize", String(pageSize));
if (pageToken) url.searchParams.set("pageToken", pageToken);
return url;
}
function normalizePositiveInt(value: number | undefined, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallback;
const normalized = Math.floor(value);
return normalized > 0 ? normalized : fallback;
}
function normalizePageToken(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const token = value.trim();
return token.length > 0 ? token : undefined;
}
function normalizeModel(item: VertexOpenAIModelItem, baseUrl: string): Model<"openai-completions"> | null {
const id = normalizeModelId(item.id ?? item.name);
if (!id) return null;
return {
id,
name: normalizeModelName(item.displayName, id),
api: "openai-completions",
provider: "google-vertex",
baseUrl,
reasoning: inferReasoning(id),
input: inferInput(id),
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: UNK_CONTEXT_WINDOW,
maxTokens: UNK_MAX_TOKENS,
};
}
function normalizeModelId(value: string | undefined): string | null {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed) return null;
const marker = "/models/";
const markerIndex = trimmed.lastIndexOf(marker);
if (markerIndex >= 0) {
const modelId = trimmed.slice(markerIndex + marker.length);
const publisher = extractPublisher(trimmed.slice(0, markerIndex));
return publisher ? `${publisher}/${modelId}` : modelId;
}
return trimmed;
}
function extractPublisher(prefix: string): string | undefined {
const marker = "/publishers/";
const markerIndex = prefix.lastIndexOf(marker);
if (markerIndex < 0) return undefined;
const publisher = prefix.slice(markerIndex + marker.length).trim();
return publisher.length > 0 ? publisher : undefined;
}
function normalizeModelName(displayName: string | undefined, id: string): string {
const trimmed = displayName?.trim();
return trimmed ? trimmed : id;
}
function inferReasoning(id: string): boolean {
const normalized = id.toLowerCase();
return (
normalized.includes("thinking") ||
normalized.includes("reasoning") ||
normalized.includes("glm-4.5") ||
normalized.includes("glm-4.6") ||
normalized.includes("glm-4.7") ||
normalized.includes("glm-5") ||
normalized.includes("gemini-2.5") ||
normalized.includes("gemini-3")
);
}
function inferInput(id: string): ("text" | "image")[] {
const normalized = id.toLowerCase();
if (
normalized.includes("gemini") ||
normalized.includes("vision") ||
normalized.includes("image") ||
normalized.includes("vl")
) {
return ["text", "image"];
}
return ["text"];
}
+72 -115
View File
@@ -1,131 +1,88 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it } from "bun:test";
import { resolveProviderModels } from "../src/model-manager";
import { googleVertexModelManagerOptions } from "../src/provider-models/google";
import { __resetVertexTokenCache } from "../src/providers/google-auth";
import { MODELS_DEV_PROVIDER_DESCRIPTORS, mapModelsDevToModels } from "../src/provider-models/openai-compat";
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
const METADATA_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
describe("google-vertex model discovery", () => {
let tempDir = "";
let dbPath = "";
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-ai-vertex-models-"));
dbPath = path.join(tempDir, "models.db");
});
afterEach(async () => {
__resetVertexTokenCache();
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = "";
dbPath = "";
}
});
it("uses the Vertex OpenAI-compatible model list as the authoritative project catalog", async () => {
const urls: string[] = [];
const options = googleVertexModelManagerOptions({
project: "vertex-project",
location: "global",
fetch: async input => {
const url = input instanceof Request ? input.url : input.toString();
urls.push(url);
if (url === METADATA_TOKEN_URL || url === OAUTH_TOKEN_URL) {
return new Response(JSON.stringify({ access_token: "vertex-token", expires_in: 3600 }));
}
if (
url.startsWith(
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/models",
)
) {
return new Response(
JSON.stringify({
data: [
{ id: "zai-org/glm-4.7-maas", displayName: "GLM-4.7" },
{
name: "projects/vertex-project/locations/global/publishers/anthropic/models/claude-sonnet-4-5",
displayName: "Claude Sonnet 4.5",
},
],
}),
);
}
return new Response("not found", { status: 404 });
const googleVertexModelsDevPayload = {
"google-vertex": {
models: {
"gemini-3.5-flash": {
name: "Gemini 3.5 Flash",
tool_call: true,
reasoning: true,
modalities: { input: ["text", "image", "pdf"] },
limit: { context: 1_048_576, output: 65_536 },
cost: { input: 0.3, output: 2.5, cache_read: 0.03, cache_write: 0.75 },
provider: { npm: "@ai-sdk/google-vertex" },
},
});
"deepseek-ai/deepseek-v3.2-maas": {
name: "DeepSeek V3.2",
tool_call: true,
reasoning: true,
modalities: { input: ["text", "pdf"] },
limit: { context: 163_840, output: 65_536 },
provider: { npm: "@ai-sdk/openai-compatible" },
},
"claude-sonnet-4@20250514": {
name: "Claude Sonnet 4",
tool_call: true,
reasoning: true,
modalities: { input: ["text", "image", "pdf"] },
limit: { context: 200_000, output: 64_000 },
provider: { npm: "@ai-sdk/google-vertex/anthropic" },
},
"gemini-embedding-001": {
name: "Gemini Embedding 001",
tool_call: false,
provider: { npm: "@ai-sdk/google-vertex" },
},
},
},
} satisfies Record<string, unknown>;
const result = await resolveProviderModels({ ...options, cacheDbPath: dbPath }, "online");
expect(result.stale).toBe(false);
expect(result.models.map(model => model.id)).toEqual(["anthropic/claude-sonnet-4-5", "zai-org/glm-4.7-maas"]);
expect(result.models.every(model => model.provider === "google-vertex")).toBe(true);
expect(result.models.every(model => model.api === "openai-completions")).toBe(true);
expect(
result.models.every(
model =>
model.baseUrl ===
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi",
),
).toBe(true);
expect(result.models.some(model => model.id === "gemini-1.5-pro")).toBe(false);
expect(urls).toContain(
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/models?pageSize=100",
describe("google-vertex model catalog", () => {
it("maps the models.dev Vertex catalog instead of the project discovery endpoint", () => {
const models = mapModelsDevToModels(googleVertexModelsDevPayload, MODELS_DEV_PROVIDER_DESCRIPTORS).filter(
model => model.provider === "google-vertex",
);
expect(models.map(model => model.id)).toEqual([
"gemini-3.5-flash",
"deepseek-ai/deepseek-v3.2-maas",
"claude-sonnet-4@20250514",
]);
const gemini = models.find(model => model.id === "gemini-3.5-flash");
expect(gemini?.api).toBe("google-vertex");
expect(gemini?.baseUrl).toBe("https://{location}-aiplatform.googleapis.com");
expect(gemini?.input).toEqual(["text", "image"]);
expect(gemini?.contextWindow).toBe(1_048_576);
const deepseek = models.find(model => model.id === "deepseek-ai/deepseek-v3.2-maas");
expect(deepseek?.api).toBe("openai-completions");
expect(deepseek?.baseUrl).toBe(
"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi",
);
const claude = models.find(model => model.id === "claude-sonnet-4@20250514");
expect(claude?.api).toBe("google-vertex");
expect(claude?.reasoning).toBe(true);
});
it("keeps the API-key Vertex Gemini catalog when project and location are also configured", async () => {
it("uses the bundled Vertex catalog without ADC project discovery", async () => {
const options = googleVertexModelManagerOptions({
apiKey: "vertex-api-key",
project: "vertex-project",
location: "global",
fetch: async () => new Response("unexpected", { status: 500 }),
});
const result = await resolveProviderModels({ ...options, cacheDbPath: dbPath }, "offline");
expect(options.fetchDynamicModels).toBeUndefined();
expect(options.staticModels).toBeUndefined();
expect(result.models.some(model => model.id === "gemini-2.5-pro")).toBe(true);
expect(result.models.every(model => model.provider === "google-vertex")).toBe(true);
expect(result.models.every(model => model.api === "google-vertex")).toBe(true);
});
it("omits the bundled Vertex Gemini static fallback when neither ADC project nor API key are configured", async () => {
const previousProject = Bun.env.GOOGLE_CLOUD_PROJECT;
const previousGcpProject = Bun.env.GCP_PROJECT;
const previousGcloudProject = Bun.env.GCLOUD_PROJECT;
const previousVertexLocation = Bun.env.GOOGLE_VERTEX_LOCATION;
const previousCloudLocation = Bun.env.GOOGLE_CLOUD_LOCATION;
const previousLocation = Bun.env.VERTEX_LOCATION;
const previousApiKey = Bun.env.GOOGLE_CLOUD_API_KEY;
delete Bun.env.GOOGLE_CLOUD_PROJECT;
delete Bun.env.GCP_PROJECT;
delete Bun.env.GCLOUD_PROJECT;
delete Bun.env.GOOGLE_VERTEX_LOCATION;
delete Bun.env.GOOGLE_CLOUD_LOCATION;
delete Bun.env.VERTEX_LOCATION;
delete Bun.env.GOOGLE_CLOUD_API_KEY;
try {
const options = googleVertexModelManagerOptions();
const result = await resolveProviderModels({ ...options, cacheDbPath: dbPath }, "offline");
expect(result.models).toEqual([]);
} finally {
if (previousProject === undefined) delete Bun.env.GOOGLE_CLOUD_PROJECT;
else Bun.env.GOOGLE_CLOUD_PROJECT = previousProject;
if (previousGcpProject === undefined) delete Bun.env.GCP_PROJECT;
else Bun.env.GCP_PROJECT = previousGcpProject;
if (previousGcloudProject === undefined) delete Bun.env.GCLOUD_PROJECT;
else Bun.env.GCLOUD_PROJECT = previousGcloudProject;
if (previousVertexLocation === undefined) delete Bun.env.GOOGLE_VERTEX_LOCATION;
else Bun.env.GOOGLE_VERTEX_LOCATION = previousVertexLocation;
if (previousCloudLocation === undefined) delete Bun.env.GOOGLE_CLOUD_LOCATION;
else Bun.env.GOOGLE_CLOUD_LOCATION = previousCloudLocation;
if (previousLocation === undefined) delete Bun.env.VERTEX_LOCATION;
else Bun.env.VERTEX_LOCATION = previousLocation;
if (previousApiKey === undefined) delete Bun.env.GOOGLE_CLOUD_API_KEY;
else Bun.env.GOOGLE_CLOUD_API_KEY = previousApiKey;
}
const result = await resolveProviderModels(options, "offline");
expect(result.stale).toBe(false);
expect(result.models.some(model => model.id === "deepseek-ai/deepseek-v3.2-maas")).toBe(true);
expect(result.models.some(model => model.id === "gemini-3.5-flash")).toBe(true);
expect(result.models.some(model => model.id === "gemini-1.5-pro")).toBe(false);
});
});