diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb8c037a31..f07739a3a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - Agents/OpenAI: surface selected-model capacity failures from PI, Codex, and auto-reply harness paths with a model-switch hint instead of the generic empty-response error. Thanks @vincentkoc. - Providers/OpenAI: route `openai/gpt-image-2` through configured Codex OAuth directly when an `openai-codex` profile is active, instead of probing `OPENAI_API_KEY` first. - Providers/OpenAI: harden image generation auth routing and Codex OAuth response parsing so fallback only applies to public OpenAI API routes and bounded SSE results. Thanks @Takhoffman. +- Providers/OpenAI: honor the private-network SSRF opt-in for OpenAI-compatible image generation endpoints, so trusted LocalAI/LAN `image_generate` routes work without disabling SSRF checks globally. Fixes #62879. Thanks @seitzbg. - Providers/OpenAI: stop advertising the removed `gpt-5.3-codex-spark` Codex model through fallback catalogs, and suppress stale rows with a GPT-5.5 recovery hint. - Plugins/QR: replace legacy `qrcode-terminal` QR rendering with bounded `qrcode-tui` helpers for plugin login/setup flows. (#65969) Thanks @vincentkoc. - Voice-call/realtime: wait for OpenAI session configuration before greeting or forwarding buffered audio, and reject non-allowlisted Twilio callers before stream setup. (#43501) Thanks @forrestblount. diff --git a/docs/providers/openai.md b/docs/providers/openai.md index a1a07659688..5981056a301 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -236,6 +236,10 @@ does not first try `OPENAI_API_KEY` or silently fall back to an API key for that request. Configure `models.providers.openai` explicitly with an API key, custom base URL, or Azure endpoint when you want the direct OpenAI Images API route instead. +If that custom image endpoint is on a trusted LAN/private address, also set +`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork: true`; OpenClaw keeps +private/internal OpenAI-compatible image endpoints blocked unless this opt-in is +present. Generate: diff --git a/docs/tools/image-generation.md b/docs/tools/image-generation.md index 8f9595cc425..944f7cf1b99 100644 --- a/docs/tools/image-generation.md +++ b/docs/tools/image-generation.md @@ -35,6 +35,10 @@ Codex OAuth uses the same `openai/gpt-image-2` model ref. When an through that same OAuth profile instead of first trying `OPENAI_API_KEY`. Explicit custom `models.providers.openai` image config, such as an API key or custom/Azure base URL, opts back into the direct OpenAI Images API route. +For OpenAI-compatible LAN endpoints such as LocalAI, keep the custom +`models.providers.openai.baseUrl` and explicitly opt in with +`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork: true`; private/internal +image endpoints remain blocked by default. 3. Ask the agent: _"Generate an image of a friendly robot mascot."_ diff --git a/extensions/openai/image-generation-provider.test.ts b/extensions/openai/image-generation-provider.test.ts index 0b22155a8df..2098f863b72 100644 --- a/extensions/openai/image-generation-provider.test.ts +++ b/extensions/openai/image-generation-provider.test.ts @@ -316,6 +316,47 @@ describe("openai image generation provider", () => { expect(result.images).toHaveLength(1); }); + it("allows OpenAI-compatible private image endpoints when browser SSRF policy opts in", async () => { + mockGeneratedPngResponse(); + + const provider = buildOpenAIImageGenerationProvider(); + const result = await provider.generateImage({ + provider: "openai", + model: "flux2-klein", + prompt: "A simple, clean illustration of a red apple with a green leaf", + cfg: { + browser: { + ssrfPolicy: { + dangerouslyAllowPrivateNetwork: true, + }, + }, + models: { + providers: { + openai: { + baseUrl: "http://192.168.1.15:8082/v1", + apiKey: "local-noauth", + models: [], + }, + }, + }, + }, + }); + + expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "http://192.168.1.15:8082/v1", + allowPrivateNetwork: true, + }), + ); + expect(postJsonRequestMock).toHaveBeenCalledWith( + expect.objectContaining({ + url: "http://192.168.1.15:8082/v1/images/generations", + allowPrivateNetwork: true, + }), + ); + expect(result.images).toHaveLength(1); + }); + it("forwards generation count and custom size overrides", async () => { mockGeneratedPngResponse(); diff --git a/extensions/openai/image-generation-provider.ts b/extensions/openai/image-generation-provider.ts index d6c916c6e3a..6c2e6442fc1 100644 --- a/extensions/openai/image-generation-provider.ts +++ b/extensions/openai/image-generation-provider.ts @@ -21,6 +21,7 @@ import { resolveProviderHttpRequestConfig, sanitizeConfiguredModelProviderRequest, } from "openclaw/plugin-sdk/provider-http"; +import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; import { OPENAI_DEFAULT_IMAGE_MODEL as DEFAULT_OPENAI_IMAGE_MODEL } from "./default-models.js"; import { resolveConfiguredOpenAIBaseUrl } from "./shared.js"; @@ -190,6 +191,9 @@ function shouldAllowPrivateImageEndpoint(req: { if (req.provider === MOCK_OPENAI_PROVIDER_ID) { return true; } + if (isPrivateNetworkOptInEnabled(req.cfg?.browser?.ssrfPolicy)) { + return true; + } const baseUrl = resolveConfiguredOpenAIBaseUrl(req.cfg); if (!baseUrl.startsWith("http://127.0.0.1:") && !baseUrl.startsWith("http://localhost:")) { return false;