feat: PLANNING体系首版(00路线图/02任务总表/03执行协议/I-03/I-04/registry42口径)+M1止血交付

This commit is contained in:
2026-09-12 14:25:25 +08:00
commit 39af400fc8
472 changed files with 36277 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# mimo2codex 模型能力实测与配置报告
日期:2026-09-06 | 网关:OpenCode Zen (Console Go) `https://opencode.ai/zen/go/v1` | 代理:mimo2codex v0.5.29 @ `http://127.0.0.1:8788`
## 一、做了什么
对 mimo2codex 中**所有分组**(deepseek / mimo / generic)可达的全部 **35 个上游模型**逐一实测:
- **思考(reasoning)检测**:分别用 `thinking:{type:"enabled"}`、`reasoning_effort:"high"`、两者组合探测,检查响应中的 `reasoning_content` / `reasoning` 字段或 `<think>` 标签。
- **多模态(vision)检测**:发送 64×64 红色 PNG(data URL),验证模型能否真正说出"红色"。仅返回 200 但回答"没看到图片"或出现 `[Unsupported Image]` 占位符的,判为**不支持**(排除了第一轮的误报:deepseek-v4-pro / hy4-preview / longcat-2.0)。
## 二、实测结果(25 个可用模型)
| 模型 | 思考 | 多模态 | 备注 |
|---|---|---|---|
| mimo-v2.5 | ✅ | ✅ | 代理默认模型 |
| deepseek-v4-pro | ✅ | ❌ | 图片被上游替换为占位符 |
| deepseek-v4-flash | ✅ | ❌ | 400 text-only |
| deepseek-v4-flash-vision-exp | ✅ | ✅ | 实验版视觉模型 |
| glm-5.3 | ✅ | ❌ | 已同步修正 Codex catalog 为 text-only |
| glm-5.3-flash | ✅ | ✅ | |
| glm-5.2 / 5.1 / 5 | ✅ | ❌ | |
| kimi-k3 / k2.7-code / k2.6 | ✅ | ✅ | reasoning 字段风格 |
| qwen3.8-max / 3.8-flash / 3.7-plus / 3.6-plus / 3.5-plus | ✅ | ✅ | 1×1 图会因尺寸限制被拒,需 ≥64px |
| qwen3.7-max | ✅ | ❌ | 拒绝 image_url 内容格式 |
| minimax-m3 | ✅ | ✅ | `<think>` 标签风格;不认 thinking.type=enabled |
| minimax-m2.5 | ✅ | ❌ | |
| mimo-v2.5-pro | ✅ | ❌ | 404 no image endpoints |
| hy3 / hy4-preview | ✅ | ❌ | hy4 图片不透传 |
| longcat-2.0 | ✅ | ❌ | 声称看不到图片 |
| omen-alpha | ✅ | ✅ | 仅 thinking 参数可触发 |
**当前不可用(无法实测,保留路由,出错时上游给出明确报错)**:
gpt-5.6-luna(上游 500)、grok-4.5(503)、grok-4.6(不支持 oa-compat)、kimi-k2.5(无可用供应商)、minimax-m2.7(500)、mimo-v2-pro / mimo-v2-omni(套餐不支持)、hy3-preview(不可用)、muse-spark-1.2/1.3-contributor(地区限制 403)。
## 三、配置变更
1. **新建 `C:\Users\12914\.mimo2codex\providers.json`**:generic 分组(别名 zen)指向 Zen 网关,登记全部 35 个模型并写入实测的 `supportsReasoning` / `supportsImages` / `contextWindow` 标志。代理启动时自动种子进 `data.db` 的 models 表(已验证)。
- **修复了核心路由 bug**:此前 `glm-5.3`、`deepseek-v4-pro` 等不在任何分组目录中的模型会被静默改写成 `mimo-v2.5`(chat_logs 有 `gpt-5.6-luna -> mimo-v2.5` 记录为证)。现在全部**按原样透传**(已验证 `glm-5.3 -> glm-5.3`、`deepseek-v4-pro -> deepseek-v4-pro`,均 200)。
2. **`.env` 追加 `GENERIC_API_KEY`**(与 MIMO_API_KEY 相同的 Zen key)。
3. **启用多模态兜底**(settings 表 `codex.visionFallbackEnabled=1`,`codex.visionFallbackModel=mimo-v2.5`)。注意:该兜底按代理实现只对内置 mimo 分组生效;generic 分组下非视觉模型的图片请求走"图片落盘 + OCR 提示"的降级路径(已实测)。
4. **修正 Codex catalog** `C:\Users\12914\.codex\cc-switch-model-catalog.json`:`glm-5.3` 的 `input_modalities` 由 `[text,image]` 改为 `[text]`(与实测一致)。
5. 内置 deepseek 分组无官方 API key(只有 Zen key),其 4 个模型经 generic 分组照常可用;若日后配置 `DS_API_KEY`,需从 providers.json 移除 deepseek 条目才会切回官方直连。
## 四、端到端验证(全部通过)
| 测试 | 结果 |
|---|---|
| `/v1/chat/completions` glm-5.3 | 200,upstream_model=glm-5.3(无改写) |
| `/v1/chat/completions` deepseek-v4-pro | 200,upstream_model=deepseek-v4-pro |
| `/v1/responses` mimo-v2.5 + 图片 | 200,回答"红色" |
| `/v1/responses` mimo-v2.5 文本 | 200,回答"12" |
| `/v1/responses` kimi-k3 / glm-5.3 + 图片 | 200,图片落盘 `~/.mimo2codex/cache/images/` 并附 OCR 指引 |
| `/v1/models` | 列出全部 35 个模型 |
代理以独立进程常驻(pid 见端口 8788 监听),不依赖本会话。管理面板:http://127.0.0.1:8788/admin/
## 五、产物文件
- `providers.json` → `C:\Users\12914\.mimo2codex\providers.json`
- 实测原始数据 → `.mimo2codex-audit/probe-results.json`、`vision-retest.json`
- 测试脚本 → `.mimo2codex-audit/probe-zen.mjs`、`vision-retest.mjs`、`responses-test.mjs`、`verify-db.mjs`
+30
View File
@@ -0,0 +1,30 @@
import { createRequire } from "node:module";
const require = createRequire("C:/Users/12914/AppData/Roaming/npm/node_modules/mimo2codex/package.json");
const Database = require("better-sqlite3");
const db = new Database("C:/Users/12914/.mimo2codex/data.db", { readonly: true, fileMustExist: true });
const providers = db.prepare("SELECT * FROM providers ORDER BY id").all();
console.log("=== PROVIDERS (分组) ===");
for (const p of providers) {
console.log(JSON.stringify(p));
}
console.log("\n=== MODELS ===");
const models = db.prepare("SELECT * FROM models ORDER BY provider_id, sort_order, upstream_id").all();
for (const m of models) {
console.log(JSON.stringify({
id: m.id, provider: m.provider_id, upstream_id: m.upstream_id,
display_name: m.display_name,
images: m.supports_images, reasoning: m.supports_reasoning, web: m.supports_web_search,
ctx: m.context_window, builtin: m.is_builtin
}));
}
console.log("\n=== ALIASES ===");
for (const a of db.prepare("SELECT * FROM model_aliases").all()) console.log(JSON.stringify(a));
console.log("\n=== SETTINGS (keys) ===");
for (const s of db.prepare("SELECT key FROM settings").all()) console.log(s.key);
db.close();
+74
View File
@@ -0,0 +1,74 @@
{
"glm-5.3-flash": {
"baseline": {
"status": 200,
"content": "5",
"err": null
},
"enable_thinking": {
"status": 200,
"reasoning": "reasoning_content",
"err": null
},
"thinking_type": {
"status": 200,
"reasoning": "reasoning_content",
"err": null
},
"reasoning_effort_high": {
"status": 200,
"reasoning": "reasoning_content",
"err": null
},
"vision": {
"status": 200,
"answer": "红色",
"err": null
},
"developer_role": {
"status": 400,
"content": null,
"err": "角色信息不正确"
},
"max_tokens_131072": {
"status": 200,
"err": null
}
},
"qwen3.8-flash": {
"baseline": {
"status": 200,
"content": "\n\n5",
"err": null
},
"enable_thinking": {
"status": 200,
"reasoning": "reasoning",
"err": null
},
"thinking_type": {
"status": 400,
"reasoning": null,
"err": "\"thinking\" is not supported on /v1/chat/completions and was not applied. Use \"reasoning_effort\" (or \"***.effort\") to control thinking."
},
"reasoning_effort_high": {
"status": 400,
"reasoning": null,
"err": "Unexpected reasoning effort high. Supported types are xhigh (default), medium, and low."
},
"vision": {
"status": 200,
"answer": "\n\n红色",
"err": null
},
"developer_role": {
"status": 400,
"content": null,
"err": "Unexpected message role."
},
"max_tokens_131072": {
"status": 200,
"err": null
}
}
}
+82
View File
@@ -0,0 +1,82 @@
// Probe lt-fl (platform.lantian.pro) models: thinking / vision / developer-role / max_tokens cap.
import { readFileSync, writeFileSync } from "node:fs";
const KEY = "sk-yXtdmUJNnJQKHPKhZGsDrTgbQtg46choVzfqueBnIyuXMybB";
const BASE = "https://platform.lantian.pro/v1";
const B64 = readFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/red64.png").toString("base64");
const MODELS = ["glm-5.3-flash", "qwen3.8-flash"];
async function call(model, extra = {}, messages = null, maxTokens = 900) {
const body = {
model,
messages: messages ?? [{ role: "user", content: "只回答一个数字:2+3=?" }],
max_tokens: maxTokens,
stream: false,
...extra,
};
const started = Date.now();
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const ms = Date.now() - started;
const raw = await res.text();
let json = null;
try { json = JSON.parse(raw); } catch {}
return { status: res.status, ms, json, raw: raw.slice(0, 260) };
}
function reasoningKind(json) {
const m = json?.choices?.[0]?.message;
if (!m) return null;
if (typeof m.reasoning_content === "string" && m.reasoning_content.trim()) return "reasoning_content";
if (typeof m.reasoning === "string" && m.reasoning.trim()) return "reasoning";
if (typeof m.content === "string" && /<think>/i.test(m.content)) return "think_tags";
return null;
}
const err = (r) => r.json?.error?.message?.slice(0, 180) ?? (r.status !== 200 ? r.raw : null);
const results = {};
for (const model of MODELS) {
const out = {};
// baseline
const p0 = await call(model);
out.baseline = { status: p0.status, content: p0.json?.choices?.[0]?.message?.content?.slice(0, 60) ?? null, err: err(p0) };
// thinking variants
for (const [name, extra] of [
["enable_thinking", { enable_thinking: true }],
["thinking_type", { thinking: { type: "enabled" } }],
["reasoning_effort_high", { reasoning_effort: "high" }],
]) {
const r = await call(model, extra);
out[name] = { status: r.status, reasoning: r.json ? reasoningKind(r.json) : null, err: err(r) };
}
// vision
const v = await call(model, {}, [{
role: "user",
content: [
{ type: "text", text: "这张图片是什么颜色?只回答颜色词。" },
{ type: "image_url", image_url: { url: `data:image/png;base64,${B64}` } },
],
}], 400);
out.vision = {
status: v.status,
answer: v.json?.choices?.[0]?.message?.content?.slice(0, 60) ?? null,
err: err(v),
};
// developer role
const d = await call(model, {}, [
{ role: "developer", content: "You are a helpful assistant." },
{ role: "user", content: "只回答一个数字:1+1=?" },
], 200);
out.developer_role = { status: d.status, content: d.json?.choices?.[0]?.message?.content?.slice(0, 40) ?? null, err: err(d) };
// max_tokens ceiling probe
const big = await call(model, {}, [{ role: "user", content: "只回答一个数字:9+9=?" }], 131072);
out.max_tokens_131072 = { status: big.status, err: err(big) };
results[model] = out;
writeFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/lt-fl-probe.json", JSON.stringify(results, null, 2));
console.log(`--- ${model} ---`);
console.log(JSON.stringify(out, null, 2));
}
console.log("DONE");
File diff suppressed because it is too large Load Diff
+143
View File
@@ -0,0 +1,143 @@
// Probe every OpenCode Zen model for: (a) thinking/reasoning support, (b) vision (multimodal) support.
// Run with Node 20: "C:\Program Files\nodejs\node.exe" probe-zen.mjs
import { readFileSync, writeFileSync } from "node:fs";
const envText = readFileSync("C:/Users/12914/.mimo2codex/.env", "utf8");
const KEY = envText.match(/^MIMO_API_KEY=(.+)$/m)?.[1]?.trim();
const BASE = "https://opencode.ai/zen/go/v1";
if (!KEY) { console.error("no key"); process.exit(1); }
const TINY_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
async function call(body, timeoutMs = 90000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
const started = Date.now();
try {
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: ctrl.signal,
});
const ms = Date.now() - started;
let json = null, text = null;
const raw = await res.text();
try { json = JSON.parse(raw); } catch { text = raw.slice(0, 300); }
return { status: res.status, ms, json, text };
} catch (e) {
return { status: 0, ms: Date.now() - started, error: String(e?.message || e) };
} finally { clearTimeout(t); }
}
function extractReasoning(json) {
try {
const msg = json?.choices?.[0]?.message;
if (!msg) return null;
if (typeof msg.reasoning_content === "string" && msg.reasoning_content.trim()) return "reasoning_content";
if (typeof msg.reasoning === "string" && msg.reasoning.trim()) return "reasoning";
if (Array.isArray(msg.reasoning_content) && msg.reasoning_content.length) return "reasoning_content(array)";
if (typeof msg.content === "string" && /<think>/i.test(msg.content)) return "think_tags";
return null;
} catch { return null; }
}
async function probeBaseline(model) {
const r = await call({
model,
messages: [{ role: "user", content: "只回答一个数字:1+1=?" }],
max_tokens: 512,
stream: false,
});
return r;
}
// Try several common "enable thinking" conventions; any success that yields
// reasoning output means the model supports thinking.
async function probeThinking(model) {
const variants = [
{ name: "thinking+effort", body: { thinking: { type: "enabled" }, reasoning_effort: "high" } },
{ name: "effort-only", body: { reasoning_effort: "high" } },
{ name: "thinking-only", body: { thinking: { type: "enabled" } } },
];
const detail = [];
for (const v of variants) {
const r = await call({
model,
messages: [{ role: "user", content: "只回答一个数字:2+2=?" }],
max_tokens: 900,
stream: false,
...v.body,
});
const reason = r.json ? extractReasoning(r.json) : null;
detail.push({
variant: v.name,
status: r.status,
ms: r.ms,
reasoning: reason,
err: r.json?.error?.message?.slice(0, 200) ?? r.error ?? r.text ?? null,
contentPreview: r.json?.choices?.[0]?.message?.content?.slice(0, 80) ?? null,
});
if (reason) return { supported: true, via: v.name, reason, detail };
// If this variant 400'd on unknown field, try the next variant.
}
return { supported: false, via: null, reason: null, detail };
}
async function probeVision(model) {
const r = await call({
model,
messages: [{
role: "user",
content: [
{ type: "text", text: "这是什么颜色的图片?只回答颜色词。" },
{ type: "image_url", image_url: { url: `data:image/png;base64,${TINY_PNG}` } },
],
}],
max_tokens: 300,
stream: false,
});
const ok = r.status === 200 && r.json?.choices?.[0]?.message;
return {
supported: !!ok,
status: r.status,
ms: r.ms,
answer: ok ? String(r.json.choices[0].message.content).slice(0, 80) : null,
err: r.json?.error?.message?.slice(0, 200) ?? r.error ?? r.text ?? null,
};
}
async function main() {
// 1. list models
const lr = await fetch(`${BASE}/models`, { headers: { Authorization: `Bearer ${KEY}` } });
const lj = await lr.json();
const models = (lj?.data ?? []).map((m) => m.id).sort();
console.log(`models: ${models.length}`);
const results = {};
const CONC = 4;
const queue = [...models];
async function worker(id) {
while (queue.length) {
const m = queue.shift();
if (!m) break;
process.stdout.write(`[${m}] probing...\n`);
const base = await probeBaseline(m);
const think = await probeThinking(m);
const vision = await probeVision(m);
results[m] = {
baseline: { status: base.status, ms: base.ms, content: base.json?.choices?.[0]?.message?.content?.slice(0, 60) ?? null, err: base.json?.error?.message?.slice(0, 150) ?? base.error ?? null },
thinking: think,
vision,
};
process.stdout.write(`[${m}] think=${think.supported ? think.reason : false} vision=${vision.supported}\n`);
writeFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/probe-results.json", JSON.stringify(results, null, 2));
}
}
await Promise.all(Array.from({ length: CONC }, (_, i) => worker(i)));
writeFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/probe-results.json", JSON.stringify(results, null, 2));
console.log("DONE");
}
main();
Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

+43
View File
@@ -0,0 +1,43 @@
// Responses API end-to-end test through the proxy (UTF-8 safe).
import { readFileSync } from "node:fs";
const B64 = readFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/red64.png").toString("base64");
async function responsesTest(model, content) {
const item = typeof content === "string"
? { type: "message", role: "user", content }
: { type: "message", role: "user", content };
const res = await fetch("http://127.0.0.1:8788/v1/responses", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, stream: false, input: [item] }),
});
const j = await res.json().catch(() => null);
const msg = j?.output?.find((o) => o.type === "message");
const text = msg?.content?.find((c) => c.type === "output_text")?.text;
return { status: res.status, text: text?.slice(0, 160), err: j?.error?.message?.slice(0, 200) };
}
// 1. multimodal to mimo-v2.5 (vision-capable, Responses wire)
const r1 = await responsesTest("mimo-v2.5", [
{ type: "input_text", text: "这张图片是什么颜色?只回答颜色词。" },
{ type: "input_image", image_url: `data:image/png;base64,${B64}` },
]);
console.log("mimo-v2.5 + image :", JSON.stringify(r1));
// 2. plain text to mimo-v2.5
const r2 = await responsesTest("mimo-v2.5", "只回答一个数字:6+6=?");
console.log("mimo-v2.5 text :", JSON.stringify(r2));
// 3. image to kimi-k3 (vision-capable upstream but translator strips for non-mimo) — expect OCR note
const r3 = await responsesTest("kimi-k3", [
{ type: "input_text", text: "这张图片是什么颜色?" },
{ type: "input_image", image_url: `data:image/png;base64,${B64}` },
]);
console.log("kimi-k3 + image :", JSON.stringify(r3));
// 4. image to glm-5.3 (non-vision) — expect OCR strip note
const r4 = await responsesTest("glm-5.3", [
{ type: "input_text", text: "这张图片是什么颜色?" },
{ type: "input_image", image_url: `data:image/png;base64,${B64}` },
]);
console.log("glm-5.3 + image :", JSON.stringify(r4));
+15
View File
@@ -0,0 +1,15 @@
// Enable multimodal vision fallback: image requests to non-vision models are
// rerouted to mimo-v2.5 (verified vision-capable). Same settings table the admin UI writes.
import { createRequire } from "node:module";
const require = createRequire("C:/Users/12914/AppData/Roaming/npm/node_modules/mimo2codex/package.json");
const Database = require("better-sqlite3");
const db = new Database("C:/Users/12914/.mimo2codex/data.db");
const up = db.prepare("INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at");
const now = Date.now();
up.run("codex.visionFallbackEnabled", "1", now);
up.run("codex.visionFallbackModel", "mimo-v2.5", now);
for (const row of db.prepare("SELECT key, value FROM settings WHERE key LIKE 'codex.%'").all()) {
console.log(row.key, "=", row.value);
}
db.close();
+15
View File
@@ -0,0 +1,15 @@
import { createRequire } from "node:module";
const require = createRequire("C:/Users/12914/AppData/Roaming/npm/node_modules/mimo2codex/package.json");
const Database = require("better-sqlite3");
const db = new Database("C:/Users/12914/.mimo2codex/data.db", { readonly: true });
console.log("=== last 6 chat_logs ===");
for (const l of db.prepare("SELECT ts, provider_id, client_model, upstream_model, status_code, error_code FROM chat_logs ORDER BY id DESC LIMIT 6").all()) {
console.log(new Date(l.ts).toISOString().slice(11, 19), l.provider_id, `${l.client_model} -> ${l.upstream_model}`, l.status_code, l.error_code ?? "");
}
console.log("\n=== models table by provider ===");
for (const m of db.prepare("SELECT provider_id, upstream_id, supports_images AS img, supports_reasoning AS r, context_window AS ctx FROM models ORDER BY provider_id, sort_order").all()) {
console.log(`${m.provider_id.padEnd(8)} ${m.upstream_id.padEnd(30)} img=${m.img} reasoning=${m.r} ctx=${m.ctx ?? "-"}`);
}
db.close();
+27
View File
@@ -0,0 +1,27 @@
import { readFileSync } from "node:fs";
const envText = readFileSync("C:/Users/12914/.mimo2codex/.env", "utf8");
const KEY = envText.match(/^MIMO_API_KEY=(.+)$/m)?.[1]?.trim();
const BASE = "https://opencode.ai/zen/go/v1";
const B64 = readFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/red64.png").toString("base64");
for (const m of ["deepseek-v4-pro", "longcat-2.0", "hy4-preview"]) {
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
model: m,
messages: [{
role: "user",
content: [
{ type: "text", text: "这张图片是什么颜色?只回答颜色词。" },
{ type: "image_url", image_url: { url: `data:image/png;base64,${B64}` } },
],
}],
max_tokens: 500,
stream: false,
}),
});
const j = await res.json().catch(() => null);
console.log(`=== ${m} (${res.status}) ===`);
console.log(JSON.stringify(j?.choices?.[0]?.message ?? j, null, 2).slice(0, 900));
}
+80
View File
@@ -0,0 +1,80 @@
{
"gpt-5.6-luna": {
"status": 500,
"ok": false,
"answer": null,
"err": "Internal server error"
},
"deepseek-v4-pro": {
"status": 200,
"ok": true,
"answer": null,
"err": null
},
"deepseek-v4-flash": {
"status": 400,
"ok": false,
"answer": null,
"err": "Error from provider (Console Go): Upstream request failed: [400] Model only supports text input; received unsupported content type 'image_url'."
},
"longcat-2.0": {
"status": 200,
"ok": true,
"answer": null,
"err": null
},
"hy4-preview": {
"status": 200,
"ok": true,
"answer": null,
"err": null
},
"qwen3.5-plus": {
"status": 200,
"ok": true,
"answer": "红色",
"err": null
},
"qwen3.6-plus": {
"status": 200,
"ok": true,
"answer": "红色",
"err": null
},
"qwen3.7-max": {
"status": 400,
"ok": false,
"answer": null,
"err": "Error from provider (Console Go): Upstream request failed: [invalid_parameter_error] <400> InternalError.Algo.InvalidParameter: The provided messages input is invalid. The error info is [Unexpected item type in content.]"
},
"qwen3.7-plus": {
"status": 200,
"ok": true,
"answer": "红色",
"err": null
},
"qwen3.8-flash": {
"status": 200,
"ok": true,
"answer": "红色",
"err": null
},
"qwen3.8-max": {
"status": 200,
"ok": true,
"answer": "红色",
"err": null
},
"glm-5.3": {
"status": 400,
"ok": false,
"answer": null,
"err": "Error from provider (Console Go): Upstream request failed: [invalid_request_error] This model does not support image inputs"
},
"minimax-m2.5": {
"status": 400,
"ok": false,
"answer": null,
"err": "Error from provider (Console Go): Upstream request failed: [404] No endpoints found that support image input"
}
}
+70
View File
@@ -0,0 +1,70 @@
// Retest vision for ambiguous models with a 64x64 red PNG.
import { readFileSync, writeFileSync } from "node:fs";
const envText = readFileSync("C:/Users/12914/.mimo2codex/.env", "utf8");
const KEY = envText.match(/^MIMO_API_KEY=(.+)$/m)?.[1]?.trim();
const BASE = "https://opencode.ai/zen/go/v1";
const B64 = readFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/red64.png").toString("base64");
const MODELS = [
"gpt-5.6-luna", // retry in case 500 was transient
"deepseek-v4-pro",
"deepseek-v4-flash",
"longcat-2.0",
"hy4-preview",
"qwen3.5-plus",
"qwen3.6-plus",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.8-flash",
"qwen3.8-max",
"glm-5.3", // sanity: previously rejected image input
"minimax-m2.5", // sanity: previously rejected image input
];
async function call(body, timeoutMs = 90000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: ctrl.signal,
});
const raw = await res.text();
let json = null;
try { json = JSON.parse(raw); } catch {}
return { status: res.status, json, raw: raw.slice(0, 200) };
} catch (e) {
return { status: 0, json: null, raw: String(e?.message || e) };
} finally { clearTimeout(t); }
}
const results = {};
for (const m of MODELS) {
const r = await call({
model: m,
messages: [{
role: "user",
content: [
{ type: "text", text: "这张图片是什么颜色?只回答颜色词。" },
{ type: "image_url", image_url: { url: `data:image/png;base64,${B64}` } },
],
}],
max_tokens: 400,
stream: false,
});
const msg = r.json?.choices?.[0]?.message;
const content = typeof msg?.content === "string" ? msg.content : null;
const thinkTag = content && /<think>/i.test(content);
results[m] = {
status: r.status,
ok: r.status === 200 && !!msg,
answer: content ? content.replace(/<think>[\s\S]*?<\/think>/g, "").trim().slice(0, 100) : null,
err: r.json?.error?.message?.slice(0, 220) ?? (r.status !== 200 ? r.raw : null),
};
console.log(`${m}: ${r.status} ${results[m].ok ? "answer=" + results[m].answer : "err=" + results[m].err}`);
writeFileSync("C:/Users/12914/Desktop/vscode/.mimo2codex-audit/vision-retest.json", JSON.stringify(results, null, 2));
}
console.log("DONE");