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
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veryfront",
"version": "0.1.1240",
"version": "0.1.1241",
"license": "Apache-2.0",
"nodeModulesDir": "auto",
"minimumDependencyAge": {
Expand Down
319 changes: 319 additions & 0 deletions extensions/ext-llm-openai/src/openai-chat-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,325 @@ describe("ext-llm-openai/openai-chat-stream", () => {
}
});

it("treats null reasoning, role, and refusal deltas as absent", async () => {
// Verbatim chunk shape from the Veryfront Cloud Moonshot gateway
// (`kimi-k2.6`): optional delta fields are encoded as `null` rather than
// being omitted, on both the opening chunk and the finish chunk.
assertEquals(
await collectParts(streamFromText([
data({
choices: [{
index: 0,
delta: { reasoning_content: null, role: "assistant", content: "" },
logprobs: null,
finish_reason: null,
matched_stop: null,
}],
}),
data({
choices: [{
index: 0,
delta: { reasoning_content: "weighing it" },
finish_reason: null,
}],
}),
data({
choices: [{
index: 0,
delta: { role: null, refusal: null, content: "hi there friend" },
finish_reason: null,
}],
}),
data({
choices: [{
index: 0,
delta: { reasoning_content: null },
logprobs: null,
finish_reason: "stop",
matched_stop: null,
}],
}),
"data: [DONE]\r\n\r\n",
].join(""))),
[
{ type: "reasoning-start", id: "reasoning-0" },
{ type: "reasoning-delta", id: "reasoning-0", delta: "weighing it" },
{ type: "reasoning-end", id: "reasoning-0" },
{ type: "text-delta", delta: "hi there friend" },
{ type: "finish", finishReason: "stop" },
],
);
});

it("treats null tool-call id, type, name, and arguments as absent", async () => {
// Verbatim tool-call chunk shape from the Veryfront Cloud Moonshot gateway
// (`kimi-k2.6`): only the opening fragment carries `id` and `function.name`;
// every continuation fragment repeats them as `null`.
assertEquals(
await collectParts(streamFromText([
data({
choices: [{
index: 0,
delta: {
tool_calls: [{
id: "functions.list_events:0",
index: 0,
type: "function",
function: { name: "list_events", arguments: '{"date":"' },
}],
},
logprobs: null,
finish_reason: null,
matched_stop: null,
}],
}),
data({
choices: [{
index: 0,
delta: {
tool_calls: [{
id: null,
index: 0,
type: "function",
function: { name: null, arguments: "2026-08" },
}],
},
finish_reason: null,
}],
}),
data({
choices: [{
index: 0,
delta: {
tool_calls: [{
id: null,
index: 0,
type: null,
function: { name: null, arguments: '-18"}' },
}],
},
finish_reason: null,
}],
}),
data({
choices: [{
index: 0,
delta: { reasoning_content: null },
logprobs: null,
finish_reason: "tool_calls",
matched_stop: 163586,
}],
}),
"data: [DONE]\r\n\r\n",
].join(""))),
[
{
type: "tool-input-start",
id: "functions.list_events:0",
toolName: "list_events",
},
{ type: "tool-input-delta", id: "functions.list_events:0", delta: '{"date":"' },
{ type: "tool-input-delta", id: "functions.list_events:0", delta: "2026-08" },
{ type: "tool-input-delta", id: "functions.list_events:0", delta: '-18"}' },
{
type: "tool-call",
toolCallId: "functions.list_events:0",
toolName: "list_events",
input: '{"date":"2026-08-18"}',
},
{
type: "finish",
finishReason: { unified: "tool-calls", raw: "tool_calls" },
},
],
);
});

it("still rejects tool-call fields of a genuinely wrong type", async () => {
await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{ delta: { tool_calls: [{ index: 0, id: 42 }] } }],
}))),
ProviderRequestError,
"tool call id was malformed",
);

await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{
delta: {
tool_calls: [{ index: 0, id: "call_1", function: { name: 7 } }],
},
}],
}))),
ProviderRequestError,
"tool call function name was malformed",
);

await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{
delta: {
tool_calls: [{
index: 0,
id: "call_1",
function: { name: "lookup", arguments: 5 },
}],
},
}],
}))),
ProviderRequestError,
"tool call arguments delta was malformed",
);

await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{
delta: { tool_calls: [{ index: 0, id: "call_1", type: "not_function" }] },
}],
}))),
ProviderRequestError,
"tool call type was not function",
);
});

it("keeps tool-call identity and ordering guards intact when fields are null", async () => {
// Null must behave exactly like an omitted field: it may not let a call
// change identity, collide at another index, skip the id/name ordering
// rule, or emit a tool call that never received an id, name, or arguments.
await assertRejects(
() =>
collectParts(streamFromText([
data({
choices: [{
delta: {
tool_calls: [{
index: 0,
id: null,
type: null,
function: { name: null, arguments: null },
}],
},
}],
}),
data({ choices: [{ delta: {}, finish_reason: "tool_calls" }] }),
"data: [DONE]\r\n\r\n",
].join(""))),
ProviderRequestError,
"tool call was incomplete",
);

await assertRejects(
() =>
collectParts(streamFromText([
data({
choices: [{
delta: {
tool_calls: [{
index: 0,
id: "call_a",
function: { name: "lookup", arguments: "{}" },
}],
},
}],
}),
data({
choices: [{
delta: {
tool_calls: [{ index: 0, id: "call_b", function: { arguments: "" } }],
},
}],
}),
].join(""))),
ProviderRequestError,
"tool call id changed while streaming",
);

await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{
delta: {
tool_calls: [{
index: 0,
id: null,
function: { name: null, arguments: "{}" },
}],
},
}],
}))),
ProviderRequestError,
"tool call arguments arrived before its id and name",
);

// A null fragment must not launder an id into a second index.
await assertRejects(
() =>
collectParts(streamFromText([
data({
choices: [{
delta: {
tool_calls: [{
index: 0,
id: "call_dup",
function: { name: "lookup", arguments: "{}" },
}],
},
}],
}),
data({
choices: [{
delta: {
tool_calls: [{ index: 1, id: null, function: { name: null } }],
},
}],
}),
data({
choices: [{
delta: {
tool_calls: [{ index: 1, id: "call_dup", function: { name: "other" } }],
},
}],
}),
].join(""))),
ProviderRequestError,
"tool call id was reused at another index",
);
});

it("still rejects reasoning and role deltas of a genuinely wrong type", async () => {
await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{ delta: { reasoning_content: 42 } }],
}))),
ProviderRequestError,
"reasoning delta was malformed",
);

await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{ delta: { reasoning_content: { text: "think" } } }],
}))),
ProviderRequestError,
"reasoning delta was malformed",
);

await assertRejects(
() =>
collectParts(streamFromText(data({
choices: [{ delta: { role: "user", content: "hi" } }],
}))),
ProviderRequestError,
"choice delta role was not assistant",
);
});

it("rejects structurally empty and unterminated successful streams", async () => {
await assertRejects(
() => collectParts(streamFromText(data({}))),
Expand Down
24 changes: 18 additions & 6 deletions extensions/ext-llm-openai/src/openai-chat-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,14 +300,19 @@ export async function* streamOpenAICompatibleParts(
delta = deltaRecord;
}

// Optional delta fields are `null` rather than absent on many
// OpenAI-compatible gateways (Moonshot/Kimi sends `reasoning_content: null`
// on both the opening and the finish chunk). Treat `null` as "not present
// on this chunk", matching how `content`, `refusal`, `tool_calls`, and
// `finish_reason` are already handled below.
if (
delta.role !== undefined &&
delta.role !== undefined && delta.role !== null &&
delta.role !== "assistant"
) {
throw invalidOpenAIStream(context, "choice delta role was not assistant");
}
if (
delta.reasoning_content !== undefined &&
delta.reasoning_content !== undefined && delta.reasoning_content !== null &&
typeof delta.reasoning_content !== "string"
) {
throw invalidOpenAIStream(context, "reasoning delta was malformed");
Expand Down Expand Up @@ -364,8 +369,12 @@ export async function* streamOpenAICompatibleParts(
throw invalidOpenAIStream(context, "tool call index was malformed");
}
const toolCallIndex = index as number;
// As with the reasoning delta above, gateways repeat `id`, `type`, and
// `function.name` as `null` on continuation fragments instead of omitting
// them; only the opening fragment carries real values. Treat `null` as
// absent so the fragment merges into the call already being assembled.
if (
toolCallRecord.id !== undefined &&
toolCallRecord.id !== undefined && toolCallRecord.id !== null &&
!isBoundedOpenAIStreamString(
toolCallRecord.id,
MAX_OPENAI_STREAM_IDENTIFIER_BYTES,
Expand All @@ -374,7 +383,7 @@ export async function* streamOpenAICompatibleParts(
throw invalidOpenAIStream(context, "tool call id was malformed");
}
if (
toolCallRecord.type !== undefined &&
toolCallRecord.type !== undefined && toolCallRecord.type !== null &&
toolCallRecord.type !== "function"
) {
throw invalidOpenAIStream(context, "tool call type was not function");
Expand Down Expand Up @@ -410,7 +419,7 @@ export async function* streamOpenAICompatibleParts(
throw invalidOpenAIStream(context, "tool call function was not an object");
}
if (
fn.name !== undefined &&
fn.name !== undefined && fn.name !== null &&
!isBoundedOpenAIStreamString(fn.name, MAX_OPENAI_STREAM_TOOL_NAME_BYTES)
) {
throw invalidOpenAIStream(context, "tool call function name was malformed");
Expand All @@ -431,7 +440,10 @@ export async function* streamOpenAICompatibleParts(
};
}

if (fn.arguments !== undefined && typeof fn.arguments !== "string") {
if (
fn.arguments !== undefined && fn.arguments !== null &&
typeof fn.arguments !== "string"
) {
throw invalidOpenAIStream(context, "tool call arguments delta was malformed");
}
if (typeof fn.arguments === "string") {
Expand Down
Loading