@@ -3,15 +3,20 @@ import type { Context } from "hono"
33import consola from "consola"
44import { streamSSE } from "hono/streaming"
55
6+ import type { ResponsesApiResponse } from "~/routes/responses/types"
7+ import type { Model } from "~/services/copilot/get-models"
8+
69import { awaitApproval } from "~/lib/approval"
710import { fitContext } from "~/lib/context-manager"
811import { checkRateLimit } from "~/lib/rate-limit"
912import { state } from "~/lib/state"
1013import {
1114 createChatCompletions ,
1215 type ChatCompletionChunk ,
16+ type ChatCompletionsPayload ,
1317 type ChatCompletionResponse ,
1418} from "~/services/copilot/create-chat-completions"
19+ import { createResponses } from "~/services/copilot/create-responses"
1520
1621import {
1722 type AnthropicMessagesPayload ,
@@ -22,6 +27,11 @@ import {
2227 translateToAnthropic ,
2328 translateToOpenAI ,
2429} from "./non-stream-translation"
30+ import {
31+ translateAnthropicMessagesToResponses ,
32+ translateResponsesToAnthropicMessage ,
33+ writeResponsesAsAnthropicStream ,
34+ } from "./responses-bridge"
2535import {
2636 translateChunkToAnthropicEvents ,
2737 translateErrorToAnthropicErrorEvent ,
@@ -30,6 +40,8 @@ import {
3040/** Heartbeat interval for SSE keepalive. Claude Code's idle timeout is 90s
3141 * (CLAUDE_STREAM_IDLE_TIMEOUT_MS); 15s gives a 6× safety margin. */
3242const PING_INTERVAL_MS = 15_000
43+ const CHAT_COMPLETIONS_ENDPOINT = "/chat/completions"
44+ const RESPONSES_ENDPOINT = "/responses"
3345
3446type SSEStream = Parameters < Parameters < typeof streamSSE > [ 1 ] > [ 0 ]
3547type ChatCompletionStream = Exclude <
@@ -42,6 +54,10 @@ interface ChatCompletionFlowOptions {
4254 requestId : string
4355}
4456
57+ interface ResponsesFlowOptions extends ChatCompletionFlowOptions {
58+ model : string
59+ }
60+
4561interface ChatCompletionStreamOptions extends ChatCompletionFlowOptions {
4662 response : ChatCompletionStream
4763 streamState : AnthropicStreamState
@@ -81,19 +97,28 @@ export async function handleCompletion(c: Context) {
8197
8298 // Async preprocessing: PDF document block extraction, etc.
8399 const preprocessed = await preprocessAnthropicPayload ( anthropicPayload )
100+ const openAIPayload = translateToOpenAI ( preprocessed )
101+ const model = state . models ?. data . find ( ( m ) => m . id === openAIPayload . model )
84102
85- return await handleChatCompletions ( c , preprocessed , {
103+ if ( shouldUseResponsesForMessages ( model ) ) {
104+ return await handleResponsesMessages ( c , preprocessed , {
105+ clientModel,
106+ model : openAIPayload . model ,
107+ requestId,
108+ } )
109+ }
110+
111+ return await handleChatCompletions ( c , openAIPayload , {
86112 clientModel,
87113 requestId,
88114 } )
89115}
90116
91117async function handleChatCompletions (
92118 c : Context ,
93- payload : AnthropicMessagesPayload ,
119+ openAIPayload : ChatCompletionsPayload ,
94120 options : ChatCompletionFlowOptions ,
95121) {
96- const openAIPayload = translateToOpenAI ( payload )
97122 if ( consola . level >= 4 ) {
98123 consola . debug (
99124 `[${ options . requestId } ] Translated OpenAI request payload:` ,
@@ -125,6 +150,55 @@ async function handleChatCompletions(
125150 return handleStreamingChatCompletion ( c , response , options )
126151}
127152
153+ async function handleResponsesMessages (
154+ c : Context ,
155+ payload : AnthropicMessagesPayload ,
156+ options : ResponsesFlowOptions ,
157+ ) {
158+ if ( state . manualApprove ) {
159+ await awaitApproval ( )
160+ }
161+
162+ const responsesPayload = translateAnthropicMessagesToResponses (
163+ payload ,
164+ options . model ,
165+ )
166+
167+ if ( payload . stream ) {
168+ return streamSSE ( c , async ( stream ) => {
169+ const stopPings = startPings ( stream )
170+ try {
171+ const response = await createResponses ( responsesPayload )
172+ const body = ( await response . json ( ) ) as ResponsesApiResponse
173+ await writeResponsesAsAnthropicStream ( stream , body , options . clientModel )
174+ } catch ( error ) {
175+ await stream . writeSSE ( {
176+ event : "error" ,
177+ data : JSON . stringify (
178+ translateErrorToAnthropicErrorEvent (
179+ error instanceof Error ? error . message : undefined ,
180+ ) ,
181+ ) ,
182+ } )
183+ } finally {
184+ stopPings ( )
185+ }
186+ } )
187+ }
188+
189+ const response = await createResponses ( responsesPayload )
190+ const body = ( await response . json ( ) ) as ResponsesApiResponse
191+ return c . json ( translateResponsesToAnthropicMessage ( body , options . clientModel ) )
192+ }
193+
194+ function shouldUseResponsesForMessages ( model : Model | undefined ) : boolean {
195+ if ( ! model ?. supported_endpoints ) return false
196+ return (
197+ model . supported_endpoints . includes ( RESPONSES_ENDPOINT )
198+ && ! model . supported_endpoints . includes ( CHAT_COMPLETIONS_ENDPOINT )
199+ )
200+ }
201+
128202function handleNonStreamingChatCompletion (
129203 c : Context ,
130204 response : ChatCompletionResponse ,
0 commit comments