@@ -16,6 +16,7 @@ import {
1616 normalizeToolChoiceForCapability ,
1717 type YagrModelCapabilityProfile ,
1818} from './model-capabilities.js' ;
19+ import { getCachedProviderModelMetadata , primeProviderModelMetadata } from './provider-metadata.js' ;
1920
2021export const DEFAULT_COPILOT_API_BASE_URL = 'https://api.individual.githubcopilot.com' ;
2122export const GITHUB_COPILOT_DEFAULT_MODEL = 'gpt-4.1' ;
@@ -286,20 +287,53 @@ async function runGitHubCopilotCompletion(
286287 const regularMode = options . mode . type === 'regular' ? options . mode : undefined ;
287288 const tools = getFunctionTools ( options . mode , capabilityProfile ) ;
288289 const warnings = buildCopilotWarnings ( options , tools ) ;
290+ const endpoint = await resolveCopilotEndpoint ( modelId , runtimeAuth . token , runtimeAuth . baseUrl ) ;
291+ const execution = endpoint === '/responses'
292+ ? await runCopilotResponsesCompletion ( modelId , options . prompt , tools , regularMode ?. toolChoice , capabilityProfile , runtimeAuth )
293+ : await runCopilotChatCompletions ( modelId , options . prompt , tools , regularMode ?. toolChoice , capabilityProfile , runtimeAuth ) ;
294+ return {
295+ ...execution ,
296+ warnings,
297+ } ;
298+ }
299+
300+ async function resolveCopilotEndpoint ( modelId : string , token : string , baseUrl : string ) : Promise < '/chat/completions' | '/responses' > {
301+ const cached = getCachedProviderModelMetadata ( 'copilot-proxy' , modelId ) ;
302+ const cachedEndpoints = cached ?. supportedEndpoints ?? [ ] ;
303+ if ( cachedEndpoints . includes ( '/responses' ) && ! cachedEndpoints . includes ( '/chat/completions' ) ) {
304+ return '/responses' ;
305+ }
306+
307+ await primeProviderModelMetadata ( 'copilot-proxy' , modelId , token , baseUrl ) . catch ( ( ) => undefined ) ;
308+ const metadata = getCachedProviderModelMetadata ( 'copilot-proxy' , modelId ) ;
309+ const supportedEndpoints = metadata ?. supportedEndpoints ?? [ ] ;
310+ if ( supportedEndpoints . includes ( '/responses' ) && ! supportedEndpoints . includes ( '/chat/completions' ) ) {
311+ return '/responses' ;
312+ }
313+
314+ return '/chat/completions' ;
315+ }
316+
317+ async function runCopilotChatCompletions (
318+ modelId : string ,
319+ prompt : LanguageModelV1Prompt ,
320+ tools : LanguageModelV1FunctionTool [ ] ,
321+ toolChoice : LanguageModelV1ToolChoice | undefined ,
322+ capabilityProfile : YagrModelCapabilityProfile | undefined ,
323+ runtimeAuth : { token : string ; baseUrl : string } ,
324+ ) : Promise < {
325+ text : string ;
326+ finishReason : 'stop' | 'error' | 'tool-calls' | 'length' | 'content-filter' | 'other' | 'unknown' ;
327+ usage : { promptTokens : number ; completionTokens : number } ;
328+ toolCalls ?: LanguageModelV1FunctionToolCall [ ] ;
329+ } > {
289330 const response = await fetch ( `${ runtimeAuth . baseUrl } /chat/completions` , {
290331 method : 'POST' ,
291- headers : {
292- Authorization : `Bearer ${ runtimeAuth . token } ` ,
293- 'Content-Type' : 'application/json' ,
294- Accept : 'application/json' ,
295- 'User-Agent' : COPILOT_USER_AGENT ,
296- 'Editor-Version' : COPILOT_EDITOR_VERSION ,
297- 'Editor-Plugin-Version' : COPILOT_EDITOR_PLUGIN_VERSION ,
298- } ,
332+ headers : buildCopilotHeaders ( runtimeAuth . token ) ,
299333 body : JSON . stringify ( {
300334 model : modelId ,
301- messages : toOpenAiMessages ( options . prompt ) ,
302- ...( tools . length > 0 ? { tools : toOpenAiTools ( tools ) , tool_choice : toOpenAiToolChoice ( regularMode ?. toolChoice , capabilityProfile ) } : { } ) ,
335+ messages : toOpenAiMessages ( prompt ) ,
336+ ...( tools . length > 0 ? { tools : toOpenAiTools ( tools ) , tool_choice : toOpenAiToolChoice ( toolChoice , capabilityProfile ) } : { } ) ,
303337 stream : false ,
304338 } ) ,
305339 } ) ;
@@ -311,13 +345,65 @@ async function runGitHubCopilotCompletion(
311345
312346 const payload = await response . json ( ) as Record < string , unknown > ;
313347 const toolCalls = extractOpenAiToolCalls ( payload ) ;
314- const finishReason = normalizeFinishReason ( payload , toolCalls ) ;
348+ const finishReason = normalizeChatCompletionsFinishReason ( payload , toolCalls ) ;
315349 return {
316- text : extractCopilotText ( payload ) ,
350+ text : extractChatCompletionsText ( payload ) ,
317351 finishReason,
318- usage : extractOpenAiUsage ( payload ) ,
352+ usage : extractChatCompletionsUsage ( payload ) ,
319353 ...( toolCalls . length > 0 ? { toolCalls } : { } ) ,
320- warnings,
354+ } ;
355+ }
356+
357+ async function runCopilotResponsesCompletion (
358+ modelId : string ,
359+ prompt : LanguageModelV1Prompt ,
360+ tools : LanguageModelV1FunctionTool [ ] ,
361+ toolChoice : LanguageModelV1ToolChoice | undefined ,
362+ capabilityProfile : YagrModelCapabilityProfile | undefined ,
363+ runtimeAuth : { token : string ; baseUrl : string } ,
364+ ) : Promise < {
365+ text : string ;
366+ finishReason : 'stop' | 'error' | 'tool-calls' | 'length' | 'content-filter' | 'other' | 'unknown' ;
367+ usage : { promptTokens : number ; completionTokens : number } ;
368+ toolCalls ?: LanguageModelV1FunctionToolCall [ ] ;
369+ } > {
370+ const { instructions, input } = convertPromptToResponsesInput ( prompt ) ;
371+ const response = await fetch ( `${ runtimeAuth . baseUrl } /responses` , {
372+ method : 'POST' ,
373+ headers : buildCopilotHeaders ( runtimeAuth . token ) ,
374+ body : JSON . stringify ( {
375+ model : modelId ,
376+ ...( instructions ? { instructions } : { } ) ,
377+ input,
378+ ...( tools . length > 0 ? { tools : toResponsesTools ( tools ) , tool_choice : toResponsesToolChoice ( toolChoice , capabilityProfile ) } : { } ) ,
379+ stream : false ,
380+ } ) ,
381+ } ) ;
382+
383+ if ( ! response . ok ) {
384+ const body = await response . text ( ) ;
385+ throw new Error ( body . trim ( ) || `GitHub Copilot responses completion failed: HTTP ${ response . status } ` ) ;
386+ }
387+
388+ const payload = await response . json ( ) as Record < string , unknown > ;
389+ const toolCalls = extractResponsesToolCalls ( payload ) ;
390+ const finishReason = normalizeResponsesFinishReason ( payload , toolCalls ) ;
391+ return {
392+ text : extractResponsesText ( payload ) ,
393+ finishReason,
394+ usage : extractResponsesUsage ( payload ) ,
395+ ...( toolCalls . length > 0 ? { toolCalls } : { } ) ,
396+ } ;
397+ }
398+
399+ function buildCopilotHeaders ( token : string ) : Record < string , string > {
400+ return {
401+ Authorization : `Bearer ${ token } ` ,
402+ 'Content-Type' : 'application/json' ,
403+ Accept : 'application/json' ,
404+ 'User-Agent' : COPILOT_USER_AGENT ,
405+ 'Editor-Version' : COPILOT_EDITOR_VERSION ,
406+ 'Editor-Plugin-Version' : COPILOT_EDITOR_PLUGIN_VERSION ,
321407 } ;
322408}
323409
@@ -711,7 +797,90 @@ function toOpenAiMessages(prompt: LanguageModelV1Prompt): Array<Record<string, u
711797 } ) . flat ( ) ;
712798}
713799
714- function extractCopilotText ( payload : Record < string , unknown > ) : string {
800+ function convertPromptToResponsesInput ( prompt : LanguageModelV1Prompt ) : {
801+ instructions : string | undefined ;
802+ input : Array < Record < string , unknown > > ;
803+ } {
804+ let instructions : string | undefined ;
805+ const input : Array < Record < string , unknown > > = [ ] ;
806+
807+ for ( const message of prompt ) {
808+ if ( message . role === 'system' ) {
809+ instructions = message . content ;
810+ continue ;
811+ }
812+ if ( message . role === 'user' ) {
813+ const text = message . content . map ( ( p ) => p . type === 'text' ? p . text : `[${ p . type } ]` ) . join ( '\n' ) ;
814+ input . push ( { role : 'user' , content : [ { type : 'input_text' , text } ] } ) ;
815+ } else if ( message . role === 'assistant' ) {
816+ const text = message . content
817+ . filter ( ( p ) => p . type === 'text' || p . type === 'reasoning' )
818+ . map ( ( p ) => p . text )
819+ . join ( '\n' )
820+ . trim ( ) ;
821+ if ( text ) {
822+ input . push ( { role : 'assistant' , content : [ { type : 'output_text' , text } ] } ) ;
823+ }
824+
825+ for ( const part of message . content ) {
826+ if ( part . type !== 'tool-call' ) {
827+ continue ;
828+ }
829+ input . push ( {
830+ type : 'function_call' ,
831+ call_id : part . toolCallId ,
832+ name : part . toolName ,
833+ arguments : JSON . stringify ( part . args ?? { } ) ,
834+ } ) ;
835+ }
836+ } else {
837+ for ( const part of message . content ) {
838+ input . push ( {
839+ type : 'function_call_output' ,
840+ call_id : part . toolCallId ,
841+ output : stringifyToolResult ( part . result ) ,
842+ } ) ;
843+ }
844+ }
845+ }
846+
847+ return { instructions, input } ;
848+ }
849+
850+ function toResponsesTools ( tools : LanguageModelV1FunctionTool [ ] ) : Array < Record < string , unknown > > {
851+ return tools . map ( ( tool ) => ( {
852+ type : 'function' ,
853+ name : tool . name ,
854+ ...( tool . description ? { description : tool . description } : { } ) ,
855+ parameters : tool . parameters ,
856+ strict : true ,
857+ } ) ) ;
858+ }
859+
860+ function toResponsesToolChoice (
861+ toolChoice : LanguageModelV1ToolChoice | undefined ,
862+ capabilityProfile ?: YagrModelCapabilityProfile ,
863+ ) : unknown {
864+ const normalizedToolChoice = capabilityProfile
865+ ? normalizeToolChoiceForCapability ( toolChoice , capabilityProfile )
866+ : toolChoice ;
867+
868+ if ( ! normalizedToolChoice || normalizedToolChoice . type === 'auto' ) {
869+ return 'auto' ;
870+ }
871+ if ( normalizedToolChoice . type === 'none' || normalizedToolChoice . type === 'required' ) {
872+ return normalizedToolChoice . type ;
873+ }
874+ if ( normalizedToolChoice . type === 'tool' ) {
875+ return {
876+ type : 'function' ,
877+ name : normalizedToolChoice . toolName ,
878+ } ;
879+ }
880+ return 'auto' ;
881+ }
882+
883+ function extractChatCompletionsText ( payload : Record < string , unknown > ) : string {
715884 const choices = payload . choices ;
716885 if ( ! Array . isArray ( choices ) || choices . length === 0 ) {
717886 return '' ;
@@ -731,14 +900,49 @@ function extractCopilotText(payload: Record<string, unknown>): string {
731900 return '' ;
732901}
733902
734- function extractOpenAiUsage ( payload : Record < string , unknown > ) : { promptTokens : number ; completionTokens : number } {
903+ function extractResponsesText ( payload : Record < string , unknown > ) : string {
904+ const outputText = readOptionalString ( payload . output_text ) ;
905+ if ( outputText ) {
906+ return outputText ;
907+ }
908+
909+ const output = Array . isArray ( payload . output ) ? payload . output : [ ] ;
910+ return output . flatMap ( ( entry ) => {
911+ if ( ! entry || typeof entry !== 'object' ) {
912+ return [ ] ;
913+ }
914+
915+ const record = entry as Record < string , unknown > ;
916+ if ( record . type === 'message' && Array . isArray ( record . content ) ) {
917+ return record . content . flatMap ( ( part ) => {
918+ if ( ! part || typeof part !== 'object' ) {
919+ return [ ] ;
920+ }
921+ const partRecord = part as Record < string , unknown > ;
922+ return typeof partRecord . text === 'string' ? [ partRecord . text ] : [ ] ;
923+ } ) ;
924+ }
925+
926+ return [ ] ;
927+ } ) . join ( '' ) ;
928+ }
929+
930+ function extractChatCompletionsUsage ( payload : Record < string , unknown > ) : { promptTokens : number ; completionTokens : number } {
735931 const usage = payload . usage as Record < string , unknown > | undefined ;
736932 return {
737933 promptTokens : typeof usage ?. prompt_tokens === 'number' ? usage . prompt_tokens : 0 ,
738934 completionTokens : typeof usage ?. completion_tokens === 'number' ? usage . completion_tokens : 0 ,
739935 } ;
740936}
741937
938+ function extractResponsesUsage ( payload : Record < string , unknown > ) : { promptTokens : number ; completionTokens : number } {
939+ const usage = payload . usage as Record < string , unknown > | undefined ;
940+ return {
941+ promptTokens : typeof usage ?. input_tokens === 'number' ? usage . input_tokens : 0 ,
942+ completionTokens : typeof usage ?. output_tokens === 'number' ? usage . output_tokens : 0 ,
943+ } ;
944+ }
945+
742946function extractOpenAiToolCalls ( payload : Record < string , unknown > ) : LanguageModelV1FunctionToolCall [ ] {
743947 const choices = payload . choices ;
744948 if ( ! Array . isArray ( choices ) || choices . length === 0 ) {
@@ -775,7 +979,37 @@ function extractOpenAiToolCalls(payload: Record<string, unknown>): LanguageModel
775979 . filter ( ( entry ) : entry is LanguageModelV1FunctionToolCall => Boolean ( entry ) ) ;
776980}
777981
778- function normalizeFinishReason (
982+ function extractResponsesToolCalls ( payload : Record < string , unknown > ) : LanguageModelV1FunctionToolCall [ ] {
983+ const output = Array . isArray ( payload . output ) ? payload . output : [ ] ;
984+ return output
985+ . map ( ( entry , index ) => {
986+ if ( ! entry || typeof entry !== 'object' ) {
987+ return undefined ;
988+ }
989+
990+ const call = entry as Record < string , unknown > ;
991+ if ( call . type !== 'function_call' ) {
992+ return undefined ;
993+ }
994+
995+ const id = readOptionalString ( call . call_id ) || readOptionalString ( call . id ) || `copilot-responses-tool-call-${ index + 1 } ` ;
996+ const toolName = readOptionalString ( call . name ) ;
997+ const args = readOptionalString ( call . arguments ) ;
998+ if ( ! toolName || ! args ) {
999+ return undefined ;
1000+ }
1001+
1002+ return {
1003+ toolCallType : 'function' as const ,
1004+ toolCallId : id ,
1005+ toolName,
1006+ args,
1007+ } ;
1008+ } )
1009+ . filter ( ( entry ) : entry is LanguageModelV1FunctionToolCall => Boolean ( entry ) ) ;
1010+ }
1011+
1012+ function normalizeChatCompletionsFinishReason (
7791013 payload : Record < string , unknown > ,
7801014 toolCalls : LanguageModelV1FunctionToolCall [ ] ,
7811015) : 'stop' | 'error' | 'tool-calls' | 'length' | 'content-filter' | 'other' | 'unknown' {
@@ -803,6 +1037,30 @@ function normalizeFinishReason(
8031037 return 'other' ;
8041038}
8051039
1040+ function normalizeResponsesFinishReason (
1041+ payload : Record < string , unknown > ,
1042+ toolCalls : LanguageModelV1FunctionToolCall [ ] ,
1043+ ) : 'stop' | 'error' | 'tool-calls' | 'length' | 'content-filter' | 'other' | 'unknown' {
1044+ if ( toolCalls . length > 0 ) {
1045+ return 'tool-calls' ;
1046+ }
1047+
1048+ const raw = readOptionalString ( payload . status ) ;
1049+ if ( ! raw ) {
1050+ return 'unknown' ;
1051+ }
1052+ if ( raw === 'completed' ) {
1053+ return 'stop' ;
1054+ }
1055+ if ( raw === 'incomplete' ) {
1056+ return 'length' ;
1057+ }
1058+ if ( raw === 'failed' ) {
1059+ return 'error' ;
1060+ }
1061+ return 'other' ;
1062+ }
1063+
8061064function stringifyToolResult ( value : unknown ) : string {
8071065 if ( typeof value === 'string' ) {
8081066 return value ;
0 commit comments