Skip to content

fix(gateway): log missing tool_call_id as client_error - #2748

Merged
steebchen merged 2 commits into
mainfrom
fix/missing-tool-call-id-log
Jun 21, 2026
Merged

steebchen merged 2 commits into
mainfrom
fix/missing-tool-call-id-log

Conversation

@steebchen

@steebchen steebchen commented Jun 18, 2026 •

Copy link
Copy Markdown
Member

Problem

When a tool message reached the OpenAI Responses API transform without a tool_call_id, transformMessagesForResponsesApi threw a plain Error. app.onError mapped this to a generic 500, and crucially no log row was written, so the rejected request never appeared in the user's activity history.

Fix

Introduce a reusable typed RequestError in prepare-request-body.ts (exported via the package barrel) for invalid client requests detected before hitting the upstream provider. It carries a statusCode (default 400) so it can be reused for other similar pre-upstream validation failures.

  • Throw RequestError for the missing tool_call_id case.
  • In chat.ts, include it in the prepareRequestBody catch block so a client_error insertLog entry is written, using the error's statusCode.
  • In app.ts, map it to the carried status code instead of a generic 500.

The rejected request now shows up in history as a client error, like the other pre-upstream input validation errors.

Test

Added a unit test asserting prepareRequestBody rejects with RequestError for a tool message lacking tool_call_id under the Responses API.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request validation with specific HTTP status codes and clearer error messages for invalid inputs
    • Enhanced message validation to catch missing required fields before processing
  • Tests

    • Added validation test coverage for message format requirements

The Responses API transform threw a plain Error when a tool message
lacked tool_call_id, which app.onError mapped to a generic 500 with no
log row. Introduce a typed MissingToolCallIdError so the gateway maps it
to a 400 and writes a client_error insertLog entry, surfacing the
rejected request in activity history like the other pre-upstream input
errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Introduces an exported RequestError class (with a statusCode field, defaulting to 400) in packages/actions/src/prepare-request-body.ts. A previously generic Error thrown when a tool-role message lacks tool_call_id is replaced with RequestError. The gateway chat handler and top-level error hook are updated to detect and surface this typed error with its status code.

Changes

RequestError typed validation error

Layer / File(s) Summary
RequestError class, throw site, and test
packages/actions/src/prepare-request-body.ts, packages/actions/src/prepare-request-body.spec.ts
Exports RequestError extends Error with a statusCode (default 400). Replaces the generic Error throw when a tool-role message is missing tool_call_id with RequestError. Adds a Vitest test asserting that prepareRequestBody rejects with RequestError for that input.
Gateway chat handler and top-level error hook
apps/gateway/src/chat/chat.ts, apps/gateway/src/app.ts
Imports RequestError in both files. In chat.ts, extends the prepareRequestBody catch block to detect RequestError and derive statusCode from it, using that value in the logged errorDetails. In app.ts, adds an app.onError branch that logs an "Invalid request" warning and calls renderGatewayError with the error's statusCode and message.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • theopenco/llmgateway#2532: Both PRs modify apps/gateway/src/app.ts's app.onError error-dispatching logic to render standardized gateway error envelopes; this PR inserts a new RequestError-specific branch into that same handler.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 title references 'missing tool_call_id' and 'client_error', which are central to the changes—the PR introduces RequestError handling for missing tool_call_id cases and ensures they log as client errors with 400 status instead of 500.
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 fix/missing-tool-call-id-log

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.

Replace MissingToolCallIdError with a generic RequestError carrying a
statusCode (default 400) so it can be reused for other pre-upstream
request validation failures. The gateway maps it to the carried status
and writes a client_error log row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (2)
packages/actions/src/prepare-request-body.ts (1)

39-43: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Constrain RequestError.statusCode to the client-error range.

statusCode is currently accepted verbatim, then used for response rendering/log classification downstream. Guarding to 400-499 keeps this type aligned with pre-upstream client validation failures.

Suggested change
 export class RequestError extends Error {
 	public readonly statusCode: number;
 	public constructor(message: string, statusCode = 400) {
 		super(message);
 		this.name = "RequestError";
-		this.statusCode = statusCode;
+		if (!Number.isInteger(statusCode) || statusCode < 400 || statusCode > 499) {
+			throw new RangeError(
+				"RequestError statusCode must be an integer between 400 and 499",
+			);
+		}
+		this.statusCode = statusCode;
 	}
 }
🤖 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 `@packages/actions/src/prepare-request-body.ts` around lines 39 - 43, The
RequestError constructor accepts any statusCode value without validation, but it
should be constrained to the client-error range of 400-499 to ensure proper
response rendering and log classification downstream. Add validation in the
RequestError constructor to ensure the statusCode parameter is within the
400-499 range, either by rejecting values outside this range or
defaulting/normalizing invalid values to a valid client-error code like 400.
packages/actions/src/prepare-request-body.spec.ts (1)

511-545: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Assert statusCode in addition to error type.

The regression currently checks only the class instance. Add a statusCode: 400 assertion to lock the new contract introduced by RequestError.

🤖 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 `@packages/actions/src/prepare-request-body.spec.ts` around lines 511 - 545,
The test function "should throw a typed RequestError for tool messages without
tool_call_id" currently only validates that the thrown error is an instance of
RequestError using toBeInstanceOf. To properly lock in the contract, add an
additional assertion to verify that the error has a statusCode property set to
400. Update the expect block to check both the error type and the statusCode
value together.
🤖 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.

Nitpick comments:
In `@packages/actions/src/prepare-request-body.spec.ts`:
- Around line 511-545: The test function "should throw a typed RequestError for
tool messages without tool_call_id" currently only validates that the thrown
error is an instance of RequestError using toBeInstanceOf. To properly lock in
the contract, add an additional assertion to verify that the error has a
statusCode property set to 400. Update the expect block to check both the error
type and the statusCode value together.

In `@packages/actions/src/prepare-request-body.ts`:
- Around line 39-43: The RequestError constructor accepts any statusCode value
without validation, but it should be constrained to the client-error range of
400-499 to ensure proper response rendering and log classification downstream.
Add validation in the RequestError constructor to ensure the statusCode
parameter is within the 400-499 range, either by rejecting values outside this
range or defaulting/normalizing invalid values to a valid client-error code like
400.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1abd7be6-5d86-4f06-b87f-45dd7c7552d5

📥 Commits

Reviewing files that changed from the base of the PR and between 07154f0 and b9d2534.

📒 Files selected for processing (4)
  • apps/gateway/src/app.ts
  • apps/gateway/src/chat/chat.ts
  • packages/actions/src/prepare-request-body.spec.ts
  • packages/actions/src/prepare-request-body.ts

@steebchen
steebchen enabled auto-merge June 21, 2026 22:31
@steebchen
steebchen added this pull request to the merge queue Jun 21, 2026
Merged via the queue into main with commit 45f7e22 Jun 21, 2026
18 checks passed
@steebchen
steebchen deleted the fix/missing-tool-call-id-log branch June 21, 2026 22:48
steebchen added a commit that referenced this pull request Jun 24, 2026
Tool result messages are converted to Responses API function_call_output
items, which require a call_id referencing a prior function_call. #2748
made the missing-tool_call_id case a clean 400 (RequestError) instead of
a 500, but still rejected recoverable requests.

The Chat Completions spec requires tool_call_id on tool messages, but
real callers omit it: legacy `function` role results carry only `name`,
and some clients drop the id when replaying history. Recover the id from
preceding unmatched function calls, but only when the pairing is
unambiguous (explicit id, unique name match, or exactly one unmatched
call); ambiguous cases throw RequestError rather than risk attaching a
result to the wrong call. Also handle the legacy `function` role.

Reuses the existing RequestError (no new error class / wiring).

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

1 participant