@@ -400,6 +400,88 @@ export class MarkdownToolAdapter extends ToolFormatAdapter {
400400 }
401401}
402402
403+ /**
404+ * Adapter for OpenAI/Generic function-call style format
405+ * Format: <function=tool_name>{"param": "value"}</function>
406+ *
407+ * This is what Groq, Together, and some other models naturally produce.
408+ * Examples from chat:
409+ * - <function=adapter_search> {"query": "embedding module"} </function>
410+ * - <function=code/search>{"query": "memory clustering"}</function>
411+ */
412+ export class FunctionStyleToolAdapter extends ToolFormatAdapter {
413+ readonly formatName = 'function-style' ;
414+
415+ formatToolsForPrompt ( tools : ToolDefinition [ ] ) : string {
416+ // Use Anthropic format for prompting, this is just for parsing
417+ return '' ;
418+ }
419+
420+ formatResultsForContext ( results : Array < { toolName : string ; success : boolean ; content ?: string ; error ?: string } > ) : string {
421+ return results . map ( r => {
422+ if ( r . success && r . content ) {
423+ return `<function_result name="${ r . toolName } " status="success">\n${ r . content } \n</function_result>` ;
424+ } else {
425+ return `<function_result name="${ r . toolName } " status="error">\n${ r . error || 'Unknown error' } \n</function_result>` ;
426+ }
427+ } ) . join ( '\n\n' ) ;
428+ }
429+
430+ matches ( text : string ) : ToolCallMatch [ ] {
431+ const matches : ToolCallMatch [ ] = [ ] ;
432+ // Match <function=name>...</function> or <function=name> {...} </function>
433+ const regex = / < f u n c t i o n = ( [ ^ > \s ] + ) > \s * ( [ \s \S ] * ?) \s * < \/ f u n c t i o n > / gi;
434+
435+ let match : RegExpExecArray | null ;
436+ while ( ( match = regex . exec ( text ) ) !== null ) {
437+ matches . push ( {
438+ fullMatch : match [ 0 ] ,
439+ startIndex : match . index ,
440+ endIndex : regex . lastIndex
441+ } ) ;
442+ }
443+
444+ return matches ;
445+ }
446+
447+ parse ( match : ToolCallMatch ) : ToolCall | null {
448+ // Extract tool name from <function=NAME>
449+ const nameMatch = match . fullMatch . match ( / < f u n c t i o n = ( [ ^ > \s ] + ) > / i) ;
450+ if ( ! nameMatch ) {
451+ return null ;
452+ }
453+
454+ const toolName = nameMatch [ 1 ] . trim ( ) ;
455+ const parameters : Record < string , string > = { } ;
456+
457+ // Extract JSON body between the tags
458+ const bodyMatch = match . fullMatch . match ( / < f u n c t i o n = [ ^ > ] + > \s * ( [ \s \S ] * ?) \s * < \/ f u n c t i o n > / i) ;
459+ if ( bodyMatch && bodyMatch [ 1 ] ) {
460+ const jsonStr = bodyMatch [ 1 ] . trim ( ) ;
461+ if ( jsonStr ) {
462+ try {
463+ const parsed = JSON . parse ( jsonStr ) ;
464+ // Flatten to string values for consistency with other adapters
465+ for ( const [ key , value ] of Object . entries ( parsed ) ) {
466+ parameters [ key ] = typeof value === 'string' ? value : JSON . stringify ( value ) ;
467+ }
468+ } catch {
469+ // If not valid JSON, try key=value parsing
470+ const kvMatch = jsonStr . match ( / [ " ' ] ? ( \w + ) [ " ' ] ? \s * [: = ] \s * [ " ' ] ? ( [ ^ " ' , } ] + ) [ " ' ] ? / g) ;
471+ if ( kvMatch ) {
472+ for ( const kv of kvMatch ) {
473+ const [ k , v ] = kv . split ( / [: = ] / ) . map ( s => s . trim ( ) . replace ( / [ " ' ] / g, '' ) ) ;
474+ if ( k && v ) parameters [ k ] = v ;
475+ }
476+ }
477+ }
478+ }
479+ }
480+
481+ return { toolName, parameters } ;
482+ }
483+ }
484+
403485/**
404486 * Registry of all supported tool format adapters
405487 * Add new adapters here to support additional formats
@@ -409,6 +491,7 @@ export class MarkdownToolAdapter extends ToolFormatAdapter {
409491export function getToolFormatAdapters ( ) : ToolFormatAdapter [ ] {
410492 return [
411493 new AnthropicStyleToolAdapter ( ) , // Primary/default format
494+ new FunctionStyleToolAdapter ( ) , // OpenAI/Groq/Together function style
412495 new MarkdownToolAdapter ( ) , // Local model backtick format
413496 new OldStyleToolAdapter ( ) // Legacy XML support
414497 ] ;
0 commit comments