From dcfb3ed4e3887973b9eb133c52be1f960e08fd4a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 9 Apr 2026 01:48:40 +0100 Subject: [PATCH] plugins: keep google provider policy lightweight --- extensions/google/api.ts | 148 ++---------------- extensions/google/provider-policy-api.test.ts | 31 ++++ extensions/google/provider-policy-api.ts | 6 + extensions/google/provider-policy.ts | 139 ++++++++++++++++ ...els-config.providers.policy.lookup.test.ts | 52 ++++++ .../models-config.providers.policy.lookup.ts | 12 ++ 6 files changed, 253 insertions(+), 135 deletions(-) create mode 100644 extensions/google/provider-policy-api.test.ts create mode 100644 extensions/google/provider-policy-api.ts create mode 100644 extensions/google/provider-policy.ts create mode 100644 src/agents/models-config.providers.policy.lookup.test.ts diff --git a/extensions/google/api.ts b/extensions/google/api.ts index 9c573eac125..9883e5dca2c 100644 --- a/extensions/google/api.ts +++ b/extensions/google/api.ts @@ -1,147 +1,25 @@ import { - resolveProviderEndpoint, resolveProviderHttpRequestConfig, type ProviderRequestTransportOverrides, } from "openclaw/plugin-sdk/provider-http"; -import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; import { applyAgentDefaultModelPrimary, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; -import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js"; import { parseGoogleOauthApiKey } from "./oauth-token-shared.js"; -export { normalizeAntigravityModelId, normalizeGoogleModelId }; - -type GoogleApiCarrier = { - api?: string | null; -}; - -type GoogleProviderConfigLike = GoogleApiCarrier & { - models?: ReadonlyArray | null; -}; - -export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; - -function trimTrailingSlashes(value: string): string { - return value.replace(/\/+$/, ""); -} - -function isCanonicalGoogleApiOriginShorthand(value: string): boolean { - return /^https:\/\/generativelanguage\.googleapis\.com\/?$/i.test(value); -} - -export function normalizeGoogleApiBaseUrl(baseUrl?: string): string { - const raw = trimTrailingSlashes(normalizeOptionalString(baseUrl) || DEFAULT_GOOGLE_API_BASE_URL); - try { - const url = new URL(raw); - url.hash = ""; - url.search = ""; - if ( - resolveProviderEndpoint(url.toString()).endpointClass === "google-generative-ai" && - trimTrailingSlashes(url.pathname || "") === "" - ) { - url.pathname = "/v1beta"; - } - return trimTrailingSlashes(url.toString()); - } catch { - if (isCanonicalGoogleApiOriginShorthand(raw)) { - return DEFAULT_GOOGLE_API_BASE_URL; - } - return raw; - } -} - -export function isGoogleGenerativeAiApi(api?: string | null): boolean { - return api === "google-generative-ai"; -} - -export function normalizeGoogleGenerativeAiBaseUrl(baseUrl?: string): string | undefined { - return baseUrl ? normalizeGoogleApiBaseUrl(baseUrl) : baseUrl; -} - -export function resolveGoogleGenerativeAiTransport(params: { - api: TApi; - baseUrl?: string; -}): { api: TApi; baseUrl?: string } { - return { - api: params.api, - baseUrl: isGoogleGenerativeAiApi(params.api) - ? normalizeGoogleGenerativeAiBaseUrl(params.baseUrl) - : params.baseUrl, - }; -} - -export function resolveGoogleGenerativeAiApiOrigin(baseUrl?: string): string { - return normalizeGoogleApiBaseUrl(baseUrl).replace(/\/v1beta$/i, ""); -} - -export function shouldNormalizeGoogleGenerativeAiProviderConfig( - providerKey: string, - provider: GoogleProviderConfigLike, -): boolean { - if (providerKey === "google" || providerKey === "google-vertex") { - return true; - } - if (isGoogleGenerativeAiApi(provider.api)) { - return true; - } - return provider.models?.some((model) => isGoogleGenerativeAiApi(model?.api)) ?? false; -} - -export function shouldNormalizeGoogleProviderConfig( - providerKey: string, - provider: GoogleProviderConfigLike, -): boolean { - return ( - providerKey === "google-antigravity" || - shouldNormalizeGoogleGenerativeAiProviderConfig(providerKey, provider) - ); -} - -function normalizeProviderModels( - provider: ModelProviderConfig, - normalizeId: (id: string) => string, -): ModelProviderConfig { - const models = provider.models; - if (!Array.isArray(models) || models.length === 0) { - return provider; - } - - let mutated = false; - const nextModels = models.map((model) => { - const nextId = normalizeId(model.id); - if (nextId === model.id) { - return model; - } - mutated = true; - return { ...model, id: nextId }; - }); - - return mutated ? { ...provider, models: nextModels } : provider; -} - -export function normalizeGoogleProviderConfig( - providerKey: string, - provider: ModelProviderConfig, -): ModelProviderConfig { - let nextProvider = provider; - - if (shouldNormalizeGoogleGenerativeAiProviderConfig(providerKey, nextProvider)) { - const modelNormalized = normalizeProviderModels(nextProvider, normalizeGoogleModelId); - const normalizedBaseUrl = normalizeGoogleGenerativeAiBaseUrl(modelNormalized.baseUrl); - nextProvider = - normalizedBaseUrl !== modelNormalized.baseUrl - ? { ...modelNormalized, baseUrl: normalizedBaseUrl ?? modelNormalized.baseUrl } - : modelNormalized; - } - - if (providerKey === "google-antigravity") { - nextProvider = normalizeProviderModels(nextProvider, normalizeAntigravityModelId); - } - - return nextProvider; -} +import { DEFAULT_GOOGLE_API_BASE_URL, normalizeGoogleApiBaseUrl } from "./provider-policy.js"; +export { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js"; +export { + DEFAULT_GOOGLE_API_BASE_URL, + isGoogleGenerativeAiApi, + normalizeGoogleApiBaseUrl, + normalizeGoogleGenerativeAiBaseUrl, + normalizeGoogleProviderConfig, + resolveGoogleGenerativeAiApiOrigin, + resolveGoogleGenerativeAiTransport, + shouldNormalizeGoogleGenerativeAiProviderConfig, + shouldNormalizeGoogleProviderConfig, +} from "./provider-policy.js"; export function parseGeminiAuth(apiKey: string): { headers: Record } { const parsed = apiKey.startsWith("{") ? parseGoogleOauthApiKey(apiKey) : null; diff --git a/extensions/google/provider-policy-api.test.ts b/extensions/google/provider-policy-api.test.ts new file mode 100644 index 00000000000..a9996207aee --- /dev/null +++ b/extensions/google/provider-policy-api.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { normalizeConfig } from "./provider-policy-api.js"; + +describe("google provider policy public artifact", () => { + it("normalizes Google provider config without loading the full provider plugin", () => { + expect( + normalizeConfig({ + provider: "google", + providerConfig: { + baseUrl: "https://generativelanguage.googleapis.com", + api: "google-generative-ai", + apiKey: "GEMINI_API_KEY", + models: [ + { + id: "gemini-3-pro", + name: "Gemini 3 Pro", + reasoning: true, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_048_576, + maxTokens: 65_536, + }, + ], + }, + }), + ).toMatchObject({ + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [{ id: "gemini-3-pro-preview" }], + }); + }); +}); diff --git a/extensions/google/provider-policy-api.ts b/extensions/google/provider-policy-api.ts new file mode 100644 index 00000000000..3da6b425b3a --- /dev/null +++ b/extensions/google/provider-policy-api.ts @@ -0,0 +1,6 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types"; +import { normalizeGoogleProviderConfig } from "./provider-policy.js"; + +export function normalizeConfig(params: { provider: string; providerConfig: ModelProviderConfig }) { + return normalizeGoogleProviderConfig(params.provider, params.providerConfig); +} diff --git a/extensions/google/provider-policy.ts b/extensions/google/provider-policy.ts new file mode 100644 index 00000000000..938657a3a41 --- /dev/null +++ b/extensions/google/provider-policy.ts @@ -0,0 +1,139 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types"; +import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js"; + +type GoogleApiCarrier = { + api?: string | null; +}; + +type GoogleProviderConfigLike = GoogleApiCarrier & { + models?: ReadonlyArray | null; +}; + +export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; + +function normalizeOptionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function trimTrailingSlashes(value: string): string { + return value.replace(/\/+$/, ""); +} + +function isCanonicalGoogleApiOriginShorthand(value: string): boolean { + return /^https:\/\/generativelanguage\.googleapis\.com\/?$/i.test(value); +} + +function isGoogleGenerativeAiUrl(url: URL): boolean { + return ( + url.protocol === "https:" && url.hostname.toLowerCase() === "generativelanguage.googleapis.com" + ); +} + +export function normalizeGoogleApiBaseUrl(baseUrl?: string): string { + const raw = trimTrailingSlashes(normalizeOptionalString(baseUrl) || DEFAULT_GOOGLE_API_BASE_URL); + try { + const url = new URL(raw); + url.hash = ""; + url.search = ""; + if (isGoogleGenerativeAiUrl(url) && trimTrailingSlashes(url.pathname || "") === "") { + url.pathname = "/v1beta"; + } + return trimTrailingSlashes(url.toString()); + } catch { + if (isCanonicalGoogleApiOriginShorthand(raw)) { + return DEFAULT_GOOGLE_API_BASE_URL; + } + return raw; + } +} + +export function isGoogleGenerativeAiApi(api?: string | null): boolean { + return api === "google-generative-ai"; +} + +export function normalizeGoogleGenerativeAiBaseUrl(baseUrl?: string): string | undefined { + return baseUrl ? normalizeGoogleApiBaseUrl(baseUrl) : baseUrl; +} + +export function resolveGoogleGenerativeAiTransport(params: { + api: TApi; + baseUrl?: string; +}): { api: TApi; baseUrl?: string } { + return { + api: params.api, + baseUrl: isGoogleGenerativeAiApi(params.api) + ? normalizeGoogleGenerativeAiBaseUrl(params.baseUrl) + : params.baseUrl, + }; +} + +export function resolveGoogleGenerativeAiApiOrigin(baseUrl?: string): string { + return normalizeGoogleApiBaseUrl(baseUrl).replace(/\/v1beta$/i, ""); +} + +export function shouldNormalizeGoogleGenerativeAiProviderConfig( + providerKey: string, + provider: GoogleProviderConfigLike, +): boolean { + if (providerKey === "google" || providerKey === "google-vertex") { + return true; + } + if (isGoogleGenerativeAiApi(provider.api)) { + return true; + } + return provider.models?.some((model) => isGoogleGenerativeAiApi(model?.api)) ?? false; +} + +export function shouldNormalizeGoogleProviderConfig( + providerKey: string, + provider: GoogleProviderConfigLike, +): boolean { + return ( + providerKey === "google-antigravity" || + shouldNormalizeGoogleGenerativeAiProviderConfig(providerKey, provider) + ); +} + +function normalizeProviderModels( + provider: ModelProviderConfig, + normalizeId: (id: string) => string, +): ModelProviderConfig { + const models = provider.models; + if (!Array.isArray(models) || models.length === 0) { + return provider; + } + + let mutated = false; + const nextModels = models.map((model) => { + const nextId = normalizeId(model.id); + if (nextId === model.id) { + return model; + } + mutated = true; + return { ...model, id: nextId }; + }); + + return mutated ? { ...provider, models: nextModels } : provider; +} + +export function normalizeGoogleProviderConfig( + providerKey: string, + provider: ModelProviderConfig, +): ModelProviderConfig { + let nextProvider = provider; + + if (shouldNormalizeGoogleGenerativeAiProviderConfig(providerKey, nextProvider)) { + const modelNormalized = normalizeProviderModels(nextProvider, normalizeGoogleModelId); + const normalizedBaseUrl = normalizeGoogleGenerativeAiBaseUrl(modelNormalized.baseUrl); + nextProvider = + normalizedBaseUrl !== modelNormalized.baseUrl + ? { ...modelNormalized, baseUrl: normalizedBaseUrl ?? modelNormalized.baseUrl } + : modelNormalized; + } + + if (providerKey === "google-antigravity") { + nextProvider = normalizeProviderModels(nextProvider, normalizeAntigravityModelId); + } + + return nextProvider; +} diff --git a/src/agents/models-config.providers.policy.lookup.test.ts b/src/agents/models-config.providers.policy.lookup.test.ts new file mode 100644 index 00000000000..bb82b8b3935 --- /dev/null +++ b/src/agents/models-config.providers.policy.lookup.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { resolveProviderPluginLookupKey } from "./models-config.providers.policy.lookup.js"; + +describe("resolveProviderPluginLookupKey", () => { + it("routes Google Generative AI custom providers to the google policy artifact", () => { + expect( + resolveProviderPluginLookupKey("google-paid", { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + api: "google-generative-ai", + models: [], + }), + ).toBe("google"); + }); + + it("routes model-level Google Generative AI providers to the google policy artifact", () => { + expect( + resolveProviderPluginLookupKey("custom-google", { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [ + { + id: "gemini-3-pro", + name: "Gemini 3 Pro", + api: "google-generative-ai", + reasoning: true, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_048_576, + maxTokens: 65_536, + }, + ], + }), + ).toBe("google"); + }); + + it("routes google-antigravity to the google policy artifact", () => { + expect( + resolveProviderPluginLookupKey("google-antigravity", { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [], + }), + ).toBe("google"); + }); + + it("routes google-vertex to the google policy artifact", () => { + expect( + resolveProviderPluginLookupKey("google-vertex", { + baseUrl: "https://aiplatform.googleapis.com", + models: [], + }), + ).toBe("google"); + }); +}); diff --git a/src/agents/models-config.providers.policy.lookup.ts b/src/agents/models-config.providers.policy.lookup.ts index 214acc0bc7f..a4e8e4a7368 100644 --- a/src/agents/models-config.providers.policy.lookup.ts +++ b/src/agents/models-config.providers.policy.lookup.ts @@ -14,6 +14,18 @@ export function resolveProviderPluginLookupKey( provider?: ProviderConfig, ): string { const api = normalizeOptionalString(provider?.api) ?? ""; + if ( + providerKey === "google-antigravity" || + providerKey === "google-vertex" || + api === "google-generative-ai" + ) { + return "google"; + } + if ( + provider?.models?.some((model) => normalizeOptionalString(model.api) === "google-generative-ai") + ) { + return "google"; + } if ( api && MODEL_APIS.includes(api as (typeof MODEL_APIS)[number]) &&