diff --git a/anthropic/anthropic.go b/anthropic/anthropic.go index ef0bdd953..e13612ac3 100644 --- a/anthropic/anthropic.go +++ b/anthropic/anthropic.go @@ -1,4 +1,3 @@ -// Package anthropic provides core transformation logic for compatibility with the Anthropic Messages API package anthropic import ( @@ -7,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "strings" "time" @@ -673,6 +673,13 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent { c.textStarted = false } + // Marshal arguments first to check for errors before starting block + argsJSON, err := json.Marshal(tc.Function.Arguments) + if err != nil { + slog.Error("failed to marshal tool arguments", "error", err, "tool_id", tc.ID) + continue + } + // Start tool use block events = append(events, StreamEvent{ Event: "content_block_start", @@ -689,7 +696,6 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent { }) // Send input as JSON delta - argsJSON, _ := json.Marshal(tc.Function.Arguments) events = append(events, StreamEvent{ Event: "content_block_delta", Data: ContentBlockDeltaEvent{ diff --git a/anthropic/anthropic_test.go b/anthropic/anthropic_test.go index 31a2ec67c..e331f6dce 100644 --- a/anthropic/anthropic_test.go +++ b/anthropic/anthropic_test.go @@ -665,3 +665,113 @@ func TestStreamConverter_WithToolCalls(t *testing.T) { t.Error("expected input_json_delta event") } } + +func TestStreamConverter_ToolCallWithUnmarshalableArgs(t *testing.T) { + // Test that unmarshalable arguments (like channels) are handled gracefully + // and don't cause a panic or corrupt stream + conv := NewStreamConverter("msg_123", "test-model") + + // Create a channel which cannot be JSON marshaled + unmarshalable := make(chan int) + + resp := api.ChatResponse{ + Model: "test-model", + Message: api.Message{ + Role: "assistant", + ToolCalls: []api.ToolCall{ + { + ID: "call_bad", + Function: api.ToolCallFunction{ + Name: "bad_function", + Arguments: map[string]any{"channel": unmarshalable}, + }, + }, + }, + }, + Done: true, + DoneReason: "stop", + } + + // Should not panic and should skip the unmarshalable tool call + events := conv.Process(resp) + + // Verify no tool_use block was started (since marshal failed before block start) + hasToolStart := false + for _, e := range events { + if e.Event == "content_block_start" { + if start, ok := e.Data.(ContentBlockStartEvent); ok { + if start.ContentBlock.Type == "tool_use" { + hasToolStart = true + } + } + } + } + + if hasToolStart { + t.Error("expected no tool_use block when arguments cannot be marshaled") + } +} + +func TestStreamConverter_MultipleToolCallsWithMixedValidity(t *testing.T) { + // Test that valid tool calls still work when mixed with invalid ones + conv := NewStreamConverter("msg_123", "test-model") + + unmarshalable := make(chan int) + + resp := api.ChatResponse{ + Model: "test-model", + Message: api.Message{ + Role: "assistant", + ToolCalls: []api.ToolCall{ + { + ID: "call_good", + Function: api.ToolCallFunction{ + Name: "good_function", + Arguments: map[string]any{"location": "Paris"}, + }, + }, + { + ID: "call_bad", + Function: api.ToolCallFunction{ + Name: "bad_function", + Arguments: map[string]any{"channel": unmarshalable}, + }, + }, + }, + }, + Done: true, + DoneReason: "stop", + } + + events := conv.Process(resp) + + // Count tool_use blocks - should only have 1 (the valid one) + toolStartCount := 0 + toolDeltaCount := 0 + for _, e := range events { + if e.Event == "content_block_start" { + if start, ok := e.Data.(ContentBlockStartEvent); ok { + if start.ContentBlock.Type == "tool_use" { + toolStartCount++ + if start.ContentBlock.Name != "good_function" { + t.Errorf("expected tool name 'good_function', got %q", start.ContentBlock.Name) + } + } + } + } + if e.Event == "content_block_delta" { + if delta, ok := e.Data.(ContentBlockDeltaEvent); ok { + if delta.Delta.Type == "input_json_delta" { + toolDeltaCount++ + } + } + } + } + + if toolStartCount != 1 { + t.Errorf("expected 1 tool_use block, got %d", toolStartCount) + } + if toolDeltaCount != 1 { + t.Errorf("expected 1 input_json_delta, got %d", toolDeltaCount) + } +} diff --git a/docs/api/anthropic-compatibility.mdx b/docs/api/anthropic-compatibility.mdx index b8953d1d8..67c266b17 100644 --- a/docs/api/anthropic-compatibility.mdx +++ b/docs/api/anthropic-compatibility.mdx @@ -4,6 +4,16 @@ title: Anthropic compatibility Ollama provides compatibility with the [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) to help connect existing applications to Ollama, including tools like Claude Code. +## Recommended models + +For coding use cases, models like `qwen3-coder` are recommended. + +Pull a model before use: +```shell +ollama pull qwen3-coder +ollama pull glm-4.7:cloud +``` + ## Usage ### Environment variables @@ -28,7 +38,7 @@ client = anthropic.Anthropic( ) message = client.messages.create( - model='llama3.2:3b', + model='qwen3-coder', max_tokens=1024, messages=[ {'role': 'user', 'content': 'Hello, how are you?'} @@ -46,7 +56,7 @@ const anthropic = new Anthropic({ }); const message = await anthropic.messages.create({ - model: "llama3.2:3b", + model: "qwen3-coder", max_tokens: 1024, messages: [{ role: "user", content: "Hello, how are you?" }], }); @@ -60,7 +70,7 @@ curl -X POST http://localhost:11434/v1/messages \ -H "x-api-key: ollama" \ -H "anthropic-version: 2023-06-01" \ -d '{ - "model": "llama3.2:3b", + "model": "qwen3-coder", "max_tokens": 1024, "messages": [{ "role": "user", "content": "Hello, how are you?" }] }' @@ -81,7 +91,7 @@ client = anthropic.Anthropic( ) with client.messages.stream( - model='llama3.2:3b', + model='qwen3-coder', max_tokens=1024, messages=[{'role': 'user', 'content': 'Count from 1 to 10'}] ) as stream: @@ -98,7 +108,7 @@ const anthropic = new Anthropic({ }); const stream = await anthropic.messages.stream({ - model: "llama3.2:3b", + model: "qwen3-coder", max_tokens: 1024, messages: [{ role: "user", content: "Count from 1 to 10" }], }); @@ -117,7 +127,7 @@ for await (const event of stream) { curl -X POST http://localhost:11434/v1/messages \ -H "Content-Type: application/json" \ -d '{ - "model": "llama3.2:3b", + "model": "qwen3-coder", "max_tokens": 1024, "stream": true, "messages": [{ "role": "user", "content": "Count from 1 to 10" }] @@ -139,7 +149,7 @@ client = anthropic.Anthropic( ) message = client.messages.create( - model='llama3.2:3b', + model='qwen3-coder', max_tokens=1024, tools=[ { @@ -170,7 +180,7 @@ for block in message.content: curl -X POST http://localhost:11434/v1/messages \ -H "Content-Type: application/json" \ -d '{ - "model": "llama3.2:3b", + "model": "qwen3-coder", "max_tokens": 1024, "tools": [ { @@ -199,7 +209,7 @@ curl -X POST http://localhost:11434/v1/messages \ [Claude Code](https://docs.anthropic.com/en/docs/claude-code) can be configured to use Ollama as its backend: ```shell -ANTHROPIC_BASE_URL=http://localhost:11434 ANTHROPIC_API_KEY=ollama claude --model llama3.2:3b +ANTHROPIC_BASE_URL=http://localhost:11434 ANTHROPIC_API_KEY=ollama claude --model qwen3-coder ``` Or set the environment variables in your shell profile: @@ -212,9 +222,13 @@ export ANTHROPIC_API_KEY=ollama Then run Claude Code with any Ollama model: ```shell -claude --model llama3.2:3b -claude --model qwen3:8b -claude --model deepseek-r1:14b +# Local models +claude --model qwen3-coder +claude --model gpt-oss:20b + +# Cloud models +claude --model glm-4.7:cloud +claude --model minimax-m2.1:cloud ``` ## Endpoints @@ -277,18 +291,33 @@ claude --model deepseek-r1:14b ## Models -Before using a model, pull it locally with `ollama pull`: +Ollama supports both local and cloud models. + +### Local models + +Pull a local model before use: ```shell -ollama pull llama3.2:3b +ollama pull qwen3-coder ``` +Recommended local models: +- `qwen3-coder` - Excellent for coding tasks +- `gpt-oss:20b` - Strong general-purpose model + +### Cloud models + +Cloud models are available immediately without pulling: + +- `glm-4.7:cloud` - High-performance cloud model +- `minimax-m2.1:cloud` - Fast cloud model + ### Default model names For tooling that relies on default Anthropic model names such as `claude-3-5-sonnet`, use `ollama cp` to copy an existing model name: ```shell -ollama cp llama3.2:3b claude-3-5-sonnet +ollama cp qwen3-coder claude-3-5-sonnet ``` Afterwards, this new model name can be specified in the `model` field: