144 lines
5.3 KiB
JavaScript
144 lines
5.3 KiB
JavaScript
// 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();
|