@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
598 lines (597 loc) • 23.1 kB
JavaScript
/**
* Wire-format translation: Anthropic `ClaudeResponse` shape -> the Codex
* Responses shape a native Codex client expects.
*
* Pure wire-format only — no account selection, retry policy, or transport.
* Mirrors `claudeFormat.ts`'s role for the opposite direction: that module
* serializes NeuroLink's internal result into Claude SSE, this one serializes
* an already-Claude-shaped result into Codex SSE (or one JSON envelope).
*/
import { randomBytes } from "node:crypto";
import { formatSSE } from "./claudeFormat.js";
/** Empty by default so every existing caller (no `toolKindByName` argument)
* keeps treating every tool_use block as a `function` call, unchanged. */
const NO_TOOL_KINDS = new Map();
/**
* A `custom` tool was wrapped, on the request side, into a single-field
* `{ input: "..." }` schema (§3.4 of `codexOutboundFallback.ts`), so the
* client's grammar text is that one field. Undefined when the record does not
* carry it as a string.
*/
function unwrapCustomToolInput(value) {
return typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
"input" in value &&
typeof value.input === "string"
? value.input
: undefined;
}
/** The accumulated `input_json_delta` text of a custom call, unwrapped, or
* undefined when it is not (yet) a complete `{ input: "..." }` object. */
function unwrapCustomToolInputJson(text) {
try {
return unwrapCustomToolInput(JSON.parse(text));
}
catch {
return undefined;
}
}
/** Same idiom as `generateToolUseId` (`claudeFormat.ts`), Codex's own prefix. */
export function generateCodexResponseId() {
return `resp_${randomBytes(24).toString("base64url")}`;
}
const CODEX_ITEM_ID_PREFIX = {
message: "msg_",
function_call: "fc_",
custom_tool_call: "ctc_",
};
/** Item ids are prefixed by kind so a client can tell message/tool items apart at a glance. */
export function generateCodexItemId(kind) {
return `${CODEX_ITEM_ID_PREFIX[kind]}${randomBytes(18).toString("base64url").slice(0, 24)}`;
}
/**
* Reuse the Anthropic `tool_use.id` (`toolu_...`) verbatim as the Codex `call_id`.
* No id-mapping table exists anywhere in this codebase's translation modules, and
* nothing on the way in validates `call_id` format — see the design doc's §5 for
* the full reasoning and its one unresolved edge (a real, non-fallback Codex
* backend rejecting a non-`call_`-prefixed id on a later turn).
*/
export function codexCallIdFromToolUseId(toolUseId) {
return toolUseId;
}
/**
* `ClaudeResponse.stop_reason` is `string | null` with no enum in this repo
* (`proxy.ts`), so any value this repo cannot name — including a real
* Anthropic "refusal" or "pause_turn" — falls into "completed" rather than
* surfacing as unusual. Deliberate simplification, stated explicitly.
*/
function mapClaudeStopReasonToCodex(stopReason) {
if (stopReason === "max_tokens") {
return {
status: "incomplete",
incomplete_details: { reason: "max_output_tokens" },
};
}
return { status: "completed", incomplete_details: null };
}
/**
* Absent cache fields are omitted rather than zeroed (never conflate "not
* observed" with "observed as zero" — the field's own doc comment on
* `CodexResponseUsage` states this contract).
*/
export function synthesizeCodexUsage(usage) {
const cacheRead = usage.cache_read_input_tokens;
const cacheCreation = usage.cache_creation_input_tokens;
const cacheCreation1h = usage.cacheCreation1hTokens;
const inputTokens = usage.input_tokens + (cacheRead ?? 0) + (cacheCreation ?? 0);
const details = cacheRead === undefined &&
cacheCreation === undefined &&
cacheCreation1h === undefined
? undefined
: {
...(cacheRead === undefined ? {} : { cached_tokens: cacheRead }),
...(cacheCreation === undefined
? {}
: { cache_write_tokens: cacheCreation }),
...(cacheCreation1h === undefined
? {}
: { cache_write_1h_tokens: cacheCreation1h }),
};
return {
input_tokens: inputTokens,
output_tokens: usage.output_tokens,
total_tokens: inputTokens + usage.output_tokens,
...(details ? { input_tokens_details: details } : {}),
};
}
/**
* Shared by the streaming serializer's non-failure item close and the
* non-streaming path (`serializeCodexResponse`), so item shape is defined
* once. Always produces a "completed" item — the streaming failure path
* (an item closed "incomplete" mid-turn) builds its item directly, since it
* must preserve the raw accumulated text/argument bytes rather than a value
* reconstructed through this function.
*/
export function buildCodexResponseItem(block, itemId, toolKindByName = NO_TOOL_KINDS) {
if (block.type === "text") {
// The streaming path opens a message item only on a block's first delta,
// so an empty text block produces no item there; match it here.
if (block.text === "") {
return null;
}
return {
id: itemId,
type: "message",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: block.text, annotations: [] }],
};
}
if (block.type === "tool_use" &&
toolKindByName.get(block.name) === "custom") {
return {
id: itemId,
type: "custom_tool_call",
status: "completed",
call_id: codexCallIdFromToolUseId(block.id),
name: block.name,
input: unwrapCustomToolInput(block.input) ?? JSON.stringify(block.input),
};
}
if (block.type === "tool_use") {
return {
id: itemId,
type: "function_call",
status: "completed",
call_id: codexCallIdFromToolUseId(block.id),
name: block.name,
arguments: JSON.stringify(block.input),
};
}
// thinking / image / tool_result — not emitted; see the design doc's §5.
return null;
}
/** Non-streaming path: `§7` of the design — the document root IS a `CodexResponseEnvelope`. */
export function serializeCodexResponse(result, requestModel, toolKindByName = NO_TOOL_KINDS) {
const items = [];
for (const block of result.content) {
const item = buildCodexResponseItem(block, generateCodexItemId(block.type !== "tool_use"
? "message"
: toolKindByName.get(block.name) === "custom"
? "custom_tool_call"
: "function_call"), toolKindByName);
if (item) {
items.push(item);
}
}
const { status, incomplete_details } = mapClaudeStopReasonToCodex(result.stop_reason);
return {
id: generateCodexResponseId(),
object: "response",
created_at: Math.floor(Date.now() / 1000),
status,
model: requestModel,
output: items,
usage: synthesizeCodexUsage(result.usage),
incomplete_details,
error: null,
};
}
/**
* Push-based streaming serializer. A genuinely incremental tool-call-argument
* path (`openToolCall`/`pushToolCallArgsDelta`/`closeToolCall`) forwards each
* raw `input_json_delta` chunk as it arrives rather than buffering the whole
* call before emitting anything — `partial_json` is not individually valid
* JSON, so a serializer that validated per-chunk could not do this.
*/
export class CodexResponsesStreamSerializer {
model;
toolKindByName;
state = "idle";
responseId = generateCodexResponseId();
outputIndex = 0;
openKind = null;
openItemId = "";
textAccum = "";
argsAccum = "";
toolCallId = "";
toolName = "";
closedItems = [];
terminalEnvelope = null;
constructor(model, toolKindByName = NO_TOOL_KINDS) {
this.model = model;
this.toolKindByName = toolKindByName;
}
*start() {
this.assertNotTerminal();
this.state = "streaming";
const response = {
id: this.responseId,
object: "response",
status: "in_progress",
created_at: Math.floor(Date.now() / 1000),
model: this.model,
output: [],
usage: null,
incomplete_details: null,
error: null,
};
yield formatSSE("response.created", { type: "response.created", response });
// Whether the real CLI tolerates a stream without in_progress was never
// bisected (test/fixtures/sse-bisection-findings.md), so the order the
// real backend uses is kept: created, then in_progress, then items.
yield formatSSE("response.in_progress", {
type: "response.in_progress",
response,
});
}
*pushDelta(text) {
this.assertNotTerminal();
if (this.openKind !== "message") {
yield* this.closeOpenItem(false);
yield* this.openMessage();
}
this.textAccum += text;
yield formatSSE("response.output_text.delta", {
type: "response.output_text.delta",
output_index: this.outputIndex,
item_id: this.openItemId,
content_index: 0,
delta: text,
});
}
/** Opens the tool-call item header at content_block_start, before any argument text
* is known — mirrors what content_block_start already gives us (id, name). A tool
* the request declared `custom` opens as a `custom_tool_call` item. */
*openToolCall(toolUseId, name) {
this.assertNotTerminal();
yield* this.closeOpenItem(false);
const kind = this.toolKindByName.get(name) === "custom"
? "custom_tool_call"
: "function_call";
this.openKind = kind;
this.openItemId = generateCodexItemId(kind);
this.toolCallId = codexCallIdFromToolUseId(toolUseId);
this.toolName = name;
this.argsAccum = "";
const item = kind === "custom_tool_call"
? {
id: this.openItemId,
type: kind,
status: "in_progress",
call_id: this.toolCallId,
name: this.toolName,
input: "",
}
: {
id: this.openItemId,
type: kind,
status: "in_progress",
call_id: this.toolCallId,
name: this.toolName,
arguments: "",
};
yield formatSSE("response.output_item.added", {
type: "response.output_item.added",
output_index: this.outputIndex,
item,
});
}
/** Forwards one raw partial_json fragment as-is — no JSON.parse here, because a lone
* fragment is not required to be valid JSON on its own. A custom call's fragments
* are the `{"input":"..."}` wrapper, not client text, so they are only buffered;
* `closeToolCall` emits the unwrapped input once the whole wrapper is known. */
*pushToolCallArgsDelta(rawChunk) {
this.assertNotTerminal();
if (this.openKind !== "function_call" &&
this.openKind !== "custom_tool_call") {
throw new Error("pushToolCallArgsDelta with no open tool-call item");
}
this.argsAccum += rawChunk;
if (this.openKind === "custom_tool_call") {
return;
}
yield formatSSE("response.function_call_arguments.delta", {
type: "response.function_call_arguments.delta",
output_index: this.outputIndex,
item_id: this.openItemId,
delta: rawChunk,
});
}
/**
* Public close for a tool-use block at `content_block_stop`. A no-op when no
* tool-call item is open, so a driver can call it unconditionally.
* Validates the accumulated arguments once, here — the one place validity
* is required. Throws on malformed JSON; the caller must catch and route to
* `emitFailure` (never swallowed, never retried after this point).
*/
*closeToolCall() {
this.assertNotTerminal();
if (this.openKind === "custom_tool_call") {
yield* this.closeCustomToolCall();
return;
}
if (this.openKind !== "function_call") {
return;
}
// A zero-argument call arrives with no input_json_delta at all. The value
// that was validated is the value emitted, so it becomes "{}" everywhere,
// and one delta carries it so the client sees the same shape as any call.
const args = this.argsAccum === "" ? "{}" : this.argsAccum;
JSON.parse(args); // throws on malformed JSON
if (this.argsAccum === "") {
yield formatSSE("response.function_call_arguments.delta", {
type: "response.function_call_arguments.delta",
output_index: this.outputIndex,
item_id: this.openItemId,
delta: args,
});
}
yield formatSSE("response.function_call_arguments.done", {
type: "response.function_call_arguments.done",
output_index: this.outputIndex,
item_id: this.openItemId,
arguments: args,
});
const item = {
id: this.openItemId,
type: "function_call",
status: "completed",
call_id: this.toolCallId,
name: this.toolName,
arguments: args,
};
yield formatSSE("response.output_item.done", {
type: "response.output_item.done",
output_index: this.outputIndex,
item,
});
this.closedItems.push(item);
this.openKind = null;
this.outputIndex++;
}
/**
* Public close for a text block at `content_block_stop`, mirroring
* `closeToolCall`'s role for the message item. A no-op when no message item
* is open, so a driver can call it unconditionally alongside `closeToolCall`
* without first inspecting which kind is currently open.
*/
*closeTextBlock() {
this.assertNotTerminal();
if (this.openKind !== "message") {
return;
}
yield* this.closeMessageItem(false);
}
*finish(stopReason, usage) {
this.assertNotTerminal();
yield* this.closeOpenItem(false);
const { status, incomplete_details } = mapClaudeStopReasonToCodex(stopReason);
const envelope = {
id: this.responseId,
object: "response",
created_at: Math.floor(Date.now() / 1000),
status,
model: this.model,
output: this.closedItems,
usage: synthesizeCodexUsage(usage),
incomplete_details,
error: null,
};
this.state = "done";
this.terminalEnvelope = envelope;
yield formatSSE(status === "incomplete" ? "response.incomplete" : "response.completed", {
type: status === "incomplete"
? "response.incomplete"
: "response.completed",
response: envelope,
});
return envelope;
}
/**
* Post-commit terminal path. Closes any open item as "incomplete" — now
* representable via `CodexResponseItemStatus` — then emits a single
* `response.failed`. Idempotent: a second call after this one already ran
* returns the same envelope without emitting further frames, so a driver
* that reaches it defensively from more than one place never double-emits.
*/
// `status` is accepted (not folded into the wire envelope, which has no
// HTTP-status field) so a caller's classification — e.g. 502 upstream vs.
// 400 malformed-arguments — is available to whatever logs or maps this
// failure, mirroring `CodexFallbackStreamError`'s own `status` field on
// the reverse direction.
*emitFailure(_status, message, code) {
if (this.terminalEnvelope) {
return this.terminalEnvelope;
}
yield* this.closeOpenItem(true);
this.state = "error";
const envelope = {
id: this.responseId,
object: "response",
created_at: Math.floor(Date.now() / 1000),
status: "failed",
model: this.model,
output: this.closedItems,
incomplete_details: null,
error: { code: code ?? "upstream_error", message },
};
this.terminalEnvelope = envelope;
yield formatSSE("response.failed", {
type: "response.failed",
response: envelope,
});
return envelope;
}
assertNotTerminal() {
if (this.state === "done" || this.state === "error") {
throw new Error("CodexResponsesStreamSerializer: push after terminal event");
}
}
/**
* The custom-kind close: nothing was streamed live, so one input delta
* carries the whole unwrapped input, then its done event. Lenient where the
* function path throws: the wrapper is Claude's own output for the
* single-field schema `mapCodexCustomToolToClaude` declared, so a parse
* failure is an internal bug, and the raw accumulated text is emitted rather
* than failing the turn over it.
*/
*closeCustomToolCall() {
const input = unwrapCustomToolInputJson(this.argsAccum) ?? this.argsAccum;
yield formatSSE("response.custom_tool_call_input.delta", {
type: "response.custom_tool_call_input.delta",
output_index: this.outputIndex,
item_id: this.openItemId,
delta: input,
});
yield formatSSE("response.custom_tool_call_input.done", {
type: "response.custom_tool_call_input.done",
output_index: this.outputIndex,
item_id: this.openItemId,
input,
});
const item = {
id: this.openItemId,
type: "custom_tool_call",
status: "completed",
call_id: this.toolCallId,
name: this.toolName,
input,
};
yield formatSSE("response.output_item.done", {
type: "response.output_item.done",
output_index: this.outputIndex,
item,
});
this.closedItems.push(item);
this.openKind = null;
this.outputIndex++;
}
*openMessage() {
this.openKind = "message";
this.openItemId = generateCodexItemId("message");
this.textAccum = "";
yield formatSSE("response.output_item.added", {
type: "response.output_item.added",
output_index: this.outputIndex,
item: {
id: this.openItemId,
type: "message",
role: "assistant",
status: "in_progress",
content: [],
},
});
yield formatSSE("response.content_part.added", {
type: "response.content_part.added",
output_index: this.outputIndex,
item_id: this.openItemId,
content_index: 0,
part: { type: "output_text", text: "", annotations: [] },
});
}
/** Closes whichever item is currently open, as "incomplete" when `failed`. */
*closeOpenItem(failed) {
if (this.openKind === "function_call") {
if (failed) {
yield* this.closeFailedFunctionCall();
return;
}
yield* this.closeToolCall();
return;
}
if (this.openKind === "custom_tool_call") {
if (failed) {
yield* this.closeFailedCustomToolCall();
return;
}
yield* this.closeToolCall();
return;
}
if (this.openKind === "message") {
yield* this.closeMessageItem(failed);
}
}
/**
* The failure-close counterpart to `closeToolCall`. Arguments are carried
* verbatim from `argsAccum` and never re-validated — `JSON.parse` is not
* called on the failure path, and no `function_call_arguments.done` is
* emitted, since the arguments never reached a validated, complete state.
*/
*closeFailedFunctionCall() {
const item = {
id: this.openItemId,
type: "function_call",
status: "incomplete",
call_id: this.toolCallId,
name: this.toolName,
arguments: this.argsAccum,
};
yield formatSSE("response.output_item.done", {
type: "response.output_item.done",
output_index: this.outputIndex,
item,
});
this.closedItems.push(item);
this.openKind = null;
this.outputIndex++;
}
/**
* The failure close for a custom call. Its accumulated bytes are Anthropic's
* JSON wrapper, not the client's grammar text, so `input` is the unwrapped
* string when the wrapper already parses and "" otherwise — an incomplete
* item is never executed. No input done event, as on the function path.
*/
*closeFailedCustomToolCall() {
const item = {
id: this.openItemId,
type: "custom_tool_call",
status: "incomplete",
call_id: this.toolCallId,
name: this.toolName,
input: unwrapCustomToolInputJson(this.argsAccum) ?? "",
};
yield formatSSE("response.output_item.done", {
type: "response.output_item.done",
output_index: this.outputIndex,
item,
});
this.closedItems.push(item);
this.openKind = null;
this.outputIndex++;
}
*closeMessageItem(failed) {
const text = this.textAccum;
yield formatSSE("response.output_text.done", {
type: "response.output_text.done",
output_index: this.outputIndex,
item_id: this.openItemId,
content_index: 0,
text,
});
yield formatSSE("response.content_part.done", {
type: "response.content_part.done",
output_index: this.outputIndex,
item_id: this.openItemId,
content_index: 0,
part: { type: "output_text", text, annotations: [] },
});
const item = {
id: this.openItemId,
type: "message",
role: "assistant",
status: failed ? "incomplete" : "completed",
content: [{ type: "output_text", text, annotations: [] }],
};
yield formatSSE("response.output_item.done", {
type: "response.output_item.done",
output_index: this.outputIndex,
item,
});
this.closedItems.push(item);
this.openKind = null;
this.textAccum = "";
this.outputIndex++;
}
}