Skip to content

Commit dacd6a3

Browse files
Share SSE null-delta patch between openai-compatible and opencode adapters
1 parent c76410f commit dacd6a3

3 files changed

Lines changed: 47 additions & 59 deletions

File tree

‎src/provider/openai-compatible-adapter.ts‎

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type BuiltRequest, type ProviderAdapter } from "@intx/inference";
22
import { createOpenAIAdapter } from "@intx/inference/providers";
3+
import { normalizeNullDeltaFields } from "./sse-delta-patch.js";
34

45
// The stock OpenAI adapter builds the request body from a fixed set of fields
56
// (max_tokens, temperature, tools, messages, response_format) and ignores
@@ -58,37 +59,9 @@ export function createOpenAICompatibleAdapter(
5859
return ensureAccept(merged);
5960
};
6061

61-
// DeepSeek via NVIDIA NIM sends null for delta fields the upstream schema
62-
// requires to be non-null (role: string, tool_calls: array). Fields that
63-
// legitimately accept null (content, reasoning_content, etc.) are left alone.
64-
const NULL_REJECTED_DELTA_FIELDS = new Set(["role", "tool_calls"]);
6562
const parseResponse: ProviderAdapter["parseResponse"] = (sseData: string) => {
6663
if (!needsDeepSeekPatch) return base.parseResponse(sseData);
67-
let data = sseData;
68-
try {
69-
const parsed = JSON.parse(sseData) as Record<string, unknown>;
70-
const choices = parsed["choices"];
71-
if (Array.isArray(choices)) {
72-
let patched = false;
73-
for (const choice of choices) {
74-
if (choice !== null && typeof choice === "object") {
75-
const delta = (choice as Record<string, unknown>)["delta"];
76-
if (delta !== null && typeof delta === "object") {
77-
for (const key of NULL_REJECTED_DELTA_FIELDS) {
78-
if ((delta as Record<string, unknown>)[key] === null) {
79-
Reflect.deleteProperty(delta as object, key);
80-
patched = true;
81-
}
82-
}
83-
}
84-
}
85-
}
86-
if (patched) data = JSON.stringify(parsed);
87-
}
88-
} catch {
89-
/* not JSON — pass through */
90-
}
91-
return base.parseResponse(data);
64+
return base.parseResponse(normalizeNullDeltaFields(sseData));
9265
};
9366

9467
return { ...base, buildRequest, parseResponse };

‎src/provider/opencode-go-adapter.ts‎

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,10 @@
11
import type { BuiltRequest, ProviderAdapter } from "@intx/inference";
22
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";
33
import { OPENCODE_SESSION_ID_OPTION, optionString } from "./responses-adapters.js";
4+
import { normalizeNullDeltaFields } from "./sse-delta-patch.js";
45

56
type AdapterSource = Parameters<typeof createOpenAICompatibleAdapter>[0];
67

7-
const NULL_DELTA_FIELDS = ["role", "tool_calls"] as const;
8-
9-
function normalizeNullDeltaFields(sseData: string): string {
10-
let parsed: unknown;
11-
try {
12-
parsed = JSON.parse(sseData);
13-
} catch {
14-
return sseData;
15-
}
16-
if (parsed === null || typeof parsed !== "object") return sseData;
17-
18-
const choices = (parsed as Record<string, unknown>)["choices"];
19-
if (!Array.isArray(choices)) return sseData;
20-
21-
let normalized = false;
22-
for (const choice of choices) {
23-
if (choice === null || typeof choice !== "object") continue;
24-
const delta = (choice as Record<string, unknown>)["delta"];
25-
if (delta === null || typeof delta !== "object") continue;
26-
for (const field of NULL_DELTA_FIELDS) {
27-
if ((delta as Record<string, unknown>)[field] === null) {
28-
Reflect.deleteProperty(delta, field);
29-
normalized = true;
30-
}
31-
}
32-
}
33-
34-
return normalized ? JSON.stringify(parsed) : sseData;
35-
}
36-
378
export function createOpenCodeGoAdapter(source: AdapterSource, quirks?: unknown): ProviderAdapter {
389
const base = createOpenAICompatibleAdapter(source, quirks);
3910
const buildRequest: ProviderAdapter["buildRequest"] = (messages, model, options) => {

‎src/provider/sse-delta-patch.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Chat Completions SSE frames from some backends send `null` for delta fields
2+
// the upstream schema requires to be non-null (`role: string`, `tool_calls:
3+
// array`): DeepSeek via NVIDIA NIM and OpenCode Go both do this. Deleting the
4+
// null fields lets the stock OpenAI adapter parse the frame; fields that
5+
// legitimately accept null (content, reasoning_content, etc.) are left alone.
6+
//
7+
// Shared by the openai-compatible and OpenCode Go adapters so the patch stays
8+
// in one place.
9+
10+
/** Delta fields that must never be null in a valid Chat Completions frame. */
11+
export const NULL_REJECTED_DELTA_FIELDS = ["role", "tool_calls"] as const;
12+
13+
/**
14+
* Delete null-valued non-nullable delta fields from a Chat Completions SSE
15+
* frame. Returns the input unchanged when the payload is not JSON, has no
16+
* `choices[].delta` objects, or contains no null fields to remove.
17+
*/
18+
export function normalizeNullDeltaFields(sseData: string): string {
19+
let parsed: unknown;
20+
try {
21+
parsed = JSON.parse(sseData);
22+
} catch {
23+
return sseData;
24+
}
25+
if (parsed === null || typeof parsed !== "object") return sseData;
26+
27+
const choices = (parsed as Record<string, unknown>)["choices"];
28+
if (!Array.isArray(choices)) return sseData;
29+
30+
let normalized = false;
31+
for (const choice of choices) {
32+
if (choice === null || typeof choice !== "object") continue;
33+
const delta = (choice as Record<string, unknown>)["delta"];
34+
if (delta === null || typeof delta !== "object") continue;
35+
for (const field of NULL_REJECTED_DELTA_FIELDS) {
36+
if ((delta as Record<string, unknown>)[field] === null) {
37+
Reflect.deleteProperty(delta, field);
38+
normalized = true;
39+
}
40+
}
41+
}
42+
43+
return normalized ? JSON.stringify(parsed) : sseData;
44+
}

0 commit comments

Comments
 (0)