feat(gateway): add Anthropic Messages API support to openrouter proxy#1122
feat(gateway): add Anthropic Messages API support to openrouter proxy#1122kilo-code-bot[bot] wants to merge 4 commits intomainfrom
Conversation
| body.stream_options = { ...(body.stream_options || {}), include_usage: true }; | ||
| requestBodyParsed = { kind: 'chat_completions', body }; | ||
| } else if (path === '/messages') { | ||
| const body: GatewayMessagesRequest = JSON.parse(requestBodyText); |
There was a problem hiding this comment.
WARNING: Invalid /messages payloads can crash before you return a 400
This is only a type cast, so a request with messages: null or a non-array value gets through here. Downstream, both classifyAbuse and extractMessagesPromptInfo call .filter() on body.messages, which turns a bad client payload into a 500 instead of invalidRequestResponse().
| const usage = responseJson?.usage as MessagesApiUsage | undefined; | ||
|
|
||
| const responseContent = | ||
| responseJson?.content |
There was a problem hiding this comment.
WARNING: Non-streaming error responses throw during usage parsing
When OpenRouter returns an error JSON payload for /messages, content is usually missing. The unguarded .filter().map().join() chain throws before hasError can be recorded, so failed non-streaming requests skip usage accounting and can bubble an unexpected exception out of countAndStoreUsage.
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Other Observations (not in diff)None. Files Reviewed (11 files)
Reviewed by gpt-5.4-20260305 · 1,329,359 tokens |
| : Array.isArray(systemContent) | ||
| ? systemContent.map(b => b.text).join('\n') | ||
| : null; | ||
| const lastUserMessage = request.body.messages.filter(m => m.role === 'user').at(-1); |
There was a problem hiding this comment.
WARNING: Tool-result turns erase the user prompt for abuse checks
Anthropic tool loops send tool results as a user message. When that is the latest user turn, this picks it, filters out the non-text blocks, and returns null even though an earlier user text prompt is still in history. That leaves the abuse classifier blind on follow-up tool calls.
| const cacheWriteTokens = usage?.cache_creation_input_tokens ?? 0; | ||
|
|
||
| // OpenRouter path: cost fields are present directly in usage | ||
| if (usage?.cost != null || usage?.is_byok != null) { |
There was a problem hiding this comment.
WARNING: Vercel-routed Messages requests are recorded with zero cost
This only understands OpenRouter's extra usage.cost / is_byok fields. The PR also allows GatewayMessagesRequest to route through Vercel for BYOK, and Anthropic-compatible responses there only carry token counts, so these requests fall through to the zero-cost path and market_cost is underreported.
| ? body.system.map(b => b.text).join('\n') | ||
| : ''; | ||
|
|
||
| const lastUserMessage = body.messages.filter(m => m.role === 'user').at(-1); |
There was a problem hiding this comment.
WARNING: Follow-up tool calls lose the logged user prompt
The Messages API encodes tool results as a user message with tool_result blocks. When that message is last, this extractor emits an empty prefix instead of the previous natural-language user turn, so prompt logging and downstream analytics lose the actual request text.
Summary
Adds support for the Anthropic Messages API to the OpenRouter proxy route (
/api/openrouter/messagesand/api/gateway/messages), in addition to the existing OpenAI chat completions (/chat/completions) and Responses API (/responses) support.Key changes:
types.ts: AddedGatewayMessagesRequesttype (Anthropic Messages format withmodel,max_tokens,messages,system,stream,tools, etc.) and extended theGatewayRequestdiscriminated union with themessageskind.route.ts: ExtendedvalidatePathto accept/messages, added body parsing for the messages format, applied the same admin-only guard as the Responses API, handled prompt info extraction and free-model rewriting for the new kind.processUsage.messages.ts(new file): Streaming and non-streaming usage parsing for Anthropic's SSE format (message_start→ input tokens,message_delta→ output tokens + stop reason), with OpenRouter cost field handling mirroring the existing chat completions and responses parsers.processUsage.ts: Wired the newmessagesapi_kind intocountAndStoreUsage.request-helpers.ts:getMaxTokensnow handles the messages kind (returnsmax_tokens).api-metrics.server.ts:getToolsAvailableandgetToolsUsedhandle Anthropic tool format (tools have a top-levelname, tool use appears astool_usecontent blocks in assistant messages).abuse-service.ts:extractFullPromptshandles the messages kind (top-levelsystemfield + user message content extraction).providers/index.ts:openRouterRequestbody parameter type updated to includeGatewayMessagesRequest.The Messages API endpoint is gated behind
is_admin(same as the Responses API) while it's experimental.Verification
message_startcarriesusage.input_tokens,message_deltacarriesusage.output_tokens.Visual Changes
N/A
Reviewer Notes
/messages) is forwarded as-is to OpenRouter at${provider.apiUrl}/messages— OpenRouter supports the native Anthropic Messages API format at this path.wrapInSafeNextResponse.customLlmRequestonly handles chat completions.applyAnthropicModelSettingsare already guarded behindkind === 'chat_completions'; clients using the native Messages API are responsible for their own cache control markup.