Skip to content

fix(gateway): openai-compatible error envelopes - #2532

Merged
steebchen merged 5 commits into
mainfrom
openai-compatible-error-format
Jun 4, 2026
Merged

steebchen merged 5 commits into
mainfrom
openai-compatible-error-format

Conversation

@steebchen

@steebchen steebchen commented Jun 4, 2026 •

Copy link
Copy Markdown
Member

Problem

The Chat Completions endpoint (and the other OpenAI-compatible endpoints) is supposed to be OpenAI-compatible for everything, but gateway-level errors were not. Errors raised by the gateway itself — auth failure, usage/rate limit, validation, timeouts — came back as:

{"error":true,"status":401,"message":"Unauthorized: LLMGateway API key reached its usage limit."}

instead of OpenAI's envelope:

{"error":{"message":"...","type":"invalid_request_error","param":null,"code":"invalid_api_key"}}

This breaks OpenAI SDK clients, which expect error to be an object with message/type/param/code. The format was also inconsistent internally: some endpoints (embeddings, images, parts of chat) already returned the OpenAI shape, while everything flowing through the global error handler did not.

Fix

  • New shared helper apps/gateway/src/lib/error-response.ts with buildOpenAIErrorBody / buildAnthropicErrorBody and status→type/code mappings.
  • The gateway's global app.onError handler now renders every error through these builders:
    • OpenAI shape {error: {message, type, param, code}} for all OpenAI-compatible endpoints (chat, embeddings, images, models, moderations, responses, videos).
    • Anthropic shape {type: "error", error: {type, message}} for /v1/messages (path-aware, so the Anthropic Messages API stays Anthropic-compatible).
  • Migrated the remaining ad-hoc {error: true, ...} returns (Anthropic non-streaming passthrough, moderations fallback) to the same builders.

Status → error mapping (OpenAI)

Status type code
400 invalid_request_error null
401 invalid_request_error invalid_api_key
402 invalid_request_error billing_error
403 invalid_request_error permission_denied
404 invalid_request_error not_found
408/504 timeout_error timeout
413 invalid_request_error request_too_large
429 rate_limit_error rate_limit_exceeded
5xx api_error null

Tests

  • New error-response.spec.ts unit tests for the builders/mappings.
  • Strengthened the missing-Authorization test to assert the OpenAI envelope.
  • Updated existing gateway error assertions from json.message → json.error.message (api.spec.ts, chat-custom-provider.e2e.ts).
  • pnpm build and pnpm format pass; affected unit tests pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Standardized error responses so endpoints return consistent OpenAI- or Anthropic-compatible error envelopes (including stream vs pre-stream failures).
  • Refactor

    • Centralized gateway error rendering and unified on-error handling across endpoints and error types.
  • Tests

    • Updated and added unit and E2E assertions to validate the new error envelope shapes and mappings.
  • Documentation

    • Added docs describing the error format, status-to-type mappings, streaming behavior, and backward-compatibility fields.

Gateway-level errors (auth, usage/rate limit, validation, timeouts) were
returned as `{error: true, status, message}` instead of OpenAI's
`{error: {message, type, param, code}}`, breaking OpenAI SDK clients.

Route the global error handler and the remaining ad-hoc error returns
through shared builders: OpenAI shape for chat/embeddings/images/etc and
Anthropic's `{type: "error", error: {type, message}}` for /v1/messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 4, 2026 20:07
@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 05bfccb3-83fb-4305-b067-54c3ce4788fc

📥 Commits

Reviewing files that changed from the base of the PR and between 6c3bacf and b66012d.

📒 Files selected for processing (4)
  • apps/docs/content/resources/error-handling.mdx
  • apps/gateway/src/anthropic/anthropic.ts
  • apps/gateway/src/api.spec.ts
  • apps/gateway/src/app.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/docs/content/resources/error-handling.mdx

Walkthrough

Adds OpenAI/Anthropic error-body builders, refactors app.onError to emit provider-shaped error envelopes, updates Anthropic and Moderations handlers to use the builders, and adjusts API/E2E tests and docs to expect the structured error envelope.

Changes

Standardized Gateway Error Responses

Layer / File(s) Summary
Error response types and mapping utilities
apps/gateway/src/lib/error-response.ts
Defines OpenAIErrorBody and AnthropicErrorBody interfaces, status→error-type/code mapping helpers, and exports buildOpenAIErrorBody / buildAnthropicErrorBody builders that include legacy message/status fields.
Error response utility tests
apps/gateway/src/lib/error-response.spec.ts
Tests builder outputs and status-to-meta mappings for OpenAI and Anthropic envelopes, and verifies explicit override behavior for code/param/type.
Gateway error handler refactor
apps/gateway/src/app.ts
Adds renderGatewayError and updates app.onError to return provider-compatible envelopes (Anthropic for /v1/messages, OpenAI otherwise) for unsupported formats, HTTP exceptions, timeouts, client aborts, and generic errors.
Provider endpoint error updates
apps/gateway/src/anthropic/anthropic.ts, apps/gateway/src/moderations/moderations.ts
Replaces inline upstream-error JSON construction with buildAnthropicErrorBody (Anthropic chat) and buildOpenAIErrorBody (Moderations) to standardize upstream-failure responses.
Test assertion updates
apps/gateway/src/api.spec.ts, apps/gateway/src/chat-custom-provider.e2e.ts
Updates assertions to check json.error.message and structured json.error fields (type, param, code) instead of top-level json.message across multiple failure tests.
Docs and meta
apps/docs/content/resources/error-handling.mdx, apps/docs/content/resources/rate-limits.mdx, apps/docs/content/resources/meta.json
Adds an error-handling doc describing the OpenAI-compatible envelope, links rate-limit note to it, and adds resources meta.json.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant appOnError
  participant renderGatewayError
  participant buildOpenAIErrorBody
  participant buildAnthropicErrorBody
  Client->>Router: request triggers handler (may throw)
  Router->>appOnError: error delivered
  appOnError->>renderGatewayError: renderGatewayError(ctx, error, status)
  alt path starts with /v1/messages
    renderGatewayError->>buildAnthropicErrorBody: buildAnthropicErrorBody(message, status)
    buildAnthropicErrorBody-->>renderGatewayError: Anthropic envelope
  else
    renderGatewayError->>buildOpenAIErrorBody: buildOpenAIErrorBody(message, status)
    buildOpenAIErrorBody-->>renderGatewayError: OpenAI envelope
  end
  renderGatewayError-->>appOnError: standardized envelope
  appOnError-->>Router: c.json(response)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • theopenco/llmgateway#2271: Related changes to Anthropic streaming/non-OK failure handling and emitting standardized Anthropic SSE error events.

Suggested reviewers

  • smakosh
  • proxysoul
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the primary change: implementing OpenAI-compatible error envelopes across gateway endpoints, which is the core fix described in the PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch openai-compatible-error-format

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Standardizes gateway-level error responses so OpenAI-compatible endpoints always return an OpenAI-style error envelope, while the Anthropic Messages endpoint (/v1/messages) returns an Anthropic-style error envelope. This improves SDK compatibility and removes remaining ad-hoc error shapes.

Changes:

  • Added shared error envelope builders + status→type/code mappings (buildOpenAIErrorBody, buildAnthropicErrorBody).
  • Updated the global gateway error handler to render errors via these builders (path-aware for /v1/messages).
  • Updated/modded affected endpoints and tests to assert json.error.message and the OpenAI error object shape.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
apps/gateway/src/moderations/moderations.ts Uses the shared OpenAI error envelope builder for moderation upstream/fallback error responses.
apps/gateway/src/lib/error-response.ts Introduces shared OpenAI/Anthropic error envelope builders and HTTP status mappings.
apps/gateway/src/lib/error-response.spec.ts Adds unit tests covering envelope construction and status mappings.
apps/gateway/src/chat-custom-provider.e2e.ts Updates E2E assertions to expect OpenAI envelope (json.error.message).
apps/gateway/src/app.ts Centralizes gateway error rendering via the new builders and switches envelope based on request path (/v1/messages vs others).
apps/gateway/src/api.spec.ts Updates assertions to expect OpenAI envelope and strengthens missing-Authorization expectations.
apps/gateway/src/anthropic/anthropic.ts Migrates non-streaming Anthropic passthrough errors to the Anthropic envelope builder.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/moderations/moderations.ts (1)

133-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize all moderation error payloads to OpenAI envelope and align schema nullability.

This branch still returns raw upstreamJson objects when non-string, which can break the standardized { error: { message, type, param, code } } contract. Also, buildOpenAIErrorBody can emit code: null, but moderationErrorSchema currently requires code: string.

🧩 Proposed fix
 const moderationErrorSchema = z.object({
 	error: z.object({
 		message: z.string(),
 		type: z.string(),
 		param: z.string().nullable(),
-		code: z.string(),
+		code: z.string().nullable(),
 	}),
 });
-		return c.json(
-			(typeof upstreamJson === "string"
-				? buildOpenAIErrorBody({
-						message: upstreamJson,
-						status: upstreamResponse.status,
-					})
-				: upstreamJson) ??
-				buildOpenAIErrorBody({
-					message: "An error occurred",
-					status: upstreamResponse.status,
-				}),
+		const upstreamError =
+			typeof upstreamJson === "object" &&
+			upstreamJson !== null &&
+			"error" in upstreamJson
+				? (upstreamJson as {
+						error?: {
+							message?: unknown;
+							type?: unknown;
+							param?: unknown;
+							code?: unknown;
+						};
+					}).error
+				: undefined;
+
+		const message =
+			typeof upstreamError?.message === "string"
+				? upstreamError.message
+				: typeof upstreamJson === "string"
+					? upstreamJson
+					: "An error occurred";
+
+		return c.json(
+			buildOpenAIErrorBody({
+				message,
+				status: upstreamResponse.status,
+				type: typeof upstreamError?.type === "string" ? upstreamError.type : undefined,
+				param:
+					typeof upstreamError?.param === "string" || upstreamError?.param === null
+						? upstreamError.param
+						: undefined,
+				code:
+					typeof upstreamError?.code === "string" || upstreamError?.code === null
+						? upstreamError.code
+						: undefined,
+			}),
 			upstreamResponse.status as
 				| 400
 				| 401
 				| 403

Also applies to: 654-663

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/gateway/src/moderations/moderations.ts` around lines 133 - 140, The
moderation handler currently returns raw upstreamJson for non-string payloads
and uses a schema that requires code to be a string even though
buildOpenAIErrorBody can emit code: null; update moderationErrorSchema to allow
code: z.string().nullable() and ensure the response path (where upstreamJson is
returned) instead always wraps any non-string upstreamJson into the standardized
OpenAI error envelope via buildOpenAIErrorBody (or an equivalent normalization
function) so all responses conform to { error: { message, type, param, code } };
adjust the other occurrence noted (lines ~654-663) to use the same normalization
and nullable code schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/gateway/src/app.ts`:
- Around line 135-137: In renderGatewayError, remove the unsafe casts to any on
the status passed to c.json and instead import the type ContentfulStatusCode
from "hono/utils/http-status", then coerce once into a local const (e.g. const
jsonStatus = status as ContentfulStatusCode) and pass jsonStatus to both c.json
calls that use buildAnthropicErrorBody and buildOpenAIErrorBody; update the
imports at the top of the file to include the ContentfulStatusCode type and
delete the `as any` usages.

---

Outside diff comments:
In `@apps/gateway/src/moderations/moderations.ts`:
- Around line 133-140: The moderation handler currently returns raw upstreamJson
for non-string payloads and uses a schema that requires code to be a string even
though buildOpenAIErrorBody can emit code: null; update moderationErrorSchema to
allow code: z.string().nullable() and ensure the response path (where
upstreamJson is returned) instead always wraps any non-string upstreamJson into
the standardized OpenAI error envelope via buildOpenAIErrorBody (or an
equivalent normalization function) so all responses conform to { error: {
message, type, param, code } }; adjust the other occurrence noted (lines
~654-663) to use the same normalization and nullable code schema.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 533f34f2-dcce-4a58-a085-ae8f1eed9a40

📥 Commits

Reviewing files that changed from the base of the PR and between e443262 and 4b5dca1.

📒 Files selected for processing (7)
  • apps/gateway/src/anthropic/anthropic.ts
  • apps/gateway/src/api.spec.ts
  • apps/gateway/src/app.ts
  • apps/gateway/src/chat-custom-provider.e2e.ts
  • apps/gateway/src/lib/error-response.spec.ts
  • apps/gateway/src/lib/error-response.ts
  • apps/gateway/src/moderations/moderations.ts

Comment thread apps/gateway/src/app.ts Outdated
steebchen and others added 2 commits June 4, 2026 22:20
Temporarily include the deprecated top-level `message` and `status`
fields alongside the new OpenAI/Anthropic error envelopes so existing
consumers of the old `{error, status, message}` shape keep working
during migration. Both fields are marked deprecated and will be removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the OpenAI-compatible error envelope returned across all
endpoints, the status-code-to-type/code mapping, streaming error
behavior, the Anthropic error shape for /v1/messages, and the
deprecated legacy top-level message/status compat fields. Cross-link
from the rate limits page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c3bacf5ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/gateway/src/app.ts Outdated
if (c.req.path.startsWith("/v1/messages")) {
return c.json(buildAnthropicErrorBody({ message, status }), status as any);
}
return c.json(buildOpenAIErrorBody({ message, status }), status as any);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Anthropic error types for streaming

When /v1/messages handles a streaming request, it forwards to /v1/chat/completions; gateway auth/limit failures from that internal call now come back from this line as an OpenAI envelope (type: invalid_request_error, code: invalid_api_key). The existing Anthropic streaming translator prefers error.type over error.code, so streamed Anthropic clients receive an invalid_request_error event instead of the authentication_error/billing_error shape that the non-streaming path now returns, making SDK error handling inconsistent for the same auth/credit failures. Please either return Anthropic-shaped errors for these internal calls or map the new OpenAI codes/statuses in the streaming translator.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/docs/content/resources/rate-limits.mdx (1)

61-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

429 JSON example should include error.param for shape parity.

The gateway’s OpenAI envelope includes error.param (null when not applicable). This example omits it, which can mislead users about the exact response contract.

Suggested doc patch
 {
 	"error": {
 		"message": "Rate limit exceeded. Try again later.",
 		"type": "rate_limit_error",
+		"param": null,
 		"code": "rate_limit_exceeded"
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/resources/rate-limits.mdx` around lines 61 - 69, The JSON
example for 429 responses in rate-limits.mdx is missing the error.param field
required for parity with the gateway OpenAI envelope; update the example JSON
inside the code block to include "param": null (or the appropriate value when
applicable) under the "error" object so the shape matches the real response
contract used by the API.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/content/resources/error-handling.mdx`:
- Line 32: In the error.param table row update the description to fix the typo
by replacing "parameter-spec't." with a proper phrase such as
"parameter-specific"; specifically change the `error.param` description to read
something like: "The request parameter that caused the error, or `null` when not
parameter-specific." Ensure the backticks and punctuation remain consistent with
surrounding MDX.
- Around line 3-11: Update the doc text under "Error Handling" to avoid claiming
OpenAI-format errors apply universally; change phrases like "across all
endpoints" and "every error" to specify "all OpenAI-compatible endpoints" and
add a brief caveat that some endpoints (e.g., /v1/messages) use provider-native
formats such as Anthropic-native errors. Locate the paragraph that currently
states the OpenAI compatibility, replace the sweeping language with the narrowed
wording, and include a short parenthetical note referencing /v1/messages as an
exception.
- Around line 39-53: The table in error-handling.mdx omits the runtime mapping
for HTTP 499; update the table to include a row for 499 mapping to type
`invalid_request_error` and code `request_cancelled` to match the actual
behavior implemented in apps/gateway/src/lib/error-response.ts (where 499 is
mapped to `invalid_request_error` / `request_cancelled`), or if you prefer not
to assert exhaustiveness, change the table caption/text to “common statuses” to
indicate it’s not exhaustive.

---

Outside diff comments:
In `@apps/docs/content/resources/rate-limits.mdx`:
- Around line 61-69: The JSON example for 429 responses in rate-limits.mdx is
missing the error.param field required for parity with the gateway OpenAI
envelope; update the example JSON inside the code block to include "param": null
(or the appropriate value when applicable) under the "error" object so the shape
matches the real response contract used by the API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 40f54e13-f9f8-44ef-98ce-55f382a75505

📥 Commits

Reviewing files that changed from the base of the PR and between 8d4b25d and 6c3bacf.

📒 Files selected for processing (3)
  • apps/docs/content/resources/error-handling.mdx
  • apps/docs/content/resources/meta.json
  • apps/docs/content/resources/rate-limits.mdx
✅ Files skipped from review due to trivial changes (1)
  • apps/docs/content/resources/meta.json

Comment thread apps/docs/content/resources/error-handling.mdx Outdated
Comment thread apps/docs/content/resources/error-handling.mdx Outdated
Comment thread apps/docs/content/resources/error-handling.mdx
- Replace `as any` status casts in renderGatewayError with Hono's
  ContentfulStatusCode.
- Derive the Anthropic streaming error type from the HTTP status so
  streamed /v1/messages errors match the non-streaming path (previously
  the OpenAI envelope `type` leaked through the translator).
- Docs: narrow OpenAI-format scope wording, note the /v1/messages
  exception, fix a typo, and add 499/504 rows to the status table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

The missing-provider-key test asserted the legacy `{error, status,
message}` body via an inline snapshot. Update it to the OpenAI-compatible
envelope (with the temporary deprecated top-level message/status fields).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@steebchen
steebchen enabled auto-merge June 4, 2026 20:51
@steebchen
steebchen added this pull request to the merge queue Jun 4, 2026
@steebchen
steebchen removed this pull request from the merge queue due to a manual request Jun 4, 2026
@steebchen
steebchen merged commit cb8d2bd into main Jun 4, 2026
17 checks passed
@steebchen
steebchen deleted the openai-compatible-error-format branch June 4, 2026 21:00
steebchen added a commit to RATCHAW/llmgateway that referenced this pull request Jun 4, 2026
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
analogpvt pushed a commit to analogpvt/llmgateway that referenced this pull request Jun 5, 2026
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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