anthropic: fix error handling and update docs

- Add proper error handling for JSON marshal in StreamConverter to
  prevent corrupted streams when tool arguments cannot be serialized
- Add tests for unmarshalable arguments and mixed validity scenarios
- Fix documentation typo and update recommended models to qwen3-coder
This commit is contained in:
ParthSareen 2026-01-04 22:53:11 -08:00
parent 6229df5b90
commit ed1e17bb35
3 changed files with 162 additions and 17 deletions

View File

@ -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{

View File

@ -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)
}
}

View File

@ -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: