Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions llm/pipeline/empty_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ func TestPipeline_Process_StreamEmptyResponseDetection(t *testing.T) {
if streamCalls == 1 {
return streams.SliceStream([]*llm.Response{
{
RequestType: llm.RequestTypeSpeech,
APIFormat: llm.APIFormatOpenAISpeech,
RequestType: llm.RequestTypeSpeech,
APIFormat: llm.APIFormatOpenAISpeech,
SpeechStreamEvent: &llm.SpeechStreamEvent{Type: "speech.audio.done"},
},
llm.DoneResponse,
Expand Down
1 change: 0 additions & 1 deletion llm/pipeline/streaming_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,6 @@ func TestPipeline_NonStreaming_AutoAggregateUpgradedStream_EmptyAggregatedBody(t
require.ErrorContains(t, err, "empty aggregated body")
}


func TestPipeline_NonStreaming_AutoAggregateUpgradedStream_EmptyJSONObjectAggregatedBodyAllowed(t *testing.T) {
ctx := context.Background()

Expand Down
8 changes: 7 additions & 1 deletion llm/transformer/anthropic/outbound_convert.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package anthropic

import (
"encoding/json"

"github.com/samber/lo"

"github.com/looplj/axonhub/llm"
Expand Down Expand Up @@ -225,10 +227,14 @@ func convertToolsAnthropic(tools []llm.Tool, config *Config) []Tool {
for _, tool := range tools {
switch tool.Type {
case llm.ToolTypeFunction:
inputSchema := tool.Function.Parameters
if len(inputSchema) == 0 {
inputSchema = json.RawMessage(`{"type":"object","properties":{}}`)
}
anthropicTools = append(anthropicTools, Tool{
Name: tool.Function.Name,
Description: tool.Function.Description,
InputSchema: tool.Function.Parameters,
InputSchema: inputSchema,
CacheControl: convertToAnthropicCacheControl(tool.CacheControl),
})
case llm.ToolTypeWebSearch:
Expand Down
59 changes: 59 additions & 0 deletions llm/transformer/anthropic/outbound_convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1115,3 +1115,62 @@ func TestConvertToAnthropicRequest(t *testing.T) {
})
}
}

func TestConvertToolsAnthropic_NilParameters(t *testing.T) {
t.Run("nil parameters defaults to empty object schema", func(t *testing.T) {
tools := []llm.Tool{
{
Type: llm.ToolTypeFunction,
Function: llm.Function{
Name: "web_search",
Description: "Search the web",
Parameters: nil,
},
},
}
result := convertToolsAnthropic(tools, nil)
require.Len(t, result, 1)
require.Equal(t, "web_search", result[0].Name)
require.NotNil(t, result[0].InputSchema)
require.True(t, json.Valid(result[0].InputSchema))
var schema map[string]any
require.NoError(t, json.Unmarshal(result[0].InputSchema, &schema))
require.Equal(t, "object", schema["type"])
require.NotNil(t, schema["properties"])
})

t.Run("empty parameters defaults to empty object schema", func(t *testing.T) {
tools := []llm.Tool{
{
Type: llm.ToolTypeFunction,
Function: llm.Function{
Name: "no_params_func",
Description: "A function with no parameters",
Parameters: json.RawMessage{},
},
},
}
result := convertToolsAnthropic(tools, nil)
require.Len(t, result, 1)
require.NotNil(t, result[0].InputSchema)
var schema map[string]any
require.NoError(t, json.Unmarshal(result[0].InputSchema, &schema))
require.Equal(t, "object", schema["type"])
})

t.Run("explicit parameters are preserved unchanged", func(t *testing.T) {
params := json.RawMessage(`{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}`)
tools := []llm.Tool{
{
Type: llm.ToolTypeFunction,
Function: llm.Function{
Name: "search",
Parameters: params,
},
},
}
result := convertToolsAnthropic(tools, nil)
require.Len(t, result, 1)
require.Equal(t, params, result[0].InputSchema)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ func TestOutboundTransformer_ServerToolUse_NoPanic(t *testing.T) {
sseEvent(t, "message_start", map[string]any{
"type": "message_start",
"message": map[string]any{
"id": "msg_01AEsGpin3gJumakZWMTyQp3",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-7",
"id": "msg_01AEsGpin3gJumakZWMTyQp3",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-7",
"content": []any{},
"usage": map[string]any{
"input_tokens": 6,
Expand Down