| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- 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
- }
|