From ff5de7624110e9d417bbb008f83ddcc6a8e2f0e7 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 27 Apr 2026 06:16:23 -0500 Subject: [PATCH] auditor + gateway: 2 fixes from kimi_architect's first real run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acted on 2 of 10 findings Kimi caught when auditing its own integration on PR #11 head 8d02c7f. Skipped 8 (false positives or out-of-scope). 1. crates/gateway/src/v1/kimi.rs — flatten OpenAI multimodal content array to plain string before forwarding to api.kimi.com. The Kimi coding endpoint is text-only; passing a [{type,text},...] array returns 400. Use Message::text() to concat text-parts and drop non-text. Verified with curl using array-shape content: gateway now returns "PONG-ARRAY" instead of upstream error. 2. auditor/checks/kimi_architect.ts — computeGrounding switched from readFileSync to async readFile inside Promise.all. Doesn't matter at 10 findings; would matter at 100+. Removed unused readFileSync import. Skipped findings (with reason): - drift_report.ts:18 schema bump migration concern: the strict schema_version refusal IS the migration boundary (v1 readers explicitly fail on v2; not a silent corruption risk). - replay.ts:383 ISO timestamp precision: Date.toISOString always emits "YYYY-MM-DDTHH:mm:ss.sssZ" (ms precision). False positive. - mode.rs:1035 matrix_corpus deserializer compat: deserialize_string _or_vec at mode.rs:175 already accepts both shapes. Confabulation from not seeing the deserializer in the input bundle. - /etc/lakehouse/kimi.env world-readable: actually 0600 root. Real concern would be permission-drift; not a code bug. - callKimi response.json hang: obsolete; we use curl now. - parseFindings silent-drop: ergonomic concern, not a bug. - appendMetrics join with "..": works for current path; deferred. - stubFinding dead-type extension: cosmetic. Self-audit grounding rate at v1.0.0: 10/10 file:line citations verified by grep. 2 of 10 actionable bugs landed. The other 8 were correctly flagged as concerns but didn't earn a code change. Co-Authored-By: Claude Opus 4.7 (1M context) --- auditor/checks/kimi_architect.ts | 25 +++++++++++++++---------- crates/gateway/src/v1/kimi.rs | 8 +++++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/auditor/checks/kimi_architect.ts b/auditor/checks/kimi_architect.ts index 65bd600..fc834ec 100644 --- a/auditor/checks/kimi_architect.ts +++ b/auditor/checks/kimi_architect.ts @@ -27,7 +27,7 @@ // Off by default: caller checks LH_AUDITOR_KIMI=1 before invoking. import { readFile, writeFile, mkdir, appendFile, stat } from "node:fs/promises"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; import type { Finding, CheckKind } from "../types.ts"; @@ -265,31 +265,36 @@ function parseFindings(content: string): Finding[] { // is appended into the evidence array so the reader can see which // citations were verified. async function computeGrounding(findings: Finding[]): Promise<{ total: number; verified: number; rate: number }> { - let verified = 0; - for (const f of findings) { + // readFile (async) instead of readFileSync — caught 2026-04-27 by + // Kimi's self-audit. Sync I/O in an async fn blocks the event loop + // for every cited file; doesn't matter at 10 findings, would matter + // at 100+. + const checks = await Promise.all(findings.map(async (f) => { const cite = f.evidence[0] ?? ""; const m = /^(\S+?):(\d+)/.exec(cite); - if (!m) continue; + if (!m) return false; const [, relpath, lineStr] = m; const line = Number(lineStr); - if (!line || !relpath) continue; + if (!line || !relpath) return false; const abs = relpath.startsWith("/") ? relpath : resolve(REPO_ROOT, relpath); if (!existsSync(abs)) { f.evidence.push("[grounding: file not found]"); - continue; + return false; } try { - const lines = readFileSync(abs, "utf8").split("\n"); + const lines = (await readFile(abs, "utf8")).split("\n"); if (line < 1 || line > lines.length) { f.evidence.push(`[grounding: line ${line} > EOF (${lines.length})]`); - continue; + return false; } f.evidence.push(`[grounding: verified at ${relpath}:${line}]`); - verified++; + return true; } catch (e) { f.evidence.push(`[grounding: read failed: ${(e as Error).message.slice(0, 80)}]`); + return false; } - } + })); + const verified = checks.filter(Boolean).length; const total = findings.length; return { total, verified, rate: total === 0 ? 0 : verified / total }; } diff --git a/crates/gateway/src/v1/kimi.rs b/crates/gateway/src/v1/kimi.rs index ee0cf92..9ff2b7e 100644 --- a/crates/gateway/src/v1/kimi.rs +++ b/crates/gateway/src/v1/kimi.rs @@ -64,11 +64,17 @@ pub async fn chat( // upstream API sees the bare model id (e.g. "kimi-for-coding"). let model = req.model.strip_prefix("kimi/").unwrap_or(&req.model).to_string(); + // Flatten content to a plain String. api.kimi.com is text-only on + // the coding endpoint; the OpenAI multimodal array shape + // ([{type:"text",text:"..."},{type:"image_url",...}]) returns 400. + // Message::text() concats text-parts and drops non-text. Caught + // 2026-04-27 by Kimi's self-audit (kimi.rs:137 — content as raw + // serde_json::Value risked upstream rejection). let body = KimiChatBody { model: model.clone(), messages: req.messages.iter().map(|m| KimiMessage { role: m.role.clone(), - content: m.content.clone(), + content: serde_json::Value::String(m.text()), }).collect(), max_tokens: req.max_tokens.unwrap_or(800), temperature: req.temperature.unwrap_or(0.3),