mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
Three correctness/compat defects on the request path:
1. Non-array `messages` (e.g. {"messages":"x"}) passed the `!messages?.length`
guard but threw in `messages.reduce(...)`; since the handler runs without
await, the rejection silently hung the client until socket timeout. Now
guarded with `Array.isArray(messages) && length>0` → 400 invalid_request_error,
placed before the first use of `messages`.
2. OpenAI array `content` ([{type:"text",text},{type:"image_url",...}]) was
JSON.stringify'd into the prompt as literal noise. New `contentToText()` helper
flattens text parts and replaces non-text parts with a placeholder; used in
messagesToPrompt, extractSystemPrompt, and all promptChars char-count sites
(zero `JSON.stringify(m.content)` remain).
3. A streamed upstream error arriving after eager SSE headers emitted a bare
finish_reason:"stop" + [DONE] — byte-identical to a successful empty completion.
Both streaming error paths (the parsed.error result branch AND the non-zero-exit
close-handler branch — the latter folded in per reviewer) now emit an SSE
`data:{"error":{...}}` frame so clients can distinguish failure from empty.
Error-text sanitization across all emit sites is intentionally deferred to #110's
sibling issue #111 (security layer).
ALIGNMENT.md: these are OpenAI-compat shim + input-validation behaviors; OCP does
not forward, add, or alter any Anthropic API operation here (cli.js does not speak
the OpenAI wire format), so a cli.js citation is N/A under Rule 2. No blacklisted
tokens or port literals introduced; alignment.yml passes.
Independent fresh-context reviewer (opus): APPROVE WITH MINOR (Iron Rule 10) —
the close-handler sibling fold-in addresses MINOR #1; this commit body addresses
MINOR #2.
Closes #110.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1563,6 +1563,66 @@ test("(localhost=true, flag=true) → include key (both true)", () => {
|
||||
assert.equal(shouldAdvertiseAnonKey(true, true), true);
|
||||
});
|
||||
|
||||
// ── contentToText helper tests (issue #110) ──────────────────────────────────
|
||||
// MIRRORS server.mjs contentToText — copied verbatim to avoid importing server.mjs
|
||||
// (top-level server.listen() would start a live HTTP server).
|
||||
// Keep in sync with the definition in server.mjs above messagesToPrompt.
|
||||
console.log("\ncontentToText helper (issue #110):");
|
||||
|
||||
function contentToText(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(p =>
|
||||
p && p.type === "text" && typeof p.text === "string" ? p.text : "[non-text content omitted]"
|
||||
).join("");
|
||||
}
|
||||
return content == null ? "" : JSON.stringify(content);
|
||||
}
|
||||
|
||||
test("contentToText: string input returned unchanged", () => {
|
||||
assert.equal(contentToText("hello"), "hello");
|
||||
});
|
||||
|
||||
test("contentToText: array of text parts concatenated", () => {
|
||||
assert.equal(
|
||||
contentToText([{ type: "text", text: "hello" }, { type: "text", text: " world" }]),
|
||||
"hello world"
|
||||
);
|
||||
});
|
||||
|
||||
test("contentToText: non-text part (image_url) replaced with placeholder", () => {
|
||||
assert.equal(
|
||||
contentToText([{ type: "image_url", image_url: { url: "https://example.com/img.png" } }]),
|
||||
"[non-text content omitted]"
|
||||
);
|
||||
});
|
||||
|
||||
test("contentToText: empty array returns empty string", () => {
|
||||
assert.equal(contentToText([]), "");
|
||||
});
|
||||
|
||||
test("contentToText: null returns empty string", () => {
|
||||
assert.equal(contentToText(null), "");
|
||||
});
|
||||
|
||||
// ── messages guard predicate truth-table (issue #110) ────────────────────────
|
||||
// Mirrors the guard at server.mjs line ~1650: Array.isArray(x) && x.length > 0
|
||||
console.log("\nmessages guard predicate (issue #110):");
|
||||
|
||||
function isValidMessages(x) { return Array.isArray(x) && x.length > 0; }
|
||||
|
||||
test("messages guard: string 'x' → invalid (non-array)", () => {
|
||||
assert.equal(isValidMessages("x"), false);
|
||||
});
|
||||
|
||||
test("messages guard: empty array [] → invalid", () => {
|
||||
assert.equal(isValidMessages([]), false);
|
||||
});
|
||||
|
||||
test("messages guard: [{role:'user',content:'hi'}] → valid", () => {
|
||||
assert.equal(isValidMessages([{ role: "user", content: "hi" }]), true);
|
||||
});
|
||||
|
||||
// ── Cleanup ──
|
||||
closeDb();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user