Skip to content

Commit 307ebdb

Browse files
rosetta-livekit-bot[bot]davidzhaoclaude
authored
fix(openai): handle top-level code/param on Responses WS error frames (#2024)
Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Co-authored-by: David Zhao <dz@livekit.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 3649e27 commit 307ebdb

4 files changed

Lines changed: 30 additions & 2 deletions

File tree

.changeset/quiet-ws-fields.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/agents-plugin-openai': patch
3+
---
4+
5+
Handle top-level `code` and `param` fields on OpenAI Responses WebSocket error frames so request-validation errors surface cleanly and error-code routing (e.g. retry on `previous_response_not_found`) works for top-level codes.

plugins/openai/src/responses/llm.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,32 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
import { llm, llmStrict } from '@livekit/agents-plugins-test';
5-
import { describe, it } from 'vitest';
5+
import { describe, expect, it } from 'vitest';
6+
import { wsServerEventSchema } from '../ws/types.js';
67
import { LLM } from './llm.js';
78

89
const hasOpenAIApiKey = Boolean(process.env.OPENAI_API_KEY);
910

11+
describe('OpenAI Responses WebSocket', () => {
12+
it('preserves top-level code and param on error frames', () => {
13+
const frame = {
14+
type: 'error',
15+
message:
16+
"Invalid type for 'reasoning.mode': expected one of 'standard' or 'pro', but got null instead.",
17+
code: 'invalid_type',
18+
param: 'reasoning.mode',
19+
status: 400,
20+
};
21+
22+
const parsed = wsServerEventSchema.parse(frame);
23+
24+
expect(parsed.type).toBe('error');
25+
if (parsed.type !== 'error') throw new Error('expected error event');
26+
expect(parsed.message).toBe(frame.message);
27+
expect(parsed.param).toBe('reasoning.mode');
28+
});
29+
});
30+
1031
if (hasOpenAIApiKey) {
1132
describe('OpenAI Responses', async () => {
1233
await llm(

plugins/openai/src/ws/llm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ export class WSLLMStream extends llm.LLMStream {
544544
* (`previous_response_not_found`), throws for all other errors.
545545
*/
546546
#handleError(event: WsServerEvent & { type: 'error' }, conn: ResponsesWebSocket): boolean {
547-
const code = event.error?.code;
547+
const code = event.error?.code ?? event.code;
548548

549549
if (code === 'previous_response_not_found') {
550550
// The server-side in-memory cache was evicted (e.g. after a failed turn

plugins/openai/src/ws/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export const wsResponseFailedEventSchema = z.object({
101101
export const wsErrorEventSchema = z.object({
102102
type: z.literal('error'),
103103
status: z.number().optional(),
104+
code: z.string().optional(),
105+
param: z.string().optional(),
104106
error: z
105107
.object({
106108
type: z.string().optional(),

0 commit comments

Comments
 (0)