Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
712 lines
20 KiB
Go
712 lines
20 KiB
Go
package apicompat
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Non-streaming: ResponsesResponse → AnthropicResponse
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// ResponsesToAnthropic converts a Responses API response directly into an
|
||
// Anthropic Messages response. Reasoning output items are mapped to thinking
|
||
// blocks; function_call items become tool_use blocks.
|
||
func ResponsesToAnthropic(resp *ResponsesResponse, model string) *AnthropicResponse {
|
||
out := &AnthropicResponse{
|
||
ID: resp.ID,
|
||
Type: "message",
|
||
Role: "assistant",
|
||
Model: model,
|
||
}
|
||
|
||
var blocks []AnthropicContentBlock
|
||
|
||
for _, item := range resp.Output {
|
||
switch item.Type {
|
||
case "reasoning":
|
||
summaryText := ""
|
||
for _, s := range item.Summary {
|
||
if s.Type == "summary_text" && s.Text != "" {
|
||
summaryText += s.Text
|
||
}
|
||
}
|
||
// Always surface encrypted_content as thinking.signature so Claude
|
||
// Code / multi-turn clients can send it back. Signature-only
|
||
// thinking blocks are valid when the model omits a visible summary.
|
||
if summaryText != "" || strings.TrimSpace(item.EncryptedContent) != "" {
|
||
blocks = append(blocks, AnthropicContentBlock{
|
||
Type: "thinking",
|
||
Thinking: summaryText,
|
||
Signature: item.EncryptedContent,
|
||
})
|
||
}
|
||
case "message":
|
||
for _, part := range item.Content {
|
||
if part.Type == "output_text" && part.Text != "" {
|
||
blocks = append(blocks, AnthropicContentBlock{
|
||
Type: "text",
|
||
Text: part.Text,
|
||
})
|
||
}
|
||
}
|
||
case "function_call":
|
||
blocks = append(blocks, AnthropicContentBlock{
|
||
Type: "tool_use",
|
||
ID: fromResponsesCallID(item.CallID),
|
||
Name: item.Name,
|
||
Input: sanitizeAnthropicToolUseInput(item.Name, item.Arguments),
|
||
})
|
||
case "web_search_call":
|
||
toolUseID := "srvtoolu_" + item.ID
|
||
query := ""
|
||
if item.Action != nil {
|
||
query = item.Action.Query
|
||
}
|
||
inputJSON, _ := json.Marshal(map[string]string{"query": query})
|
||
blocks = append(blocks, AnthropicContentBlock{
|
||
Type: "server_tool_use",
|
||
ID: toolUseID,
|
||
Name: "web_search",
|
||
Input: inputJSON,
|
||
})
|
||
emptyResults, _ := json.Marshal([]struct{}{})
|
||
blocks = append(blocks, AnthropicContentBlock{
|
||
Type: "web_search_tool_result",
|
||
ToolUseID: toolUseID,
|
||
Content: emptyResults,
|
||
})
|
||
}
|
||
}
|
||
|
||
if len(blocks) == 0 {
|
||
blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: ""})
|
||
}
|
||
out.Content = blocks
|
||
|
||
out.StopReason = AnthropicStopReasonPtr(responsesStatusToAnthropicStopReason(resp.Status, resp.IncompleteDetails, blocks))
|
||
|
||
if resp.Usage != nil {
|
||
out.Usage = anthropicUsageFromResponsesUsage(resp.Usage)
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
func anthropicUsageFromResponsesUsage(usage *ResponsesUsage) AnthropicUsage {
|
||
if usage == nil {
|
||
return AnthropicUsage{}
|
||
}
|
||
|
||
cachedTokens := 0
|
||
if usage.InputTokensDetails != nil {
|
||
cachedTokens = usage.InputTokensDetails.CachedTokens
|
||
}
|
||
|
||
inputTokens := usage.InputTokens - cachedTokens - usage.CacheCreationInputTokens
|
||
if inputTokens < 0 {
|
||
inputTokens = 0
|
||
}
|
||
|
||
return AnthropicUsage{
|
||
InputTokens: inputTokens,
|
||
OutputTokens: usage.OutputTokens,
|
||
CacheReadInputTokens: cachedTokens,
|
||
CacheCreationInputTokens: usage.CacheCreationInputTokens,
|
||
}
|
||
}
|
||
|
||
func responsesStatusToAnthropicStopReason(status string, details *ResponsesIncompleteDetails, blocks []AnthropicContentBlock) string {
|
||
switch status {
|
||
case "incomplete":
|
||
if details != nil && details.Reason == "max_output_tokens" {
|
||
return "max_tokens"
|
||
}
|
||
return "end_turn"
|
||
case "completed":
|
||
if containsAnthropicToolUseBlock(blocks) {
|
||
return "tool_use"
|
||
}
|
||
return "end_turn"
|
||
default:
|
||
return "end_turn"
|
||
}
|
||
}
|
||
|
||
func containsAnthropicToolUseBlock(blocks []AnthropicContentBlock) bool {
|
||
for _, block := range blocks {
|
||
if block.Type == "tool_use" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func sanitizeAnthropicToolUseInput(name string, raw string) json.RawMessage {
|
||
if name != "Read" || raw == "" {
|
||
return json.RawMessage(raw)
|
||
}
|
||
|
||
var input map[string]json.RawMessage
|
||
if err := json.Unmarshal([]byte(raw), &input); err != nil {
|
||
return json.RawMessage(raw)
|
||
}
|
||
|
||
if pages, ok := input["pages"]; !ok || string(pages) != `""` {
|
||
return json.RawMessage(raw)
|
||
}
|
||
|
||
delete(input, "pages")
|
||
sanitized, err := json.Marshal(input)
|
||
if err != nil {
|
||
return json.RawMessage(raw)
|
||
}
|
||
return sanitized
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Streaming: ResponsesStreamEvent → []AnthropicStreamEvent (stateful converter)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// ResponsesEventToAnthropicState tracks state for converting a sequence of
|
||
// Responses SSE events directly into Anthropic SSE events.
|
||
type ResponsesEventToAnthropicState struct {
|
||
MessageStartSent bool
|
||
MessageStopSent bool
|
||
|
||
ContentBlockIndex int
|
||
ContentBlockOpen bool
|
||
CurrentBlockType string // "text" | "thinking" | "tool_use"
|
||
CurrentToolName string
|
||
CurrentToolArgs string
|
||
CurrentToolHadDelta bool
|
||
// PendingThinkingSignature is filled from reasoning.encrypted_content and
|
||
// emitted as signature_delta before the thinking block is closed.
|
||
PendingThinkingSignature string
|
||
HasToolCall bool
|
||
|
||
// OutputIndexToBlockIdx maps Responses output_index → Anthropic content block index.
|
||
OutputIndexToBlockIdx map[int]int
|
||
|
||
InputTokens int
|
||
OutputTokens int
|
||
CacheReadInputTokens int
|
||
CacheCreationInputTokens int
|
||
|
||
ResponseID string
|
||
Model string
|
||
Created int64
|
||
}
|
||
|
||
// NewResponsesEventToAnthropicState returns an initialised stream state.
|
||
func NewResponsesEventToAnthropicState() *ResponsesEventToAnthropicState {
|
||
return &ResponsesEventToAnthropicState{
|
||
OutputIndexToBlockIdx: make(map[int]int),
|
||
Created: time.Now().Unix(),
|
||
}
|
||
}
|
||
|
||
// ResponsesEventToAnthropicEvents converts a single Responses SSE event into
|
||
// zero or more Anthropic SSE events, updating state as it goes.
|
||
func ResponsesEventToAnthropicEvents(
|
||
evt *ResponsesStreamEvent,
|
||
state *ResponsesEventToAnthropicState,
|
||
) []AnthropicStreamEvent {
|
||
switch evt.Type {
|
||
case "response.created":
|
||
return resToAnthHandleCreated(evt, state)
|
||
case "response.output_item.added":
|
||
return resToAnthHandleOutputItemAdded(evt, state)
|
||
case "response.output_text.delta":
|
||
return resToAnthHandleTextDelta(evt, state)
|
||
case "response.output_text.done":
|
||
return resToAnthHandleBlockDone(state)
|
||
case "response.function_call_arguments.delta",
|
||
// custom/freeform 工具的输入增量与 function_call 参数增量同形。
|
||
"response.custom_tool_call_input.delta":
|
||
return resToAnthHandleFuncArgsDelta(evt, state)
|
||
case "response.function_call_arguments.done":
|
||
return resToAnthHandleFuncArgsDone(evt, state)
|
||
case "response.output_item.done":
|
||
return resToAnthHandleOutputItemDone(evt, state)
|
||
case "response.reasoning_summary_text.delta",
|
||
// 原始推理文本增量,与 reasoning summary 一样映射为 thinking。
|
||
"response.reasoning_text.delta":
|
||
return resToAnthHandleReasoningDelta(evt, state)
|
||
case "response.reasoning_summary_text.done":
|
||
// Keep the thinking block open until response.output_item.done.
|
||
// Grok/Codex attach encrypted_content on the finished reasoning item;
|
||
// closing early would drop signature_delta and break multi-turn cache.
|
||
return nil
|
||
// response.done 是 Realtime/WS 与项目透传路径使用的终止别名;
|
||
// 普通 Responses HTTP SSE 的公开终止事件仍以 response.completed 为主。
|
||
case "response.completed", "response.done", "response.incomplete", "response.failed":
|
||
return resToAnthHandleCompleted(evt, state)
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// FinalizeResponsesAnthropicStream emits synthetic termination events if the
|
||
// stream ended without a proper completion event.
|
||
func FinalizeResponsesAnthropicStream(state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if !state.MessageStartSent || state.MessageStopSent {
|
||
return nil
|
||
}
|
||
|
||
var events []AnthropicStreamEvent
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
|
||
stopReason := "end_turn"
|
||
if state.HasToolCall {
|
||
stopReason = "tool_use"
|
||
}
|
||
|
||
events = append(events,
|
||
AnthropicStreamEvent{
|
||
Type: "message_delta",
|
||
Delta: &AnthropicDelta{
|
||
StopReason: stopReason,
|
||
},
|
||
Usage: &AnthropicUsage{
|
||
InputTokens: state.InputTokens,
|
||
OutputTokens: state.OutputTokens,
|
||
CacheReadInputTokens: state.CacheReadInputTokens,
|
||
CacheCreationInputTokens: state.CacheCreationInputTokens,
|
||
},
|
||
},
|
||
AnthropicStreamEvent{Type: "message_stop"},
|
||
)
|
||
state.MessageStopSent = true
|
||
return events
|
||
}
|
||
|
||
// ResponsesAnthropicEventToSSE formats an AnthropicStreamEvent as an SSE line pair.
|
||
func ResponsesAnthropicEventToSSE(evt AnthropicStreamEvent) (string, error) {
|
||
data, err := json.Marshal(evt)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return fmt.Sprintf("event: %s\ndata: %s\n\n", evt.Type, data), nil
|
||
}
|
||
|
||
// --- internal handlers ---
|
||
|
||
func resToAnthHandleCreated(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if evt.Response != nil {
|
||
state.ResponseID = evt.Response.ID
|
||
// Only use upstream model if no override was set (e.g. originalModel)
|
||
if state.Model == "" {
|
||
state.Model = evt.Response.Model
|
||
}
|
||
}
|
||
|
||
if state.MessageStartSent {
|
||
return nil
|
||
}
|
||
state.MessageStartSent = true
|
||
|
||
// Official Anthropic message_start uses stop_reason: null and usage with
|
||
// input_tokens when known. We leave StopReason nil (JSON null) and usage
|
||
// zeros until response.completed; never emit stop_reason:"" which breaks
|
||
// strict clients' turn-finalization / session usage accounting.
|
||
return []AnthropicStreamEvent{{
|
||
Type: "message_start",
|
||
Message: &AnthropicResponse{
|
||
ID: state.ResponseID,
|
||
Type: "message",
|
||
Role: "assistant",
|
||
Content: []AnthropicContentBlock{},
|
||
Model: state.Model,
|
||
StopReason: nil,
|
||
Usage: AnthropicUsage{
|
||
InputTokens: 0,
|
||
OutputTokens: 0,
|
||
},
|
||
},
|
||
}}
|
||
}
|
||
|
||
func resToAnthHandleOutputItemAdded(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if evt.Item == nil {
|
||
return nil
|
||
}
|
||
|
||
switch evt.Item.Type {
|
||
// function_call 与 custom_tool_call(custom/freeform 工具,如新版 apply_patch)
|
||
// 同样映射为 Anthropic 的 tool_use 块。
|
||
case "function_call", "custom_tool_call":
|
||
var events []AnthropicStreamEvent
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
|
||
idx := state.ContentBlockIndex
|
||
state.OutputIndexToBlockIdx[evt.OutputIndex] = idx
|
||
state.ContentBlockOpen = true
|
||
state.CurrentBlockType = "tool_use"
|
||
state.CurrentToolName = evt.Item.Name
|
||
state.CurrentToolArgs = ""
|
||
state.CurrentToolHadDelta = false
|
||
state.HasToolCall = true
|
||
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_start",
|
||
Index: &idx,
|
||
ContentBlock: &AnthropicContentBlock{
|
||
Type: "tool_use",
|
||
ID: fromResponsesCallID(evt.Item.CallID),
|
||
Name: evt.Item.Name,
|
||
Input: json.RawMessage("{}"),
|
||
},
|
||
})
|
||
return events
|
||
|
||
case "reasoning":
|
||
var events []AnthropicStreamEvent
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
|
||
idx := state.ContentBlockIndex
|
||
state.OutputIndexToBlockIdx[evt.OutputIndex] = idx
|
||
state.ContentBlockOpen = true
|
||
state.CurrentBlockType = "thinking"
|
||
state.PendingThinkingSignature = strings.TrimSpace(evt.Item.EncryptedContent)
|
||
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_start",
|
||
Index: &idx,
|
||
ContentBlock: &AnthropicContentBlock{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
},
|
||
})
|
||
return events
|
||
|
||
case "message":
|
||
return nil
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func resToAnthHandleTextDelta(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if evt.Delta == "" {
|
||
return nil
|
||
}
|
||
|
||
var events []AnthropicStreamEvent
|
||
|
||
if !state.ContentBlockOpen || state.CurrentBlockType != "text" {
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
|
||
idx := state.ContentBlockIndex
|
||
state.ContentBlockOpen = true
|
||
state.CurrentBlockType = "text"
|
||
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_start",
|
||
Index: &idx,
|
||
ContentBlock: &AnthropicContentBlock{
|
||
Type: "text",
|
||
Text: "",
|
||
},
|
||
})
|
||
}
|
||
|
||
idx := state.ContentBlockIndex
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_delta",
|
||
Index: &idx,
|
||
Delta: &AnthropicDelta{
|
||
Type: "text_delta",
|
||
Text: evt.Delta,
|
||
},
|
||
})
|
||
return events
|
||
}
|
||
|
||
func resToAnthHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if evt.Delta == "" {
|
||
return nil
|
||
}
|
||
|
||
if state.CurrentBlockType == "tool_use" && state.CurrentToolName == "Read" {
|
||
state.CurrentToolArgs += evt.Delta
|
||
if state.CurrentToolHadDelta || !json.Valid([]byte(state.CurrentToolArgs)) {
|
||
return nil
|
||
}
|
||
|
||
blockIdx, ok := state.OutputIndexToBlockIdx[evt.OutputIndex]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
state.CurrentToolHadDelta = true
|
||
sanitized := sanitizeAnthropicToolUseInput(state.CurrentToolName, state.CurrentToolArgs)
|
||
return []AnthropicStreamEvent{{
|
||
Type: "content_block_delta",
|
||
Index: &blockIdx,
|
||
Delta: &AnthropicDelta{
|
||
Type: "input_json_delta",
|
||
PartialJSON: string(sanitized),
|
||
},
|
||
}}
|
||
}
|
||
|
||
if state.CurrentBlockType == "tool_use" {
|
||
state.CurrentToolHadDelta = true
|
||
}
|
||
|
||
blockIdx, ok := state.OutputIndexToBlockIdx[evt.OutputIndex]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
return []AnthropicStreamEvent{{
|
||
Type: "content_block_delta",
|
||
Index: &blockIdx,
|
||
Delta: &AnthropicDelta{
|
||
Type: "input_json_delta",
|
||
PartialJSON: evt.Delta,
|
||
},
|
||
}}
|
||
}
|
||
|
||
func resToAnthHandleFuncArgsDone(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if !state.ContentBlockOpen {
|
||
return nil
|
||
}
|
||
if state.CurrentBlockType != "tool_use" {
|
||
return resToAnthHandleBlockDone(state)
|
||
}
|
||
|
||
raw := evt.Arguments
|
||
if raw == "" {
|
||
raw = state.CurrentToolArgs
|
||
}
|
||
if raw == "" || state.CurrentToolHadDelta {
|
||
return closeCurrentBlock(state)
|
||
}
|
||
if state.CurrentToolName == "Read" {
|
||
sanitized := sanitizeAnthropicToolUseInput(state.CurrentToolName, raw)
|
||
if len(sanitized) == 0 {
|
||
return closeCurrentBlock(state)
|
||
}
|
||
raw = string(sanitized)
|
||
}
|
||
|
||
// 从事件的 OutputIndex 解析正确的 block index,与 resToAnthHandleFuncArgsDelta 对齐
|
||
blockIdx, ok := state.OutputIndexToBlockIdx[evt.OutputIndex]
|
||
if !ok {
|
||
blockIdx = state.ContentBlockIndex
|
||
}
|
||
|
||
// 如果 block 已关闭(ContentBlockIndex 已越过它),说明 arguments 已通过 delta 流式发完,不再补发
|
||
if !state.ContentBlockOpen || blockIdx != state.ContentBlockIndex {
|
||
return nil
|
||
}
|
||
|
||
events := []AnthropicStreamEvent{{
|
||
Type: "content_block_delta",
|
||
Index: &blockIdx,
|
||
Delta: &AnthropicDelta{
|
||
Type: "input_json_delta",
|
||
PartialJSON: raw,
|
||
},
|
||
}}
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
return events
|
||
}
|
||
|
||
func resToAnthHandleReasoningDelta(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if evt.Delta == "" {
|
||
return nil
|
||
}
|
||
|
||
blockIdx, ok := state.OutputIndexToBlockIdx[evt.OutputIndex]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
return []AnthropicStreamEvent{{
|
||
Type: "content_block_delta",
|
||
Index: &blockIdx,
|
||
Delta: &AnthropicDelta{
|
||
Type: "thinking_delta",
|
||
Thinking: evt.Delta,
|
||
},
|
||
}}
|
||
}
|
||
|
||
func resToAnthHandleBlockDone(state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if !state.ContentBlockOpen {
|
||
return nil
|
||
}
|
||
return closeCurrentBlock(state)
|
||
}
|
||
|
||
func resToAnthHandleOutputItemDone(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if evt.Item == nil {
|
||
return nil
|
||
}
|
||
|
||
// Handle web_search_call → synthesize server_tool_use + web_search_tool_result blocks.
|
||
if evt.Item.Type == "web_search_call" && evt.Item.Status == "completed" {
|
||
return resToAnthHandleWebSearchDone(evt, state)
|
||
}
|
||
|
||
// Capture encrypted_content on reasoning item done (often only present here).
|
||
if evt.Item.Type == "reasoning" {
|
||
if sig := strings.TrimSpace(evt.Item.EncryptedContent); sig != "" {
|
||
state.PendingThinkingSignature = sig
|
||
}
|
||
}
|
||
|
||
if state.ContentBlockOpen {
|
||
return closeCurrentBlock(state)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// resToAnthHandleWebSearchDone converts an OpenAI web_search_call output item
|
||
// into Anthropic server_tool_use + web_search_tool_result content block pairs.
|
||
// This allows Claude Code to count the searches performed.
|
||
func resToAnthHandleWebSearchDone(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
var events []AnthropicStreamEvent
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
|
||
toolUseID := "srvtoolu_" + evt.Item.ID
|
||
query := ""
|
||
if evt.Item.Action != nil {
|
||
query = evt.Item.Action.Query
|
||
}
|
||
inputJSON, _ := json.Marshal(map[string]string{"query": query})
|
||
|
||
// Emit server_tool_use block (start + stop).
|
||
idx1 := state.ContentBlockIndex
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_start",
|
||
Index: &idx1,
|
||
ContentBlock: &AnthropicContentBlock{
|
||
Type: "server_tool_use",
|
||
ID: toolUseID,
|
||
Name: "web_search",
|
||
Input: inputJSON,
|
||
},
|
||
})
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_stop",
|
||
Index: &idx1,
|
||
})
|
||
state.ContentBlockIndex++
|
||
|
||
// Emit web_search_tool_result block (start + stop).
|
||
// Content is empty because OpenAI does not expose individual search results;
|
||
// the model consumes them internally and produces text output.
|
||
emptyResults, _ := json.Marshal([]struct{}{})
|
||
idx2 := state.ContentBlockIndex
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_start",
|
||
Index: &idx2,
|
||
ContentBlock: &AnthropicContentBlock{
|
||
Type: "web_search_tool_result",
|
||
ToolUseID: toolUseID,
|
||
Content: emptyResults,
|
||
},
|
||
})
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_stop",
|
||
Index: &idx2,
|
||
})
|
||
state.ContentBlockIndex++
|
||
|
||
return events
|
||
}
|
||
|
||
func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if state.MessageStopSent {
|
||
return nil
|
||
}
|
||
|
||
var events []AnthropicStreamEvent
|
||
events = append(events, closeCurrentBlock(state)...)
|
||
|
||
stopReason := "end_turn"
|
||
if evt.Usage != nil {
|
||
usage := anthropicUsageFromResponsesUsage(evt.Usage)
|
||
state.InputTokens = usage.InputTokens
|
||
state.OutputTokens = usage.OutputTokens
|
||
state.CacheReadInputTokens = usage.CacheReadInputTokens
|
||
state.CacheCreationInputTokens = usage.CacheCreationInputTokens
|
||
}
|
||
if evt.Response != nil {
|
||
if evt.Response.Usage != nil {
|
||
usage := anthropicUsageFromResponsesUsage(evt.Response.Usage)
|
||
state.InputTokens = usage.InputTokens
|
||
state.OutputTokens = usage.OutputTokens
|
||
state.CacheReadInputTokens = usage.CacheReadInputTokens
|
||
state.CacheCreationInputTokens = usage.CacheCreationInputTokens
|
||
}
|
||
switch evt.Response.Status {
|
||
case "incomplete":
|
||
if evt.Response.IncompleteDetails != nil && evt.Response.IncompleteDetails.Reason == "max_output_tokens" {
|
||
stopReason = "max_tokens"
|
||
}
|
||
case "completed":
|
||
if state.HasToolCall {
|
||
stopReason = "tool_use"
|
||
}
|
||
}
|
||
}
|
||
|
||
events = append(events,
|
||
AnthropicStreamEvent{
|
||
Type: "message_delta",
|
||
Delta: &AnthropicDelta{
|
||
StopReason: stopReason,
|
||
},
|
||
Usage: &AnthropicUsage{
|
||
InputTokens: state.InputTokens,
|
||
OutputTokens: state.OutputTokens,
|
||
CacheReadInputTokens: state.CacheReadInputTokens,
|
||
CacheCreationInputTokens: state.CacheCreationInputTokens,
|
||
},
|
||
},
|
||
AnthropicStreamEvent{Type: "message_stop"},
|
||
)
|
||
state.MessageStopSent = true
|
||
return events
|
||
}
|
||
|
||
func closeCurrentBlock(state *ResponsesEventToAnthropicState) []AnthropicStreamEvent {
|
||
if !state.ContentBlockOpen {
|
||
return nil
|
||
}
|
||
idx := state.ContentBlockIndex
|
||
var events []AnthropicStreamEvent
|
||
// Emit signature_delta before stop so Claude clients retain encrypted
|
||
// reasoning for the next turn (required for Grok multi-turn cache).
|
||
if state.CurrentBlockType == "thinking" {
|
||
if sig := strings.TrimSpace(state.PendingThinkingSignature); sig != "" {
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_delta",
|
||
Index: &idx,
|
||
Delta: &AnthropicDelta{
|
||
Type: "signature_delta",
|
||
Signature: sig,
|
||
},
|
||
})
|
||
}
|
||
state.PendingThinkingSignature = ""
|
||
}
|
||
state.ContentBlockOpen = false
|
||
state.ContentBlockIndex++
|
||
state.CurrentToolName = ""
|
||
state.CurrentToolArgs = ""
|
||
state.CurrentToolHadDelta = false
|
||
events = append(events, AnthropicStreamEvent{
|
||
Type: "content_block_stop",
|
||
Index: &idx,
|
||
})
|
||
return events
|
||
}
|