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
84 changes: 73 additions & 11 deletions src/proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,42 @@ function openCompaction(value, secret) {
}
}

// DeepSeek deserializes the content of an input message into a closed enum — input_text,
// output_text, input_image, input_file — and rejects the entire request with a 422 on
// anything else. Codex keeps introducing block types (an inter-agent task arrives as
// `encrypted_content`), and a single unknown block takes a whole agent turn offline, so
// translate every block into something DeepSeek accepts rather than waiting to be taught
// each new type one incident at a time.
const DEEPSEEK_CONTENT_TYPES = new Set(["input_text", "output_text", "input_image", "input_file"]);

const acceptsBlock = (block) => DEEPSEEK_CONTENT_TYPES.has(block?.type);

function textCarriedBy(block) {
for (const value of [block.text, block.refusal, block.content]) {
if (typeof value === "string" && value.length > 0) return value;
}
return null;
}

function contentBlockForDeepSeek(block, compactionSecret) {
if (!block || typeof block !== "object") return block;
if (acceptsBlock(block)) return block;
if (block.type === "encrypted_content") {
const value = block.encrypted_content;
if (typeof value !== "string" || value.length === 0) return null;
if (value.startsWith(COMPACTION_PREFIX)) {
const summary = openCompaction(value, compactionSecret);
return summary ? { type: "input_text", text: summary } : null;
}
// Unreadable bytes are never handed to the model as if they were prose.
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value)) return null;
return { type: "input_text", text: value };
}
// An unrecognised block: keep the text it carries, drop what cannot be read as text.
const text = textCarriedBy(block);
return text === null ? null : { type: "input_text", text };
}

function convertInputItem(item, compactionSecret) {
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
if (item.type === "compaction") {
Expand All @@ -134,31 +170,57 @@ function convertInputItem(item, compactionSecret) {
}
const converted = { ...item };
delete converted.id;
delete converted.internal_chat_message_metadata_passthrough;
if (converted.type === "agent_message") {
// An inter-agent message is a task handed *to* this agent, not a turn the model
// produced. Replaying it as `assistant` makes DeepSeek treat it as its own prior
// thinking turn: with tools in the request it answers "The `reasoning_text` in
// the thinking mode must be passed back to the API." and the whole request fails,
// which is fatal for a freshly spawned child agent whose task message is the last
// item. `user` carries the same text without claiming prior reasoning.
converted.type = "message";
converted.role = "assistant";
converted.role = "user";
}
if (Array.isArray(converted.content) && converted.content.some((block) => !acceptsBlock(block))) {
converted.content = converted.content
.map((block) => contentBlockForDeepSeek(block, compactionSecret))
.filter((block) => block != null);
}
return converted;
}

// ChatGPT verifies every encrypted payload it is handed and fails the whole turn with
// "the encrypted content could not be verified / decrypted" when one was issued by another
// provider. That is what breaks a GPT sub-agent spawned from a DeepSeek session: the child's
// request replays history carrying DeepSeek-issued encrypted fields — and DeepSeek returns a
// non-null `encrypted_content` that is not ChatGPT ciphertext, so the old "is the field
// empty?" test never caught it. ChatGPT's own ciphertext is base64 beginning "gAAAAA", so
// that is the only encrypted payload allowed through; DSCodex-sealed compactions are unwrapped
// below; everything else is dropped rather than replayed for the upstream to reject.
const CHATGPT_SEALED_PREFIX = "gAAAAA";
const sealedByChatGpt = (value) =>
typeof value === "string" && value.startsWith(CHATGPT_SEALED_PREFIX);

// Provider-specific replay artifacts cannot be sent to ChatGPT. Keep native GPT
// reasoning intact and avoid re-encoding ordinary GPT requests at all.
function buildChatGptBody(body, compactionSecret) {
if (!Array.isArray(body?.input)) return null;
let changed = false;
const input = body.input.flatMap((item) => {
if (item?.type === "reasoning"
&& !item.encrypted_content
&& Array.isArray(item.content)
&& item.content.some((part) => part?.type === "reasoning_text")) {
changed = true;
return [];
if (item?.type === "reasoning" && !sealedByChatGpt(item.encrypted_content)) {
const foreign = item.encrypted_content != null
|| (Array.isArray(item.content) && item.content.some((part) => part?.type === "reasoning_text"));
if (foreign) {
changed = true;
return [];
}
}
if (item?.type === "compaction"
&& typeof item.encrypted_content === "string"
&& item.encrypted_content.startsWith(COMPACTION_PREFIX)) {
if (item?.type === "compaction" && !sealedByChatGpt(item.encrypted_content)) {
changed = true;
const summary = openCompaction(item.encrypted_content, compactionSecret);
const summary = typeof item.encrypted_content === "string"
&& item.encrypted_content.startsWith(COMPACTION_PREFIX)
? openCompaction(item.encrypted_content, compactionSecret)
: null;
return summary ? [{
type: "message", role: "assistant",
content: [{ type: "output_text", text: `[Compacted prior context]\n${summary}` }],
Expand Down
152 changes: 147 additions & 5 deletions test/proxy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,115 @@ test("routes Flash and legacy task aliases to the current Flash model", async (t
assert.equal(request.body.store, false);
assert.equal("previous_response_id" in request.body, false);
assert.equal("metadata" in request.body, false);
assert.deepEqual(request.body.input[0], { type: "message", role: "assistant", content: "prior answer" });
assert.deepEqual(request.body.input[0], { type: "message", role: "user", content: "prior answer" });
assert.deepEqual(request.body.input[1], { type: "function_call_output", call_id: "call_7", output: "done" });
}
});

test("forwards an inter-agent task message as text instead of an encrypted_content block", async (t) => {
const observed = [];
const upstream = http.createServer(async (request, response) => {
observed.push(JSON.parse(await bodyOf(request)));
response.writeHead(200, { "content-type": "text/event-stream" });
response.end("event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n");
});
const upstreamUrl = await listen(upstream);
const proxy = createProxyServer({
deepSeekKey: "test-key",
deepSeekBaseUrl: upstreamUrl,
chatGptBaseUrl: upstreamUrl,
logger: { info() {}, error() {} },
routerToken: ROUTER_TOKEN,
});
const proxyUrl = await listen(proxy);
t.after(async () => { await close(proxy); await close(upstream); });

// Codex ships a task handed to another agent as a message whose payload block is
// typed `encrypted_content` even though the text is plain. DeepSeek's content enum
// only knows input_text/output_text/input_image/input_file, so forwarding the block
// unchanged fails the whole request with a 422 and the child agent never starts.
const taskText = "Message Type: NEW_TASK\nTask name: /root/child\nSender: /root\nPayload:\n";
const payload = "reply with banana";
const response = await fetch(route(proxyUrl), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
model: "deepseek/deepseek-flash",
stream: true,
input: [{
id: "amsg_1",
type: "agent_message",
author: "/root",
recipient: "/root/child",
content: [
{ type: "input_text", text: taskText },
{ type: "encrypted_content", encrypted_content: payload },
],
internal_chat_message_metadata_passthrough: { turn_id: "turn_1" },
}],
}),
});

assert.equal(response.status, 200);
await response.text();
const forwarded = observed.at(-1).input[0];
assert.equal(forwarded.type, "message");
// `user`, not `assistant`: DeepSeek rejects a replayed assistant turn without
// reasoning_text once the request carries tools, which killed every child agent.
assert.equal(forwarded.role, "user");
assert.deepEqual(forwarded.content, [
{ type: "input_text", text: taskText },
{ type: "input_text", text: payload },
]);
assert.equal("internal_chat_message_metadata_passthrough" in forwarded, false);
});

test("never forwards a content block DeepSeek cannot deserialize", async (t) => {
const observed = [];
const upstream = http.createServer(async (request, response) => {
observed.push(JSON.parse(await bodyOf(request)));
response.writeHead(200, { "content-type": "text/event-stream" });
response.end("event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n");
});
const upstreamUrl = await listen(upstream);
const proxy = createProxyServer({
deepSeekKey: "test-key",
deepSeekBaseUrl: upstreamUrl,
chatGptBaseUrl: upstreamUrl,
logger: { info() {}, error() {} },
routerToken: ROUTER_TOKEN,
});
const proxyUrl = await listen(proxy);
t.after(async () => { await close(proxy); await close(upstream); });

// DeepSeek deserializes an input message's content into a closed enum: input_text,
// output_text, input_image, input_file. Anything else fails the entire request with a
// 422 — one unknown block type is enough to take a whole agent turn offline — so the
// router has to guarantee the enum on the way out instead of waiting to be taught each
// new block type one at a time. Known blocks must survive byte for byte.
const response = await fetch(route(proxyUrl), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
model: "deepseek/deepseek-flash",
stream: true,
input: [
{ type: "message", role: "assistant", content: [{ type: "refusal", refusal: "I cannot help with that." }] },
{ type: "message", role: "assistant", content: [{ type: "encrypted_content", encrypted_content: null }] },
{ type: "message", role: "user", content: [{ type: "input_text", text: "go on" }] },
],
}),
});

assert.equal(response.status, 200);
await response.text();
const input = observed.at(-1).input;
assert.deepEqual(input[0].content, [{ type: "input_text", text: "I cannot help with that." }]);
// A block that carries no recoverable text is dropped rather than guessed at.
assert.deepEqual(input[1].content, []);
assert.deepEqual(input[2].content, [{ type: "input_text", text: "go on" }]);
});

test("adapts Codex remote compaction v2 to a DeepSeek summary and restores it on replay", async (t) => {
const observed = [];
const summary = "The user approved the router fix; tests and a restart are still pending.";
Expand Down Expand Up @@ -703,10 +807,10 @@ for (const compressed of [false, true]) {
const url = await listen(proxy);
t.after(async () => { await close(proxy); await close(upstream); });
const retained = [
{ type: "reasoning", encrypted_content: "gpt-sealed", content: [{ type: "reasoning_text", text: "native" }] },
{ type: "reasoning", summary: [], content: [], encrypted_content: "gAAAAABmSealedByChatGptForReplay0000000000000000" },
{ type: "reasoning", summary: [{ type: "summary_text", text: "keep" }] },
{ type: "message", role: "user", content: "reasoning_text is literal user text" },
{ type: "compaction", encrypted_content: "gpt-compaction" },
{ type: "compaction", encrypted_content: "gAAAAABmSealedByChatGptForReplay0000000000000001" },
{ type: "function_call", call_id: "call_1", name: "shell", arguments: "{}" },
{ type: "function_call_output", call_id: "call_1", output: "done" },
];
Expand All @@ -727,6 +831,44 @@ for (const compressed of [false, true]) {
});
}

test("GPT replay drops encrypted payloads that another provider issued", async (t) => {
let observed;
const upstream = http.createServer(async (request, response) => {
observed = JSON.parse(await bodyOf(request));
response.end('{}');
});
const proxy = createProxyServer({ chatGptBaseUrl: await listen(upstream), routerToken: ROUTER_TOKEN, logger: { info() {}, error() {} } });
const url = await listen(proxy);
t.after(async () => { await close(proxy); await close(upstream); });

// ChatGPT verifies every encrypted payload it is handed. A reasoning item that came back
// from a different provider — a DeepSeek token, a DSCodex-sealed blob — cannot be verified,
// and Codex then fails the turn with "the encrypted content could not be verified /
// decrypted". That is what breaks a GPT sub-agent spawned from a DeepSeek session: the
// child's request replays the DeepSeek-flavoured history. Only ChatGPT-issued ciphertext
// (base64 "gAAAAA…") may be replayed; everything else is dropped.
const body = JSON.stringify({
model: "gpt-5.6-sol",
input: [
{
type: "reasoning",
summary: [],
content: [{ type: "reasoning_text", text: "deepseek thinking" }],
encrypted_content: "9591cfc5-c41a-4b44-9f51-a82a2f61d6ff-0",
},
{ type: "reasoning", summary: [], content: [], encrypted_content: "gAAAAABmSealedByChatGptForReplay0000000000000002" },
{ type: "compaction", encrypted_content: "ZGVlcHNlZWstc2VhbGVkLWJsb2I=" },
{ type: "message", role: "user", content: [{ type: "input_text", text: "go on" }] },
],
});
const response = await fetch(route(url), { method: "POST", headers: { "content-type": "application/json" }, body });

assert.equal(response.status, 200);
await response.text();
assert.deepEqual(observed.input.map((item) => item.type), ["reasoning", "message"]);
assert.equal(observed.input[0].encrypted_content, "gAAAAABmSealedByChatGptForReplay0000000000000002");
});

test("ordinary compressed GPT traffic preserves exact bytes", async (t) => {
let observed;
const upstream = http.createServer(async (request, response) => {
Expand Down Expand Up @@ -926,14 +1068,14 @@ test("GPT websocket rewrite strips foreign DeepSeek reasoning_text", async (t) =
type: "response.create",
model: "gpt-6-astra",
input: [
{ type: "reasoning", encrypted_content: "gpt-sealed", content: [{ type: "reasoning_text", text: "native" }] },
{ type: "reasoning", encrypted_content: "gAAAAABmSealedByChatGptForReplay0000000000000003", content: [{ type: "reasoning_text", text: "native" }] },
{ type: "reasoning", encrypted_content: null, content: [{ type: "reasoning_text", text: "foreign" }] },
{ type: "message", role: "user", content: "hi" },
],
}));
await waitUntil(() => upstream.state.messages.length >= 1);
assert.deepEqual(JSON.parse(upstream.state.messages[0]).input, [
{ type: "reasoning", encrypted_content: "gpt-sealed", content: [{ type: "reasoning_text", text: "native" }] },
{ type: "reasoning", encrypted_content: "gAAAAABmSealedByChatGptForReplay0000000000000003", content: [{ type: "reasoning_text", text: "native" }] },
{ type: "message", role: "user", content: "hi" },
]);
});
Expand Down