Spaces:
Paused
Paused
feat: add Anthropic Messages API compatibility (/v1/messages)
Browse filesImplement full Anthropic Messages API translation layer including:
- Request/response model types (messages, content blocks, streaming events)
- Claude model name resolution to GLM backend models
- Streaming (SSE) and non-streaming response conversion
- Thinking/reasoning block passthrough
- Tool use (tool_call → tool_use) format translation
- System prompt, tool_choice, and multi-turn conversation support
- internal/handler/anthropic.go +958 -0
- internal/model/anthropic.go +164 -0
- main.go +1 -0
internal/handler/anthropic.go
ADDED
|
@@ -0,0 +1,958 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"strings"
|
| 10 |
+
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
|
| 13 |
+
"zai-proxy/internal/auth"
|
| 14 |
+
"zai-proxy/internal/filter"
|
| 15 |
+
"zai-proxy/internal/logger"
|
| 16 |
+
"zai-proxy/internal/model"
|
| 17 |
+
"zai-proxy/internal/upstream"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
// HandleMessages handles Anthropic Messages API requests (/v1/messages)
|
| 21 |
+
func HandleMessages(w http.ResponseWriter, r *http.Request) {
|
| 22 |
+
// Extract token from x-api-key or Authorization header
|
| 23 |
+
token := r.Header.Get("x-api-key")
|
| 24 |
+
if token == "" {
|
| 25 |
+
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
| 26 |
+
}
|
| 27 |
+
if token == "" {
|
| 28 |
+
writeAnthropicError(w, http.StatusUnauthorized, "authentication_error", "Missing API key")
|
| 29 |
+
return
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
if token == "free" {
|
| 33 |
+
anonymousToken, err := auth.GetAnonymousToken()
|
| 34 |
+
if err != nil {
|
| 35 |
+
logger.LogError("Failed to get anonymous token: %v", err)
|
| 36 |
+
writeAnthropicError(w, http.StatusInternalServerError, "api_error", "Failed to get anonymous token")
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
token = anonymousToken
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
var req model.AnthropicRequest
|
| 43 |
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
| 44 |
+
writeAnthropicError(w, http.StatusBadRequest, "invalid_request_error", "Invalid request body")
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if req.MaxTokens == 0 {
|
| 49 |
+
req.MaxTokens = 8192
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Determine if thinking is enabled
|
| 53 |
+
thinkingEnabled := false
|
| 54 |
+
if req.Thinking != nil && req.Thinking.Type == "enabled" {
|
| 55 |
+
thinkingEnabled = true
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Resolve Claude model name to GLM model name
|
| 59 |
+
resolvedModel, _ := model.ResolveClaudeModel(req.Model, thinkingEnabled)
|
| 60 |
+
|
| 61 |
+
// Convert Anthropic messages to internal format
|
| 62 |
+
messages, tools, toolChoice := convertAnthropicToInternal(req)
|
| 63 |
+
|
| 64 |
+
resp, modelName, err := upstream.MakeUpstreamRequest(token, messages, resolvedModel, tools, toolChoice)
|
| 65 |
+
if err != nil {
|
| 66 |
+
logger.LogError("Upstream request failed: %v", err)
|
| 67 |
+
writeAnthropicError(w, http.StatusBadGateway, "api_error", "Upstream error")
|
| 68 |
+
return
|
| 69 |
+
}
|
| 70 |
+
defer resp.Body.Close()
|
| 71 |
+
|
| 72 |
+
if resp.StatusCode != http.StatusOK {
|
| 73 |
+
body, _ := io.ReadAll(resp.Body)
|
| 74 |
+
bodyStr := string(body)
|
| 75 |
+
if len(bodyStr) > 500 {
|
| 76 |
+
bodyStr = bodyStr[:500]
|
| 77 |
+
}
|
| 78 |
+
logger.LogError("Upstream error: status=%d, body=%s", resp.StatusCode, bodyStr)
|
| 79 |
+
writeAnthropicError(w, resp.StatusCode, "api_error", "Upstream error")
|
| 80 |
+
return
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
messageID := fmt.Sprintf("msg_%s", uuid.New().String()[:24])
|
| 84 |
+
|
| 85 |
+
if req.Stream {
|
| 86 |
+
handleAnthropicStream(w, resp.Body, messageID, modelName, req.Model, tools)
|
| 87 |
+
} else {
|
| 88 |
+
handleAnthropicNonStream(w, resp.Body, messageID, modelName, req.Model, tools)
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// convertAnthropicToInternal converts Anthropic request format to internal Message/Tool format
|
| 93 |
+
func convertAnthropicToInternal(req model.AnthropicRequest) ([]model.Message, []model.Tool, interface{}) {
|
| 94 |
+
var messages []model.Message
|
| 95 |
+
|
| 96 |
+
// Convert system field to a system role message
|
| 97 |
+
if req.System != nil {
|
| 98 |
+
systemText := ""
|
| 99 |
+
switch s := req.System.(type) {
|
| 100 |
+
case string:
|
| 101 |
+
systemText = s
|
| 102 |
+
case []interface{}:
|
| 103 |
+
// Array of content blocks
|
| 104 |
+
for _, item := range s {
|
| 105 |
+
if block, ok := item.(map[string]interface{}); ok {
|
| 106 |
+
if t, ok := block["text"].(string); ok {
|
| 107 |
+
systemText += t
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
if systemText != "" {
|
| 113 |
+
messages = append(messages, model.Message{
|
| 114 |
+
Role: "system",
|
| 115 |
+
Content: systemText,
|
| 116 |
+
})
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// Convert Anthropic messages to internal format
|
| 121 |
+
for _, msg := range req.Messages {
|
| 122 |
+
switch msg.Role {
|
| 123 |
+
case "user":
|
| 124 |
+
text, blocks := msg.ParseContent()
|
| 125 |
+
if len(blocks) == 0 {
|
| 126 |
+
// Simple text message
|
| 127 |
+
messages = append(messages, model.Message{
|
| 128 |
+
Role: "user",
|
| 129 |
+
Content: text,
|
| 130 |
+
})
|
| 131 |
+
} else {
|
| 132 |
+
// Process content blocks - may contain tool_result
|
| 133 |
+
for _, block := range blocks {
|
| 134 |
+
switch block.Type {
|
| 135 |
+
case "text":
|
| 136 |
+
messages = append(messages, model.Message{
|
| 137 |
+
Role: "user",
|
| 138 |
+
Content: block.Text,
|
| 139 |
+
})
|
| 140 |
+
case "tool_result":
|
| 141 |
+
// Convert tool_result to tool role message
|
| 142 |
+
resultContent := ""
|
| 143 |
+
switch c := block.Content.(type) {
|
| 144 |
+
case string:
|
| 145 |
+
resultContent = c
|
| 146 |
+
case []interface{}:
|
| 147 |
+
for _, item := range c {
|
| 148 |
+
if part, ok := item.(map[string]interface{}); ok {
|
| 149 |
+
if t, ok := part["text"].(string); ok {
|
| 150 |
+
resultContent += t
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
messages = append(messages, model.Message{
|
| 156 |
+
Role: "tool",
|
| 157 |
+
Content: resultContent,
|
| 158 |
+
ToolCallID: block.ToolUseID,
|
| 159 |
+
})
|
| 160 |
+
case "image":
|
| 161 |
+
// Skip image blocks for now
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
case "assistant":
|
| 167 |
+
_, blocks := msg.ParseContent()
|
| 168 |
+
if len(blocks) == 0 {
|
| 169 |
+
// Simple text
|
| 170 |
+
text, _ := msg.ParseContent()
|
| 171 |
+
messages = append(messages, model.Message{
|
| 172 |
+
Role: "assistant",
|
| 173 |
+
Content: text,
|
| 174 |
+
})
|
| 175 |
+
} else {
|
| 176 |
+
// Assistant message with content blocks
|
| 177 |
+
var textContent string
|
| 178 |
+
var toolCalls []model.ToolCall
|
| 179 |
+
for _, block := range blocks {
|
| 180 |
+
switch block.Type {
|
| 181 |
+
case "text":
|
| 182 |
+
textContent += block.Text
|
| 183 |
+
case "thinking":
|
| 184 |
+
// Skip thinking blocks in history - upstream doesn't need them
|
| 185 |
+
case "tool_use":
|
| 186 |
+
argsStr := "{}"
|
| 187 |
+
if block.Input != nil {
|
| 188 |
+
argsStr = string(block.Input)
|
| 189 |
+
}
|
| 190 |
+
toolCalls = append(toolCalls, model.ToolCall{
|
| 191 |
+
ID: block.ID,
|
| 192 |
+
Type: "function",
|
| 193 |
+
Function: model.FunctionCall{
|
| 194 |
+
Name: block.Name,
|
| 195 |
+
Arguments: argsStr,
|
| 196 |
+
},
|
| 197 |
+
})
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
messages = append(messages, model.Message{
|
| 201 |
+
Role: "assistant",
|
| 202 |
+
Content: textContent,
|
| 203 |
+
ToolCalls: toolCalls,
|
| 204 |
+
})
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// Convert Anthropic tools to OpenAI format
|
| 210 |
+
var tools []model.Tool
|
| 211 |
+
for _, t := range req.Tools {
|
| 212 |
+
tools = append(tools, model.Tool{
|
| 213 |
+
Type: "function",
|
| 214 |
+
Function: model.ToolFunction{
|
| 215 |
+
Name: t.Name,
|
| 216 |
+
Description: t.Description,
|
| 217 |
+
Parameters: t.InputSchema,
|
| 218 |
+
},
|
| 219 |
+
})
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Convert tool_choice
|
| 223 |
+
var toolChoice interface{}
|
| 224 |
+
if req.ToolChoice != nil {
|
| 225 |
+
switch tc := req.ToolChoice.(type) {
|
| 226 |
+
case map[string]interface{}:
|
| 227 |
+
tcType, _ := tc["type"].(string)
|
| 228 |
+
switch tcType {
|
| 229 |
+
case "auto":
|
| 230 |
+
toolChoice = "auto"
|
| 231 |
+
case "any":
|
| 232 |
+
toolChoice = "required"
|
| 233 |
+
case "none":
|
| 234 |
+
toolChoice = "none"
|
| 235 |
+
case "tool":
|
| 236 |
+
if name, ok := tc["name"].(string); ok {
|
| 237 |
+
toolChoice = map[string]interface{}{
|
| 238 |
+
"type": "function",
|
| 239 |
+
"function": map[string]interface{}{"name": name},
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return messages, tools, toolChoice
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// handleAnthropicStream processes upstream SSE and converts to Anthropic streaming format
|
| 250 |
+
func handleAnthropicStream(w http.ResponseWriter, body io.ReadCloser, messageID, modelName, requestModel string, tools []model.Tool) {
|
| 251 |
+
w.Header().Set("Content-Type", "text/event-stream")
|
| 252 |
+
w.Header().Set("Cache-Control", "no-cache")
|
| 253 |
+
w.Header().Set("Connection", "keep-alive")
|
| 254 |
+
|
| 255 |
+
flusher, ok := w.(http.Flusher)
|
| 256 |
+
if !ok {
|
| 257 |
+
writeAnthropicError(w, http.StatusInternalServerError, "api_error", "Streaming not supported")
|
| 258 |
+
return
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// Send message_start
|
| 262 |
+
msgStart := model.AnthropicMessageStart{
|
| 263 |
+
Type: "message_start",
|
| 264 |
+
Message: model.AnthropicResponse{
|
| 265 |
+
ID: messageID,
|
| 266 |
+
Type: "message",
|
| 267 |
+
Role: "assistant",
|
| 268 |
+
Content: []model.AnthropicContentBlock{},
|
| 269 |
+
Model: requestModel,
|
| 270 |
+
StopReason: "",
|
| 271 |
+
Usage: model.AnthropicUsage{InputTokens: 0, OutputTokens: 0},
|
| 272 |
+
},
|
| 273 |
+
}
|
| 274 |
+
sendAnthropicSSE(w, flusher, "message_start", msgStart)
|
| 275 |
+
|
| 276 |
+
scanner := bufio.NewScanner(body)
|
| 277 |
+
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
| 278 |
+
|
| 279 |
+
searchRefFilter := filter.NewSearchRefFilter()
|
| 280 |
+
thinkingFilter := &filter.ThinkingFilter{}
|
| 281 |
+
|
| 282 |
+
contentBlockIndex := 0
|
| 283 |
+
inThinkingBlock := false
|
| 284 |
+
inTextBlock := false
|
| 285 |
+
inToolUseBlock := false
|
| 286 |
+
hasContent := false
|
| 287 |
+
totalContentOutputLength := 0
|
| 288 |
+
hasToolCalls := false
|
| 289 |
+
var collectedToolCalls []model.ToolCall
|
| 290 |
+
promptToolBuffer := ""
|
| 291 |
+
|
| 292 |
+
for scanner.Scan() {
|
| 293 |
+
line := scanner.Text()
|
| 294 |
+
logger.LogDebug("[Anthropic-Upstream] %s", line)
|
| 295 |
+
|
| 296 |
+
if !strings.HasPrefix(line, "data: ") {
|
| 297 |
+
continue
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
payload := strings.TrimPrefix(line, "data: ")
|
| 301 |
+
if payload == "[DONE]" {
|
| 302 |
+
break
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
var upstreamData model.UpstreamData
|
| 306 |
+
if err := json.Unmarshal([]byte(payload), &upstreamData); err != nil {
|
| 307 |
+
continue
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
if upstreamData.Data.Phase == "done" {
|
| 311 |
+
break
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// Handle thinking phase
|
| 315 |
+
if upstreamData.Data.Phase == "thinking" && upstreamData.Data.DeltaContent != "" {
|
| 316 |
+
isNewThinkingRound := false
|
| 317 |
+
if thinkingFilter.LastPhase != "" && thinkingFilter.LastPhase != "thinking" {
|
| 318 |
+
thinkingFilter.ResetForNewRound()
|
| 319 |
+
thinkingFilter.ThinkingRoundCount++
|
| 320 |
+
isNewThinkingRound = true
|
| 321 |
+
}
|
| 322 |
+
thinkingFilter.LastPhase = "thinking"
|
| 323 |
+
|
| 324 |
+
reasoningContent := thinkingFilter.ProcessThinking(upstreamData.Data.DeltaContent)
|
| 325 |
+
|
| 326 |
+
if isNewThinkingRound && thinkingFilter.ThinkingRoundCount > 1 && reasoningContent != "" {
|
| 327 |
+
reasoningContent = "\n\n" + reasoningContent
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
if reasoningContent != "" {
|
| 331 |
+
thinkingFilter.LastOutputChunk = reasoningContent
|
| 332 |
+
reasoningContent = searchRefFilter.Process(reasoningContent)
|
| 333 |
+
|
| 334 |
+
if reasoningContent != "" {
|
| 335 |
+
// Close previous non-thinking block if open
|
| 336 |
+
if inTextBlock {
|
| 337 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 338 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 339 |
+
})
|
| 340 |
+
contentBlockIndex++
|
| 341 |
+
inTextBlock = false
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Start thinking block if not already in one
|
| 345 |
+
if !inThinkingBlock {
|
| 346 |
+
sendAnthropicSSE(w, flusher, "content_block_start", model.AnthropicContentBlockStart{
|
| 347 |
+
Type: "content_block_start",
|
| 348 |
+
Index: contentBlockIndex,
|
| 349 |
+
ContentBlock: model.AnthropicContentBlock{Type: "thinking", Thinking: ""},
|
| 350 |
+
})
|
| 351 |
+
inThinkingBlock = true
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
hasContent = true
|
| 355 |
+
sendAnthropicSSE(w, flusher, "content_block_delta", model.AnthropicContentBlockDelta{
|
| 356 |
+
Type: "content_block_delta",
|
| 357 |
+
Index: contentBlockIndex,
|
| 358 |
+
Delta: model.AnthropicContentBlockDelta2{Type: "thinking_delta", Thinking: reasoningContent},
|
| 359 |
+
})
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
continue
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
if upstreamData.Data.Phase != "" {
|
| 366 |
+
thinkingFilter.LastPhase = upstreamData.Data.Phase
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// Filter search results, image searches, mcp, etc.
|
| 370 |
+
editContent := upstreamData.GetEditContent()
|
| 371 |
+
if editContent != "" && filter.IsSearchResultContent(editContent) {
|
| 372 |
+
if results := filter.ParseSearchResults(editContent); len(results) > 0 {
|
| 373 |
+
searchRefFilter.AddSearchResults(results)
|
| 374 |
+
}
|
| 375 |
+
continue
|
| 376 |
+
}
|
| 377 |
+
if editContent != "" && strings.Contains(editContent, `"search_image"`) {
|
| 378 |
+
textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
|
| 379 |
+
if textBeforeBlock != "" {
|
| 380 |
+
emitAnthropicTextDelta(w, flusher, &contentBlockIndex, &inThinkingBlock, &inTextBlock, &inToolUseBlock, &hasContent, searchRefFilter.Process(textBeforeBlock))
|
| 381 |
+
}
|
| 382 |
+
continue
|
| 383 |
+
}
|
| 384 |
+
if editContent != "" && strings.Contains(editContent, `"mcp"`) {
|
| 385 |
+
textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
|
| 386 |
+
if textBeforeBlock != "" {
|
| 387 |
+
emitAnthropicTextDelta(w, flusher, &contentBlockIndex, &inThinkingBlock, &inTextBlock, &inToolUseBlock, &hasContent, searchRefFilter.Process(textBeforeBlock))
|
| 388 |
+
}
|
| 389 |
+
continue
|
| 390 |
+
}
|
| 391 |
+
if editContent != "" && filter.IsSearchToolCall(editContent, upstreamData.Data.Phase) {
|
| 392 |
+
continue
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
// Handle function tool calls
|
| 396 |
+
if len(tools) > 0 && editContent != "" && filter.IsFunctionToolCall(editContent, upstreamData.Data.Phase) {
|
| 397 |
+
if toolCalls := filter.ParseFunctionToolCalls(editContent); len(toolCalls) > 0 {
|
| 398 |
+
for i := range toolCalls {
|
| 399 |
+
if toolCalls[i].ID == "" {
|
| 400 |
+
toolCalls[i].ID = fmt.Sprintf("toolu_%s", uuid.New().String()[:24])
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
collectedToolCalls = toolCalls
|
| 404 |
+
hasToolCalls = true
|
| 405 |
+
|
| 406 |
+
// Close thinking/text blocks
|
| 407 |
+
if inThinkingBlock {
|
| 408 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 409 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 410 |
+
})
|
| 411 |
+
contentBlockIndex++
|
| 412 |
+
inThinkingBlock = false
|
| 413 |
+
}
|
| 414 |
+
if inTextBlock {
|
| 415 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 416 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 417 |
+
})
|
| 418 |
+
contentBlockIndex++
|
| 419 |
+
inTextBlock = false
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
for _, tc := range toolCalls {
|
| 423 |
+
emitAnthropicToolUse(w, flusher, &contentBlockIndex, &inToolUseBlock, tc)
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
continue
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
// Flush thinking filter
|
| 430 |
+
if thinkingRemaining := thinkingFilter.Flush(); thinkingRemaining != "" {
|
| 431 |
+
thinkingFilter.LastOutputChunk = thinkingRemaining
|
| 432 |
+
processedRemaining := searchRefFilter.Process(thinkingRemaining)
|
| 433 |
+
if processedRemaining != "" {
|
| 434 |
+
if !inThinkingBlock {
|
| 435 |
+
// Close text block if open
|
| 436 |
+
if inTextBlock {
|
| 437 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 438 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 439 |
+
})
|
| 440 |
+
contentBlockIndex++
|
| 441 |
+
inTextBlock = false
|
| 442 |
+
}
|
| 443 |
+
sendAnthropicSSE(w, flusher, "content_block_start", model.AnthropicContentBlockStart{
|
| 444 |
+
Type: "content_block_start",
|
| 445 |
+
Index: contentBlockIndex,
|
| 446 |
+
ContentBlock: model.AnthropicContentBlock{Type: "thinking", Thinking: ""},
|
| 447 |
+
})
|
| 448 |
+
inThinkingBlock = true
|
| 449 |
+
}
|
| 450 |
+
hasContent = true
|
| 451 |
+
sendAnthropicSSE(w, flusher, "content_block_delta", model.AnthropicContentBlockDelta{
|
| 452 |
+
Type: "content_block_delta",
|
| 453 |
+
Index: contentBlockIndex,
|
| 454 |
+
Delta: model.AnthropicContentBlockDelta2{Type: "thinking_delta", Thinking: processedRemaining},
|
| 455 |
+
})
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
// Extract content
|
| 460 |
+
content := ""
|
| 461 |
+
if upstreamData.Data.Phase == "answer" && upstreamData.Data.DeltaContent != "" {
|
| 462 |
+
content = upstreamData.Data.DeltaContent
|
| 463 |
+
} else if upstreamData.Data.Phase == "answer" && editContent != "" {
|
| 464 |
+
if strings.Contains(editContent, "</details>") {
|
| 465 |
+
if idx := strings.Index(editContent, "</details>"); idx != -1 {
|
| 466 |
+
afterDetails := editContent[idx+len("</details>"):]
|
| 467 |
+
if strings.HasPrefix(afterDetails, "\n") {
|
| 468 |
+
content = afterDetails[1:]
|
| 469 |
+
} else {
|
| 470 |
+
content = afterDetails
|
| 471 |
+
}
|
| 472 |
+
totalContentOutputLength = len([]rune(content))
|
| 473 |
+
}
|
| 474 |
+
}
|
| 475 |
+
} else if (upstreamData.Data.Phase == "other" || upstreamData.Data.Phase == "tool_call") && editContent != "" {
|
| 476 |
+
fullContentRunes := []rune(editContent)
|
| 477 |
+
if len(fullContentRunes) > totalContentOutputLength {
|
| 478 |
+
content = string(fullContentRunes[totalContentOutputLength:])
|
| 479 |
+
totalContentOutputLength = len(fullContentRunes)
|
| 480 |
+
} else {
|
| 481 |
+
content = editContent
|
| 482 |
+
}
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
if content == "" {
|
| 486 |
+
continue
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
content = searchRefFilter.Process(content)
|
| 490 |
+
if content == "" {
|
| 491 |
+
continue
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
hasContent = true
|
| 495 |
+
if upstreamData.Data.Phase == "answer" && upstreamData.Data.DeltaContent != "" {
|
| 496 |
+
totalContentOutputLength += len([]rune(content))
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// Prompt tool extraction: buffer answer text for <tool_call> detection
|
| 500 |
+
if len(tools) > 0 {
|
| 501 |
+
promptToolBuffer += content
|
| 502 |
+
for {
|
| 503 |
+
openIdx := strings.Index(promptToolBuffer, "<tool_call>")
|
| 504 |
+
if openIdx == -1 {
|
| 505 |
+
break
|
| 506 |
+
}
|
| 507 |
+
if openIdx > 0 {
|
| 508 |
+
safeContent := promptToolBuffer[:openIdx]
|
| 509 |
+
promptToolBuffer = promptToolBuffer[openIdx:]
|
| 510 |
+
if safeContent != "" {
|
| 511 |
+
emitAnthropicTextDelta(w, flusher, &contentBlockIndex, &inThinkingBlock, &inTextBlock, &inToolUseBlock, &hasContent, safeContent)
|
| 512 |
+
}
|
| 513 |
+
}
|
| 514 |
+
afterOpen := promptToolBuffer[len("<tool_call>"):]
|
| 515 |
+
closeIdx := strings.Index(promptToolBuffer, "</tool_call>")
|
| 516 |
+
thinkCloseIdx := strings.Index(afterOpen, "</think>")
|
| 517 |
+
nextOpenIdx := strings.Index(afterOpen, "<tool_call>")
|
| 518 |
+
|
| 519 |
+
blockEnd := -1
|
| 520 |
+
if closeIdx != -1 {
|
| 521 |
+
blockEnd = closeIdx + len("</tool_call>")
|
| 522 |
+
}
|
| 523 |
+
if thinkCloseIdx != -1 {
|
| 524 |
+
candidate := len("<tool_call>") + thinkCloseIdx + len("</think>")
|
| 525 |
+
if blockEnd == -1 || candidate < blockEnd {
|
| 526 |
+
blockEnd = candidate
|
| 527 |
+
}
|
| 528 |
+
}
|
| 529 |
+
if nextOpenIdx != -1 {
|
| 530 |
+
candidate := len("<tool_call>") + nextOpenIdx
|
| 531 |
+
if blockEnd == -1 || candidate < blockEnd {
|
| 532 |
+
blockEnd = candidate
|
| 533 |
+
}
|
| 534 |
+
}
|
| 535 |
+
if blockEnd == -1 {
|
| 536 |
+
break
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
block := promptToolBuffer[:blockEnd]
|
| 540 |
+
promptToolBuffer = promptToolBuffer[blockEnd:]
|
| 541 |
+
|
| 542 |
+
_, ptToolCalls := filter.ExtractPromptToolCalls(block)
|
| 543 |
+
if len(ptToolCalls) > 0 {
|
| 544 |
+
collectedToolCalls = append(collectedToolCalls, ptToolCalls...)
|
| 545 |
+
hasToolCalls = true
|
| 546 |
+
|
| 547 |
+
// Close thinking/text blocks before emitting tool use
|
| 548 |
+
if inThinkingBlock {
|
| 549 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 550 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 551 |
+
})
|
| 552 |
+
contentBlockIndex++
|
| 553 |
+
inThinkingBlock = false
|
| 554 |
+
}
|
| 555 |
+
if inTextBlock {
|
| 556 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 557 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 558 |
+
})
|
| 559 |
+
contentBlockIndex++
|
| 560 |
+
inTextBlock = false
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
for _, tc := range ptToolCalls {
|
| 564 |
+
tc.ID = fmt.Sprintf("toolu_%s", uuid.New().String()[:24])
|
| 565 |
+
emitAnthropicToolUse(w, flusher, &contentBlockIndex, &inToolUseBlock, tc)
|
| 566 |
+
}
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
continue
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
emitAnthropicTextDelta(w, flusher, &contentBlockIndex, &inThinkingBlock, &inTextBlock, &inToolUseBlock, &hasContent, content)
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
if err := scanner.Err(); err != nil {
|
| 576 |
+
logger.LogError("[Anthropic-Upstream] scanner error: %v", err)
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
// Flush remaining prompt tool buffer
|
| 580 |
+
if promptToolBuffer != "" {
|
| 581 |
+
cleanContent, ptToolCalls := filter.ExtractPromptToolCalls(promptToolBuffer)
|
| 582 |
+
if len(ptToolCalls) > 0 {
|
| 583 |
+
collectedToolCalls = append(collectedToolCalls, ptToolCalls...)
|
| 584 |
+
hasToolCalls = true
|
| 585 |
+
|
| 586 |
+
if inThinkingBlock {
|
| 587 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 588 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 589 |
+
})
|
| 590 |
+
contentBlockIndex++
|
| 591 |
+
inThinkingBlock = false
|
| 592 |
+
}
|
| 593 |
+
if inTextBlock {
|
| 594 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 595 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 596 |
+
})
|
| 597 |
+
contentBlockIndex++
|
| 598 |
+
inTextBlock = false
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
for _, tc := range ptToolCalls {
|
| 602 |
+
tc.ID = fmt.Sprintf("toolu_%s", uuid.New().String()[:24])
|
| 603 |
+
emitAnthropicToolUse(w, flusher, &contentBlockIndex, &inToolUseBlock, tc)
|
| 604 |
+
}
|
| 605 |
+
}
|
| 606 |
+
if cleanContent != "" {
|
| 607 |
+
emitAnthropicTextDelta(w, flusher, &contentBlockIndex, &inThinkingBlock, &inTextBlock, &inToolUseBlock, &hasContent, cleanContent)
|
| 608 |
+
}
|
| 609 |
+
promptToolBuffer = ""
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
// Flush search ref filter
|
| 613 |
+
if remaining := searchRefFilter.Flush(); remaining != "" {
|
| 614 |
+
emitAnthropicTextDelta(w, flusher, &contentBlockIndex, &inThinkingBlock, &inTextBlock, &inToolUseBlock, &hasContent, remaining)
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
if !hasContent && !hasToolCalls {
|
| 618 |
+
logger.LogError("Anthropic stream response 200 but no content received")
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
// Close any open blocks
|
| 622 |
+
if inThinkingBlock {
|
| 623 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 624 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 625 |
+
})
|
| 626 |
+
contentBlockIndex++
|
| 627 |
+
inThinkingBlock = false
|
| 628 |
+
}
|
| 629 |
+
if inTextBlock {
|
| 630 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 631 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 632 |
+
})
|
| 633 |
+
contentBlockIndex++
|
| 634 |
+
inTextBlock = false
|
| 635 |
+
}
|
| 636 |
+
if inToolUseBlock {
|
| 637 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 638 |
+
Type: "content_block_stop", Index: contentBlockIndex,
|
| 639 |
+
})
|
| 640 |
+
contentBlockIndex++
|
| 641 |
+
inToolUseBlock = false
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
// Determine stop reason
|
| 645 |
+
stopReason := "end_turn"
|
| 646 |
+
if hasToolCalls {
|
| 647 |
+
stopReason = "tool_use"
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
// Send message_delta with stop_reason and usage
|
| 651 |
+
sendAnthropicSSE(w, flusher, "message_delta", model.AnthropicMessageDelta{
|
| 652 |
+
Type: "message_delta",
|
| 653 |
+
Delta: struct {
|
| 654 |
+
StopReason string `json:"stop_reason"`
|
| 655 |
+
StopSequence *string `json:"stop_sequence"`
|
| 656 |
+
}{
|
| 657 |
+
StopReason: stopReason,
|
| 658 |
+
},
|
| 659 |
+
Usage: model.AnthropicUsage{OutputTokens: contentBlockIndex * 100}, // Rough estimate
|
| 660 |
+
})
|
| 661 |
+
|
| 662 |
+
// Send message_stop
|
| 663 |
+
sendAnthropicSSE(w, flusher, "message_stop", model.AnthropicMessageStop{Type: "message_stop"})
|
| 664 |
+
|
| 665 |
+
// Suppress unused variable warnings
|
| 666 |
+
_ = inThinkingBlock
|
| 667 |
+
_ = inTextBlock
|
| 668 |
+
_ = inToolUseBlock
|
| 669 |
+
_ = contentBlockIndex
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
// handleAnthropicNonStream collects all upstream data and returns an Anthropic response
|
| 673 |
+
func handleAnthropicNonStream(w http.ResponseWriter, body io.ReadCloser, messageID, modelName, requestModel string, tools []model.Tool) {
|
| 674 |
+
scanner := bufio.NewScanner(body)
|
| 675 |
+
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
| 676 |
+
|
| 677 |
+
var chunks []string
|
| 678 |
+
var reasoningChunks []string
|
| 679 |
+
thinkingFilter := &filter.ThinkingFilter{}
|
| 680 |
+
searchRefFilter := filter.NewSearchRefFilter()
|
| 681 |
+
hasThinking := false
|
| 682 |
+
var collectedToolCalls []model.ToolCall
|
| 683 |
+
|
| 684 |
+
for scanner.Scan() {
|
| 685 |
+
line := scanner.Text()
|
| 686 |
+
if !strings.HasPrefix(line, "data: ") {
|
| 687 |
+
continue
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
payload := strings.TrimPrefix(line, "data: ")
|
| 691 |
+
if payload == "[DONE]" {
|
| 692 |
+
break
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
var upstreamData model.UpstreamData
|
| 696 |
+
if err := json.Unmarshal([]byte(payload), &upstreamData); err != nil {
|
| 697 |
+
continue
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
if upstreamData.Data.Phase == "done" {
|
| 701 |
+
break
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
if upstreamData.Data.Phase == "thinking" && upstreamData.Data.DeltaContent != "" {
|
| 705 |
+
if thinkingFilter.LastPhase != "" && thinkingFilter.LastPhase != "thinking" {
|
| 706 |
+
thinkingFilter.ResetForNewRound()
|
| 707 |
+
thinkingFilter.ThinkingRoundCount++
|
| 708 |
+
if thinkingFilter.ThinkingRoundCount > 1 {
|
| 709 |
+
reasoningChunks = append(reasoningChunks, "\n\n")
|
| 710 |
+
}
|
| 711 |
+
}
|
| 712 |
+
thinkingFilter.LastPhase = "thinking"
|
| 713 |
+
hasThinking = true
|
| 714 |
+
reasoningContent := thinkingFilter.ProcessThinking(upstreamData.Data.DeltaContent)
|
| 715 |
+
if reasoningContent != "" {
|
| 716 |
+
thinkingFilter.LastOutputChunk = reasoningContent
|
| 717 |
+
reasoningChunks = append(reasoningChunks, reasoningContent)
|
| 718 |
+
}
|
| 719 |
+
continue
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
if upstreamData.Data.Phase != "" {
|
| 723 |
+
thinkingFilter.LastPhase = upstreamData.Data.Phase
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
editContent := upstreamData.GetEditContent()
|
| 727 |
+
if editContent != "" && filter.IsSearchResultContent(editContent) {
|
| 728 |
+
if results := filter.ParseSearchResults(editContent); len(results) > 0 {
|
| 729 |
+
searchRefFilter.AddSearchResults(results)
|
| 730 |
+
}
|
| 731 |
+
continue
|
| 732 |
+
}
|
| 733 |
+
if editContent != "" && strings.Contains(editContent, `"search_image"`) {
|
| 734 |
+
textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
|
| 735 |
+
if textBeforeBlock != "" {
|
| 736 |
+
chunks = append(chunks, textBeforeBlock)
|
| 737 |
+
}
|
| 738 |
+
continue
|
| 739 |
+
}
|
| 740 |
+
if editContent != "" && strings.Contains(editContent, `"mcp"`) {
|
| 741 |
+
textBeforeBlock := filter.ExtractTextBeforeGlmBlock(editContent)
|
| 742 |
+
if textBeforeBlock != "" {
|
| 743 |
+
chunks = append(chunks, textBeforeBlock)
|
| 744 |
+
}
|
| 745 |
+
continue
|
| 746 |
+
}
|
| 747 |
+
if editContent != "" && filter.IsSearchToolCall(editContent, upstreamData.Data.Phase) {
|
| 748 |
+
continue
|
| 749 |
+
}
|
| 750 |
+
if len(tools) > 0 && editContent != "" && filter.IsFunctionToolCall(editContent, upstreamData.Data.Phase) {
|
| 751 |
+
if toolCalls := filter.ParseFunctionToolCalls(editContent); len(toolCalls) > 0 {
|
| 752 |
+
for i := range toolCalls {
|
| 753 |
+
if toolCalls[i].ID == "" {
|
| 754 |
+
toolCalls[i].ID = fmt.Sprintf("toolu_%s", uuid.New().String()[:24])
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
collectedToolCalls = toolCalls
|
| 758 |
+
}
|
| 759 |
+
continue
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
content := ""
|
| 763 |
+
if upstreamData.Data.Phase == "answer" && upstreamData.Data.DeltaContent != "" {
|
| 764 |
+
content = upstreamData.Data.DeltaContent
|
| 765 |
+
} else if upstreamData.Data.Phase == "answer" && editContent != "" {
|
| 766 |
+
if strings.Contains(editContent, "</details>") {
|
| 767 |
+
reasoningContent := thinkingFilter.ExtractIncrementalThinking(editContent)
|
| 768 |
+
if reasoningContent != "" {
|
| 769 |
+
reasoningChunks = append(reasoningChunks, reasoningContent)
|
| 770 |
+
}
|
| 771 |
+
if idx := strings.Index(editContent, "</details>"); idx != -1 {
|
| 772 |
+
afterDetails := editContent[idx+len("</details>"):]
|
| 773 |
+
if strings.HasPrefix(afterDetails, "\n") {
|
| 774 |
+
content = afterDetails[1:]
|
| 775 |
+
} else {
|
| 776 |
+
content = afterDetails
|
| 777 |
+
}
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
} else if (upstreamData.Data.Phase == "other" || upstreamData.Data.Phase == "tool_call") && editContent != "" {
|
| 781 |
+
content = editContent
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
if content != "" {
|
| 785 |
+
chunks = append(chunks, content)
|
| 786 |
+
}
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
fullContent := strings.Join(chunks, "")
|
| 790 |
+
fullContent = searchRefFilter.Process(fullContent) + searchRefFilter.Flush()
|
| 791 |
+
fullReasoning := strings.Join(reasoningChunks, "")
|
| 792 |
+
fullReasoning = searchRefFilter.Process(fullReasoning) + searchRefFilter.Flush()
|
| 793 |
+
|
| 794 |
+
// Extract prompt tool calls from answer text
|
| 795 |
+
if len(tools) > 0 && len(collectedToolCalls) == 0 {
|
| 796 |
+
cleanContent, promptToolCalls := filter.ExtractPromptToolCalls(fullContent)
|
| 797 |
+
if len(promptToolCalls) > 0 {
|
| 798 |
+
collectedToolCalls = promptToolCalls
|
| 799 |
+
fullContent = cleanContent
|
| 800 |
+
}
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
// Build response content blocks
|
| 804 |
+
var contentBlocks []model.AnthropicContentBlock
|
| 805 |
+
|
| 806 |
+
if hasThinking && fullReasoning != "" {
|
| 807 |
+
contentBlocks = append(contentBlocks, model.AnthropicContentBlock{
|
| 808 |
+
Type: "thinking",
|
| 809 |
+
Thinking: fullReasoning,
|
| 810 |
+
})
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
if fullContent != "" {
|
| 814 |
+
contentBlocks = append(contentBlocks, model.AnthropicContentBlock{
|
| 815 |
+
Type: "text",
|
| 816 |
+
Text: fullContent,
|
| 817 |
+
})
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
for _, tc := range collectedToolCalls {
|
| 821 |
+
if tc.ID == "" {
|
| 822 |
+
tc.ID = fmt.Sprintf("toolu_%s", uuid.New().String()[:24])
|
| 823 |
+
}
|
| 824 |
+
contentBlocks = append(contentBlocks, model.AnthropicContentBlock{
|
| 825 |
+
Type: "tool_use",
|
| 826 |
+
ID: tc.ID,
|
| 827 |
+
Name: tc.Function.Name,
|
| 828 |
+
Input: json.RawMessage(tc.Function.Arguments),
|
| 829 |
+
})
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
if len(contentBlocks) == 0 {
|
| 833 |
+
contentBlocks = append(contentBlocks, model.AnthropicContentBlock{
|
| 834 |
+
Type: "text",
|
| 835 |
+
Text: "",
|
| 836 |
+
})
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
stopReason := "end_turn"
|
| 840 |
+
if len(collectedToolCalls) > 0 {
|
| 841 |
+
stopReason = "tool_use"
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
response := model.AnthropicResponse{
|
| 845 |
+
ID: messageID,
|
| 846 |
+
Type: "message",
|
| 847 |
+
Role: "assistant",
|
| 848 |
+
Content: contentBlocks,
|
| 849 |
+
Model: requestModel,
|
| 850 |
+
StopReason: stopReason,
|
| 851 |
+
Usage: model.AnthropicUsage{InputTokens: 100, OutputTokens: len(fullContent) / 4},
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
w.Header().Set("Content-Type", "application/json")
|
| 855 |
+
json.NewEncoder(w).Encode(response)
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
// emitAnthropicTextDelta sends a text content delta, managing block lifecycle
|
| 859 |
+
func emitAnthropicTextDelta(w http.ResponseWriter, flusher http.Flusher, contentBlockIndex *int, inThinkingBlock, inTextBlock, inToolUseBlock *bool, hasContent *bool, text string) {
|
| 860 |
+
if text == "" {
|
| 861 |
+
return
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
// Close thinking block if transitioning to text
|
| 865 |
+
if *inThinkingBlock {
|
| 866 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 867 |
+
Type: "content_block_stop", Index: *contentBlockIndex,
|
| 868 |
+
})
|
| 869 |
+
*contentBlockIndex++
|
| 870 |
+
*inThinkingBlock = false
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
// Close tool_use block if transitioning to text
|
| 874 |
+
if *inToolUseBlock {
|
| 875 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 876 |
+
Type: "content_block_stop", Index: *contentBlockIndex,
|
| 877 |
+
})
|
| 878 |
+
*contentBlockIndex++
|
| 879 |
+
*inToolUseBlock = false
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
// Start text block if not in one
|
| 883 |
+
if !*inTextBlock {
|
| 884 |
+
sendAnthropicSSE(w, flusher, "content_block_start", model.AnthropicContentBlockStart{
|
| 885 |
+
Type: "content_block_start",
|
| 886 |
+
Index: *contentBlockIndex,
|
| 887 |
+
ContentBlock: model.AnthropicContentBlock{Type: "text", Text: ""},
|
| 888 |
+
})
|
| 889 |
+
*inTextBlock = true
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
*hasContent = true
|
| 893 |
+
sendAnthropicSSE(w, flusher, "content_block_delta", model.AnthropicContentBlockDelta{
|
| 894 |
+
Type: "content_block_delta",
|
| 895 |
+
Index: *contentBlockIndex,
|
| 896 |
+
Delta: model.AnthropicContentBlockDelta2{Type: "text_delta", Text: text},
|
| 897 |
+
})
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
// emitAnthropicToolUse sends a tool_use content block (start + input_json_delta + stop)
|
| 901 |
+
func emitAnthropicToolUse(w http.ResponseWriter, flusher http.Flusher, contentBlockIndex *int, inToolUseBlock *bool, tc model.ToolCall) {
|
| 902 |
+
// Close previous tool_use block if open
|
| 903 |
+
if *inToolUseBlock {
|
| 904 |
+
sendAnthropicSSE(w, flusher, "content_block_stop", model.AnthropicContentBlockStop{
|
| 905 |
+
Type: "content_block_stop", Index: *contentBlockIndex,
|
| 906 |
+
})
|
| 907 |
+
*contentBlockIndex++
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
toolID := tc.ID
|
| 911 |
+
if toolID == "" {
|
| 912 |
+
toolID = fmt.Sprintf("toolu_%s", uuid.New().String()[:24])
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
// Send content_block_start with tool_use
|
| 916 |
+
sendAnthropicSSE(w, flusher, "content_block_start", model.AnthropicContentBlockStart{
|
| 917 |
+
Type: "content_block_start",
|
| 918 |
+
Index: *contentBlockIndex,
|
| 919 |
+
ContentBlock: model.AnthropicContentBlock{
|
| 920 |
+
Type: "tool_use",
|
| 921 |
+
ID: toolID,
|
| 922 |
+
Name: tc.Function.Name,
|
| 923 |
+
Input: json.RawMessage("{}"),
|
| 924 |
+
},
|
| 925 |
+
})
|
| 926 |
+
*inToolUseBlock = true
|
| 927 |
+
|
| 928 |
+
// Send input as a single delta
|
| 929 |
+
sendAnthropicSSE(w, flusher, "content_block_delta", model.AnthropicContentBlockDelta{
|
| 930 |
+
Type: "content_block_delta",
|
| 931 |
+
Index: *contentBlockIndex,
|
| 932 |
+
Delta: model.AnthropicContentBlockDelta2{Type: "input_json_delta", PartialJSON: tc.Function.Arguments},
|
| 933 |
+
})
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
// sendAnthropicSSE writes an SSE event in Anthropic format: "event: <type>\ndata: <json>\n\n"
|
| 937 |
+
func sendAnthropicSSE(w http.ResponseWriter, flusher http.Flusher, eventType string, data interface{}) {
|
| 938 |
+
jsonData, err := json.Marshal(data)
|
| 939 |
+
if err != nil {
|
| 940 |
+
logger.LogError("[Anthropic-SSE] marshal error: %v", err)
|
| 941 |
+
return
|
| 942 |
+
}
|
| 943 |
+
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, jsonData)
|
| 944 |
+
flusher.Flush()
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
// writeAnthropicError writes an error response in Anthropic format
|
| 948 |
+
func writeAnthropicError(w http.ResponseWriter, statusCode int, errorType, message string) {
|
| 949 |
+
w.Header().Set("Content-Type", "application/json")
|
| 950 |
+
w.WriteHeader(statusCode)
|
| 951 |
+
json.NewEncoder(w).Encode(map[string]interface{}{
|
| 952 |
+
"type": "error",
|
| 953 |
+
"error": map[string]interface{}{
|
| 954 |
+
"type": errorType,
|
| 955 |
+
"message": message,
|
| 956 |
+
},
|
| 957 |
+
})
|
| 958 |
+
}
|
internal/model/anthropic.go
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package model
|
| 2 |
+
|
| 3 |
+
import "encoding/json"
|
| 4 |
+
|
| 5 |
+
// AnthropicRequest represents a request to the Anthropic Messages API
|
| 6 |
+
type AnthropicRequest struct {
|
| 7 |
+
Model string `json:"model"`
|
| 8 |
+
MaxTokens int `json:"max_tokens"`
|
| 9 |
+
System interface{} `json:"system,omitempty"` // string or []AnthropicContentBlock
|
| 10 |
+
Messages []AnthropicMessage `json:"messages"`
|
| 11 |
+
Tools []AnthropicTool `json:"tools,omitempty"`
|
| 12 |
+
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
| 13 |
+
Stream bool `json:"stream"`
|
| 14 |
+
Thinking *AnthropicThinking `json:"thinking,omitempty"`
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// AnthropicThinking controls thinking/reasoning behavior
|
| 18 |
+
type AnthropicThinking struct {
|
| 19 |
+
Type string `json:"type"` // "enabled" or "disabled"
|
| 20 |
+
BudgetTokens int `json:"budget_tokens,omitempty"`
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// AnthropicMessage represents a message in Anthropic format
|
| 24 |
+
type AnthropicMessage struct {
|
| 25 |
+
Role string `json:"role"`
|
| 26 |
+
Content interface{} `json:"content"` // string or []AnthropicContentBlock
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// ParseContent extracts text content from an Anthropic message.
|
| 30 |
+
// Content can be a plain string or an array of content blocks.
|
| 31 |
+
func (m *AnthropicMessage) ParseContent() (text string, blocks []AnthropicContentBlock) {
|
| 32 |
+
switch c := m.Content.(type) {
|
| 33 |
+
case string:
|
| 34 |
+
return c, nil
|
| 35 |
+
case []interface{}:
|
| 36 |
+
for _, item := range c {
|
| 37 |
+
raw, err := json.Marshal(item)
|
| 38 |
+
if err != nil {
|
| 39 |
+
continue
|
| 40 |
+
}
|
| 41 |
+
var block AnthropicContentBlock
|
| 42 |
+
if err := json.Unmarshal(raw, &block); err != nil {
|
| 43 |
+
continue
|
| 44 |
+
}
|
| 45 |
+
blocks = append(blocks, block)
|
| 46 |
+
if block.Type == "text" {
|
| 47 |
+
text += block.Text
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
return text, blocks
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// AnthropicContentBlock represents a content block in Anthropic messages
|
| 55 |
+
type AnthropicContentBlock struct {
|
| 56 |
+
Type string `json:"type"`
|
| 57 |
+
|
| 58 |
+
// text block
|
| 59 |
+
Text string `json:"text,omitempty"`
|
| 60 |
+
|
| 61 |
+
// thinking block
|
| 62 |
+
Thinking string `json:"thinking,omitempty"`
|
| 63 |
+
|
| 64 |
+
// tool_use block
|
| 65 |
+
ID string `json:"id,omitempty"`
|
| 66 |
+
Name string `json:"name,omitempty"`
|
| 67 |
+
Input json.RawMessage `json:"input,omitempty"`
|
| 68 |
+
|
| 69 |
+
// tool_result block
|
| 70 |
+
ToolUseID string `json:"tool_use_id,omitempty"`
|
| 71 |
+
Content interface{} `json:"content,omitempty"` // string or []AnthropicContentBlock
|
| 72 |
+
IsError bool `json:"is_error,omitempty"`
|
| 73 |
+
|
| 74 |
+
// image block
|
| 75 |
+
Source *AnthropicImageSource `json:"source,omitempty"`
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// AnthropicImageSource for base64 image content
|
| 79 |
+
type AnthropicImageSource struct {
|
| 80 |
+
Type string `json:"type"` // "base64"
|
| 81 |
+
MediaType string `json:"media_type"` // "image/png" etc
|
| 82 |
+
Data string `json:"data"`
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// AnthropicTool represents a tool definition in Anthropic format
|
| 86 |
+
type AnthropicTool struct {
|
| 87 |
+
Name string `json:"name"`
|
| 88 |
+
Description string `json:"description,omitempty"`
|
| 89 |
+
InputSchema interface{} `json:"input_schema"`
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// AnthropicResponse represents a non-streaming response
|
| 93 |
+
type AnthropicResponse struct {
|
| 94 |
+
ID string `json:"id"`
|
| 95 |
+
Type string `json:"type"` // "message"
|
| 96 |
+
Role string `json:"role"` // "assistant"
|
| 97 |
+
Content []AnthropicContentBlock `json:"content"`
|
| 98 |
+
Model string `json:"model"`
|
| 99 |
+
StopReason string `json:"stop_reason"` // "end_turn", "tool_use", "max_tokens"
|
| 100 |
+
StopSequence *string `json:"stop_sequence"`
|
| 101 |
+
Usage AnthropicUsage `json:"usage"`
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// AnthropicUsage tracks token usage
|
| 105 |
+
type AnthropicUsage struct {
|
| 106 |
+
InputTokens int `json:"input_tokens"`
|
| 107 |
+
OutputTokens int `json:"output_tokens"`
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// Streaming event types
|
| 111 |
+
|
| 112 |
+
// AnthropicStreamEvent wraps all SSE event data
|
| 113 |
+
type AnthropicStreamEvent struct {
|
| 114 |
+
Type string `json:"type"`
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// AnthropicMessageStart is the message_start event
|
| 118 |
+
type AnthropicMessageStart struct {
|
| 119 |
+
Type string `json:"type"` // "message_start"
|
| 120 |
+
Message AnthropicResponse `json:"message"`
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// AnthropicContentBlockStart is the content_block_start event
|
| 124 |
+
type AnthropicContentBlockStart struct {
|
| 125 |
+
Type string `json:"type"` // "content_block_start"
|
| 126 |
+
Index int `json:"index"`
|
| 127 |
+
ContentBlock AnthropicContentBlock `json:"content_block"`
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// AnthropicContentBlockDelta is the content_block_delta event
|
| 131 |
+
type AnthropicContentBlockDelta struct {
|
| 132 |
+
Type string `json:"type"` // "content_block_delta"
|
| 133 |
+
Index int `json:"index"`
|
| 134 |
+
Delta AnthropicContentBlockDelta2 `json:"delta"`
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// AnthropicContentBlockDelta2 is the delta payload within content_block_delta
|
| 138 |
+
type AnthropicContentBlockDelta2 struct {
|
| 139 |
+
Type string `json:"type"` // "text_delta", "thinking_delta", "input_json_delta"
|
| 140 |
+
Text string `json:"text,omitempty"` // for text_delta
|
| 141 |
+
Thinking string `json:"thinking,omitempty"` // for thinking_delta
|
| 142 |
+
PartialJSON string `json:"partial_json,omitempty"` // for input_json_delta
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// AnthropicContentBlockStop is the content_block_stop event
|
| 146 |
+
type AnthropicContentBlockStop struct {
|
| 147 |
+
Type string `json:"type"` // "content_block_stop"
|
| 148 |
+
Index int `json:"index"`
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// AnthropicMessageDelta is the message_delta event
|
| 152 |
+
type AnthropicMessageDelta struct {
|
| 153 |
+
Type string `json:"type"` // "message_delta"
|
| 154 |
+
Delta struct {
|
| 155 |
+
StopReason string `json:"stop_reason"`
|
| 156 |
+
StopSequence *string `json:"stop_sequence"`
|
| 157 |
+
} `json:"delta"`
|
| 158 |
+
Usage AnthropicUsage `json:"usage"`
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// AnthropicMessageStop is the message_stop event
|
| 162 |
+
type AnthropicMessageStop struct {
|
| 163 |
+
Type string `json:"type"` // "message_stop"
|
| 164 |
+
}
|
main.go
CHANGED
|
@@ -16,6 +16,7 @@ func main() {
|
|
| 16 |
|
| 17 |
http.HandleFunc("/v1/models", handler.HandleModels)
|
| 18 |
http.HandleFunc("/v1/chat/completions", handler.HandleChatCompletions)
|
|
|
|
| 19 |
|
| 20 |
addr := ":" + config.Cfg.Port
|
| 21 |
logger.LogInfo("Server starting on %s", addr)
|
|
|
|
| 16 |
|
| 17 |
http.HandleFunc("/v1/models", handler.HandleModels)
|
| 18 |
http.HandleFunc("/v1/chat/completions", handler.HandleChatCompletions)
|
| 19 |
+
http.HandleFunc("/v1/messages", handler.HandleMessages)
|
| 20 |
|
| 21 |
addr := ":" + config.Cfg.Port
|
| 22 |
logger.LogInfo("Server starting on %s", addr)
|