// Google provider module implements model/runtime integration. import type { ProviderDefaultThinkingPolicyContext, ProviderThinkingProfile, } from "openclaw/plugin-sdk/core"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js"; import { isGoogleGemini3ProModel, isGoogleGemini3ThinkingLevelModel } from "./thinking-api.js"; type GoogleApiCarrier = { api?: string | null; }; type GoogleProviderConfigLike = GoogleApiCarrier & { baseUrl?: string | null; models?: ReadonlyArray | null; }; export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; const GOOGLE_MODEL_ID_PROVIDERS = new Set(["google", "google-gemini-cli", "google-vertex"]); 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" ); } function stripUrlUserInfo(url: URL): void { url.username = ""; url.password = ""; } const GOOGLE_VERTEX_HOST = "aiplatform.googleapis.com"; const GOOGLE_VERTEX_REGION_HOST_SUFFIX = "-aiplatform.googleapis.com"; const GOOGLE_VERTEX_MULTI_REGION_HOSTS = new Set([ "aiplatform.eu.rep.googleapis.com", "aiplatform.us.rep.googleapis.com", ]); export function isGoogleVertexHostname(hostname: string): boolean { const normalized = hostname.toLowerCase(); return ( normalized === GOOGLE_VERTEX_HOST || normalized.endsWith(GOOGLE_VERTEX_REGION_HOST_SUFFIX) || GOOGLE_VERTEX_MULTI_REGION_HOSTS.has(normalized) ); } export function isGoogleVertexBaseUrl(baseUrl?: string | null): boolean { const raw = normalizeOptionalString(baseUrl); if (!raw) { return false; } try { return isGoogleVertexHostname(new URL(raw).hostname); } catch { return false; } } 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 = ""; stripUrlUserInfo(url); if (isGoogleGenerativeAiUrl(url)) { const normalizedPath = trimTrailingSlashes(url.pathname || ""); url.pathname = normalizedPath || "/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 { if (!baseUrl) { return baseUrl; } const normalized = normalizeGoogleApiBaseUrl(baseUrl); try { const url = new URL(normalized); stripUrlUserInfo(url); if (isGoogleGenerativeAiUrl(url)) { url.pathname = trimTrailingSlashes(url.pathname || "").replace(/\/openai$/i, "") || "/v1beta"; return trimTrailingSlashes(url.toString()); } } catch { // `normalizeGoogleApiBaseUrl` already returned the best-effort input form. } return normalized; } export function resolveGoogleGenerativeAiTransport(params: { provider?: string; api: TApi; baseUrl?: string; }): { api: TApi | "google-generative-ai" | "google-vertex"; baseUrl?: string } { const api = params.api ?? (params.provider === "google-vertex" && isGoogleVertexBaseUrl(params.baseUrl) ? "google-vertex" : undefined) ?? (params.provider === "google" && params.baseUrl ? "google-generative-ai" : params.api); return { api, baseUrl: isGoogleGenerativeAiApi(api) ? normalizeGoogleGenerativeAiBaseUrl(params.baseUrl) : params.baseUrl, }; } export function resolveGoogleGenerativeAiApiOrigin(baseUrl?: string): string { return ( normalizeGoogleGenerativeAiBaseUrl(baseUrl) ?? normalizeGoogleApiBaseUrl(baseUrl) ).replace(/\/v1beta$/i, ""); } export function shouldNormalizeGoogleGenerativeAiProviderConfig( providerKey: string, provider: GoogleProviderConfigLike, ): boolean { if (providerKey === "google-vertex" && isGoogleVertexBaseUrl(provider.baseUrl)) { return false; } if (isGoogleGenerativeAiApi(provider.api)) { return true; } const hasGoogleGenerativeAiModelApi = provider.models?.some((model) => isGoogleGenerativeAiApi(model?.api)) ?? false; if (hasGoogleGenerativeAiModelApi) { return true; } if (providerKey !== "google" && providerKey !== "google-vertex") { return false; } const hasExplicitNonGoogleApi = normalizeOptionalString(provider.api) !== undefined; return !hasExplicitNonGoogleApi; } 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 Object.assign({}, model, { id: nextId }); }); return mutated ? { ...provider, models: nextModels } : provider; } export function normalizeGoogleProviderConfig( providerKey: string, provider: ModelProviderConfig, ): ModelProviderConfig { let nextProvider = provider; const shouldNormalizeModelIds = GOOGLE_MODEL_ID_PROVIDERS.has(providerKey); if (shouldNormalizeModelIds) { const modelNormalized = normalizeProviderModels(nextProvider, normalizeGoogleModelId); if (shouldNormalizeGoogleGenerativeAiProviderConfig(providerKey, modelNormalized)) { const normalizedBaseUrl = normalizeGoogleGenerativeAiBaseUrl(modelNormalized.baseUrl); nextProvider = normalizedBaseUrl !== modelNormalized.baseUrl ? { ...modelNormalized, baseUrl: normalizedBaseUrl ?? modelNormalized.baseUrl } : modelNormalized; } else { nextProvider = modelNormalized; } } if (providerKey === "google-antigravity") { nextProvider = normalizeProviderModels(nextProvider, normalizeAntigravityModelId); } return nextProvider; } export function resolveGoogleThinkingProfile({ modelId, reasoning, }: ProviderDefaultThinkingPolicyContext): ProviderThinkingProfile | undefined { const normalizedModelId = normalizeGoogleModelId(modelId); const isGemini3ThinkingModel = isGoogleGemini3ThinkingLevelModel(normalizedModelId); if (reasoning === false && !isGemini3ThinkingModel) { return undefined; } const levels: ProviderThinkingProfile["levels"] = isGoogleGemini3ProModel(normalizedModelId) ? [{ id: "off" }, { id: "low" }, { id: "adaptive" }, { id: "high" }] : [ { id: "off" }, { id: "minimal" }, { id: "low" }, { id: "medium" }, { id: "adaptive" }, { id: "high" }, ]; return { levels, ...(isGemini3ThinkingModel ? { preserveWhenCatalogReasoningFalse: true } : {}), }; }