Skip to content
Merged
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
30 changes: 18 additions & 12 deletions src/providers/registry/model-seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,13 @@ export const DEEPSEEK_VISION_PREVIEW_MODEL = "deepseek-v4-flash-vision-exp";
*/
export const COMMAND_CODE_IMAGE_MODELS = [
`deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`,
// Probed 2026-09-18 through a running 2.58.0 proxy: a 3x3 random-color grid
// (180x180 PNG, six candidate colors) came back 9/9 correct both as a user
// message and as a tool_result, and the request logs show the route served
// the image natively — no vision-sidecar call in either window. #4505 asked
// for exactly this upstream probe before promoting the id. The sibling
// deepseek/deepseek-v4-flash route remains verified-negative above.
"deepseek/deepseek-v4.1-flash",
"gpt-5.6-luna",
"gpt-5.6-sol",
"MiniMaxAI/MiniMax-M3",
Expand All @@ -334,20 +341,19 @@ export const COMMAND_CODE_IMAGE_MODELS = [
/**
* Native image stays sourced from COMMAND_CODE_IMAGE_MODELS. Text-only routes
* sit beside that list so the catalog can still advertise sidecar coverage
* without claiming the gateway itself accepts a picture.
*
* The gateway-prefixed DeepSeek V4.1 Flash route has no verified native image
* support, so declaring it image-capable would hand it a picture it drops. A
* positive text-only declaration makes it a vision-sidecar consumer
* without claiming the gateway itself accepts a picture. A positive text-only
* declaration makes the route a vision-sidecar consumer
* (src/vision/eligibility.ts), so the catalog advertises image input on its
* behalf and the four-target combo in #4505 intersects to ["text","image"]
* instead of ["text"] — without claiming native vision. modelInputModalities
* is per-key filled, so this reaches an existing install even when
* noVisionModels was persisted before the id joined that list.
* behalf — without claiming native vision — and modelInputModalities is
* per-key filled, so that reaches an existing install even when noVisionModels
* was persisted before the id joined a list.
*
* Empty as of 2026-09-18. Its only entry, deepseek/deepseek-v4.1-flash, moved
* to COMMAND_CODE_IMAGE_MODELS once the #4505-requested probe passed on both
* the user-message and tool-result paths (see the note at that entry). The
* mechanism stays for the next route that measures text-only.
*/
export const COMMAND_CODE_TEXT_ONLY_MODELS = [
"deepseek/deepseek-v4.1-flash",
] as const;
export const COMMAND_CODE_TEXT_ONLY_MODELS = [] as const;
export const COMMAND_CODE_MODEL_INPUT_MODALITIES: Record<string, ["text"] | ["text", "image"]> = {
...Object.fromEntries(COMMAND_CODE_IMAGE_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
...Object.fromEntries(COMMAND_CODE_TEXT_ONLY_MODELS.map(id => [id, ["text"] as ["text"]])),
Expand Down
50 changes: 26 additions & 24 deletions tests/providers/flash-route-image-modalities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
* Flash-route image modality declarations (#4505).
*
* opencode-go and command-code each serve a GLM-5.3-Flash route (native VLM) and a
* DeepSeek V4.1-Flash route (text-only upstream, covered by the vision sidecar).
* None of the four declared input modalities, so a failover combo over them
* intersected to ["text"] in deriveComboCatalogModel and the Codex app refused
* image attachments for the whole combo — combo image routing was silently
* disabled even though every member can accept an image (two natively, two
* through the sidecar).
* DeepSeek V4.1-Flash route. None of the four declared input modalities, so a
* failover combo over them intersected to ["text"] in deriveComboCatalogModel
* and the Codex app refused image attachments for the whole combo — combo image
* routing was silently disabled even though every member can accept an image.
*
* command-code's DeepSeek route was promoted from sidecar-covered to native
* image on 2026-09-18 after the upstream probe #4505 asked for passed on both
* the user-message and tool-result paths (see model-seeds.ts); opencode-go's
* route remains text-only and sidecar-covered.
*
* The fix is positive per-route modelInputModalities declarations, not a
* noVisionModels union: a text-only declaration makes the route a sidecar
Expand All @@ -34,7 +37,7 @@ import type { CatalogModel, OcxConfig, OcxProviderConfig } from "../../src/types
const OPENCODE_GO_NATIVE = "glm-5.3-flash";
const OPENCODE_GO_SIDECAR = "deepseek-v4.1-flash";
const COMMAND_CODE_NATIVE = "z-ai/glm-5.3-flash";
const COMMAND_CODE_SIDECAR = "deepseek/deepseek-v4.1-flash";
const COMMAND_CODE_DEEPSEEK = "deepseek/deepseek-v4.1-flash";

/** Seeded provider config, shaped the way an install persists it. */
function seeded(provider: string): OcxProviderConfig {
Expand All @@ -53,38 +56,37 @@ describe("flash-route registry modality declarations (#4505)", () => {
expect(entry?.modelInputModalities?.[OPENCODE_GO_SIDECAR]).toEqual(["text"]);
});

test("command-code declares z-ai/glm-5.3-flash in the image set and deepseek/deepseek-v4.1-flash text-only", () => {
test("command-code declares both flash routes image-capable (deepseek probed 2026-09-18)", () => {
const entry = PROVIDER_REGISTRY.find(e => e.id === "command-code");
expect(entry?.modelInputModalities?.[COMMAND_CODE_NATIVE]).toEqual(["text", "image"]);
// The text-only declaration must NOT come from the image allowlist: putting
// a DeepSeek route in COMMAND_CODE_IMAGE_MODELS would advertise native
// vision for a model that drops the image upstream.
expect(entry?.modelInputModalities?.[COMMAND_CODE_SIDECAR]).toEqual(["text"]);
// Promoted from COMMAND_CODE_TEXT_ONLY_MODELS: the upstream probe #4505
// asked for passed on both the user-message and tool-result paths.
expect(entry?.modelInputModalities?.[COMMAND_CODE_DEEPSEEK]).toEqual(["text", "image"]);
});
});

describe("flash-route native vs sidecar distinction (#4505)", () => {
// WHY: the issue requires routes needing a sidecar to stay distinguishable
// from native vision. A wrong fix that declares DeepSeek ["text","image"]
// passes an "everything says image" test while the upstream model silently
// drops the picture — the sidecar distinction is what keeps the image on a
// path that actually reads it.
// from native vision. The distinction must follow measured upstream behavior:
// command-code's route now carries probe evidence (user message + tool
// result) for native reading, while opencode-go's route has none and stays on
// the sidecar path.
test("the two glm-5.3-flash routes are NOT sidecar consumers (native VLM)", () => {
expect(isModelVisionSidecarConsumer(seeded("opencode-go"), OPENCODE_GO_NATIVE)).toBe(false);
expect(isModelVisionSidecarConsumer(seeded("command-code"), COMMAND_CODE_NATIVE)).toBe(false);
});

test("the two deepseek-v4.1-flash routes ARE sidecar consumers (text-only upstream)", () => {
test("opencode-go's deepseek route is a sidecar consumer; command-code's is native", () => {
expect(isModelVisionSidecarConsumer(seeded("opencode-go"), OPENCODE_GO_SIDECAR)).toBe(true);
expect(isModelVisionSidecarConsumer(seeded("command-code"), COMMAND_CODE_SIDECAR)).toBe(true);
expect(isModelVisionSidecarConsumer(seeded("command-code"), COMMAND_CODE_DEEPSEEK)).toBe(false);
});
});

describe("flash-route catalog advertisement (#4505)", () => {
// WHY: the Codex app gates attachments client-side on input_modalities, so the
// catalog row is where the combo's image capability is actually won or lost.
// The DeepSeek rows must pick up "image" from the sidecar hint, not from a
// native claim.
// The opencode-go DeepSeek row picks up "image" from the sidecar hint; the
// command-code row now carries a native declaration.
test("applyProviderConfigHints advertises image for sidecar-covered deepseek-v4.1-flash on opencode-go", () => {
const hinted = applyProviderConfigHints("opencode-go", seeded("opencode-go"), {
id: OPENCODE_GO_SIDECAR,
Expand All @@ -98,7 +100,7 @@ describe("flash-route catalog advertisement (#4505)", () => {
["opencode-go", OPENCODE_GO_NATIVE],
["opencode-go", OPENCODE_GO_SIDECAR],
["command-code", COMMAND_CODE_NATIVE],
["command-code", COMMAND_CODE_SIDECAR],
["command-code", COMMAND_CODE_DEEPSEEK],
];
for (const [provider, id] of cases) {
const hinted = applyProviderConfigHints(provider, seeded(provider), { id, provider });
Expand All @@ -118,7 +120,7 @@ describe("flash-route combo intersection (#4505)", () => {
{ provider: "opencode-go", model: OPENCODE_GO_NATIVE },
{ provider: "opencode-go", model: OPENCODE_GO_SIDECAR },
{ provider: "command-code", model: COMMAND_CODE_NATIVE },
{ provider: "command-code", model: COMMAND_CODE_SIDECAR },
{ provider: "command-code", model: COMMAND_CODE_DEEPSEEK },
],
defaultEffort: "high",
} as never;
Expand All @@ -135,7 +137,7 @@ describe("flash-route combo intersection (#4505)", () => {
hintedMember("opencode-go", OPENCODE_GO_NATIVE),
hintedMember("opencode-go", OPENCODE_GO_SIDECAR),
hintedMember("command-code", COMMAND_CODE_NATIVE),
hintedMember("command-code", COMMAND_CODE_SIDECAR),
hintedMember("command-code", COMMAND_CODE_DEEPSEEK),
];
const derived = deriveComboCatalogModel("flash_failover", combo, members);
expect(derived?.inputModalities).toEqual(["text", "image"]);
Expand All @@ -149,7 +151,7 @@ describe("flash-route combo intersection (#4505)", () => {
hintedMember("opencode-go", OPENCODE_GO_NATIVE),
hintedMember("opencode-go", OPENCODE_GO_SIDECAR),
hintedMember("command-code", COMMAND_CODE_NATIVE),
{ ...hintedMember("command-code", COMMAND_CODE_SIDECAR), inputModalities: ["text"] },
{ ...hintedMember("command-code", COMMAND_CODE_DEEPSEEK), inputModalities: ["text"] },
];
const derived = deriveComboCatalogModel("flash_failover", combo, members);
expect(derived?.inputModalities).toEqual(["text"]);
Expand Down
Loading