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:
parent
6229df5b90
commit
ed1e17bb35
|
|
@ -1,4 +1,3 @@
|
||||||
// Package anthropic provides core transformation logic for compatibility with the Anthropic Messages API
|
|
||||||
package anthropic
|
package anthropic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -7,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -673,6 +673,13 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
||||||
c.textStarted = false
|
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
|
// Start tool use block
|
||||||
events = append(events, StreamEvent{
|
events = append(events, StreamEvent{
|
||||||
Event: "content_block_start",
|
Event: "content_block_start",
|
||||||
|
|
@ -689,7 +696,6 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send input as JSON delta
|
// Send input as JSON delta
|
||||||
argsJSON, _ := json.Marshal(tc.Function.Arguments)
|
|
||||||
events = append(events, StreamEvent{
|
events = append(events, StreamEvent{
|
||||||
Event: "content_block_delta",
|
Event: "content_block_delta",
|
||||||
Data: ContentBlockDeltaEvent{
|
Data: ContentBlockDeltaEvent{
|
||||||
|
|
|
||||||
|
|
@ -665,3 +665,113 @@ func TestStreamConverter_WithToolCalls(t *testing.T) {
|
||||||
t.Error("expected input_json_delta event")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
## Usage
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
@ -28,7 +38,7 @@ client = anthropic.Anthropic(
|
||||||
)
|
)
|
||||||
|
|
||||||
message = client.messages.create(
|
message = client.messages.create(
|
||||||
model='llama3.2:3b',
|
model='qwen3-coder',
|
||||||
max_tokens=1024,
|
max_tokens=1024,
|
||||||
messages=[
|
messages=[
|
||||||
{'role': 'user', 'content': 'Hello, how are you?'}
|
{'role': 'user', 'content': 'Hello, how are you?'}
|
||||||
|
|
@ -46,7 +56,7 @@ const anthropic = new Anthropic({
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = await anthropic.messages.create({
|
const message = await anthropic.messages.create({
|
||||||
model: "llama3.2:3b",
|
model: "qwen3-coder",
|
||||||
max_tokens: 1024,
|
max_tokens: 1024,
|
||||||
messages: [{ role: "user", content: "Hello, how are you?" }],
|
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 "x-api-key: ollama" \
|
||||||
-H "anthropic-version: 2023-06-01" \
|
-H "anthropic-version: 2023-06-01" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "llama3.2:3b",
|
"model": "qwen3-coder",
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
"messages": [{ "role": "user", "content": "Hello, how are you?" }]
|
"messages": [{ "role": "user", "content": "Hello, how are you?" }]
|
||||||
}'
|
}'
|
||||||
|
|
@ -81,7 +91,7 @@ client = anthropic.Anthropic(
|
||||||
)
|
)
|
||||||
|
|
||||||
with client.messages.stream(
|
with client.messages.stream(
|
||||||
model='llama3.2:3b',
|
model='qwen3-coder',
|
||||||
max_tokens=1024,
|
max_tokens=1024,
|
||||||
messages=[{'role': 'user', 'content': 'Count from 1 to 10'}]
|
messages=[{'role': 'user', 'content': 'Count from 1 to 10'}]
|
||||||
) as stream:
|
) as stream:
|
||||||
|
|
@ -98,7 +108,7 @@ const anthropic = new Anthropic({
|
||||||
});
|
});
|
||||||
|
|
||||||
const stream = await anthropic.messages.stream({
|
const stream = await anthropic.messages.stream({
|
||||||
model: "llama3.2:3b",
|
model: "qwen3-coder",
|
||||||
max_tokens: 1024,
|
max_tokens: 1024,
|
||||||
messages: [{ role: "user", content: "Count from 1 to 10" }],
|
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 \
|
curl -X POST http://localhost:11434/v1/messages \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "llama3.2:3b",
|
"model": "qwen3-coder",
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
"stream": true,
|
"stream": true,
|
||||||
"messages": [{ "role": "user", "content": "Count from 1 to 10" }]
|
"messages": [{ "role": "user", "content": "Count from 1 to 10" }]
|
||||||
|
|
@ -139,7 +149,7 @@ client = anthropic.Anthropic(
|
||||||
)
|
)
|
||||||
|
|
||||||
message = client.messages.create(
|
message = client.messages.create(
|
||||||
model='llama3.2:3b',
|
model='qwen3-coder',
|
||||||
max_tokens=1024,
|
max_tokens=1024,
|
||||||
tools=[
|
tools=[
|
||||||
{
|
{
|
||||||
|
|
@ -170,7 +180,7 @@ for block in message.content:
|
||||||
curl -X POST http://localhost:11434/v1/messages \
|
curl -X POST http://localhost:11434/v1/messages \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "llama3.2:3b",
|
"model": "qwen3-coder",
|
||||||
"max_tokens": 1024,
|
"max_tokens": 1024,
|
||||||
"tools": [
|
"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:
|
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) can be configured to use Ollama as its backend:
|
||||||
|
|
||||||
```shell
|
```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:
|
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:
|
Then run Claude Code with any Ollama model:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
claude --model llama3.2:3b
|
# Local models
|
||||||
claude --model qwen3:8b
|
claude --model qwen3-coder
|
||||||
claude --model deepseek-r1:14b
|
claude --model gpt-oss:20b
|
||||||
|
|
||||||
|
# Cloud models
|
||||||
|
claude --model glm-4.7:cloud
|
||||||
|
claude --model minimax-m2.1:cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
@ -277,18 +291,33 @@ claude --model deepseek-r1:14b
|
||||||
|
|
||||||
## Models
|
## 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
|
```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
|
### 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:
|
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
|
```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:
|
Afterwards, this new model name can be specified in the `model` field:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue