ss22345 commited on
Commit
4f59ab4
·
1 Parent(s): 3a315de

feat: add Anthropic Messages API compatibility (/v1/messages)

Browse files

Implement 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 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)