Files
openclaw/extensions/google/api.ts
Vincent Koc 6c0bff111c fix(google): strip Gemini compat base suffixes (#66445)
* fix(google): cover Gemini image /openai base URLs

* fix(google): strip Gemini compat base suffixes

* fix(google): scope Gemini /openai normalization

* fix(google): harden base URL normalization

* fix(google): restrict Gemini auth base URLs

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-04-14 11:19:41 +01:00

115 lines
3.5 KiB
TypeScript

import {
resolveProviderHttpRequestConfig,
type ProviderRequestTransportOverrides,
} from "openclaw/plugin-sdk/provider-http";
import {
applyAgentDefaultModelPrimary,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { parseGoogleOauthApiKey } from "./oauth-token-shared.js";
import {
DEFAULT_GOOGLE_API_BASE_URL,
normalizeGoogleApiBaseUrl,
normalizeGoogleGenerativeAiBaseUrl,
} 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<string, string> } {
const parsed = apiKey.startsWith("{") ? parseGoogleOauthApiKey(apiKey) : null;
if (parsed?.token) {
return {
headers: {
Authorization: `Bearer ${parsed.token}`,
"Content-Type": "application/json",
},
};
}
return {
headers: {
"x-goog-api-key": apiKey,
"Content-Type": "application/json",
},
};
}
function resolveTrustedGoogleGenerativeAiBaseUrl(baseUrl?: string): string {
const normalized =
normalizeGoogleGenerativeAiBaseUrl(baseUrl ?? DEFAULT_GOOGLE_API_BASE_URL) ??
DEFAULT_GOOGLE_API_BASE_URL;
let url: URL;
try {
url = new URL(normalized);
} catch {
throw new Error(
"Google Generative AI baseUrl must be a valid https URL on generativelanguage.googleapis.com",
);
}
if (
url.protocol !== "https:" ||
url.hostname.toLowerCase() !== "generativelanguage.googleapis.com"
) {
throw new Error(
"Google Generative AI baseUrl must use https://generativelanguage.googleapis.com",
);
}
return normalized;
}
export function resolveGoogleGenerativeAiHttpRequestConfig(params: {
apiKey: string;
baseUrl?: string;
headers?: Record<string, string>;
request?: ProviderRequestTransportOverrides;
capability: "image" | "audio" | "video";
transport: "http" | "media-understanding";
}) {
return resolveProviderHttpRequestConfig({
baseUrl: resolveTrustedGoogleGenerativeAiBaseUrl(params.baseUrl),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: false,
headers: params.headers,
request: params.request,
defaultHeaders: parseGeminiAuth(params.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: params.capability,
transport: params.transport,
});
}
export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview";
export function applyGoogleGeminiModelDefault(cfg: OpenClawConfig): {
next: OpenClawConfig;
changed: boolean;
} {
const current = cfg.agents?.defaults?.model as unknown;
const currentPrimary =
typeof current === "string"
? current.trim() || undefined
: current &&
typeof current === "object" &&
typeof (current as { primary?: unknown }).primary === "string"
? ((current as { primary: string }).primary || "").trim() || undefined
: undefined;
if (currentPrimary === GOOGLE_GEMINI_DEFAULT_MODEL) {
return { next: cfg, changed: false };
}
return {
next: applyAgentDefaultModelPrimary(cfg, GOOGLE_GEMINI_DEFAULT_MODEL),
changed: true,
};
}