@@ -554,15 +554,50 @@ export function createApp(config) {
554554 return registry . find ( ( tool ) => tool . namespacedName === name || tool . originalName === name ) || null ;
555555 } ;
556556
557- const buildExternalToolsPrompt = ( registry ) => {
557+ const normalizeExternalToolChoice = ( toolChoice , registry ) => {
558+ if ( ! toolChoice || ! Array . isArray ( registry ) || registry . length === 0 ) {
559+ return { mode : 'auto' , requiredTool : null } ;
560+ }
561+ if ( toolChoice === 'auto' || toolChoice === 'none' ) {
562+ return { mode : toolChoice , requiredTool : null } ;
563+ }
564+ if ( toolChoice === 'required' ) {
565+ return { mode : 'required' , requiredTool : null } ;
566+ }
567+ const requestedName = toolChoice ?. function ?. name ;
568+ if ( toolChoice ?. type === 'function' && requestedName ) {
569+ const mappedTool = findExternalToolByName ( registry , requestedName ) ;
570+ return {
571+ mode : 'required' ,
572+ requiredTool : mappedTool ?. namespacedName || `${ EXTERNAL_TOOL_PREFIX } ${ requestedName } `
573+ } ;
574+ }
575+ return { mode : 'auto' , requiredTool : null } ;
576+ } ;
577+
578+ const buildExternalToolsPrompt = ( registry , toolChoice = null ) => {
558579 if ( ! Array . isArray ( registry ) || registry . length === 0 ) return '' ;
580+ const normalizedChoice = normalizeExternalToolChoice ( toolChoice , registry ) ;
581+ const choiceInstructions = [ ] ;
582+ if ( normalizedChoice . mode === 'required' ) {
583+ if ( normalizedChoice . requiredTool ) {
584+ choiceInstructions . push ( `Tool use is REQUIRED for this turn. You MUST call ${ normalizedChoice . requiredTool } before giving any final answer.` ) ;
585+ } else {
586+ choiceInstructions . push ( 'Tool use is REQUIRED for this turn. You MUST call an external tool before giving any final answer.' ) ;
587+ }
588+ } else if ( normalizedChoice . mode === 'none' ) {
589+ choiceInstructions . push ( 'Tool use is disabled for this turn. Do not emit <function_calls>.' ) ;
590+ }
559591 return [
560592 'External tools are virtualized by this proxy. They are not OpenCode tools.' ,
561- 'When you need an external tool, respond with ONLY one or more <function_calls>...</function_calls> blocks.' ,
593+ 'When you need an external tool, your entire assistant reply MUST be ONLY one or more <function_calls>...</function_calls> blocks.' ,
594+ 'Do NOT output <think>, explanations, markdown, prose, or any text before or after <function_calls> blocks when making a tool call.' ,
562595 'Each block must contain JSON with this exact shape:' ,
563596 '{"name":"external__tool_name","arguments":{}}' ,
597+ 'Arguments must be a valid JSON object that matches the declared schema.' ,
564598 'Use only the namespaced names listed below. Do not use original client tool names inside function calls.' ,
565599 'If tool results are later provided as TOOL_RESULT messages, use those results to continue normally.' ,
600+ ...choiceInstructions ,
566601 `Available external tools: ${ JSON . stringify ( registry . map ( ( tool ) => ( {
567602 name : tool . namespacedName ,
568603 client_name : tool . originalName ,
@@ -976,7 +1011,7 @@ export function createApp(config) {
9761011 let keepaliveInterval = null ;
9771012
9781013 try {
979- const { messages, model, tools = [ ] , stream : requestStream , temperature, max_tokens, top_p, frequency_penalty, presence_penalty, stop, reasoning_effort, reasoning } = req . body ;
1014+ const { messages, model, tools = [ ] , tool_choice , stream : requestStream , temperature, max_tokens, top_p, frequency_penalty, presence_penalty, stop, reasoning_effort, reasoning } = req . body ;
9801015 stream = Boolean ( requestStream ) ;
9811016 if ( ! messages || ! Array . isArray ( messages ) || messages . length === 0 ) {
9821017 return res . status ( 400 ) . json ( { error : { message : 'messages array is required' } } ) ;
@@ -1113,7 +1148,7 @@ export function createApp(config) {
11131148
11141149 const externalToolRegistry = buildExternalToolRegistry ( tools ) ;
11151150 const { parts, system : systemMsg , fullPromptText, lastUserMsg } = await buildPromptParts ( messages , externalToolRegistry ) ;
1116- const toolsPrompt = buildExternalToolsPrompt ( externalToolRegistry ) ;
1151+ const toolsPrompt = buildExternalToolsPrompt ( externalToolRegistry , tool_choice ) ;
11171152 const systemWithGuard = buildSystemPrompt (
11181153 [ systemMsg , toolsPrompt ] . filter ( Boolean ) . join ( '\n\n' ) ,
11191154 requestParams . reasoning_effort ,
@@ -1554,6 +1589,7 @@ export function createApp(config) {
15541589 max_output_tokens,
15551590 store = true ,
15561591 tools = [ ] ,
1592+ tool_choice,
15571593 instructions,
15581594 temperature,
15591595 top_p,
@@ -1564,7 +1600,7 @@ export function createApp(config) {
15641600
15651601 const reasoningLevel = normalizeReasoningEffort (
15661602 reasoning_effort || requestReasoning ?. effort ,
1567- 'medium'
1603+ null
15681604 ) ;
15691605
15701606 logDebug ( 'Responses API request' , {
@@ -1575,6 +1611,7 @@ export function createApp(config) {
15751611 } ) ;
15761612
15771613 const externalToolRegistry = buildExternalToolRegistry ( tools ) ;
1614+ const externalToolChoice = normalizeExternalToolChoice ( tool_choice , externalToolRegistry ) ;
15781615 const assistantToolCalls = new Map ( ) ;
15791616
15801617 const rememberAssistantToolCall = ( toolCallId , toolName ) => {
@@ -1703,23 +1740,50 @@ export function createApp(config) {
17031740 const parts = [ ] ;
17041741 const systemChunks = [ ] ;
17051742 let fullPromptText = '' ;
1743+ const formatResponsesRoleLine = ( role , text ) => `${ String ( role || 'user' ) . toUpperCase ( ) } : ${ text } ` ;
17061744 for ( const msg of messages ) {
17071745 if ( msg . role === 'system' ) {
17081746 if ( msg . content ) systemChunks . push ( msg . content ) ;
17091747 continue ;
17101748 }
17111749 if ( ! msg . content ) continue ;
1712- parts . push ( { type : 'text' , text : msg . content } ) ;
1713- fullPromptText += `${ msg . role } : ${ msg . content } \n\n` ;
1750+ const text = msg . role === 'tool' || String ( msg . content ) . startsWith ( 'ASSISTANT: ' ) || String ( msg . content ) . startsWith ( 'TOOL_RESULT: ' )
1751+ ? msg . content
1752+ : formatResponsesRoleLine ( msg . role , msg . content ) ;
1753+ parts . push ( { type : 'text' , text } ) ;
1754+ fullPromptText += `${ text } \n\n` ;
17141755 }
17151756
1716- const toolsPrompt = buildExternalToolsPrompt ( externalToolRegistry ) ;
1757+ const toolsPrompt = buildExternalToolsPrompt ( externalToolRegistry , tool_choice ) ;
17171758 const systemWithGuard = buildSystemPrompt (
17181759 [ instructions , ...systemChunks , toolsPrompt ] . filter ( Boolean ) . join ( '\n\n' ) ,
17191760 reasoningLevel ,
17201761 externalToolRegistry . length > 0
17211762 ) ;
17221763
1764+ const requestForcedResponsesToolCall = async ( ) => {
1765+ if ( externalToolChoice . mode !== 'required' ) return null ;
1766+ const requiredName = externalToolChoice . requiredTool || externalToolRegistry [ 0 ] ?. namespacedName ;
1767+ if ( ! requiredName ) return null ;
1768+ const forcedPromptParams = {
1769+ path : { id : sessionId } ,
1770+ body : {
1771+ model : { providerID : pID , modelID : mID } ,
1772+ ...( systemWithGuard ? { system : systemWithGuard } : { } ) ,
1773+ parts : [ {
1774+ type : 'text' ,
1775+ text : `SYSTEM: Your previous reply did not emit the required external tool call. Reply now with ONLY <function_calls>{\"name\":\"${ requiredName } \",\"arguments\":{}}</function_calls> or an array inside <function_calls>...</function_calls>. Do not output any prose, reasoning, or markdown. Infer the correct arguments from the conversation so far.`
1776+ } ]
1777+ }
1778+ } ;
1779+ const toolOverrides = await getToolOverrides ( ) ;
1780+ if ( toolOverrides && Object . keys ( toolOverrides ) . length > 0 ) {
1781+ forcedPromptParams . body . tools = toolOverrides ;
1782+ }
1783+ await promptWithTimeout ( forcedPromptParams , REQUEST_TIMEOUT_MS ) ;
1784+ return pollForAssistantResponse ( sessionId , REQUEST_TIMEOUT_MS ) ;
1785+ } ;
1786+
17231787 const promptParams = {
17241788 path : { id : sessionId } ,
17251789 body : {
@@ -1894,6 +1958,10 @@ export function createApp(config) {
18941958 delta : filtered
18951959 } ) ;
18961960 } else {
1961+ if ( ! filtered . trim ( ) ) {
1962+ content += filtered ;
1963+ return ;
1964+ }
18971965 ensureOutputScaffold ( ) ;
18981966 content += filtered ;
18991967 emit ( {
@@ -1965,7 +2033,9 @@ export function createApp(config) {
19652033 } ) ;
19662034 }
19672035
1968- if ( announcedContent ) {
2036+ const hasMeaningfulContent = Boolean ( content && content . trim ( ) ) ;
2037+
2038+ if ( announcedContent && hasMeaningfulContent ) {
19692039 emit ( {
19702040 type : 'response.output_text.done' ,
19712041 sequence_number : nextSeq ( ) ,
@@ -1996,11 +2066,32 @@ export function createApp(config) {
19962066 } ) ;
19972067 }
19982068
1999- const parsedToolCalls = streamedToolCalls . length > 0
2069+ let polledForToolCalls = null ;
2070+ if ( externalToolRegistry . length > 0 && streamedToolCalls . length === 0 ) {
2071+ try {
2072+ polledForToolCalls = await pollForAssistantResponse ( sessionId , REQUEST_TIMEOUT_MS ) ;
2073+ } catch ( e ) { }
2074+ }
2075+
2076+ let parsedToolCalls = streamedToolCalls . length > 0
20002077 ? streamedToolCalls
20012078 : ( externalToolRegistry . length > 0
2002- ? parseExternalToolCallsFromText ( externalToolRegistry , rawReasoning , rawContent )
2079+ ? parseExternalToolCallsFromText (
2080+ externalToolRegistry ,
2081+ polledForToolCalls ?. reasoning || rawReasoning ,
2082+ polledForToolCalls ?. content || rawContent
2083+ )
20032084 : [ ] ) ;
2085+ if ( parsedToolCalls . length === 0 && externalToolChoice . mode === 'required' ) {
2086+ const forcedResponse = await requestForcedResponsesToolCall ( ) ;
2087+ if ( forcedResponse ) {
2088+ parsedToolCalls = parseExternalToolCallsFromText (
2089+ externalToolRegistry ,
2090+ forcedResponse . reasoning ,
2091+ forcedResponse . content
2092+ ) ;
2093+ }
2094+ }
20042095 const safeContent = stripFunctionCallMarkup ( stripFunctionCalls ( content ) ) ;
20052096 const safeReasoning = stripFunctionCallMarkup ( stripFunctionCalls ( reasoning ) ) ;
20062097 if ( streamedToolCalls . length === 0 ) {
@@ -2009,7 +2100,7 @@ export function createApp(config) {
20092100 } ) ;
20102101 }
20112102 const streamOutput = [ ] ;
2012- const streamMessageOutputItem = buildResponsesMessageOutputItem ( safeContent ) ;
2103+ const streamMessageOutputItem = buildResponsesMessageOutputItem ( safeContent && safeContent . trim ( ) ? safeContent : '' ) ;
20132104 if ( streamMessageOutputItem ) streamOutput . push ( streamMessageOutputItem ) ;
20142105 parsedToolCalls . forEach ( ( toolCall ) => {
20152106 streamOutput . push ( buildResponsesFunctionCallOutputItem ( toolCall ) ) ;
@@ -2042,18 +2133,35 @@ export function createApp(config) {
20422133
20432134 const responseRes = await client . session . prompt ( promptParams ) ;
20442135 const responseParts = responseRes . data ?. parts || [ ] ;
2045-
2136+
20462137 content = responseParts . filter ( p => p . type === 'text' ) . map ( p => p . text ) . join ( '\n' ) ;
20472138 reasoning = responseParts . filter ( p => p . type === 'reasoning' ) . map ( p => p . text ) . join ( '\n' ) ;
20482139
2049- if ( ! content && responseRes . data ) {
2140+ const polledResponse = await pollForAssistantResponse ( sessionId , REQUEST_TIMEOUT_MS ) ;
2141+ if ( polledResponse . error && ! polledResponse . content && ! polledResponse . reasoning ) {
2142+ throw polledResponse . error ;
2143+ }
2144+ if ( polledResponse . content || polledResponse . reasoning ) {
2145+ content = polledResponse . content || content ;
2146+ reasoning = polledResponse . reasoning || reasoning ;
2147+ }
2148+
2149+ if ( ! content && ! reasoning && responseRes . data ) {
20502150 const data = responseRes . data ;
20512151 content = typeof data === 'string' ? data : data ?. message || JSON . stringify ( data ) ;
20522152 }
20532153
2054- const parsedToolCalls = externalToolRegistry . length > 0
2154+ let parsedToolCalls = externalToolRegistry . length > 0
20552155 ? parseExternalToolCallsFromText ( externalToolRegistry , reasoning , content )
20562156 : [ ] ;
2157+ if ( parsedToolCalls . length === 0 && externalToolChoice . mode === 'required' ) {
2158+ const forcedResponse = await requestForcedResponsesToolCall ( ) ;
2159+ if ( forcedResponse ) {
2160+ content = forcedResponse . content || content ;
2161+ reasoning = forcedResponse . reasoning || reasoning ;
2162+ parsedToolCalls = parseExternalToolCallsFromText ( externalToolRegistry , reasoning , content ) ;
2163+ }
2164+ }
20572165 const safeContent = stripFunctionCallMarkup ( stripFunctionCalls ( content ) ) ;
20582166 const safeReasoning = stripFunctionCallMarkup ( stripFunctionCalls ( reasoning ) ) ;
20592167
0 commit comments