11import { getArray , getRecord , getString , isRecord , unknownToMessage } from './json' ;
2+ import type { GatewayProtocol } from './types' ;
23
34export interface GatewayChatToolDefinition {
45 readonly type : 'function' ;
@@ -18,13 +19,15 @@ export interface GatewayToolCall {
1819export interface GatewayChatMessage {
1920 readonly role : 'system' | 'user' | 'assistant' | 'tool' ;
2021 readonly content : string ;
22+ readonly anthropicContent ?: readonly unknown [ ] ;
2123 readonly toolCalls ?: readonly GatewayToolCall [ ] ;
2224 readonly toolCallId ?: string ;
2325}
2426
2527export interface GatewayChatResponse {
2628 readonly text : string ;
2729 readonly toolCalls : readonly GatewayToolCall [ ] ;
30+ readonly anthropicContent ?: readonly unknown [ ] ;
2831}
2932
3033class GatewayHttpError extends Error {
@@ -37,14 +40,32 @@ class GatewayHttpError extends Error {
3740 }
3841}
3942
40- export async function createGatewayChatCompletion ( input : {
43+ interface GatewayChatCompletionInput {
44+ readonly protocol : GatewayProtocol ;
4145 readonly baseUrl : string ;
4246 readonly apiKey : string ;
4347 readonly headers : Readonly < Record < string , string > > ;
4448 readonly model : string ;
49+ readonly maxTokens ?: number ;
4550 readonly messages : readonly GatewayChatMessage [ ] ;
4651 readonly tools : readonly GatewayChatToolDefinition [ ] ;
47- } ) : Promise < GatewayChatResponse > {
52+ }
53+
54+ export function createGatewayChatCompletion (
55+ input : GatewayChatCompletionInput
56+ ) : Promise < GatewayChatResponse > {
57+ if ( input . protocol === 'anthropic' ) {
58+ if ( input . maxTokens === undefined ) {
59+ throw new Error ( 'Gateway Anthropic requests require maxTokens.' ) ;
60+ }
61+ return createAnthropicChatCompletion ( { ...input , maxTokens : input . maxTokens } ) ;
62+ }
63+ return createOpenAIChatCompletion ( input ) ;
64+ }
65+
66+ async function createOpenAIChatCompletion (
67+ input : GatewayChatCompletionInput
68+ ) : Promise < GatewayChatResponse > {
4869 const response = await fetch ( `${ input . baseUrl } /chat/completions` , {
4970 method : 'POST' ,
5071 headers : {
@@ -54,7 +75,8 @@ export async function createGatewayChatCompletion(input: {
5475 } ,
5576 body : JSON . stringify ( {
5677 model : input . model ,
57- messages : input . messages . map ( ( message ) => serializeMessage ( message ) ) ,
78+ ...( input . maxTokens === undefined ? { } : { max_tokens : input . maxTokens } ) ,
79+ messages : input . messages . map ( ( message ) => serializeOpenAIMessage ( message ) ) ,
5880 tools : input . tools ,
5981 tool_choice : 'auto' ,
6082 temperature : 0 ,
@@ -85,7 +107,51 @@ export async function createGatewayChatCompletion(input: {
85107 } ;
86108}
87109
88- function serializeMessage ( message : GatewayChatMessage ) : Record < string , unknown > {
110+ async function createAnthropicChatCompletion (
111+ input : GatewayChatCompletionInput & { readonly maxTokens : number }
112+ ) : Promise < GatewayChatResponse > {
113+ const system = input . messages
114+ . filter ( ( message ) => message . role === 'system' )
115+ . map ( ( message ) => message . content )
116+ . join ( '\n\n' ) ;
117+ const response = await fetch ( `${ input . baseUrl } /v1/messages` , {
118+ method : 'POST' ,
119+ headers : {
120+ 'Content-Type' : 'application/json' ,
121+ 'x-api-key' : input . apiKey ,
122+ 'anthropic-version' : '2023-06-01' ,
123+ ...input . headers ,
124+ } ,
125+ body : JSON . stringify ( {
126+ model : input . model ,
127+ max_tokens : input . maxTokens ,
128+ ...( system ? { system } : { } ) ,
129+ messages : serializeAnthropicMessages ( input . messages ) ,
130+ tools : input . tools . map ( ( tool ) => ( {
131+ name : tool . function . name ,
132+ description : tool . function . description ,
133+ input_schema : tool . function . parameters ,
134+ } ) ) ,
135+ } ) ,
136+ } ) ;
137+
138+ const bodyText = await response . text ( ) ;
139+ const parsed = tryParseJson ( bodyText ) ;
140+ if ( ! response . ok ) {
141+ throw httpError ( response . status , parsed ?? bodyText ) ;
142+ }
143+ if ( ! isRecord ( parsed ) ) {
144+ throw new Error ( 'Gateway returned a non-JSON response.' ) ;
145+ }
146+ const anthropicContent = getArray ( parsed , 'content' ) ;
147+ return {
148+ text : getAnthropicMessageText ( parsed ) ,
149+ toolCalls : getAnthropicToolCalls ( parsed ) ,
150+ anthropicContent,
151+ } ;
152+ }
153+
154+ function serializeOpenAIMessage ( message : GatewayChatMessage ) : Record < string , unknown > {
89155 if ( message . role === 'assistant' && message . toolCalls && message . toolCalls . length > 0 ) {
90156 return {
91157 role : 'assistant' ,
@@ -113,6 +179,48 @@ function serializeMessage(message: GatewayChatMessage): Record<string, unknown>
113179 } ;
114180}
115181
182+ function serializeAnthropicMessages (
183+ messages : readonly GatewayChatMessage [ ]
184+ ) : readonly Record < string , unknown > [ ] {
185+ const result : Record < string , unknown > [ ] = [ ] ;
186+ let pendingToolResults : Record < string , unknown > [ ] | undefined ;
187+ for ( const message of messages ) {
188+ if ( message . role === 'system' ) continue ;
189+ if ( message . role === 'tool' ) {
190+ const toolResult = {
191+ type : 'tool_result' ,
192+ tool_use_id : message . toolCallId ,
193+ content : message . content ,
194+ } ;
195+ if ( pendingToolResults ) {
196+ pendingToolResults . push ( toolResult ) ;
197+ } else {
198+ pendingToolResults = [ toolResult ] ;
199+ result . push ( { role : 'user' , content : pendingToolResults } ) ;
200+ }
201+ continue ;
202+ }
203+ pendingToolResults = undefined ;
204+ if ( message . role === 'assistant' && message . toolCalls && message . toolCalls . length > 0 ) {
205+ result . push ( {
206+ role : 'assistant' ,
207+ content : message . anthropicContent ?? [
208+ ...( message . content ? [ { type : 'text' , text : message . content } ] : [ ] ) ,
209+ ...message . toolCalls . map ( ( toolCall ) => ( {
210+ type : 'tool_use' ,
211+ id : toolCall . id ,
212+ name : toolCall . name ,
213+ input : tryParseJson ( toolCall . argumentsText ) ?? { } ,
214+ } ) ) ,
215+ ] ,
216+ } ) ;
217+ continue ;
218+ }
219+ result . push ( { role : message . role , content : message . content } ) ;
220+ }
221+ return result ;
222+ }
223+
116224function getGatewayMessageText ( message : Record < string , unknown > ) : string {
117225 const content = message . content ;
118226 if ( typeof content === 'string' ) return content ;
@@ -143,6 +251,29 @@ function getGatewayToolCalls(message: Record<string, unknown>): readonly Gateway
143251 return result ;
144252}
145253
254+ function getAnthropicMessageText ( message : Record < string , unknown > ) : string {
255+ return getArray ( message , 'content' )
256+ . filter ( ( item ) => isRecord ( item ) && getString ( item , 'type' ) === 'text' )
257+ . map ( ( item ) => ( isRecord ( item ) ? getString ( item , 'text' ) ?? '' : '' ) )
258+ . join ( '' ) ;
259+ }
260+
261+ function getAnthropicToolCalls ( message : Record < string , unknown > ) : readonly GatewayToolCall [ ] {
262+ const result : GatewayToolCall [ ] = [ ] ;
263+ for ( const item of getArray ( message , 'content' ) ) {
264+ if ( ! isRecord ( item ) || getString ( item , 'type' ) !== 'tool_use' ) continue ;
265+ const id = getString ( item , 'id' ) ;
266+ const name = getString ( item , 'name' ) ;
267+ if ( ! id || ! name ) continue ;
268+ result . push ( {
269+ id,
270+ name,
271+ argumentsText : JSON . stringify ( isRecord ( item . input ) ? item . input : { } ) ,
272+ } ) ;
273+ }
274+ return result ;
275+ }
276+
146277function httpError ( status : number , body : unknown ) : GatewayHttpError {
147278 return new GatewayHttpError ( status , buildGatewayErrorMessage ( status , body ) ) ;
148279}
0 commit comments