fix(ai): exclude text-only o-series variants from vision capability

The o1/o3/o4 prefix match also caught o1-mini, o1-preview and o3-mini,
which have no image input. A user pinned to one was told OCR works, then
the provider rejected the image block. Treat an o1/o3/o4 slug as
vision-capable only when it is not a -mini or -preview variant, applied
to both the OpenAI and Codex gates.
This commit is contained in:
Marc-André Bombeck committed 2026-06-26 09:21:54 +02:00
1 parent e4c0af0970
commit 3e4cc53ede
2 files changed
+45 -8

No files matched your search

@@ -39,7 +39,10 @@ describe("supportsVisionForConfig", () => {
expect(supportsVisionForConfig(providerType, "gpt-4o-mini")).toBe(true);
expect(supportsVisionForConfig(providerType, "gpt-4.1")).toBe(true);
expect(supportsVisionForConfig(providerType, "gpt-4-turbo")).toBe(true);
// Full o-series reasoning models read images.
expect(supportsVisionForConfig(providerType, "o1")).toBe(true);
expect(supportsVisionForConfig(providerType, "o3")).toBe(true);
expect(supportsVisionForConfig(providerType, "o4")).toBe(true);
}
});
@@ -49,6 +52,20 @@ describe("supportsVisionForConfig", () => {
expect(supportsVisionForConfig("openai", null)).toBe(false);
});
it("rejects text-only o-series variants (-mini / -preview) for openai", () => {
// o1-mini / o1-preview / o3-mini accept the `o*` prefix but have no image
// input — claiming vision here 400s the user at the image block.
for (const providerType of [
"openai",
"admin-openai",
"admin-key",
] as const) {
expect(supportsVisionForConfig(providerType, "o1-mini")).toBe(false);
expect(supportsVisionForConfig(providerType, "o1-preview")).toBe(false);
expect(supportsVisionForConfig(providerType, "o3-mini")).toBe(false);
}
});
it("trusts local (operator opted in) regardless of model string", () => {
expect(supportsVisionForConfig("local", "llava")).toBe(true);
expect(supportsVisionForConfig("local", null)).toBe(true);
@@ -72,6 +89,12 @@ describe("supportsVisionForConfig", () => {
// The `-codex` specialist slugs are text-only despite the gpt-5 prefix.
expect(supportsVisionForConfig("codex", "gpt-5-codex")).toBe(false);
expect(supportsVisionForConfig("codex", "gpt-5.1-codex-mini")).toBe(false);
// Text-only o-series variants are excluded for codex too.
expect(supportsVisionForConfig("codex", "o1-mini")).toBe(false);
expect(supportsVisionForConfig("codex", "o3-mini")).toBe(false);
expect(supportsVisionForConfig("codex", "o1-preview")).toBe(false);
// Full o-series stays vision-capable.
expect(supportsVisionForConfig("codex", "o3")).toBe(true);
});
it("never reports vision for none", () => {
+22 -8
View File
@@ -41,18 +41,32 @@ function anthropicSupportsVision(model: string): boolean {
/**
* OpenAI vision models (also covers the operator's admin key + Codex when it
* runs an OpenAI model — though Codex is excluded by `providerType`). `gpt-4o`,
* `gpt-4.1`, `gpt-4-turbo`, and the `o*` reasoning models read image content;
* `gpt-3.5*` and the legacy `gpt-4` (non-turbo) 400 on an image block.
* `gpt-4.1`, `gpt-4-turbo`, and the full `o1` / `o3` / `o4` reasoning models
* read image content; `gpt-3.5*` and the legacy `gpt-4` (non-turbo) 400 on an
* image block.
*
* The `o*` reasoning line has text-only variants that DO accept the `o*` prefix
* but reject an image block: `o1-mini` / `o1-preview` and `o3-mini` have no
* vision input (only the full `o1` / `o3` are multimodal). They must be
* excluded or a user pinned to one is told OCR works, then the provider 400s at
* the image block. We treat an `o1` / `o3` / `o4` slug as vision-capable only
* when it is NOT a `-mini` / `-preview` variant — the conservative default for
* any unverified narrow slug.
*/
function isVisionReasoningSlug(m: string): boolean {
if (!(m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4"))) {
return false;
}
return !m.includes("-mini") && !m.includes("-preview");
}
function openaiSupportsVision(model: string): boolean {
const m = model.toLowerCase();
return (
m.startsWith("gpt-4o") ||
m.startsWith("gpt-4.1") ||
m.startsWith("gpt-4-turbo") ||
m.startsWith("o1") ||
m.startsWith("o3") ||
m.startsWith("o4")
isVisionReasoningSlug(m)
);
}
@@ -78,9 +92,9 @@ function codexSupportsVision(model: string): boolean {
m.startsWith("gpt-4o") ||
m.startsWith("gpt-4.1") ||
m.startsWith("gpt-4-turbo") ||
m.startsWith("o1") ||
m.startsWith("o3") ||
m.startsWith("o4")
// Same `-mini` / `-preview` text-only exclusion as the OpenAI gate: a
// future codex slug rotation onto e.g. `o3-mini` must not claim vision.
isVisionReasoningSlug(m)
);
}