fix: keep explicit image generation model exact

This commit is contained in:
Peter Steinberger
2026-04-25 00:38:59 +01:00
parent e40d7abda9
commit fbf8b216c6
6 changed files with 55 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
- Discord/replies: run `message_sending` plugin hooks for Discord reply delivery, including DM targets, so plugins can transform or cancel outbound Discord replies consistently with other channels. Fixes #59350. (#71094) Thanks @wei840222.
- Control UI/commands: carry provider-owned thinking option ids/labels in session rows and defaults so fresh sessions show and accept dynamic modes such as `adaptive`, `xhigh`, and `max`. Fixes #71269. Thanks @Young-Khalil.
- Image generation: make explicit `model=` overrides exact-only so failed `openai/gpt-image-2` requests no longer fall through to Gemini or other configured providers, and update `image_generate list` to mention OpenAI Codex OAuth as valid auth for `openai/gpt-image-2`. Fixes #71290 and #71231. Thanks @Young-Khalil and @steipete.
- Providers/GitHub Copilot: keep the plugin stream wrapper from claiming transport selection before OpenClaw picks a boundary-aware stream path, avoiding Pi's stale fallback Copilot headers on normal model turns. Thanks @steipete.
- Discord/subagents: pass runtime config into thread-bound native subagent binding and require it at the helper boundary so Discord channel resolution keeps account-aware config. Fixes #71054. (#70945) Thanks @jai.
- Slack/Assistant: accept Slack Assistant DM `message_changed` events when their metadata identifies the human sender, while continuing to drop self-authored bot edits. Fixes #55445. Thanks @AlfredPros.

View File

@@ -172,10 +172,13 @@ When generating an image, OpenClaw tries providers in this order:
- current default provider first
- remaining registered image-generation providers in provider-id order
If a provider fails (auth error, rate limit, etc.), the next candidate is tried automatically. If all fail, the error includes details from each attempt.
If a provider fails (auth error, rate limit, etc.), the next configured candidate is tried automatically. If all fail, the error includes details from each attempt.
Notes:
- A per-call `model` override is exact: OpenClaw tries only that provider/model
and does not continue to configured primary/fallback or auto-detected
providers.
- Auto-detection is auth-aware. A provider default only enters the candidate list
when OpenClaw can actually authenticate that provider.
- Auto-detection is enabled by default. Set

View File

@@ -1070,7 +1070,9 @@ describe("createImageGenerateTool", () => {
expect(text).toContain("gemini-3.1-flash-image-preview");
expect(text).toContain("gemini-3-pro-image-preview");
expect(text).toContain("auth: set GEMINI_API_KEY / GOOGLE_API_KEY to use google/*");
expect(text).toContain("auth: set OPENAI_API_KEY to use openai/*");
expect(text).toContain(
"auth: set OPENAI_API_KEY or configure OpenAI Codex OAuth for openai/gpt-image-2",
);
expect(text).toContain("editing up to 5 refs");
expect(text).toContain("aspect ratios 1:1, 16:9");
expect(result).toMatchObject({

View File

@@ -160,6 +160,19 @@ function getImageGenerationProviderAuthEnvVars(providerId: string): string[] {
return getProviderEnvVars(providerId);
}
function formatImageGenerationAuthHint(provider: {
id: string;
authEnvVars: readonly string[];
}): string | undefined {
if (provider.id === "openai") {
return "set OPENAI_API_KEY or configure OpenAI Codex OAuth for openai/gpt-image-2";
}
if (provider.authEnvVars.length === 0) {
return undefined;
}
return `set ${provider.authEnvVars.join(" / ")} to use ${provider.id}/*`;
}
export function resolveImageGenerationModelConfigForTool(params: {
cfg?: OpenClawConfig;
agentDir?: string;
@@ -592,13 +605,12 @@ export function createImageGenerateTool(options?: {
provider.models.length > 0
? `models: ${provider.models.join(", ")}`
: "models: unknown";
const authHint = formatImageGenerationAuthHint(provider);
return [
`${provider.id}${provider.defaultModel ? ` (default ${provider.defaultModel})` : ""}`,
` ${modelLine}`,
` configured: ${provider.configured ? "yes" : "no"}`,
...(provider.authEnvVars.length > 0
? [` auth: set ${provider.authEnvVars.join(" / ")} to use ${provider.id}/*`]
: []),
...(authHint ? [` auth: ${authHint}`] : []),
...(caps.length > 0 ? [` capabilities: ${caps.join("; ")}`] : []),
];
});

View File

@@ -119,6 +119,33 @@ describe("media-generation runtime shared candidates", () => {
expect(candidates).toEqual([{ provider: "google", model: "gemini-3.1-flash-image-preview" }]);
});
it("treats an explicit model override as exact-only", () => {
const candidates = resolveCapabilityModelCandidates({
cfg: {
agents: {
defaults: {
mediaGenerationAutoProviderFallback: false,
},
},
} as OpenClawConfig,
modelConfig: {
primary: "google/gemini-3.1-flash-image-preview",
fallbacks: ["fal/fal-ai/flux/dev"],
},
modelOverride: "openai/gpt-image-2",
parseModelRef,
listProviders: () => [
{
id: "google",
defaultModel: "gemini-3.1-flash-image-preview",
isConfigured: () => true,
},
],
});
expect(candidates).toEqual([{ provider: "openai", model: "gpt-image-2" }]);
});
});
describe("media-generation runtime shared normalization", () => {

View File

@@ -178,6 +178,11 @@ export function resolveCapabilityModelCandidates(params: {
candidates.push(parsed);
};
const override = params.parseModelRef(params.modelOverride);
if (override) {
return [override];
}
add(params.modelOverride);
add(resolveAgentModelPrimaryValue(params.modelConfig));
for (const fallback of resolveAgentModelFallbackValues(params.modelConfig)) {