fix: honor Google image private-network opt-in

This commit is contained in:
Peter Steinberger
2026-04-24 01:04:06 +01:00
parent cc295fb8c9
commit 716a3a5865
5 changed files with 48 additions and 7 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Providers/Google: honor the private-network SSRF opt-in for Gemini image generation requests, so trusted proxy setups that resolve Google API hosts to private addresses can use `image_generate`. Fixes #67216.
- Agents/transport: stop embedded runs from lowering the process-wide undici stream timeouts, so slow Gemini image generation and other long-running provider requests no longer inherit short run-attempt headers timeouts. Fixes #70423. Thanks @giangthb.
- Providers/OpenRouter: send image-understanding prompts as user text before image parts, restoring non-empty vision responses for OpenRouter multimodal models. Fixes #70410.
- Memory/QMD: recreate stale managed QMD collections when startup repair finds the collection name already exists, so root memory narrows back to `MEMORY.md` instead of staying on broad workspace markdown indexing.

View File

@@ -1,4 +1,3 @@
import type { ProviderRequestTransportOverrides } from "openclaw/plugin-sdk/provider-http";
import { describe, expect, it } from "vitest";
import {
isGoogleGenerativeAiApi,
@@ -217,7 +216,7 @@ describe("google generative ai helpers", () => {
expect(normalized).toBe("https://generativelanguage.googleapis.com/v1beta/openai");
});
it("rejects non-Google Gemini base URLs and ignores smuggled private-network flags", () => {
it("rejects non-Google Gemini base URLs and honors explicit private-network opt-in", () => {
expect(() =>
resolveGoogleGenerativeAiHttpRequestConfig({
apiKey: "api-key-123",
@@ -241,8 +240,8 @@ describe("google generative ai helpers", () => {
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
capability: "image",
transport: "http",
request: { allowPrivateNetwork: true } as unknown as ProviderRequestTransportOverrides,
request: { allowPrivateNetwork: true },
});
expect(config.allowPrivateNetwork).toBe(false);
expect(config.allowPrivateNetwork).toBe(true);
});
});

View File

@@ -42,6 +42,10 @@ export {
export { buildGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
export { buildGoogleProvider } from "./provider-registration.js";
type GoogleGenerativeAiRequestOverrides = ProviderRequestTransportOverrides & {
allowPrivateNetwork?: boolean;
};
function resolveTrustedGoogleGenerativeAiBaseUrl(baseUrl?: string): string {
const normalized =
normalizeGoogleGenerativeAiBaseUrl(baseUrl ?? DEFAULT_GOOGLE_API_BASE_URL) ??
@@ -69,14 +73,14 @@ export function resolveGoogleGenerativeAiHttpRequestConfig(params: {
apiKey: string;
baseUrl?: string;
headers?: Record<string, string>;
request?: ProviderRequestTransportOverrides;
request?: GoogleGenerativeAiRequestOverrides;
capability: "image" | "audio" | "video";
transport: "http" | "media-understanding";
}) {
return resolveProviderHttpRequestConfig({
baseUrl: resolveTrustedGoogleGenerativeAiBaseUrl(params.baseUrl),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: false,
allowPrivateNetwork: params.request?.allowPrivateNetwork,
headers: params.headers,
request: params.request,
defaultHeaders: parseGeminiAuth(params.apiKey).headers,

View File

@@ -278,6 +278,36 @@ describe("Google image-generation provider", () => {
);
});
it("honors configured private-network opt-in for Google image generation", async () => {
mockGoogleApiKeyAuth();
installGoogleFetchMock();
const postJsonRequestSpy = vi.spyOn(providerHttp, "postJsonRequest");
const provider = buildGoogleImageGenerationProvider();
await provider.generateImage({
provider: "google",
model: "gemini-3.1-flash-image-preview",
prompt: "draw a fox",
cfg: {
models: {
providers: {
google: {
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
request: { allowPrivateNetwork: true },
models: [],
},
},
},
},
});
expect(postJsonRequestSpy).toHaveBeenCalledWith(
expect.objectContaining({
allowPrivateNetwork: true,
}),
);
});
it("normalizes a configured bare Google host to the v1beta API root", async () => {
mockGoogleApiKeyAuth();
const fetchMock = installGoogleFetchMock();

View File

@@ -1,7 +1,11 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { assertOkOrThrowHttpError, postJsonRequest } from "openclaw/plugin-sdk/provider-http";
import {
assertOkOrThrowHttpError,
postJsonRequest,
sanitizeConfiguredModelProviderRequest,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { normalizeGoogleModelId, resolveGoogleGenerativeAiHttpRequestConfig } from "./api.js";
@@ -132,6 +136,9 @@ export function buildGoogleImageGenerationProvider(): ImageGenerationProvider {
resolveGoogleGenerativeAiHttpRequestConfig({
apiKey: auth.apiKey,
baseUrl: req.cfg?.models?.providers?.google?.baseUrl,
request: sanitizeConfiguredModelProviderRequest(
req.cfg?.models?.providers?.google?.request,
),
capability: "image",
transport: "http",
});