@@ -29,6 +29,7 @@ import type {
2929 ToolCall ,
3030 ToolResultMessage ,
3131} from "../types.ts" ;
32+ import { splitDeferredTools } from "../utils/deferred-tools.ts" ;
3233import { AssistantMessageEventStream } from "../utils/event-stream.ts" ;
3334import { headersToRecord } from "../utils/headers.ts" ;
3435import { parseJsonWithRepair , parseStreamingJson } from "../utils/json-parse.ts" ;
@@ -177,9 +178,24 @@ function getAnthropicCompat(
177178 supportsCacheControlOnTools : model . compat ?. supportsCacheControlOnTools ?? true ,
178179 supportsTemperature : model . compat ?. supportsTemperature ?? true ,
179180 allowEmptySignature : model . compat ?. allowEmptySignature ?? false ,
181+ supportsToolReferences : model . compat ?. supportsToolReferences ?? defaultSupportsToolReferences ( model ) ,
180182 } ;
181183}
182184
185+ /**
186+ * Default for `supportsToolReferences`: first-party Anthropic models except
187+ * Haiku (rejects client-side tool_reference blocks) and models that predate
188+ * tool search (Claude 3.x, Opus/Sonnet 4.0, Opus 4.1).
189+ */
190+ function defaultSupportsToolReferences ( model : Model < "anthropic-messages" > ) : boolean {
191+ if ( model . provider !== "anthropic" || model . id . includes ( "haiku" ) ) return false ;
192+ const version = model . id . match ( / ^ c l a u d e - (?: o p u s | s o n n e t | f a b l e ) - ( \d + ) (?: - ( \d + ) ) ? (?: - | $ ) / ) ;
193+ if ( ! version ) return false ;
194+ const major = Number ( version [ 1 ] ) ;
195+ const minor = version [ 2 ] && version [ 2 ] . length < 8 ? Number ( version [ 2 ] ) : 0 ;
196+ return major > 4 || ( major === 4 && minor >= 5 ) ;
197+ }
198+
183199export interface AnthropicOptions extends StreamOptions {
184200 /**
185201 * Enable extended thinking.
@@ -907,9 +923,30 @@ function buildParams(
907923) : MessageCreateParamsStreaming {
908924 const { cacheControl } = getCacheControl ( model , options ?. cacheRetention , options ?. env ) ;
909925 const compat = getAnthropicCompat ( model ) ;
926+ const transformedMessages = transformMessages ( context . messages , model , normalizeToolCallId ) ;
927+ const normalizeToolName = isOAuthToken ? toClaudeCodeName : ( name : string ) => name ;
928+ const toolPlacement = splitDeferredTools (
929+ { ...context , messages : transformedMessages } ,
930+ compat . supportsToolReferences ,
931+ normalizeToolName ,
932+ ) ;
933+ let immediateTools = toolPlacement . immediate ;
934+ let deferredTools = [ ...toolPlacement . deferred . values ( ) ] ;
935+ if ( immediateTools . length === 0 && deferredTools . length > 0 ) {
936+ immediateTools = deferredTools ;
937+ deferredTools = [ ] ;
938+ }
939+ const deferredToolNames = new Set ( deferredTools . map ( ( tool ) => normalizeToolName ( tool . name ) ) ) ;
910940 const params : MessageCreateParamsStreaming = {
911941 model : model . id ,
912- messages : convertMessages ( context . messages , model , isOAuthToken , cacheControl , compat . allowEmptySignature ) ,
942+ messages : convertMessages (
943+ transformedMessages ,
944+ isOAuthToken ,
945+ cacheControl ,
946+ compat . allowEmptySignature ,
947+ deferredToolNames ,
948+ normalizeToolName ,
949+ ) ,
913950 max_tokens : options ?. maxTokens ?? model . maxTokens ,
914951 stream : true ,
915952 } ;
@@ -946,13 +983,16 @@ function buildParams(
946983 params . temperature = options . temperature ;
947984 }
948985
949- if ( context . tools && context . tools . length > 0 ) {
950- params . tools = convertTools (
951- context . tools ,
952- isOAuthToken ,
953- compat . supportsEagerToolInputStreaming ,
954- compat . supportsCacheControlOnTools ? cacheControl : undefined ,
955- ) ;
986+ if ( immediateTools . length > 0 || deferredTools . length > 0 ) {
987+ params . tools = [
988+ ...convertTools (
989+ immediateTools ,
990+ isOAuthToken ,
991+ compat . supportsEagerToolInputStreaming ,
992+ compat . supportsCacheControlOnTools ? cacheControl : undefined ,
993+ ) ,
994+ ...convertTools ( deferredTools , isOAuthToken , compat . supportsEagerToolInputStreaming , undefined , true ) ,
995+ ] ;
956996 }
957997
958998 // Configure thinking mode: adaptive, budget-based, or explicitly disabled.
@@ -1009,17 +1049,51 @@ function normalizeToolCallId(id: string): string {
10091049 return id . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, "_" ) . slice ( 0 , 64 ) ;
10101050}
10111051
1052+ function convertToolResult (
1053+ msg : ToolResultMessage ,
1054+ isOAuthToken : boolean ,
1055+ deferredToolNames : ReadonlySet < string > ,
1056+ loadedToolNames : Set < string > ,
1057+ normalizeToolName : ( name : string ) => string ,
1058+ ) : { toolResult : ContentBlockParam ; siblingContent : ContentBlockParam [ ] } {
1059+ const references : Array < { type : "tool_reference" ; tool_name : string } > = [ ] ;
1060+ for ( const name of msg . addedToolNames ?? [ ] ) {
1061+ const normalizedName = normalizeToolName ( name ) ;
1062+ if ( ! deferredToolNames . has ( normalizedName ) || loadedToolNames . has ( normalizedName ) ) continue ;
1063+ loadedToolNames . add ( normalizedName ) ;
1064+ references . push ( {
1065+ type : "tool_reference" ,
1066+ tool_name : isOAuthToken ? toClaudeCodeName ( name ) : name ,
1067+ } ) ;
1068+ }
1069+ const convertedContent = convertContentBlocks ( msg . content ) ;
1070+ // Anthropic rejects tool references mixed with ordinary tool-result content.
1071+ return {
1072+ toolResult : {
1073+ type : "tool_result" ,
1074+ tool_use_id : msg . toolCallId ,
1075+ content : references . length > 0 ? references : convertedContent ,
1076+ is_error : msg . isError ,
1077+ } ,
1078+ siblingContent :
1079+ references . length === 0
1080+ ? [ ]
1081+ : typeof convertedContent === "string"
1082+ ? [ { type : "text" , text : convertedContent } ]
1083+ : convertedContent ,
1084+ } ;
1085+ }
1086+
10121087function convertMessages (
1013- messages : Message [ ] ,
1014- model : Model < "anthropic-messages" > ,
1088+ transformedMessages : Message [ ] ,
10151089 isOAuthToken : boolean ,
10161090 cacheControl ?: CacheControlEphemeral ,
10171091 allowEmptySignature = false ,
1092+ deferredToolNames : ReadonlySet < string > = new Set ( ) ,
1093+ normalizeToolName : ( name : string ) => string = ( name ) => name ,
10181094) : MessageParam [ ] {
10191095 const params : MessageParam [ ] = [ ] ;
1020-
1021- // Transform messages for cross-provider compatibility
1022- const transformedMessages = transformMessages ( messages , model , normalizeToolCallId ) ;
1096+ const loadedToolNames = new Set < string > ( ) ;
10231097
10241098 for ( let i = 0 ; i < transformedMessages . length ; i ++ ) {
10251099 const msg = transformedMessages [ i ] ;
@@ -1122,37 +1196,30 @@ function convertMessages(
11221196 content : blocks ,
11231197 } ) ;
11241198 } else if ( msg . role === "toolResult" ) {
1125- // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
1199+ // Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.
11261200 const toolResults : ContentBlockParam [ ] = [ ] ;
1127-
1128- // Add the current tool result
1129- toolResults . push ( {
1130- type : "tool_result" ,
1131- tool_use_id : msg . toolCallId ,
1132- content : convertContentBlocks ( msg . content ) ,
1133- is_error : msg . isError ,
1134- } ) ;
1135-
1136- // Look ahead for consecutive toolResult messages
1137- let j = i + 1 ;
1201+ const siblingContent : ContentBlockParam [ ] = [ ] ;
1202+ let j = i ;
11381203 while ( j < transformedMessages . length && transformedMessages [ j ] . role === "toolResult" ) {
1139- const nextMsg = transformedMessages [ j ] as ToolResultMessage ; // We know it's a toolResult
1140- toolResults . push ( {
1141- type : "tool_result" ,
1142- tool_use_id : nextMsg . toolCallId ,
1143- content : convertContentBlocks ( nextMsg . content ) ,
1144- is_error : nextMsg . isError ,
1145- } ) ;
1204+ const converted = convertToolResult (
1205+ transformedMessages [ j ] as ToolResultMessage ,
1206+ isOAuthToken ,
1207+ deferredToolNames ,
1208+ loadedToolNames ,
1209+ normalizeToolName ,
1210+ ) ;
1211+ toolResults . push ( converted . toolResult ) ;
1212+ siblingContent . push ( ...converted . siblingContent ) ;
11461213 j ++ ;
11471214 }
11481215
1149- // Skip the messages we've already processed
1216+ // Skip the messages we've already processed.
11501217 i = j - 1 ;
11511218
1152- // Add a single user message with all tool results
1219+ // Displaced reference-bearing results must follow every tool_result block.
11531220 params . push ( {
11541221 role : "user" ,
1155- content : toolResults ,
1222+ content : [ ... toolResults , ... siblingContent ] ,
11561223 } ) ;
11571224 }
11581225 }
@@ -1193,6 +1260,7 @@ function convertTools(
11931260 isOAuthToken : boolean ,
11941261 supportsEagerToolInputStreaming : boolean ,
11951262 cacheControl ?: CacheControlEphemeral ,
1263+ deferLoading = false ,
11961264) : Anthropic . Messages . Tool [ ] {
11971265 if ( ! tools ) return [ ] ;
11981266
@@ -1208,6 +1276,7 @@ function convertTools(
12081276 properties : schema . properties ?? { } ,
12091277 required : schema . required ?? [ ] ,
12101278 } ,
1279+ ...( deferLoading ? { defer_loading : true } : { } ) ,
12111280 ...( cacheControl && index === tools . length - 1 ? { cache_control : cacheControl } : { } ) ,
12121281 } ;
12131282 } ) ;
0 commit comments