Skip to content

fix: emit stream errors in the client protocol - #2355

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.0from
dhaern:fix/protocol-aware-stream-errors
May 18, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.0from
dhaern:fix/protocol-aware-stream-errors

Conversation

@dhaern

@dhaern dhaern commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Make disconnect-aware stream error serialization protocol-aware instead of always emitting Chat Completions chunks.
  • Preserve the existing Chat Completions error shape for Chat Completions clients.
  • Emit OpenAI Responses-compatible response.failed SSE events for Responses clients, including the legacy openai-response alias.
  • Emit Anthropic/Claude-style event: error SSE events for Claude clients.

Motivation

When an upstream stream fails after the response pipeline has already been selected, createDisconnectAwareStream() currently serializes every mid-stream failure as a Chat Completions chunk:

{
  "object": "chat.completion.chunk",
  "choices": [{ "delta": {}, "finish_reason": "error" }],
  "error": { "type": "upstream_error" }
}

That is valid only for Chat Completions clients. For /v1/responses clients, this leaks a Chat Completions chunk into a Responses stream and strict clients fail schema validation instead of receiving a normal protocol error event.

Scope

This is intentionally limited to the shared stream wrapper:

  • chatCore now passes the selected clientResponseFormat into the stream controller.
  • streamHandler chooses the error SSE envelope based on that client format.
  • Chat Completions remains backward-compatible and still receives the existing [DONE] termination.
  • Responses clients receive response.failed events with OpenAI-like error type/code mapping.
  • Claude clients receive protocol-native event: error payloads without Chat Completions fields.

Validation

  • node --import tsx --test tests/unit/stream-handler.test.ts
  • git diff --check
  • Commit hooks: prettier, eslint, docs/env sync checks, any-budget check, CLI i18n checks

Note: npm run typecheck:core currently fails on unrelated existing errors in open-sse/services/contextManager.ts (Argument of type ... is not assignable to parameter of type 'never'). No touched file was reported in that failure.

@dhaern
dhaern requested a review from diegosouzapw as a code owner May 18, 2026 03:18

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces protocol-specific error handling for SSE streams, specifically adding support for OpenAI Responses and Claude formats. It includes a new buildStreamErrorChunks utility to generate format-appropriate error events and updates the stream controller to track the requested client response format. Comprehensive unit tests have been added to verify these changes. Feedback highlights an opportunity to refactor redundant status code mapping logic and notes a potential risk of SSE format breakage if error messages contain raw newlines.

Comment on lines +32 to +53
function responsesErrorType(statusCode: number): string {
if (statusCode === 429) return "rate_limit_error";
if (statusCode === 401 || statusCode === 403) return "authentication_error";
if (statusCode >= 400 && statusCode < 500) return "invalid_request_error";
return "server_error";
}

function responsesErrorCode(statusCode: number): string {
if (statusCode === 429) return "rate_limit_exceeded";
if (statusCode === 401) return "invalid_authentication";
if (statusCode === 403) return "permission_denied";
if (statusCode >= 400 && statusCode < 500) return "bad_request";
return "server_error";
}

function claudeErrorType(statusCode: number): string {
if (statusCode === 429) return "rate_limit_error";
if (statusCode === 401) return "authentication_error";
if (statusCode === 403) return "permission_error";
if (statusCode >= 400 && statusCode < 500) return "invalid_request_error";
return "api_error";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for mapping HTTP status codes to protocol-specific error types and codes is repeated across responsesErrorType, responsesErrorCode, and claudeErrorType. While these protocols have slight differences, a more unified mapping structure or a shared helper could improve maintainability and reduce the risk of inconsistencies as more protocols are added.

Comment on lines +184 to +232
if (isResponsesClientFormat(clientResponseFormat)) {
const errorEvent = {
type: "response.failed",
response: {
id: null,
status: "failed",
error: {
message: errorMsg,
type: responsesErrorType(statusCode),
code: responsesErrorCode(statusCode),
},
},
};

return [encoder.encode(`event: response.failed\ndata: ${JSON.stringify(errorEvent)}\n\n`)];
}

if (clientResponseFormat === FORMATS.CLAUDE) {
const errorEvent = {
type: "error",
error: {
type: claudeErrorType(statusCode),
message: errorMsg,
},
};

return [encoder.encode(`event: error\ndata: ${JSON.stringify(errorEvent)}\n\n`)];
}

const errorEvent = {
object: "chat.completion.chunk",
choices: [
{
index: 0,
delta: {},
finish_reason: "error",
},
],
error: {
message: errorMsg,
type: "upstream_error",
code: statusCode,
},
};

return [
encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`),
encoder.encode(`data: [DONE]\n\n`),
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In buildStreamErrorChunks, the SSE event construction uses manual string concatenation with \n\n and JSON.stringify. While functional, ensure that errorMsg does not contain characters that could break the SSE format (e.g., raw newlines). Although JSON.stringify handles this for the data: field, the event: field or any future fields might be vulnerable if not carefully handled.

@diegosouzapw
diegosouzapw merged commit f636442 into cheaper-inference:release/v3.8.0 May 18, 2026
2 of 3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @dhaern! Excellent fix for the stream error protocol compliance. The tests are solid. This has been merged into the upcoming v3.8.0 release.

diegosouzapw added a commit that referenced this pull request May 18, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants