Client or integration
Claude Code through the OpenCodex proxy (streaming /v1/responses), web-search enabled.
Area
Adapters · Streaming / SSE parsing · Web-search loop
Summary
All three stream adapters cast JSON.parse(payload) to Record<string, unknown> and immediately dereference it. JSON.parse("null") does not throw — it returns null — so the surrounding try/catch never fires and the next property access crashes the adapter mid-stream.
Live symptom, on AGR-OAI/claude-opus-5 (adapter: openai-chat, https://agentrouter.org/v1) with web-search on:
stream disconnected before completion: Web-search adapter stream protocol error:
adapter threw: null is not an object (evaluating 'chunk.error')
The Web-search adapter prefix is misleading — web-search/progress-stream.ts:316 only wraps whatever the adapter threw. The defect is in the adapters and reaches any streamed request; the web-search loop merely makes it frequent, because that path is where I see repeated upstream instability ([upstream-retry] connection reset (web-search-loop) — retrying (2/3), 8 occurrences in service.log).
Affected call sites — same defect, three files:
| File |
Unguarded parse |
Crashing access |
Thrown message |
src/adapters/openai-chat.ts |
:941 |
chunk.error :950 |
null is not an object (evaluating 'chunk.error') |
src/adapters/google.ts |
:495 |
chunk.error :503 |
null is not an object (evaluating 'chunk.error') |
src/adapters/anthropic.ts |
:989 |
data.type :995 |
null is not an object (evaluating 'data.type') |
Each already has a catch that emits malformed upstream SSE data frame (or drops the frame, in anthropic.ts). That guard covers syntactically invalid payloads only; a syntactically valid payload deserializing to null walks straight past it.
Reproduction
No network and no config changes — the adapters are driven directly with a synthetic Response. Save as repro-null-chunk.ts and run with the bundled Bun (node_modules/@bitkyc08/opencodex/node_modules/bun/bin/bun.exe repro-null-chunk.ts); adjust PKG to your install path.
const PKG = "<...>/node_modules/@bitkyc08/opencodex/src";
const { createOpenAIChatAdapter } = await import(`${PKG}/adapters/openai-chat.ts`);
const { createGoogleAdapter } = await import(`${PKG}/adapters/google.ts`);
const { createAnthropicAdapter } = await import(`${PKG}/adapters/anthropic.ts`);
const { createTranslatorBudget } = await import(`${PKG}/lib/translator-budget.ts`);
const providerFor = (adapter: string) => ({
adapter, baseUrl: "https://example.invalid/v1", authMode: "key", apiKey: "sk-test",
}) as any;
const ADAPTERS: Record<string, () => any> = {
"openai-chat": () => createOpenAIChatAdapter(providerFor("openai-chat")),
"google ": () => createGoogleAdapter(providerFor("google")),
"anthropic ": () => createAnthropicAdapter(providerFor("anthropic")),
};
async function run(name: string, label: string, sse: string) {
const res = new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
const events: unknown[] = [];
try {
for await (const ev of ADAPTERS[name]().parseStream(res, createTranslatorBudget())) events.push(ev);
console.log(` ${label}: no throw, events = ${JSON.stringify(events)}`);
} catch (e) {
console.log(` ${label}: THREW -> ${e instanceof Error ? e.message : String(e)}`);
}
}
for (const name of Object.keys(ADAPTERS)) {
console.log(`[${name.trim()}]`);
await run(name, "invalid-json", 'data: {not json\n\n'); // control — handled correctly
await run(name, "data-null ", "data: null\n\n"); // bug
}
Actual output on v2.10.2:
[openai-chat]
invalid-json: no throw, events = [{"type":"error","message":"malformed upstream SSE data frame"}]
data-null : THREW -> null is not an object (evaluating 'chunk.error')
[google]
invalid-json: no throw, events = [{"type":"error","message":"malformed upstream SSE data frame"}]
data-null : THREW -> null is not an object (evaluating 'chunk.error')
[anthropic]
invalid-json: no throw, events = [{"type":"error","message":"upstream stream ended before message_stop — possible truncation"}]
data-null : THREW -> null is not an object (evaluating 'data.type')
The invalid-json control passing on every adapter is the point: the existing guard works, it just does not cover this input class. Expected for data-null is the same terminal malformed upstream SSE data frame event, not an escaping TypeError.
Proposed patch
Validate the parse result instead of asserting it. For openai-chat.ts (google.ts is identical; anthropic.ts wants continue + debugDroppedFrame to match its existing handling):
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
yield { type: "error", message: "malformed upstream SSE data frame" };
return "terminate";
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
yield { type: "error", message: "malformed upstream SSE data frame" };
return "terminate";
}
const chunk = parsed as Record<string, unknown>;
null is the case seen in the wild, but the same cast also lets data: 42, data: "x", data: true and data: [] through as non-records, so the guard is worth writing against the shape rather than against null alone.
Version
2.10.2 (npm @bitkyc08/opencodex), bundled Bun runtime.
Operating system
Windows 11, Codex runtime 0.146.1 under the OpenCodex proxy.
Provider and model
- Observed:
AGR-OAI/claude-opus-5 — adapter: openai-chat, baseUrl: https://agentrouter.org/v1, streaming, web-search enabled.
- Reproduced adapter-locally for
openai-chat, google and anthropic, so it is not provider-specific.
Additional note — the failure leaves no server-side trace
The error surfaces to the client, but service.log contains no matching line: chunk.error, adapter threw and stream protocol all return zero hits after the failure. Without the client-side message there is nothing to diagnose from. A debugProviderDiagnostic on the malformed-frame path would help here regardless of the fix.
Checks
Client or integration
Claude Code through the OpenCodex proxy (streaming
/v1/responses), web-search enabled.Area
Adapters · Streaming / SSE parsing · Web-search loop
Summary
All three stream adapters cast
JSON.parse(payload)toRecord<string, unknown>and immediately dereference it.JSON.parse("null")does not throw — it returnsnull— so the surroundingtry/catchnever fires and the next property access crashes the adapter mid-stream.Live symptom, on
AGR-OAI/claude-opus-5(adapter: openai-chat,https://agentrouter.org/v1) with web-search on:The
Web-search adapterprefix is misleading —web-search/progress-stream.ts:316only wraps whatever the adapter threw. The defect is in the adapters and reaches any streamed request; the web-search loop merely makes it frequent, because that path is where I see repeated upstream instability ([upstream-retry] connection reset (web-search-loop) — retrying (2/3), 8 occurrences inservice.log).Affected call sites — same defect, three files:
src/adapters/openai-chat.ts:941chunk.error:950null is not an object (evaluating 'chunk.error')src/adapters/google.ts:495chunk.error:503null is not an object (evaluating 'chunk.error')src/adapters/anthropic.ts:989data.type:995null is not an object (evaluating 'data.type')Each already has a
catchthat emitsmalformed upstream SSE data frame(or drops the frame, inanthropic.ts). That guard covers syntactically invalid payloads only; a syntactically valid payload deserializing tonullwalks straight past it.Reproduction
No network and no config changes — the adapters are driven directly with a synthetic
Response. Save asrepro-null-chunk.tsand run with the bundled Bun (node_modules/@bitkyc08/opencodex/node_modules/bun/bin/bun.exe repro-null-chunk.ts); adjustPKGto your install path.Actual output on v2.10.2:
The
invalid-jsoncontrol passing on every adapter is the point: the existing guard works, it just does not cover this input class. Expected fordata-nullis the same terminalmalformed upstream SSE data frameevent, not an escapingTypeError.Proposed patch
Validate the parse result instead of asserting it. For
openai-chat.ts(google.tsis identical;anthropic.tswantscontinue+debugDroppedFrameto match its existing handling):nullis the case seen in the wild, but the same cast also letsdata: 42,data: "x",data: trueanddata: []through as non-records, so the guard is worth writing against the shape rather than againstnullalone.Version
2.10.2 (npm
@bitkyc08/opencodex), bundled Bun runtime.Operating system
Windows 11, Codex runtime 0.146.1 under the OpenCodex proxy.
Provider and model
AGR-OAI/claude-opus-5—adapter: openai-chat,baseUrl: https://agentrouter.org/v1, streaming, web-search enabled.openai-chat,googleandanthropic, so it is not provider-specific.Additional note — the failure leaves no server-side trace
The error surfaces to the client, but
service.logcontains no matching line:chunk.error,adapter threwandstream protocolall return zero hits after the failure. Without the client-side message there is nothing to diagnose from. AdebugProviderDiagnosticon the malformed-frame path would help here regardless of the fix.Checks