Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,19 @@ export function resolveComboCatalogMember(
&& fallback?.inputModalities !== undefined;
const addReasoning = member.reasoningEfforts === undefined
&& fallback?.reasoningEfforts !== undefined;
if (!addMaxInput && !addMaxOutput && !adjustAutoCompact && !addModalities && !addReasoning) return member;
// A sidecar-covered member is advertised image-capable on its own provider row
// (applyProviderConfigHints), but complete discovery rows reach this resolver
// un-hinted, so the combo intersection saw the raw text-only declaration and one
// blind member collapsed the whole combo to text-only — the Codex app then blocks
// attachments client-side before the sidecar can run. Mirror the direct-row
// advertisement here so derivation intersects the modalities the runtime serves.
const modalitiesAfterFallback = addModalities
? [...fallback!.inputModalities!]
: member.inputModalities;
const addSidecarImage = prov !== undefined
&& isModelVisionSidecarConsumer(prov, member.id)
&& !(modalitiesAfterFallback ?? ["text"]).includes("image");
if (!addMaxInput && !addMaxOutput && !adjustAutoCompact && !addModalities && !addReasoning && !addSidecarImage) return member;
return {
...member,
// Never claim a larger input budget than the window, and prefer the model's own
Expand All @@ -1026,6 +1038,7 @@ export function resolveComboCatalogMember(
...(adjustAutoCompact && autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
...(addModalities ? { inputModalities: [...fallback!.inputModalities!] } : {}),
...(addReasoning ? { reasoningEfforts: [...fallback!.reasoningEfforts!] } : {}),
...(addSidecarImage ? { inputModalities: [...(modalitiesAfterFallback ?? ["text"]), "image"] } : {}),
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test } from "bun:test";
import { applyProviderConfigHints, gatherRoutedModels } from "../../src/codex/catalog";
import { applyProviderConfigHints, gatherRoutedModels, resolveComboCatalogMember } from "../../src/codex/catalog";
import { clearModelCache } from "../../src/codex/model-cache";
import type { OcxProviderConfig } from "../../src/types";
import { deriveComboCatalogModel } from "../../src/codex/catalog";
Expand Down Expand Up @@ -390,6 +390,71 @@ describe("vision-capable provider models feed combo modalities", () => {
);
expect(derived?.inputModalities).toEqual(["text", "image"]);
});

test("a sidecar-covered discovery row no longer collapses combo image advertising", () => {
// The real call site resolves members through resolveComboCatalogMember, and complete
// discovery rows arrive WITHOUT the config hint that rewrites direct provider rows.
// The /planners combo hit exactly this: sidecar-covered DeepSeek rows kept their raw
// text-only modalities at derivation, so combo/planners advertised text-only and the
// Codex app blocked pasted images before the sidecar could run.
const blind = {
provider: "azu-lab1",
id: "DeepSeek-V4-Pro",
contextWindow: 128_000,
inputModalities: ["text"],
} as CatalogModel;
const covered = new Map([
["azu-lab1", {
adapter: "openai-chat",
baseUrl: "https://azu-lab1.example/v1",
noVisionModels: ["DeepSeek-V4-Pro"],
} as OcxProviderConfig],
]);
const resolved = resolveComboCatalogMember(
{ provider: "azu-lab1", model: "DeepSeek-V4-Pro" },
new Map([["azu-lab1/DeepSeek-V4-Pro", blind]]),
covered,
);
expect(resolved?.inputModalities).toEqual(["text", "image"]);

const targets = [
{ provider: "azu-lab1", model: "DeepSeek-V4-Pro" },
{ provider: "azu-lab2", model: "gpt-5.6-terra" },
];
const members = [
resolved!,
{
provider: "azu-lab2",
id: "gpt-5.6-terra",
contextWindow: 200_000,
inputModalities: ["text", "image"],
} as CatalogModel,
];
const derived = deriveComboCatalogModel(
"planners",
{ targets, defaultEffort: "high" } as never,
members,
);
expect(derived?.inputModalities).toEqual(["text", "image"]);

// combo.imageInput: "disabled" still strips image from the advertised modalities.
const disabled = deriveComboCatalogModel(
"planners",
{ targets, defaultEffort: "high", imageInput: "disabled" } as never,
members,
);
expect(disabled?.inputModalities).toEqual(["text"]);

// Providers without sidecar coverage stay untouched (identity preserved).
expect(resolveComboCatalogMember(
{ provider: "azu-lab1", model: "DeepSeek-V4-Pro" },
new Map([["azu-lab1/DeepSeek-V4-Pro", blind]]),
new Map([["azu-lab1", {
adapter: "openai-chat",
baseUrl: "https://azu-lab1.example/v1",
} as OcxProviderConfig]]),
)).toBe(blind);
});
});

describe("Cursor native vs sidecar vision registry", () => {
Expand Down
Loading