mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 00:52:57 +00:00
fix(google-vertex): support production ADC modes (#83971)
Fix Google Vertex production ADC mode support by routing explicit google-vertex models to the Vertex transport and relying on google-auth-library for request-time ADC resolution.
Verification:
- pnpm install --frozen-lockfile
- pnpm test extensions/google/transport-stream.test.ts extensions/google/index.test.ts src/config/zod-schema.models.test.ts src/agents/pi-embedded-runner/model.inline-provider.test.ts -- --reporter=verbose
- pnpm check:changed
- GitHub PR checks green on c4b7cad4df
- Live ADC smoke reached Google Vertex auth/transport and failed only because the configured redacted project has the Vertex AI API disabled
Co-authored-by: Damian Finol <damian@felixpago.com>
This commit is contained in:
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
|
||||
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.
|
||||
|
||||
## 2026.5.25
|
||||
|
||||
|
||||
@@ -199,6 +199,25 @@ describe("google provider plugin hooks", () => {
|
||||
runCase(cliProvider, "google-gemini-cli");
|
||||
});
|
||||
|
||||
it("wires Vertex transport before request-time metadata ADC detection", async () => {
|
||||
const { providers } = await registerProviderPlugin({
|
||||
plugin: googleProviderPlugin,
|
||||
id: "google",
|
||||
name: "Google Provider",
|
||||
});
|
||||
const provider = requireRegisteredProvider(providers, "google");
|
||||
|
||||
expect(
|
||||
provider.createStreamFn?.({
|
||||
model: {
|
||||
api: "google-vertex",
|
||||
provider: "google",
|
||||
id: "gemini-2.5-pro",
|
||||
},
|
||||
} as never),
|
||||
).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it("advertises adaptive thinking for Gemini dynamic thinking", async () => {
|
||||
const { providers } = await registerProviderPlugin({
|
||||
plugin: googleProviderPlugin,
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-ai": "0.75.4",
|
||||
"@google/genai": "2.5.0"
|
||||
"@google/genai": "2.5.0",
|
||||
"google-auth-library": "10.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
createGoogleGenerativeAiTransportStreamFn,
|
||||
createGoogleVertexTransportStreamFn,
|
||||
} from "./transport-stream.js";
|
||||
import { hasGoogleVertexAuthorizedUserAdcSync } from "./vertex-adc.js";
|
||||
|
||||
export function buildGoogleProvider(): ProviderPlugin {
|
||||
return {
|
||||
@@ -57,7 +56,7 @@ export function buildGoogleProvider(): ProviderPlugin {
|
||||
if (model.api === "google-generative-ai") {
|
||||
return createGoogleGenerativeAiTransportStreamFn();
|
||||
}
|
||||
if (model.api === "google-vertex" && hasGoogleVertexAuthorizedUserAdcSync()) {
|
||||
if (model.api === "google-vertex") {
|
||||
return createGoogleVertexTransportStreamFn();
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -4,21 +4,40 @@ import path from "node:path";
|
||||
import type { Model } from "@earendil-works/pi-ai";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { buildGuardedModelFetchMock, guardedFetchMock } = vi.hoisted(() => ({
|
||||
buildGuardedModelFetchMock: vi.fn(),
|
||||
guardedFetchMock: vi.fn(),
|
||||
}));
|
||||
const {
|
||||
buildGuardedModelFetchMock,
|
||||
guardedFetchMock,
|
||||
googleAuthGetAccessTokenMock,
|
||||
googleAuthMock,
|
||||
} = vi.hoisted(() => {
|
||||
const googleAuthGetAccessTokenMock = vi.fn();
|
||||
return {
|
||||
buildGuardedModelFetchMock: vi.fn(),
|
||||
guardedFetchMock: vi.fn(),
|
||||
googleAuthGetAccessTokenMock,
|
||||
googleAuthMock: vi.fn(function GoogleAuthMock() {
|
||||
return {
|
||||
getAccessToken: googleAuthGetAccessTokenMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-transport-runtime", async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
buildGuardedModelFetch: buildGuardedModelFetchMock,
|
||||
}));
|
||||
|
||||
vi.mock("google-auth-library", () => ({
|
||||
GoogleAuth: googleAuthMock,
|
||||
}));
|
||||
|
||||
let buildGoogleGenerativeAiParams: typeof import("./transport-stream.js").buildGoogleGenerativeAiParams;
|
||||
let buildGoogleGemini3FirstResponseRetryParams: typeof import("./transport-stream.js").buildGoogleGemini3FirstResponseRetryParams;
|
||||
let createGoogleGenerativeAiTransportStreamFn: typeof import("./transport-stream.js").createGoogleGenerativeAiTransportStreamFn;
|
||||
let createGoogleVertexTransportStreamFn: typeof import("./transport-stream.js").createGoogleVertexTransportStreamFn;
|
||||
let hasGoogleVertexAuthorizedUserAdcSync: typeof import("./vertex-adc.js").hasGoogleVertexAuthorizedUserAdcSync;
|
||||
let resolveGoogleVertexAuthorizedUserHeaders: typeof import("./vertex-adc.js").resolveGoogleVertexAuthorizedUserHeaders;
|
||||
let resetGoogleVertexAuthorizedUserTokenCacheForTest: typeof import("./vertex-adc.js").resetGoogleVertexAuthorizedUserTokenCacheForTest;
|
||||
|
||||
const MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL = Symbol.for(
|
||||
@@ -254,13 +273,18 @@ describe("google transport stream", () => {
|
||||
createGoogleGenerativeAiTransportStreamFn,
|
||||
createGoogleVertexTransportStreamFn,
|
||||
} = await import("./transport-stream.js"));
|
||||
({ hasGoogleVertexAuthorizedUserAdcSync, resetGoogleVertexAuthorizedUserTokenCacheForTest } =
|
||||
await import("./vertex-adc.js"));
|
||||
({
|
||||
hasGoogleVertexAuthorizedUserAdcSync,
|
||||
resolveGoogleVertexAuthorizedUserHeaders,
|
||||
resetGoogleVertexAuthorizedUserTokenCacheForTest,
|
||||
} = await import("./vertex-adc.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
buildGuardedModelFetchMock.mockReset();
|
||||
guardedFetchMock.mockReset();
|
||||
googleAuthGetAccessTokenMock.mockReset();
|
||||
googleAuthMock.mockClear();
|
||||
buildGuardedModelFetchMock.mockReturnValue(guardedFetchMock);
|
||||
resetGoogleVertexAuthorizedUserTokenCacheForTest();
|
||||
});
|
||||
@@ -271,6 +295,7 @@ describe("google transport stream", () => {
|
||||
|
||||
afterAll(() => {
|
||||
vi.doUnmock("openclaw/plugin-sdk/provider-transport-runtime");
|
||||
vi.doUnmock("google-auth-library");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
@@ -695,6 +720,89 @@ describe("google transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("detects supported Vertex ADC sources synchronously", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-detect-"));
|
||||
for (const type of ["authorized_user", "external_account", "service_account"]) {
|
||||
const credentialsPath = path.join(tempDir, `${type}.json`);
|
||||
await writeFile(credentialsPath, JSON.stringify({ type }), "utf8");
|
||||
|
||||
expect(
|
||||
hasGoogleVertexAuthorizedUserAdcSync({
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
}),
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
expect(
|
||||
hasGoogleVertexAuthorizedUserAdcSync({
|
||||
HOME: path.join(tempDir, "empty-home"),
|
||||
KUBERNETES_SERVICE_HOST: "10.0.0.1",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves non-file Vertex ADC through google-auth-library without OAuth refresh fetch", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-"));
|
||||
vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
|
||||
vi.stubEnv("HOME", path.join(tempDir, "home"));
|
||||
vi.stubEnv("APPDATA", "");
|
||||
googleAuthGetAccessTokenMock.mockResolvedValueOnce("ya29.google-auth-token");
|
||||
const tokenFetchMock = vi.fn();
|
||||
|
||||
await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
|
||||
Authorization: "Bearer ya29.google-auth-token",
|
||||
});
|
||||
|
||||
expect(googleAuthMock).toHaveBeenCalledWith({
|
||||
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
|
||||
});
|
||||
expect(googleAuthGetAccessTokenMock).toHaveBeenCalledTimes(1);
|
||||
expect(tokenFetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses google-auth-library bearer auth for Google Vertex credential marker requests", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-stream-"));
|
||||
vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
|
||||
vi.stubEnv("HOME", path.join(tempDir, "home"));
|
||||
vi.stubEnv("APPDATA", "");
|
||||
vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
|
||||
vi.stubEnv("GOOGLE_CLOUD_LOCATION", "us-central1");
|
||||
googleAuthGetAccessTokenMock.mockResolvedValueOnce("ya29.transport-token");
|
||||
const tokenFetchMock = vi.fn();
|
||||
guardedFetchMock.mockResolvedValueOnce(
|
||||
buildSseResponse([
|
||||
{
|
||||
candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const streamFn = createGoogleVertexTransportStreamFn();
|
||||
const stream = await Promise.resolve(
|
||||
streamFn(
|
||||
buildGoogleVertexModel(),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
} as Parameters<typeof streamFn>[1],
|
||||
{
|
||||
apiKey: "gcp-vertex-credentials",
|
||||
fetch: tokenFetchMock,
|
||||
} as Parameters<typeof streamFn>[2],
|
||||
),
|
||||
);
|
||||
await stream.result();
|
||||
|
||||
expect(tokenFetchMock).not.toHaveBeenCalled();
|
||||
const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
|
||||
const guardedInit = requireRequestInit(guardedCall, "guarded fetch");
|
||||
expectHeaders(guardedInit, {
|
||||
Authorization: "Bearer ya29.transport-token",
|
||||
"Content-Type": "application/json",
|
||||
accept: "text/event-stream",
|
||||
});
|
||||
expect(new Headers(guardedInit.headers).has("x-goog-api-key")).toBe(false);
|
||||
});
|
||||
|
||||
it("refreshes authorized_user ADC before Google Vertex requests", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-"));
|
||||
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
|
||||
|
||||
@@ -17,13 +17,33 @@ type GoogleVertexAuthorizedUserToken = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
type GoogleVertexAdcToken = {
|
||||
token: string;
|
||||
expiresAtMs: number;
|
||||
};
|
||||
|
||||
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
|
||||
const GOOGLE_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
const GOOGLE_VERTEX_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
|
||||
// Hold tokens slightly less long than reported expiry (Google's recommendation
|
||||
// is a 60s buffer) so we don't ship a request that's already revoked when it
|
||||
// leaves the gateway.
|
||||
const GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS = 60_000;
|
||||
|
||||
let cachedGoogleVertexAuthorizedUserToken: GoogleVertexAuthorizedUserToken | undefined;
|
||||
let cachedGoogleAuthClient:
|
||||
| {
|
||||
promise: Promise<{
|
||||
getAccessToken: () => Promise<string | null | undefined>;
|
||||
}>;
|
||||
}
|
||||
| undefined;
|
||||
let cachedGoogleVertexAdcToken: GoogleVertexAdcToken | undefined;
|
||||
|
||||
export function resetGoogleVertexAuthorizedUserTokenCacheForTest(): void {
|
||||
cachedGoogleVertexAuthorizedUserToken = undefined;
|
||||
cachedGoogleAuthClient = undefined;
|
||||
cachedGoogleVertexAdcToken = undefined;
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
@@ -85,24 +105,45 @@ async function readGoogleAuthorizedUserCredentials(
|
||||
};
|
||||
}
|
||||
|
||||
function readGoogleAdcCredentialsTypeSync(credentialsPath: string): string | undefined {
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(credentialsPath, "utf8")) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
const type = (parsed as { type?: unknown }).type;
|
||||
return typeof type === "string" ? type : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a file/env Application Default Credentials source usable
|
||||
* for Google Vertex AI is detectable synchronously. We still call the function
|
||||
* `...AuthorizedUserAdcSync` for backwards compatibility with older tests; the
|
||||
* predicate now also covers:
|
||||
*
|
||||
* 1. `authorized_user` credentials file (existing case - `gcloud auth
|
||||
* application-default login` produces this).
|
||||
* 2. `external_account` credentials file (Workload Identity Federation).
|
||||
* 3. `service_account` credentials file (raw GSA key - rarely used in
|
||||
* OpenClaw, included for completeness).
|
||||
* Metadata-server ADC is intentionally not detected here: `google-auth-library`
|
||||
* probes the default metadata hosts asynchronously at request time, and the
|
||||
* provider wires the Vertex transport without this sync predicate.
|
||||
*/
|
||||
export function hasGoogleVertexAuthorizedUserAdcSync(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const credentialsPath = resolveGoogleApplicationCredentialsPath(env);
|
||||
if (!credentialsPath) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(credentialsPath, "utf8")) as unknown;
|
||||
return (
|
||||
Boolean(parsed) &&
|
||||
typeof parsed === "object" &&
|
||||
!Array.isArray(parsed) &&
|
||||
(parsed as { type?: unknown }).type === "authorized_user"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
if (credentialsPath) {
|
||||
const type = readGoogleAdcCredentialsTypeSync(credentialsPath);
|
||||
if (type === "authorized_user" || type === "external_account" || type === "service_account") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
|
||||
@@ -123,7 +164,7 @@ async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
|
||||
if (
|
||||
cached?.credentialsPath === params.credentialsPath &&
|
||||
cached.refreshToken === refreshToken &&
|
||||
cached.expiresAtMs - Date.now() > 60_000
|
||||
cached.expiresAtMs - Date.now() > GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS
|
||||
) {
|
||||
return cached.token;
|
||||
}
|
||||
@@ -166,23 +207,83 @@ async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
|
||||
return token;
|
||||
}
|
||||
|
||||
async function resolveGoogleVertexAccessTokenViaGoogleAuth(): Promise<string> {
|
||||
// Lazy-import + cache so we don't pay the google-auth-library load cost on
|
||||
// gateway startup; only when we actually need a non-authorized_user token.
|
||||
if (!cachedGoogleAuthClient) {
|
||||
cachedGoogleAuthClient = {
|
||||
promise: import("google-auth-library").then(({ GoogleAuth }) => {
|
||||
// GoogleAuth handles every ADC variant we care about for GKE:
|
||||
// - external_account (Workload Identity Federation: STS exchange)
|
||||
// - service_account (raw GSA key: JWT-bearer)
|
||||
// - GKE Workload Identity (metadata server when no credentials file)
|
||||
// - Compute Engine / Cloud Run / GAE metadata server fallback
|
||||
// It also caches tokens internally and refreshes before expiry.
|
||||
return new GoogleAuth({
|
||||
scopes: [GOOGLE_VERTEX_OAUTH_SCOPE],
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
const auth = await cachedGoogleAuthClient.promise;
|
||||
|
||||
const cached = cachedGoogleVertexAdcToken;
|
||||
if (cached && cached.expiresAtMs - Date.now() > GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS) {
|
||||
return cached.token;
|
||||
}
|
||||
|
||||
const token = await auth.getAccessToken();
|
||||
const normalized = normalizeOptionalString(token);
|
||||
if (!normalized) {
|
||||
throw new Error(
|
||||
"Google Vertex ADC fallback (google-auth-library) did not return an access token. " +
|
||||
"Verify the GKE Workload Identity binding (KSA \u2192 GSA), `GOOGLE_APPLICATION_CREDENTIALS`, " +
|
||||
"or other ADC source is reachable from this pod.",
|
||||
);
|
||||
}
|
||||
// google-auth-library doesn't expose token expiry on the simple
|
||||
// `getAccessToken()` return type, so we cache for a conservative 5 minutes.
|
||||
// The library itself already refreshes well before its own internal expiry,
|
||||
// so this cache is mainly to avoid hot-loop calls into the auth client.
|
||||
cachedGoogleVertexAdcToken = {
|
||||
token: normalized,
|
||||
expiresAtMs: Date.now() + 5 * 60_000,
|
||||
};
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `Authorization: Bearer ...` headers for Google Vertex calls.
|
||||
*
|
||||
* We try the hand-rolled `authorized_user` refresh path first (preserves the
|
||||
* existing fetchImpl test seam and the OpenClaw upstream behaviour); when the
|
||||
* configured ADC source is anything other than `authorized_user` (the common
|
||||
* production cases on GKE: Workload Identity, Workload Identity Federation,
|
||||
* service-account JSON keys), we hand off to `google-auth-library` which
|
||||
* understands all of those natively.
|
||||
*
|
||||
* Note: the function is still named `...AuthorizedUserHeaders` to avoid a
|
||||
* symbol rename across the existing patch surface; the docstring above is
|
||||
* the truth, the name is legacy.
|
||||
*/
|
||||
export async function resolveGoogleVertexAuthorizedUserHeaders(
|
||||
fetchImpl?: typeof fetch,
|
||||
): Promise<Record<string, string>> {
|
||||
const credentialsPath = resolveGoogleApplicationCredentialsPath();
|
||||
if (!credentialsPath) {
|
||||
throw new Error(
|
||||
"Google Vertex ADC credentials not found. Set GOOGLE_APPLICATION_CREDENTIALS or run gcloud auth application-default login.",
|
||||
);
|
||||
if (credentialsPath) {
|
||||
const credentials = await readGoogleAuthorizedUserCredentials(credentialsPath);
|
||||
if (credentials) {
|
||||
const token = await refreshGoogleVertexAuthorizedUserAccessToken({
|
||||
credentialsPath,
|
||||
credentials,
|
||||
fetchImpl,
|
||||
});
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
}
|
||||
const credentials = await readGoogleAuthorizedUserCredentials(credentialsPath);
|
||||
if (!credentials) {
|
||||
throw new Error("Google Vertex ADC fallback requires an authorized_user credentials file.");
|
||||
}
|
||||
const token = await refreshGoogleVertexAuthorizedUserAccessToken({
|
||||
credentialsPath,
|
||||
credentials,
|
||||
fetchImpl,
|
||||
});
|
||||
// No file-based authorized_user ADC. Fall back to google-auth-library which
|
||||
// handles GKE Workload Identity (metadata server), Workload Identity
|
||||
// Federation (external_account), and service-account keys.
|
||||
const token = await resolveGoogleVertexAccessTokenViaGoogleAuth();
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -745,6 +745,9 @@ importers:
|
||||
'@google/genai':
|
||||
specifier: 2.5.0
|
||||
version: 2.5.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))
|
||||
google-auth-library:
|
||||
specifier: 10.6.2
|
||||
version: 10.6.2
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
|
||||
@@ -68,6 +68,24 @@ describe("buildInlineProviderModels", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves google-vertex api inherited from provider config", () => {
|
||||
const providers: Parameters<typeof buildInlineProviderModels>[0] = {
|
||||
google: {
|
||||
baseUrl: "https://us-central1-aiplatform.googleapis.com/v1",
|
||||
api: "google-vertex",
|
||||
models: [makeModel("gemini-2.5-pro")],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildInlineProviderModels(providers);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].provider).toBe("google");
|
||||
expect(result[0].baseUrl).toBe("https://us-central1-aiplatform.googleapis.com/v1");
|
||||
expect(result[0].api).toBe("google-vertex");
|
||||
expect(result[0].id).toBe("gemini-2.5-pro");
|
||||
});
|
||||
|
||||
it("model-level api takes precedence over provider-level api", () => {
|
||||
const providers: Parameters<typeof buildInlineProviderModels>[0] = {
|
||||
custom: {
|
||||
|
||||
@@ -40,6 +40,7 @@ export function normalizeResolvedTransportApi(
|
||||
case "bedrock-converse-stream":
|
||||
case "github-copilot":
|
||||
case "google-generative-ai":
|
||||
case "google-vertex":
|
||||
case "ollama":
|
||||
case "openai-codex-responses":
|
||||
case "openai-completions":
|
||||
|
||||
@@ -13,6 +13,7 @@ export const MODEL_APIS = [
|
||||
"openai-codex-responses",
|
||||
"anthropic-messages",
|
||||
"google-generative-ai",
|
||||
"google-vertex",
|
||||
"github-copilot",
|
||||
"bedrock-converse-stream",
|
||||
"ollama",
|
||||
|
||||
25
src/config/zod-schema.models.test.ts
Normal file
25
src/config/zod-schema.models.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ModelsConfigSchema } from "./zod-schema.core.js";
|
||||
|
||||
describe("ModelsConfigSchema", () => {
|
||||
it("accepts google-vertex as a model API from MODEL_APIS", () => {
|
||||
const result = ModelsConfigSchema.safeParse({
|
||||
providers: {
|
||||
"google-vertex": {
|
||||
baseUrl: "https://{location}-aiplatform.googleapis.com",
|
||||
api: "google-vertex",
|
||||
apiKey: "gcp-vertex-credentials",
|
||||
models: [
|
||||
{
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
api: "google-vertex",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user