Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/wire/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,29 @@ export type OpenAIRequestBody = {
[key: string]: unknown;
};

type Flat = { msgs: BiliMessage[] };
type Flat = { msgs: BiliMessage[]; systemText: string };

/** Hoist the contiguous leading system/developer prefix out of the fold
* space (openai-chat variant of the responses codec's systemParts and the
* anthropic codec's top-level `system` field). The system prompt is host
* runtime state: its content varies across restarts (injected reminders,
* host-composed instructions), so keeping it inside the id space made every
* downstream fingerprint spanning it unstable, and a compress range that
* covered it removed the model's system prompt from the rebuilt wire
* entirely. Mid-conversation system messages (rare, host-synthetic) stay in
* the fold space unchanged. */
export function openaiToCore(body: OpenAIRequestBody): Flat {
const msgs: BiliMessage[] = [];
const systemParts: string[] = [];
const clusters = new ClusterCounter();
for (const m of body.messages) {
switch (m.role) {
case "system":
case "developer": {
if (msgs.length === 0) {
systemParts.push(stringContent(m.content));
break;
}
const base = deriveMessageId(m.role, "text", stringContent(m.content));
msgs.push({ id: clusters.next(base), role: "system", contentType: "text", text: stringContent(m.content), originalRole: m.role });
break;
Expand Down Expand Up @@ -134,7 +148,7 @@ export function openaiToCore(body: OpenAIRequestBody): Flat {
}
}
}
return { msgs };
return { msgs, systemText: systemParts.join("\n\n") };
}

export function coreToOpenai(messages: BiliMessage[]): OpenAIMessage[] {
Expand Down
115 changes: 115 additions & 0 deletions tests/openai-system-hoist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
openaiToCore,
coreToOpenai,
injectOpenaiSystem,
} from "../src/wire/openai.js";
import { mirrorOpenaiToCore } from "../src/wire/mirror.js";
import type { MirrorMessage } from "../src/wire/mirror.js";

const conv: Array<Record<string, unknown>> = [
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi there" },
{ role: "user", content: "what is 2+2" },
{ role: "assistant", content: "4" },
];

test("openaiToCore hoists the contiguous leading system/developer prefix out of the fold space", () => {
const body = {
model: "m",
messages: [
{ role: "system", content: "you are terse" },
{ role: "developer", content: "second system part" },
...conv,
],
};
const { msgs, systemText } = openaiToCore(body as never);
assert.equal(systemText, "you are terse\n\nsecond system part");
assert.ok(msgs.length > 0);
assert.notEqual(msgs[0].role, "system");
assert.ok(
msgs.every((m) => m.role !== "system"),
"no system pieces remain in the fold space",
);
// conversation ids unchanged vs a body with no system prefix at all
const bare = openaiToCore({ model: "m", messages: conv } as never);
assert.deepEqual(
msgs.map((m) => m.id),
bare.msgs.map((m) => m.id),
);
});

test("openaiToCore with no system prefix returns empty systemText", () => {
const { systemText } = openaiToCore({ model: "m", messages: conv } as never);
assert.equal(systemText, "");
});

test("mid-conversation system messages stay in the fold space", () => {
const { msgs } = openaiToCore({
model: "m",
messages: [
...conv.slice(0, 2),
{ role: "system", content: "mid-stream system" },
...conv.slice(2),
],
} as never);
assert.equal(
msgs.filter((m) => m.role === "system").length,
1,
"mid-stream system kept as a fold piece",
);
});

test("system content changes never shift conversation ids (restart regression)", () => {
// Live wire carries the host system prompt; after restart the prime-fold
// reconstruction differs. Ids must not depend on system content, or every
// span fingerprint covering pos 0 breaks and restart replay is rejected.
const a = openaiToCore({
model: "m",
messages: [{ role: "system", content: "LIVE system, 45k chars of host state" }, ...conv],
} as never);
const b = openaiToCore({
model: "m",
messages: [{ role: "system", content: "RECONSTRUCTED system, shorter" }, ...conv],
} as never);
assert.deepEqual(
a.msgs.map((m) => m.id),
b.msgs.map((m) => m.id),
);
});

test("mirrorOpenaiToCore converges with the live space regardless of systemText", () => {
const t = (text: string): MirrorMessage => ({ role: "user", blocks: [{ type: "text", text }] });
const a = (text: string): MirrorMessage => ({ role: "assistant", blocks: [{ type: "text", text }] });
const view: MirrorMessage[] = [
t("hello"),
a("hi there"),
t("what is 2+2"),
a("4"),
];
const live = openaiToCore({ model: "m", messages: conv } as never);
const mirrorA = mirrorOpenaiToCore(view, "live 45k system");
const mirrorB = mirrorOpenaiToCore(view, "");
assert.deepEqual(
mirrorA.map((m) => m.id),
live.msgs.map((m) => m.id),
);
assert.deepEqual(
mirrorB.map((m) => m.id),
live.msgs.map((m) => m.id),
);
});

test("coreToOpenai output has no head system; injectOpenaiSystem re-injects it", () => {
const { msgs, systemText } = openaiToCore({
model: "m",
messages: [{ role: "system", content: "you are terse" }, ...conv],
} as never);
const rebuilt = coreToOpenai(msgs);
assert.notEqual(rebuilt[0]?.role, "system");
const withSystem = injectOpenaiSystem(rebuilt, [systemText, "compress prompt"]);
assert.equal(withSystem[0]?.role, "system");
assert.match(String(withSystem[0]?.content), /you are terse/);
assert.match(String(withSystem[0]?.content), /compress prompt/);
});
42 changes: 28 additions & 14 deletions tests/wire-bili-message-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,38 @@ test("anthropic: tool_result without is_error stays clean", () => {
assert.equal(tr.is_error, undefined, "no spurious is_error");
});

// 4. OpenAI developer role round-trips (was collapsed to system).
test("openai: developer role is restored", () => {
const body: OpenAIRequestBody = { messages: [{ role: "developer", content: "you are a dev" }] };
const { msgs } = openaiToCore(body);
assert.equal(msgs[0]?.role, "system", "kernel sees system");
assert.equal(msgs[0]?.originalRole, "developer", "originalRole sidecar stored");
const rebuilt = coreToOpenai(msgs);
assert.equal(rebuilt[0]?.role, "developer", "developer role reconstructed");
assert.equal(rebuilt[0]?.content, "you are a dev");
// 4. OpenAI leading system/developer prefix is hoisted OUT of the fold space
// (restart-regression fix: host system content is runtime-unstable, so it must
// never enter the id/fingerprint space); originalRole still round-trips for
// mid-conversation system traffic.
test("openai: leading developer/system prefix is hoisted, not folded", () => {
const body: OpenAIRequestBody = {
messages: [
{ role: "developer", content: "you are a dev" },
{ role: "system", content: "sys" },
{ role: "user", content: "hi" },
],
};
const { msgs, systemText } = openaiToCore(body);
assert.equal(systemText, "you are a dev\n\nsys", "prefix returned separately");
assert.ok(msgs.every((m) => m.role !== "system"), "no system piece in the fold space");
assert.equal(msgs[0]?.role, "user");
});

// 4b. Sanity: a plain system role is NOT promoted to developer.
test("openai: system role stays system", () => {
const body: OpenAIRequestBody = { messages: [{ role: "system", content: "sys" }] };
// 4b. Mid-conversation system traffic keeps the role sidecar round-trip.
test("openai: mid-conversation developer role is restored", () => {
const body: OpenAIRequestBody = {
messages: [
{ role: "user", content: "hi" },
{ role: "developer", content: "you are a dev" },
],
};
const { msgs } = openaiToCore(body);
assert.equal(msgs[0]?.originalRole, "system");
const dev = msgs.find((m) => m.role === "system")!;
assert.equal(dev.originalRole, "developer", "originalRole sidecar stored");
const rebuilt = coreToOpenai(msgs);
assert.equal(rebuilt[0]?.role, "system");
assert.equal(rebuilt[1]?.role, "developer", "developer role reconstructed");
assert.equal(rebuilt[1]?.content, "you are a dev");
});

// 5. OpenAI image_url round-trips (image was previously dropped entirely).
Expand Down
Loading