fix: harden custom image generation config

Maintainer edit: require providers.custom.apiBase before making custom image requests and allow unauthenticated local endpoints by omitting Authorization when no apiKey is configured.
This commit is contained in:
chengyongru 2026-06-04 13:46:19 +08:00 committed by Xubin Ren
parent 748b28da01
commit ae17a79bdf
3 changed files with 28 additions and 12 deletions

View File

@ -108,7 +108,7 @@ Configure:
} }
``` ```
The `apiBase` is required. The provider sends requests to `{apiBase}/images/generations` using the OpenAI Images API format with `response_format: "b64_json"`. The `apiBase` is required. The provider sends requests to `{apiBase}/images/generations` using the OpenAI Images API format with `response_format: "b64_json"`. The `apiKey` is optional for local or unauthenticated endpoints.
### AIHubMix ### AIHubMix
@ -350,7 +350,7 @@ Use the reference image. Keep the same robot and composition, change the palette
|---------|-------| |---------|-------|
| `generate_image` is not available | Set `tools.imageGeneration.enabled` to `true` and restart the gateway | | `generate_image` is not available | Set `tools.imageGeneration.enabled` to `true` and restart the gateway |
| Missing API key error | Configure `providers.<provider>.apiKey`; if using `${VAR_NAME}`, confirm the environment variable is visible to the gateway process | | Missing API key error | Configure `providers.<provider>.apiKey`; if using `${VAR_NAME}`, confirm the environment variable is visible to the gateway process |
| `unsupported image generation provider` | Use `openrouter`, `aihubmix`, `minimax`, `gemini`, `ollama`, `stepfun`, or `zhipu` | | `unsupported image generation provider` | Use `openrouter`, `custom`, `aihubmix`, `minimax`, `gemini`, `ollama`, `stepfun`, or `zhipu` |
| AIHubMix says `Incorrect model ID` | Use `model: "gpt-image-2-free"`; nanobot expands it to the required `openai/gpt-image-2-free` model path internally | | AIHubMix says `Incorrect model ID` | Use `model: "gpt-image-2-free"`; nanobot expands it to the required `openai/gpt-image-2-free` model path internally |
| Generation times out | Try a smaller/default image size, set AIHubMix `extraBody.quality` to `"low"`, or retry later | | Generation times out | Try a smaller/default image size, set AIHubMix `extraBody.quality` to `"low"`, or retry later |
| Reference image rejected | Reference image paths must be inside the workspace or nanobot media directory and must be valid image files | | Reference image rejected | Reference image paths must be inside the workspace or nanobot media directory and must be valid image files |

View File

@ -1037,8 +1037,8 @@ class CustomImageGenerationClient(ImageGenerationProvider):
"""OpenAI-compatible Images API for user-configured custom providers.""" """OpenAI-compatible Images API for user-configured custom providers."""
provider_name = "custom" provider_name = "custom"
missing_key_message = ( missing_base_message = (
"Custom image generation API key is not configured. Set providers.custom.apiKey." "Custom image generation API base is not configured. Set providers.custom.apiBase."
) )
def _default_base_url(self) -> str: def _default_base_url(self) -> str:
@ -1057,8 +1057,8 @@ class CustomImageGenerationClient(ImageGenerationProvider):
aspect_ratio: str | None = None, aspect_ratio: str | None = None,
image_size: str | None = None, image_size: str | None = None,
) -> GeneratedImageResponse: ) -> GeneratedImageResponse:
if not self.api_key: if not self.api_base:
raise ImageGenerationError(self.missing_key_message) raise ImageGenerationError(self.missing_base_message)
if reference_images: if reference_images:
logger.warning( logger.warning(
@ -1068,11 +1068,12 @@ class CustomImageGenerationClient(ImageGenerationProvider):
model, model,
) )
headers = { headers: dict[str, str] = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
**self.extra_headers,
} }
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
headers.update(self.extra_headers)
body: dict[str, Any] = { body: dict[str, Any] = {
"model": model, "model": model,

View File

@ -844,10 +844,25 @@ async def test_custom_generate_success() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_custom_generate_no_api_key() -> None: async def test_custom_generate_without_api_key_omits_authorization() -> None:
client = CustomImageGenerationClient(api_key=None) fake = FakeClient(FakeResponse({"data": [{"b64_json": RAW_B64}]}))
client = CustomImageGenerationClient(
api_key=None,
api_base="http://localhost:7860/v1",
client=fake, # type: ignore[arg-type]
)
with pytest.raises(ImageGenerationError, match="providers.custom.apiKey"): response = await client.generate(prompt="draw", model="custom-image-model")
assert response.images == [PNG_DATA_URL]
assert "Authorization" not in fake.calls[0]["headers"]
@pytest.mark.asyncio
async def test_custom_generate_requires_api_base() -> None:
client = CustomImageGenerationClient(api_key="sk-custom-test")
with pytest.raises(ImageGenerationError, match="providers.custom.apiBase"):
await client.generate(prompt="draw", model="custom-image-model") await client.generate(prompt="draw", model="custom-image-model")