Skip to content
Closed
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
4 changes: 3 additions & 1 deletion .speakeasy/in.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12186,7 +12186,9 @@ components:
message: Rate limit exceeded
code: 429
usage:
$ref: '#/components/schemas/ChatUsage'
nullable: true
allOf:
- $ref: '#/components/schemas/ChatUsage'
required:
- id
- choices
Expand Down
4 changes: 2 additions & 2 deletions src/models/chatstreamchunk.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
* @generated-id: 361f940e91e4
Expand Down Expand Up @@ -70,7 +70,7 @@
/**
* Token usage statistics
*/
usage?: ChatUsage | undefined;
usage?: ChatUsage | null | undefined;
};

/** @internal */
Expand Down Expand Up @@ -107,7 +107,7 @@
system_fingerprint: z.string().optional(),
service_tier: z.nullable(z.string()).optional(),
error: z.lazy(() => ErrorT$inboundSchema).optional(),
usage: ChatUsage$inboundSchema.optional(),
usage: z.nullable(ChatUsage$inboundSchema).optional(),
}).transform((v) => {
return remap$(v, {
"system_fingerprint": "systemFingerprint",
Expand Down
120 changes: 120 additions & 0 deletions tests/unit/issue-452-streaming-usage-null.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Regression test for GitHub issue #452
* https://github.com/OpenRouterTeam/ai-sdk-provider/issues/452
* (Moved from OpenRouterTeam/typescript-sdk#146)
*
* Reported error: ZodError invalid_union — "Invalid input: expected object, received null"
* on response.usage path when streaming
*
* This test verifies that streaming schemas accept usage: null,
* since early streaming events (e.g. response.created) and intermediate
* chat completion chunks may have usage: null before final usage is available.
*/
import { describe, it, expect } from 'vitest';
import { ChatStreamChunk$inboundSchema } from '../../src/models/chatstreamchunk.js';
import { StreamEvents$inboundSchema } from '../../src/models/streamevents.js';

describe('Issue #452: streaming usage field accepts null', () => {
describe('ChatStreamChunk schema', () => {
it('should accept usage: null in a streaming chat completion chunk', () => {
const chunk = {
id: 'chatcmpl-test123',
choices: [
{
index: 0,
delta: { content: 'Hello' },
finish_reason: null,
},
],
created: 1712345678,
model: 'anthropic/claude-sonnet-4.5',
object: 'chat.completion.chunk',
usage: null,
};

const result = ChatStreamChunk$inboundSchema.parse(chunk);
expect(result.id).toBe('chatcmpl-test123');
expect(result.usage).toBeNull();
});

it('should accept usage: undefined in a streaming chat completion chunk', () => {
const chunk = {
id: 'chatcmpl-test456',
choices: [
{
index: 0,
delta: { content: 'World' },
finish_reason: null,
},
],
created: 1712345678,
model: 'anthropic/claude-sonnet-4.5',
object: 'chat.completion.chunk',
};

const result = ChatStreamChunk$inboundSchema.parse(chunk);
expect(result.id).toBe('chatcmpl-test456');
expect(result.usage).toBeUndefined();
});

it('should accept a valid usage object in a streaming chat completion chunk', () => {
const chunk = {
id: 'chatcmpl-test789',
choices: [
{
index: 0,
delta: {},
finish_reason: 'stop',
},
],
created: 1712345678,
model: 'anthropic/claude-sonnet-4.5',
object: 'chat.completion.chunk',
usage: {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
},
};

const result = ChatStreamChunk$inboundSchema.parse(chunk);
expect(result.usage).toBeDefined();
expect(result.usage?.promptTokens).toBe(10);
expect(result.usage?.completionTokens).toBe(20);
expect(result.usage?.totalTokens).toBe(30);
});
});

describe('StreamEvents schema (Responses API)', () => {
it('should accept usage: null in a response.created event', () => {
const event = {
type: 'response.created',
response: {
id: 'resp_test123',
object: 'response',
created_at: 1712345678,
model: 'anthropic/claude-sonnet-4.5',
status: 'in_progress',
completed_at: null,
output: [],
error: null,
incomplete_details: null,
usage: null,
temperature: 1,
top_p: 1,
presence_penalty: 0,
frequency_penalty: 0,
instructions: null,
metadata: null,
tools: [],
tool_choice: 'auto',
parallel_tool_calls: true,
},
sequence_number: 0,
};

const result = StreamEvents$inboundSchema.parse(event);
expect(result.type).toBe('response.created');
});
});
});
Loading