@@ -3,6 +3,7 @@ import type { RequestOptions } from "../lib/sdks.js";
33import type { Tool , MaxToolRounds } from "../lib/tool-types.js" ;
44import type * as models from "../models/index.js" ;
55
6+ import { isClaudeStyleMessages } from "../lib/claude-type-guards.js" ;
67import { ResponseWrapper } from "../lib/response-wrapper.js" ;
78import { convertToolsToAPIFormat } from "../lib/tool-executor.js" ;
89
@@ -25,81 +26,30 @@ export type CallModelTools =
2526 | models . ToolDefinitionJson [ ]
2627 | models . OpenResponsesRequest [ "tools" ] ;
2728
29+ // isClaudeStyleMessages is imported from "../lib/claude-type-guards.js"
30+
2831/**
29- * Check if input is Anthropic Claude-style messages (ClaudeMessageParam[])
30- *
31- * Claude messages have only 'user' or 'assistant' roles (no 'system', 'tool', 'developer')
32- * and may contain Claude-specific block types in content arrays.
33- *
34- * We check for Claude-ONLY features to distinguish from OpenAI format:
35- * - 'tool_result' blocks (Claude-specific; OpenAI uses role: 'tool')
36- * - 'image' blocks with 'source' object (Claude-specific structure)
32+ * Check if input is chat-style messages (Message[])
3733 */
38- function isClaudeStyleMessages (
39- input : CallModelInput
40- ) : input is models . ClaudeMessageParam [ ] {
34+ function isChatStyleMessages ( input : CallModelInput ) : input is models . Message [ ] {
4135 if ( ! Array . isArray ( input ) || input . length === 0 ) {
4236 return false ;
4337 }
4438
45- // Check ALL messages for Claude-specific features
46- for ( const msg of input ) {
47- const m = msg as Record < string , unknown > ;
48- if ( ! m || ! ( "role" in m ) || "type" in m ) {
49- continue ;
50- }
51-
52- // OpenAI has 'system', 'developer', 'tool' roles that Claude doesn't have
53- // If we see these roles, it's definitely NOT Claude format
54- const role = m [ "role" ] ;
55- if ( role === "system" || role === "developer" || role === "tool" ) {
56- return false ;
57- }
58-
59- const content = m [ "content" ] ;
60- if ( ! Array . isArray ( content ) ) {
61- continue ;
62- }
63-
64- for ( const block of content ) {
65- const b = block as Record < string , unknown > ;
66- const blockType = b ?. [ "type" ] ;
67- // 'tool_result' is Claude-specific (OpenAI uses role: 'tool' messages instead)
68- if ( blockType === "tool_result" ) {
69- return true ;
70- }
71- // 'image' with 'source' object is Claude-specific
72- // OpenAI uses 'image_url' structure instead
73- if ( blockType === "image" && typeof b ?. [ "source" ] === "object" ) {
74- return true ;
75- }
76- // 'tool_use' blocks are Claude-specific (OpenAI uses 'tool_calls' array on message)
77- if ( blockType === "tool_use" && typeof b ?. [ "id" ] === "string" ) {
78- return true ;
79- }
80- }
39+ const first = input [ 0 ] ;
40+ // Chat-style messages have role but no 'type' field at top level
41+ // Responses-style items have 'type' field (like 'message', 'function_call', etc.)
42+ if ( first === null || typeof first !== "object" ) {
43+ return false ;
8144 }
82-
83- // No Claude-specific features found
84- // Default to NOT Claude (prefer OpenAI chat format as it's more common)
85- return false ;
45+ return "role" in first && ! ( "type" in first ) ;
8646}
8747
8848/**
89- * Check if input is chat-style messages (Message[])
49+ * Type guard helper: checks if a value is a non-null object
9050 */
91- function isChatStyleMessages ( input : CallModelInput ) : input is models . Message [ ] {
92- if ( ! Array . isArray ( input ) ) {
93- return false ;
94- }
95- if ( input . length === 0 ) {
96- return false ;
97- }
98-
99- const first = input [ 0 ] as Record < string , unknown > ;
100- // Chat-style messages have role but no 'type' field at top level
101- // Responses-style items have 'type' field (like 'message', 'function_call', etc.)
102- return first && "role" in first && ! ( "type" in first ) ;
51+ function isNonNullObject ( value : unknown ) : value is Record < string , unknown > {
52+ return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
10353}
10454
10555/**
@@ -108,26 +58,28 @@ function isChatStyleMessages(input: CallModelInput): input is models.Message[] {
10858function isChatStyleTools (
10959 tools : CallModelTools
11060) : tools is models . ToolDefinitionJson [ ] {
111- if ( ! Array . isArray ( tools ) ) {
61+ if ( ! Array . isArray ( tools ) || tools . length === 0 ) {
11262 return false ;
11363 }
114- if ( tools . length === 0 ) {
64+
65+ const first = tools [ 0 ] ;
66+ if ( ! isNonNullObject ( first ) ) {
11567 return false ;
11668 }
11769
118- const first = tools [ 0 ] as Record < string , unknown > ;
11970 // Chat-style tools have nested 'function' property with 'name' inside
12071 // Enhanced tools have 'function' with 'inputSchema'
12172 // Responses-style tools have 'name' at top level
122- const fn = first ?. [ "function" ] as Record < string , unknown > | undefined ;
123- return (
124- first &&
125- "function" in first &&
126- fn !== undefined &&
127- fn !== null &&
128- "name" in fn &&
129- ! ( "inputSchema" in fn )
130- ) ;
73+ if ( ! ( "function" in first ) ) {
74+ return false ;
75+ }
76+
77+ const fn = first . function ;
78+ if ( ! isNonNullObject ( fn ) ) {
79+ return false ;
80+ }
81+
82+ return "name" in fn && ! ( "inputSchema" in fn ) ;
13183}
13284
13385/**
@@ -463,22 +415,21 @@ export function callModel<TTools extends readonly Tool[] = Tool[]>(
463415 let isChatTools = false ;
464416
465417 if ( tools && Array . isArray ( tools ) && tools . length > 0 ) {
466- const firstTool = tools [ 0 ] as Record < string , unknown > ;
467- const fn = firstTool ?. [ "function" ] as Record < string , unknown > | undefined ;
468- isEnhancedTools =
469- "function " in firstTool &&
470- fn !== undefined &&
471- fn !== null &&
472- "inputSchema" in fn ;
418+ const firstTool = tools [ 0 ] ;
419+ if ( isNonNullObject ( firstTool ) && "function" in firstTool ) {
420+ const fn = firstTool . function ;
421+ if ( isNonNullObject ( fn ) && "inputSchema " in fn ) {
422+ isEnhancedTools = true ;
423+ }
424+ }
473425 isChatTools = ! isEnhancedTools && isChatStyleTools ( tools ) ;
474426 }
475427
476- const enhancedTools = isEnhancedTools ? ( tools as Tool [ ] ) : undefined ;
477-
478428 // Convert tools to API format based on their type
479429 let apiTools : models . OpenResponsesRequest [ "tools" ] ;
480- if ( enhancedTools ) {
481- apiTools = convertToolsToAPIFormat ( enhancedTools ) ;
430+ if ( isEnhancedTools ) {
431+ // Tools are enhanced tools with inputSchema - convert to API format
432+ apiTools = convertToolsToAPIFormat ( tools as Tool [ ] ) ;
482433 } else if ( isChatTools ) {
483434 apiTools = convertChatToResponsesTools (
484435 tools as models . ToolDefinitionJson [ ]
@@ -507,12 +458,14 @@ export function callModel<TTools extends readonly Tool[] = Tool[]>(
507458 options : options ?? { } ,
508459 } ;
509460
510- // Only pass enhanced tools to wrapper (needed for auto-execution)
511- // Double assertion needed because TTools is a generic extending readonly Tool[],
512- // while enhancedTools is Tool[]. TypeScript can't verify the specific TTools subtype
513- // at runtime, but we know it's safe since we extracted these tools from the input.
514- if ( enhancedTools ) {
515- wrapperOptions . tools = enhancedTools as unknown as TTools ;
461+ // Pass enhanced tools to wrapper for auto-execution
462+ // The tools parameter is already typed as TTools from the function signature,
463+ // so we can assign it directly when we've confirmed it matches the enhanced tools pattern
464+ if ( isEnhancedTools && tools ) {
465+ // Type assertion is necessary here because TypeScript cannot track that
466+ // isEnhancedTools === true implies tools matches TTools (the enhanced tools type).
467+ // We've verified via isNonNullObject checks that tools[0].function.inputSchema exists.
468+ wrapperOptions . tools = tools as TTools ;
516469 }
517470
518471 if ( maxToolRounds !== undefined ) {
0 commit comments