mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix(command-code): enable vision flags for CC models and fix vision-bridge reroute (#9007)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
This commit is contained in:
committed by
GitHub
parent
c996dc93c2
commit
68cb678780
@@ -17,6 +17,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "claude-opus-4-7",
|
id: "claude-opus-4-7",
|
||||||
name: "Claude Opus 4.7 (CC)",
|
name: "Claude Opus 4.7 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 200000,
|
contextLength: 200000,
|
||||||
maxOutputTokens: 32000,
|
maxOutputTokens: 32000,
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "claude-opus-4-6",
|
id: "claude-opus-4-6",
|
||||||
name: "Claude Opus 4.6 (CC)",
|
name: "Claude Opus 4.6 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 200000,
|
contextLength: 200000,
|
||||||
maxOutputTokens: 32000,
|
maxOutputTokens: 32000,
|
||||||
},
|
},
|
||||||
@@ -31,6 +33,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "claude-sonnet-4-6",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.6 (CC)",
|
name: "Claude Sonnet 4.6 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 200000,
|
contextLength: 200000,
|
||||||
maxOutputTokens: 16384,
|
maxOutputTokens: 16384,
|
||||||
},
|
},
|
||||||
@@ -38,6 +41,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "claude-haiku-4-5-20251001",
|
id: "claude-haiku-4-5-20251001",
|
||||||
name: "Claude Haiku 4.5 (CC)",
|
name: "Claude Haiku 4.5 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 200000,
|
contextLength: 200000,
|
||||||
maxOutputTokens: 8192,
|
maxOutputTokens: 8192,
|
||||||
},
|
},
|
||||||
@@ -45,6 +49,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "gpt-5.5",
|
id: "gpt-5.5",
|
||||||
name: "GPT-5.5 (CC)",
|
name: "GPT-5.5 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 256000,
|
contextLength: 256000,
|
||||||
maxOutputTokens: 128000,
|
maxOutputTokens: 128000,
|
||||||
},
|
},
|
||||||
@@ -52,6 +57,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "gpt-5.4",
|
id: "gpt-5.4",
|
||||||
name: "GPT-5.4 (CC)",
|
name: "GPT-5.4 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 256000,
|
contextLength: 256000,
|
||||||
maxOutputTokens: 128000,
|
maxOutputTokens: 128000,
|
||||||
},
|
},
|
||||||
@@ -59,6 +65,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "gpt-5.3-codex",
|
id: "gpt-5.3-codex",
|
||||||
name: "GPT-5.3 Codex (CC)",
|
name: "GPT-5.3 Codex (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 256000,
|
contextLength: 256000,
|
||||||
maxOutputTokens: 128000,
|
maxOutputTokens: 128000,
|
||||||
},
|
},
|
||||||
@@ -66,6 +73,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "gpt-5.4-mini",
|
id: "gpt-5.4-mini",
|
||||||
name: "GPT-5.4 Mini (CC)",
|
name: "GPT-5.4 Mini (CC)",
|
||||||
supportsReasoning: false,
|
supportsReasoning: false,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 256000,
|
contextLength: 256000,
|
||||||
maxOutputTokens: 128000,
|
maxOutputTokens: 128000,
|
||||||
},
|
},
|
||||||
@@ -87,6 +95,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "moonshotai/Kimi-K2.6",
|
id: "moonshotai/Kimi-K2.6",
|
||||||
name: "Kimi K2.6 (CC)",
|
name: "Kimi K2.6 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 262144,
|
contextLength: 262144,
|
||||||
maxOutputTokens: 65536,
|
maxOutputTokens: 65536,
|
||||||
},
|
},
|
||||||
@@ -94,6 +103,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "moonshotai/Kimi-K2.5",
|
id: "moonshotai/Kimi-K2.5",
|
||||||
name: "Kimi K2.5 (CC)",
|
name: "Kimi K2.5 (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 262144,
|
contextLength: 262144,
|
||||||
maxOutputTokens: 65536,
|
maxOutputTokens: 65536,
|
||||||
},
|
},
|
||||||
@@ -136,6 +146,7 @@ export const command_codeProvider: RegistryEntry = {
|
|||||||
id: "Qwen/Qwen3.6-Plus",
|
id: "Qwen/Qwen3.6-Plus",
|
||||||
name: "Qwen 3.6 Plus (CC)",
|
name: "Qwen 3.6 Plus (CC)",
|
||||||
supportsReasoning: true,
|
supportsReasoning: true,
|
||||||
|
supportsVision: true,
|
||||||
contextLength: 1000000,
|
contextLength: 1000000,
|
||||||
maxOutputTokens: 32768,
|
maxOutputTokens: 32768,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const VISION_MODEL_ID_FRAGMENTS = [
|
|||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
"gpt-4-turbo",
|
"gpt-4-turbo",
|
||||||
"gpt-4-vision",
|
"gpt-4-vision",
|
||||||
|
"gpt-5",
|
||||||
"gemini-1.5",
|
"gemini-1.5",
|
||||||
"gemini-2",
|
"gemini-2",
|
||||||
"gemini-3",
|
"gemini-3",
|
||||||
@@ -51,8 +52,10 @@ export const VISION_MODEL_ID_FRAGMENTS = [
|
|||||||
"claude-opus-4",
|
"claude-opus-4",
|
||||||
"claude-sonnet-4",
|
"claude-sonnet-4",
|
||||||
"claude-haiku-4",
|
"claude-haiku-4",
|
||||||
|
"claude-fable",
|
||||||
"mistral-medium-3",
|
"mistral-medium-3",
|
||||||
"minimax-m3",
|
"minimax-m3",
|
||||||
|
"kimi-k2.",
|
||||||
"-vision",
|
"-vision",
|
||||||
"multimodal",
|
"multimodal",
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
48
tests/unit/command-code-mimo-v2-5-safety.test.ts
Normal file
48
tests/unit/command-code-mimo-v2-5-safety.test.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Verify that mimo-v2.5 is safe to use with images across all providers
|
||||||
|
* (xiaomi-mimo, command-code, bazaarlink, opencode-go, bare model id).
|
||||||
|
*
|
||||||
|
* mimo-v2.5 is registered in ModelSpec (`modelSpecs.ts:410-415`) with
|
||||||
|
* `supportsVision: true`, so resolveVisionCapability() picks it up from
|
||||||
|
* `spec.supportsVision` without needing a registry flag.
|
||||||
|
*
|
||||||
|
* This test proves that the scenario described in the issue report
|
||||||
|
* (image-bearing request → vision-bridge auto-reroute to opencode-zen → 401)
|
||||||
|
* never applies to mimo-v2.5 — it has always been correctly identified as
|
||||||
|
* vision-capable even before the registry + heuristic fix for other CC models.
|
||||||
|
*/
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
|
||||||
|
import { isVisionModelId } from "../../src/shared/constants/visionModels.ts";
|
||||||
|
|
||||||
|
const MIMO_V25_CASES: [string, string, boolean][] = [
|
||||||
|
["xiaomi-mimo/mimo-v2.5", "via xiaomi-mimo", true],
|
||||||
|
["command-code/mimo-v2.5", "via command-code", true],
|
||||||
|
["bazaarlink/mimo-v2.5", "via bazaarlink", true],
|
||||||
|
["opencode-go/mimo-v2.5", "via opencode-go", true],
|
||||||
|
["mimo-v2.5", "bare model name", true],
|
||||||
|
// Text-only variants must stay false
|
||||||
|
["mimo-v2.5-pro", "text-only variant", false],
|
||||||
|
["command-code/mimo-v2.5-pro", "text-only via command-code", false],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [modelId, desc, expected] of MIMO_V25_CASES) {
|
||||||
|
test(`${desc} (${modelId}) → supportsVision=${expected}`, () => {
|
||||||
|
const caps = getResolvedModelCapabilities(modelId);
|
||||||
|
assert.equal(caps.supportsVision, expected, `${modelId} supportsVision must be ${expected}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("mimo-v2.5 heuristic is correct (no false positive from mimo-vl fragment)", () => {
|
||||||
|
// mimo-vl matches "mimo-vl-a3b", not "mimo-v2.5"
|
||||||
|
assert.equal(isVisionModelId("mimo-vl-a3b"), true, "mimo-vl must be detected as vision");
|
||||||
|
assert.equal(
|
||||||
|
isVisionModelId("mimo-v2.5"),
|
||||||
|
false,
|
||||||
|
"mimo-v2.5 must NOT match the mimo-vl heuristic"
|
||||||
|
);
|
||||||
|
// But getResolvedModelCapabilities still returns true via ModelSpec
|
||||||
|
assert.equal(getResolvedModelCapabilities("mimo-v2.5").supportsVision, true);
|
||||||
|
});
|
||||||
70
tests/unit/command-code-registry-vision.test.ts
Normal file
70
tests/unit/command-code-registry-vision.test.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Verify that command-code registry models with `supportsVision: true` resolve
|
||||||
|
* correctly via `getResolvedModelCapabilities`.
|
||||||
|
*
|
||||||
|
* Before the fix: the command-code registry had NO `supportsVision` flags.
|
||||||
|
* The guardrail used `getResolvedModelCapabilities` → `resolveVisionCapability`,
|
||||||
|
* which had no registry flag and no heuristic match, returning `null`/`false`.
|
||||||
|
* This caused the Vision Bridge to incorrectly reroute to opencode-zen (401).
|
||||||
|
*
|
||||||
|
* After the fix: the registry declares `supportsVision: true` for all CC
|
||||||
|
* vision-capable models, so the guardrail sees native vision support and
|
||||||
|
* passes through unmodified.
|
||||||
|
*/
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
|
||||||
|
|
||||||
|
// ── Models that SHOULD have supportsVision: true ────────────────────────────
|
||||||
|
|
||||||
|
const CC_VISION: [string, string][] = [
|
||||||
|
["Claude Opus 4.7 (CC)", "command-code/claude-opus-4-7"],
|
||||||
|
["Claude Opus 4.6 (CC)", "command-code/claude-opus-4-6"],
|
||||||
|
["Claude Sonnet 4.6 (CC)", "command-code/claude-sonnet-4-6"],
|
||||||
|
["Claude Haiku 4.5 (CC)", "command-code/claude-haiku-4-5-20251001"],
|
||||||
|
["GPT-5.5 (CC)", "command-code/gpt-5.5"],
|
||||||
|
["GPT-5.4 (CC)", "command-code/gpt-5.4"],
|
||||||
|
["GPT-5.3 Codex (CC)", "command-code/gpt-5.3-codex"],
|
||||||
|
["GPT-5.4 Mini (CC)", "command-code/gpt-5.4-mini"],
|
||||||
|
["Kimi K2.6 (CC)", "command-code/moonshotai/Kimi-K2.6"],
|
||||||
|
["Kimi K2.5 (CC)", "command-code/moonshotai/Kimi-K2.5"],
|
||||||
|
["Qwen 3.6 Plus (CC)", "command-code/Qwen/Qwen3.6-Plus"],
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Models that MUST NOT claim vision (text-only) ──────────────────────────
|
||||||
|
|
||||||
|
const CC_TEXT_ONLY: [string, string][] = [
|
||||||
|
["DeepSeek V4 Pro (CC)", "command-code/deepseek/deepseek-v4-pro"],
|
||||||
|
["DeepSeek V4 Flash (CC)", "command-code/deepseek/deepseek-v4-flash"],
|
||||||
|
["GLM-5.1 (CC)", "command-code/zai-org/GLM-5.1"],
|
||||||
|
["GLM-5 (CC)", "command-code/zai-org/GLM-5"],
|
||||||
|
["MiniMax M2.7 (CC)", "command-code/MiniMaxAI/MiniMax-M2.7"],
|
||||||
|
["MiniMax M2.5 (CC)", "command-code/MiniMaxAI/MiniMax-M2.5"],
|
||||||
|
["Qwen 3.6 Max Preview (CC)", "command-code/Qwen/Qwen3.6-Max-Preview"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [name, modelId] of CC_VISION) {
|
||||||
|
test(`${name} resolves supportsVision: true`, () => {
|
||||||
|
const caps = getResolvedModelCapabilities(modelId);
|
||||||
|
assert.equal(caps.supportsVision, true, `${modelId} must have supportsVision: true`);
|
||||||
|
assert.equal(caps.provider, "command-code");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, modelId] of CC_TEXT_ONLY) {
|
||||||
|
test(`${name} does not falsely claim vision`, () => {
|
||||||
|
const caps = getResolvedModelCapabilities(modelId);
|
||||||
|
assert.notEqual(
|
||||||
|
caps.supportsVision,
|
||||||
|
true,
|
||||||
|
`${modelId} is text-only — must not have supportsVision: true`
|
||||||
|
);
|
||||||
|
assert.equal(caps.provider, "command-code");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("MiniMax M3 via command-code keeps existing vision capability (no regression)", () => {
|
||||||
|
const caps = getResolvedModelCapabilities("command-code/MiniMaxAI/MiniMax-M3");
|
||||||
|
assert.equal(caps.supportsVision, true);
|
||||||
|
});
|
||||||
214
tests/unit/vision-bridge-cc-no-reroute.test.ts
Normal file
214
tests/unit/vision-bridge-cc-no-reroute.test.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* End-to-end guardrail tests: verify Vision Bridge does NOT reroute
|
||||||
|
* command-code vision-capable models. This is the core behavioral test
|
||||||
|
* for the bug fix.
|
||||||
|
*
|
||||||
|
* BUG FLOW (before fix):
|
||||||
|
* command-code/gpt-5.5 + image
|
||||||
|
* → getResolvedModelCapabilities("command-code/gpt-5.5")
|
||||||
|
* → supportsVision = null/false (no registry flag, no heuristic match)
|
||||||
|
* → Vision Bridge auto-reroutes to opencode-zen/gpt-5.5
|
||||||
|
* → opencode-zen's executor returns 401 "Missing API key"
|
||||||
|
*
|
||||||
|
* FIXED FLOW (after fix):
|
||||||
|
* command-code/gpt-5.5 + image
|
||||||
|
* → getResolvedModelCapabilities("command-code/gpt-5.5")
|
||||||
|
* → supportsVision = true (registry flag)
|
||||||
|
* → Vision Bridge passes through unmodified
|
||||||
|
* → request reaches command-code upstream with correct API key
|
||||||
|
*/
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
const { VisionBridgeGuardrail } = await import("../../src/lib/guardrails/visionBridge.ts");
|
||||||
|
const { resetGuardrailsForTests } = await import("../../src/lib/guardrails/registry.ts");
|
||||||
|
const { getResolvedModelCapabilities } = await import("../../src/lib/modelCapabilities.ts");
|
||||||
|
import type { GuardrailContext } from "../../src/lib/guardrails/base.ts";
|
||||||
|
import type { VisionModelConfig } from "../../src/lib/guardrails/visionBridgeHelpers.ts";
|
||||||
|
|
||||||
|
// ── Mock state ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let mockSettings: Record<string, unknown> = {
|
||||||
|
visionBridgeEnabled: true,
|
||||||
|
visionBridgeModel: "openai/gpt-4o-mini",
|
||||||
|
visionBridgePrompt: "Describe this image concisely.",
|
||||||
|
visionBridgeTimeout: 30000,
|
||||||
|
visionBridgeMaxImages: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
let visionCallCount = 0;
|
||||||
|
|
||||||
|
function createGuardrail() {
|
||||||
|
return new VisionBridgeGuardrail({
|
||||||
|
deps: {
|
||||||
|
getSettings: async () => mockSettings,
|
||||||
|
callVisionModel: async (_imageDataUri: string, _config: VisionModelConfig) => {
|
||||||
|
visionCallCount++;
|
||||||
|
return "An image description";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
resetGuardrailsForTests({ registerDefaults: false });
|
||||||
|
visionCallCount = 0;
|
||||||
|
mockSettings = {
|
||||||
|
visionBridgeEnabled: true,
|
||||||
|
visionBridgeModel: "openai/gpt-4o-mini",
|
||||||
|
visionBridgePrompt: "Describe this image concisely.",
|
||||||
|
visionBridgeTimeout: 30000,
|
||||||
|
visionBridgeMaxImages: 10,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function createContext(overrides: Partial<GuardrailContext> = {}): GuardrailContext {
|
||||||
|
return { model: "command-code/gpt-5.4", log: console, ...overrides };
|
||||||
|
}
|
||||||
|
|
||||||
|
function imagePayload(model: string) {
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "What is in this image?" },
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: "https://example.com/image.png" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sanity: capabilities must resolve first ─────────────────────────────────
|
||||||
|
|
||||||
|
test("CC-VB-SANITY: getResolvedModelCapabilities reports vision for CC models", () => {
|
||||||
|
for (const modelId of [
|
||||||
|
"command-code/gpt-5.5",
|
||||||
|
"command-code/gpt-5.4",
|
||||||
|
"command-code/gpt-5.3-codex",
|
||||||
|
"command-code/gpt-5.4-mini",
|
||||||
|
"command-code/claude-opus-4-7",
|
||||||
|
"command-code/claude-sonnet-4-6",
|
||||||
|
"command-code/claude-haiku-4-5-20251001",
|
||||||
|
"command-code/moonshotai/Kimi-K2.6",
|
||||||
|
"command-code/moonshotai/Kimi-K2.5",
|
||||||
|
"command-code/Qwen/Qwen3.6-Plus",
|
||||||
|
]) {
|
||||||
|
const caps = getResolvedModelCapabilities(modelId);
|
||||||
|
assert.equal(caps.supportsVision, true, `${modelId} must have supportsVision: true`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── THE FIX: CC vision models pass through, no reroute ──────────────────────
|
||||||
|
|
||||||
|
test("CC-VB-01: gpt-5.5 via command-code does NOT reroute", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
const payload = imagePayload("command-code/gpt-5.5");
|
||||||
|
const result = await guardrail.preCall(payload, createContext({ model: "command-code/gpt-5.5" }));
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false);
|
||||||
|
assert.strictEqual(visionCallCount, 0, "must NOT call vision API");
|
||||||
|
assert.strictEqual(result.modifiedPayload, undefined, "must not reroute");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("CC-VB-02: gpt-5.4 via command-code does NOT reroute", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
const payload = imagePayload("command-code/gpt-5.4");
|
||||||
|
const result = await guardrail.preCall(payload, createContext({ model: "command-code/gpt-5.4" }));
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false);
|
||||||
|
assert.strictEqual(visionCallCount, 0);
|
||||||
|
assert.strictEqual(result.modifiedPayload, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("CC-VB-03: Kimi K2.6 via command-code does NOT reroute", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
const payload = imagePayload("command-code/moonshotai/Kimi-K2.6");
|
||||||
|
const result = await guardrail.preCall(
|
||||||
|
payload,
|
||||||
|
createContext({ model: "command-code/moonshotai/Kimi-K2.6" })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false);
|
||||||
|
assert.strictEqual(visionCallCount, 0);
|
||||||
|
assert.strictEqual(result.modifiedPayload, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("CC-VB-04: Qwen 3.6 Plus via command-code does NOT reroute", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
const payload = imagePayload("command-code/Qwen/Qwen3.6-Plus");
|
||||||
|
const result = await guardrail.preCall(
|
||||||
|
payload,
|
||||||
|
createContext({ model: "command-code/Qwen/Qwen3.6-Plus" })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false);
|
||||||
|
assert.strictEqual(visionCallCount, 0);
|
||||||
|
assert.strictEqual(result.modifiedPayload, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("CC-VB-05: Claude Opus 4.7 via command-code does NOT reroute", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
const payload = imagePayload("command-code/claude-opus-4-7");
|
||||||
|
const result = await guardrail.preCall(
|
||||||
|
payload,
|
||||||
|
createContext({ model: "command-code/claude-opus-4-7" })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false);
|
||||||
|
assert.strictEqual(visionCallCount, 0);
|
||||||
|
assert.strictEqual(result.modifiedPayload, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("CC-VB-06: all CC vision models pass through unmodified (loop)", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
"command-code/gpt-5.5",
|
||||||
|
"command-code/gpt-5.4",
|
||||||
|
"command-code/gpt-5.3-codex",
|
||||||
|
"command-code/gpt-5.4-mini",
|
||||||
|
"command-code/claude-opus-4-7",
|
||||||
|
"command-code/claude-opus-4-6",
|
||||||
|
"command-code/claude-sonnet-4-6",
|
||||||
|
"command-code/claude-haiku-4-5-20251001",
|
||||||
|
"command-code/moonshotai/Kimi-K2.6",
|
||||||
|
"command-code/moonshotai/Kimi-K2.5",
|
||||||
|
"command-code/Qwen/Qwen3.6-Plus",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
visionCallCount = 0;
|
||||||
|
const payload = imagePayload(model);
|
||||||
|
const result = await guardrail.preCall(payload, createContext({ model }));
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false, `${model}: must not block`);
|
||||||
|
assert.strictEqual(visionCallCount, 0, `${model}: must not call vision API`);
|
||||||
|
assert.strictEqual(result.modifiedPayload, undefined, `${model}: must not reroute`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Regression: text-only CC models still handled correctly ─────────────────
|
||||||
|
|
||||||
|
test("CC-VB-REGRESSION: text-only deepseek-v4-pro via command-code still triggers guardrail", async () => {
|
||||||
|
const guardrail = createGuardrail();
|
||||||
|
const payload = imagePayload("command-code/deepseek/deepseek-v4-pro");
|
||||||
|
const result = await guardrail.preCall(
|
||||||
|
payload,
|
||||||
|
createContext({ model: "command-code/deepseek/deepseek-v4-pro" })
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(result.block, false);
|
||||||
|
const caps = getResolvedModelCapabilities("command-code/deepseek/deepseek-v4-pro");
|
||||||
|
if (caps.supportsVision !== true) {
|
||||||
|
assert.ok(
|
||||||
|
result.modifiedPayload !== undefined || visionCallCount > 0,
|
||||||
|
"text-only model with images must trigger reroute or describe"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
82
tests/unit/vision-models-cc-fragments.test.ts
Normal file
82
tests/unit/vision-models-cc-fragments.test.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Verify that the new VISION_MODEL_ID_FRAGMENTS additions correctly identify
|
||||||
|
* Command Code vision-capable models via the last-resort heuristic.
|
||||||
|
*
|
||||||
|
* Before the fix: `gpt-5`, `kimi-k2.`, and `claude-fable` were absent from
|
||||||
|
* VISION_MODEL_ID_FRAGMENTS. Without registry `supportsVision` flags either,
|
||||||
|
* the last-resort heuristic returned `false` for these models, causing the
|
||||||
|
* Vision Bridge guardrail to reroute image-bearing requests away from
|
||||||
|
* command-code's own vision-capable upstream to opencode-zen — which then
|
||||||
|
* failed with 401 "Missing API key."
|
||||||
|
*/
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { isVisionModelId } from "../../src/shared/constants/visionModels.ts";
|
||||||
|
|
||||||
|
// ── New fragments — MUST now be recognized as vision ────────────────────────
|
||||||
|
|
||||||
|
const NEWLY_VISION = [
|
||||||
|
// gpt-5 fragment covers all GPT-5.x variants
|
||||||
|
"gpt-5.5",
|
||||||
|
"gpt-5.4",
|
||||||
|
"gpt-5.3-codex",
|
||||||
|
"gpt-5.4-mini",
|
||||||
|
"gpt-5.6",
|
||||||
|
"gpt-5.6-luna",
|
||||||
|
// kimi-k2. fragment (with dot) — covers K2.5/K2.6/K2.7 but NOT bare "kimi-k2"
|
||||||
|
"moonshotai/Kimi-K2.6",
|
||||||
|
"kimi-k2.5",
|
||||||
|
"kimi-k2.7-code",
|
||||||
|
"moonshotai/Kimi-K2.7-Code",
|
||||||
|
// claude-fable fragment
|
||||||
|
"claude-fable-5",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Models that must remain non-vision ──────────────────────────────────────
|
||||||
|
|
||||||
|
const STILL_NOT_VISION = [
|
||||||
|
"kimi-k2", // bare kimi-k2 is text-only — must NOT match "kimi-k2."
|
||||||
|
"deepseek/deepseek-v4-pro",
|
||||||
|
"deepseek/deepseek-v4-flash",
|
||||||
|
"zai-org/GLM-5.1",
|
||||||
|
"zai-org/GLM-5",
|
||||||
|
"MiniMaxAI/MiniMax-M2.7",
|
||||||
|
"MiniMaxAI/MiniMax-M2.5",
|
||||||
|
"mimo-v2.5-pro",
|
||||||
|
"mimo-v2-pro",
|
||||||
|
"gemma-2-9b",
|
||||||
|
"ministral-14b-latest",
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("VISION_MODEL_ID_FRAGMENTS — Command Code coverage", () => {
|
||||||
|
for (const id of NEWLY_VISION) {
|
||||||
|
it(`recognizes ${id} as vision via new fragments`, () => {
|
||||||
|
assert.equal(isVisionModelId(id), true, `${id} must be recognized as vision-capable`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of STILL_NOT_VISION) {
|
||||||
|
it(`keeps ${id} as non-vision (no false positive)`, () => {
|
||||||
|
assert.equal(isVisionModelId(id), false, `${id} must remain non-vision`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("existing fragments remain functional (no regression)", () => {
|
||||||
|
const existing = [
|
||||||
|
"minimax-m3",
|
||||||
|
"MiniMaxAI/MiniMax-M3",
|
||||||
|
"gpt-4o",
|
||||||
|
"claude-opus-4-7",
|
||||||
|
"claude-sonnet-4-6",
|
||||||
|
"claude-haiku-4-5-20251001",
|
||||||
|
"gemini-3-pro",
|
||||||
|
"qwen3-vl-plus",
|
||||||
|
"pixtral-12b",
|
||||||
|
"mistral-medium-3",
|
||||||
|
];
|
||||||
|
for (const id of existing) {
|
||||||
|
assert.equal(isVisionModelId(id), true, `${id} must stay vision`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user