mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
refactor: centralize google API base URL handling
This commit is contained in:
@@ -262,4 +262,54 @@ describe("Google image-generation provider", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes a configured bare Google host to the v1beta API root", async () => {
|
||||
vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-test-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
inlineData: {
|
||||
mimeType: "image/png",
|
||||
data: Buffer.from("png-data").toString("base64"),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const provider = buildGoogleImageGenerationProvider();
|
||||
await provider.generateImage({
|
||||
provider: "google",
|
||||
model: "gemini-3-pro-image-preview",
|
||||
prompt: "draw a cat",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
google: {
|
||||
baseUrl: "https://generativelanguage.googleapis.com",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,9 +5,13 @@ import {
|
||||
postJsonRequest,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { normalizeGoogleModelId, parseGeminiAuth } from "openclaw/plugin-sdk/provider-google";
|
||||
import {
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
normalizeGoogleApiBaseUrl,
|
||||
normalizeGoogleModelId,
|
||||
parseGeminiAuth,
|
||||
} from "openclaw/plugin-sdk/provider-google";
|
||||
|
||||
const DEFAULT_GOOGLE_IMAGE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
const DEFAULT_GOOGLE_IMAGE_MODEL = "gemini-3.1-flash-image-preview";
|
||||
const DEFAULT_OUTPUT_MIME = "image/png";
|
||||
const GOOGLE_SUPPORTED_SIZES = [
|
||||
@@ -49,8 +53,7 @@ type GoogleGenerateImageResponse = {
|
||||
};
|
||||
|
||||
function resolveGoogleBaseUrl(cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"]): string {
|
||||
const direct = cfg?.models?.providers?.google?.baseUrl?.trim();
|
||||
return direct || DEFAULT_GOOGLE_IMAGE_BASE_URL;
|
||||
return normalizeGoogleApiBaseUrl(cfg?.models?.providers?.google?.baseUrl);
|
||||
}
|
||||
|
||||
function normalizeGoogleImageModel(model: string | undefined): string {
|
||||
@@ -131,10 +134,7 @@ export function buildGoogleImageGenerationProvider(): ImageGenerationProvider {
|
||||
}
|
||||
|
||||
const model = normalizeGoogleImageModel(req.model);
|
||||
const baseUrl = normalizeBaseUrl(
|
||||
resolveGoogleBaseUrl(req.cfg),
|
||||
DEFAULT_GOOGLE_IMAGE_BASE_URL,
|
||||
);
|
||||
const baseUrl = normalizeBaseUrl(resolveGoogleBaseUrl(req.cfg), DEFAULT_GOOGLE_API_BASE_URL);
|
||||
const allowPrivate = Boolean(req.cfg?.models?.providers?.google?.baseUrl?.trim());
|
||||
const authHeaders = parseGeminiAuth(auth.apiKey);
|
||||
const headers = new Headers(authHeaders.headers);
|
||||
|
||||
@@ -10,10 +10,15 @@ import {
|
||||
type VideoDescriptionRequest,
|
||||
type VideoDescriptionResult,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
import { normalizeGoogleModelId, parseGeminiAuth } from "./runtime-api.js";
|
||||
import {
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
normalizeGoogleApiBaseUrl,
|
||||
normalizeGoogleModelId,
|
||||
parseGeminiAuth,
|
||||
} from "./runtime-api.js";
|
||||
|
||||
export const DEFAULT_GOOGLE_AUDIO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
export const DEFAULT_GOOGLE_VIDEO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
export const DEFAULT_GOOGLE_AUDIO_BASE_URL = DEFAULT_GOOGLE_API_BASE_URL;
|
||||
export const DEFAULT_GOOGLE_VIDEO_BASE_URL = DEFAULT_GOOGLE_API_BASE_URL;
|
||||
const DEFAULT_GOOGLE_AUDIO_MODEL = "gemini-3-flash-preview";
|
||||
const DEFAULT_GOOGLE_VIDEO_MODEL = "gemini-3-flash-preview";
|
||||
const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio.";
|
||||
@@ -37,7 +42,10 @@ async function generateGeminiInlineDataText(params: {
|
||||
missingTextError: string;
|
||||
}): Promise<{ text: string; model: string }> {
|
||||
const fetchFn = params.fetchFn ?? fetch;
|
||||
const baseUrl = normalizeBaseUrl(params.baseUrl, params.defaultBaseUrl);
|
||||
const baseUrl = normalizeBaseUrl(
|
||||
normalizeGoogleApiBaseUrl(params.baseUrl ?? params.defaultBaseUrl),
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
);
|
||||
const allowPrivate = Boolean(params.baseUrl?.trim());
|
||||
const model = (() => {
|
||||
const trimmed = params.model?.trim();
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export { normalizeGoogleModelId, parseGeminiAuth } from "openclaw/plugin-sdk/provider-google";
|
||||
export {
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
normalizeGoogleApiBaseUrl,
|
||||
normalizeGoogleModelId,
|
||||
parseGeminiAuth,
|
||||
} from "openclaw/plugin-sdk/provider-google";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { DEFAULT_GOOGLE_API_BASE_URL } from "openclaw/plugin-sdk/provider-google";
|
||||
import {
|
||||
buildSearchCacheKey,
|
||||
buildUnsupportedSearchFilterResponse,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
|
||||
const DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
|
||||
const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
||||
const GEMINI_API_BASE = DEFAULT_GOOGLE_API_BASE_URL;
|
||||
|
||||
type GeminiConfig = {
|
||||
apiKey?: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../config/discord-preview-streaming.js";
|
||||
import { migrateLegacyWebSearchConfig } from "../config/legacy-web-search.js";
|
||||
import { DEFAULT_TALK_PROVIDER, normalizeTalkSection } from "../config/talk.js";
|
||||
import { DEFAULT_GOOGLE_API_BASE_URL } from "../infra/google-api-base-url.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
|
||||
export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
|
||||
@@ -580,7 +581,7 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
|
||||
if (!hasGoogleApiKey && legacyApiKey) {
|
||||
rawGoogle.apiKey = legacyApiKey;
|
||||
if (!rawGoogle.baseUrl) {
|
||||
rawGoogle.baseUrl = "https://generativelanguage.googleapis.com/v1beta";
|
||||
rawGoogle.baseUrl = DEFAULT_GOOGLE_API_BASE_URL;
|
||||
}
|
||||
if (!Array.isArray(rawGoogle.models)) {
|
||||
rawGoogle.models = [];
|
||||
|
||||
32
src/infra/google-api-base-url.test.ts
Normal file
32
src/infra/google-api-base-url.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_GOOGLE_API_BASE_URL, normalizeGoogleApiBaseUrl } from "./google-api-base-url.js";
|
||||
|
||||
describe("normalizeGoogleApiBaseUrl", () => {
|
||||
it("defaults to the Gemini v1beta API root", () => {
|
||||
expect(normalizeGoogleApiBaseUrl()).toBe(DEFAULT_GOOGLE_API_BASE_URL);
|
||||
});
|
||||
|
||||
it("normalizes the bare Google API host to the Gemini v1beta root", () => {
|
||||
expect(normalizeGoogleApiBaseUrl("https://generativelanguage.googleapis.com")).toBe(
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
);
|
||||
expect(normalizeGoogleApiBaseUrl("https://generativelanguage.googleapis.com/")).toBe(
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves explicit Google API paths", () => {
|
||||
expect(normalizeGoogleApiBaseUrl("https://generativelanguage.googleapis.com/v1beta")).toBe(
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
);
|
||||
expect(normalizeGoogleApiBaseUrl("https://generativelanguage.googleapis.com/v1")).toBe(
|
||||
"https://generativelanguage.googleapis.com/v1",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves custom proxy paths", () => {
|
||||
expect(normalizeGoogleApiBaseUrl("https://proxy.example.com/google/v1beta/")).toBe(
|
||||
"https://proxy.example.com/google/v1beta",
|
||||
);
|
||||
});
|
||||
});
|
||||
28
src/infra/google-api-base-url.ts
Normal file
28
src/infra/google-api-base-url.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
const DEFAULT_GOOGLE_API_HOST = "generativelanguage.googleapis.com";
|
||||
|
||||
export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
|
||||
function trimTrailingSlashes(value: string): string {
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function normalizeGoogleApiBaseUrl(baseUrl?: string): string {
|
||||
const raw = trimTrailingSlashes(baseUrl?.trim() || DEFAULT_GOOGLE_API_BASE_URL);
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
url.hash = "";
|
||||
url.search = "";
|
||||
if (
|
||||
url.hostname.toLowerCase() === DEFAULT_GOOGLE_API_HOST &&
|
||||
trimTrailingSlashes(url.pathname || "") === ""
|
||||
) {
|
||||
url.pathname = "/v1beta";
|
||||
}
|
||||
return trimTrailingSlashes(url.toString());
|
||||
} catch {
|
||||
if (/^https:\/\/generativelanguage\.googleapis\.com\/?$/i.test(raw)) {
|
||||
return DEFAULT_GOOGLE_API_BASE_URL;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
} from "../agents/api-key-rotation.js";
|
||||
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
|
||||
import { parseGeminiAuth } from "../infra/gemini-auth.js";
|
||||
import {
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
normalizeGoogleApiBaseUrl,
|
||||
} from "../infra/google-api-base-url.js";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import type { EmbeddingInput } from "./embedding-inputs.js";
|
||||
import { sanitizeAndNormalizeEmbedding } from "./embedding-vectors.js";
|
||||
@@ -22,7 +26,6 @@ export type GeminiEmbeddingClient = {
|
||||
outputDimensionality?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
||||
export const DEFAULT_GEMINI_EMBEDDING_MODEL = "gemini-embedding-001";
|
||||
const GEMINI_MAX_INPUT_TOKENS: Record<string, number> = {
|
||||
"text-embedding-004": 2048,
|
||||
@@ -205,9 +208,9 @@ function normalizeGeminiBaseUrl(raw: string): string {
|
||||
const trimmed = raw.replace(/\/+$/, "");
|
||||
const openAiIndex = trimmed.indexOf("/openai");
|
||||
if (openAiIndex > -1) {
|
||||
return trimmed.slice(0, openAiIndex);
|
||||
return normalizeGoogleApiBaseUrl(trimmed.slice(0, openAiIndex));
|
||||
}
|
||||
return trimmed;
|
||||
return normalizeGoogleApiBaseUrl(trimmed);
|
||||
}
|
||||
|
||||
function buildGeminiModelPath(model: string): string {
|
||||
@@ -302,7 +305,8 @@ export async function resolveGeminiEmbeddingClient(
|
||||
);
|
||||
|
||||
const providerConfig = options.config.models?.providers?.google;
|
||||
const rawBaseUrl = remoteBaseUrl || providerConfig?.baseUrl?.trim() || DEFAULT_GEMINI_BASE_URL;
|
||||
const rawBaseUrl =
|
||||
remoteBaseUrl || providerConfig?.baseUrl?.trim() || DEFAULT_GOOGLE_API_BASE_URL;
|
||||
const baseUrl = normalizeGeminiBaseUrl(rawBaseUrl);
|
||||
const ssrfPolicy = buildRemoteBaseUrlPolicy(baseUrl);
|
||||
const headerOverrides = Object.assign({}, providerConfig?.headers, remote?.headers);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// Private Google-specific helpers used by bundled Google plugins.
|
||||
|
||||
export { normalizeGoogleModelId } from "../agents/model-id-normalization.js";
|
||||
export {
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
normalizeGoogleApiBaseUrl,
|
||||
} from "../infra/google-api-base-url.js";
|
||||
export { parseGeminiAuth } from "../infra/gemini-auth.js";
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// Public Google provider helpers shared by bundled Google extensions.
|
||||
|
||||
export { normalizeGoogleModelId } from "../agents/model-id-normalization.js";
|
||||
export {
|
||||
DEFAULT_GOOGLE_API_BASE_URL,
|
||||
normalizeGoogleApiBaseUrl,
|
||||
} from "../infra/google-api-base-url.js";
|
||||
export { parseGeminiAuth } from "../infra/gemini-auth.js";
|
||||
|
||||
Reference in New Issue
Block a user