iop/apps/edge/internal/openai/anthropic_stream.go
toki e5869a53ae fix(edge): provider 호출 정규화 계층을 추가한다
caller별 예외 대신 요청 의미와 protocol profile capability로 operation을 선택해 tools와 effort 조합을 보존한다. 지원하지 않는 effort는 가장 가까운 하위 등급으로만 내리고 상향 매핑은 거부한다.
2026-08-13 23:06:50 +09:00

1342 lines
40 KiB
Go

package openai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"sync"
"time"
"go.uber.org/zap"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/streamgate"
iop "iop/proto/gen/iop"
)
const anthropicChatBridgeRejectionLogMessage = "edge_anthropic_chat_bridge_rejection"
func (s *Server) observeAnthropicChatBridgeRejection(status int) {
s.logger.Info(
anthropicChatBridgeRejectionLogMessage,
zap.String("surface", "messages"),
zap.String("bridge", "chat"),
zap.String("rejection_class", "provider_http"),
zap.Int("http_status", status),
)
}
type openAIChatStreamChunk struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
Reasoning string `json:"reasoning"`
ToolCalls []struct {
Index int `json:"index"`
ID string `json:"id"`
ExtraContent openAIChatToolExtraContent `json:"extra_content,omitempty"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"delta"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
Usage *openAIChatBridgeUsage `json:"usage,omitempty"`
Error *struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
type anthropicBridgeToolState struct {
id string
name string
extraContent openAIChatToolExtraContent
arguments strings.Builder
}
type anthropicBridgeStream struct {
w http.ResponseWriter
model string
id string
started bool
stopped bool
nextBlock int
openBlock bool
openKind string
finish *string
usage anthropicUsage
tools map[int]*anthropicBridgeToolState
pendingSSE []byte
}
func newAnthropicBridgeStream(w http.ResponseWriter, model string) *anthropicBridgeStream {
return &anthropicBridgeStream{w: w, model: model, tools: make(map[int]*anthropicBridgeToolState)}
}
func (s *anthropicBridgeStream) Feed(chunk []byte) error {
if s.stopped {
return nil
}
s.pendingSSE = append(s.pendingSSE, chunk...)
s.pendingSSE = bytes.ReplaceAll(s.pendingSSE, []byte("\r\n"), []byte("\n"))
for {
index := bytes.Index(s.pendingSSE, []byte("\n\n"))
if index < 0 {
return nil
}
event := append([]byte(nil), s.pendingSSE[:index]...)
s.pendingSSE = s.pendingSSE[index+2:]
if err := s.consumeSSEEvent(event); err != nil {
return err
}
if s.stopped {
s.pendingSSE = nil
return nil
}
}
}
func (s *anthropicBridgeStream) consumeSSEEvent(event []byte) error {
var dataLines [][]byte
for _, line := range bytes.Split(event, []byte("\n")) {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == ':' {
continue
}
if bytes.HasPrefix(line, []byte("data:")) {
dataLines = append(dataLines, bytes.TrimSpace(bytes.TrimPrefix(line, []byte("data:"))))
}
}
if len(dataLines) == 0 {
return nil
}
payload := bytes.Join(dataLines, []byte("\n"))
if bytes.Equal(payload, []byte("[DONE]")) {
return s.Finish()
}
var chunk openAIChatStreamChunk
if err := json.Unmarshal(payload, &chunk); err != nil {
return fmt.Errorf("decode Chat SSE event: %w", err)
}
if chunk.Error != nil {
message := strings.TrimSpace(chunk.Error.Message)
if message == "" {
message = "upstream provider error"
}
return s.Error(chunk.Error.Type, message)
}
if chunk.ID != "" && s.id == "" {
s.id = chunk.ID
}
if chunk.Usage != nil {
s.usage.InputTokens = chunk.Usage.PromptTokens
s.usage.OutputTokens = chunk.Usage.CompletionTokens
s.usage.CacheReadInputTokens = chunk.Usage.PromptDetails.CachedTokens
}
if err := s.start(); err != nil {
return err
}
for _, choice := range chunk.Choices {
reasoning := choice.Delta.ReasoningContent
if reasoning == "" {
reasoning = choice.Delta.Reasoning
}
if reasoning != "" {
if err := s.delta("thinking", reasoning); err != nil {
return err
}
}
if choice.Delta.Content != "" {
if err := s.delta("text", choice.Delta.Content); err != nil {
return err
}
}
for _, delta := range choice.Delta.ToolCalls {
state := s.tools[delta.Index]
if state == nil {
state = &anthropicBridgeToolState{}
s.tools[delta.Index] = state
}
if delta.ID != "" {
state.id = delta.ID
}
if delta.Function.Name != "" {
state.name = delta.Function.Name
}
if delta.ExtraContent.Google != nil && delta.ExtraContent.Google.ThoughtSignature != "" {
state.extraContent = delta.ExtraContent
}
state.arguments.WriteString(delta.Function.Arguments)
}
if choice.FinishReason != nil {
s.finish = choice.FinishReason
}
}
return nil
}
func (s *anthropicBridgeStream) start() error {
if s.started {
return nil
}
s.started = true
id := s.id
if id == "" {
id = "msg_iop"
}
return writeAnthropicSSEEvent(s.w, "message_start", map[string]any{
"type": "message_start",
"message": anthropicMessageResponse{
ID: id, Type: "message", Role: "assistant", Model: s.model,
Content: []map[string]any{}, StopReason: nil, StopSequence: nil,
Usage: anthropicUsage{InputTokens: s.usage.InputTokens},
},
})
}
func (s *anthropicBridgeStream) delta(kind, value string) error {
if !s.openBlock || s.openKind != kind {
if err := s.closeBlock(); err != nil {
return err
}
block := map[string]any{"type": kind}
if kind == "thinking" {
block["thinking"] = ""
block["signature"] = ""
} else {
block["text"] = ""
}
if err := writeAnthropicSSEEvent(s.w, "content_block_start", map[string]any{
"type": "content_block_start", "index": s.nextBlock, "content_block": block,
}); err != nil {
return err
}
s.openBlock, s.openKind = true, kind
}
deltaType, field := "text_delta", "text"
if kind == "thinking" {
deltaType, field = "thinking_delta", "thinking"
}
return writeAnthropicSSEEvent(s.w, "content_block_delta", map[string]any{
"type": "content_block_delta", "index": s.nextBlock,
"delta": map[string]any{"type": deltaType, field: value},
})
}
func (s *anthropicBridgeStream) closeBlock() error {
if !s.openBlock {
return nil
}
if err := writeAnthropicSSEEvent(s.w, "content_block_stop", map[string]any{
"type": "content_block_stop", "index": s.nextBlock,
}); err != nil {
return err
}
s.nextBlock++
s.openBlock = false
s.openKind = ""
return nil
}
func (s *anthropicBridgeStream) emitTools() error {
if len(s.tools) == 0 {
return nil
}
indices := make([]int, 0, len(s.tools))
for index := range s.tools {
indices = append(indices, index)
}
sort.Ints(indices)
for _, index := range indices {
tool := s.tools[index]
arguments := tool.arguments.String()
if tool.id == "" || tool.name == "" || !json.Valid([]byte(arguments)) {
return fmt.Errorf("Chat stream tool call has invalid id, name, or arguments")
}
if err := writeAnthropicSSEEvent(s.w, "content_block_start", map[string]any{
"type": "content_block_start", "index": s.nextBlock,
"content_block": map[string]any{
"type": "tool_use", "id": encodeAnthropicBridgeToolID(tool.id, tool.extraContent),
"name": tool.name, "input": map[string]any{},
},
}); err != nil {
return err
}
if err := writeAnthropicSSEEvent(s.w, "content_block_delta", map[string]any{
"type": "content_block_delta", "index": s.nextBlock,
"delta": map[string]any{"type": "input_json_delta", "partial_json": arguments},
}); err != nil {
return err
}
if err := writeAnthropicSSEEvent(s.w, "content_block_stop", map[string]any{
"type": "content_block_stop", "index": s.nextBlock,
}); err != nil {
return err
}
s.nextBlock++
}
return nil
}
func (s *anthropicBridgeStream) Finish() error {
if s.stopped {
return nil
}
if err := s.start(); err != nil {
return err
}
if err := s.closeBlock(); err != nil {
return err
}
if err := s.emitTools(); err != nil {
return err
}
stopReason, err := anthropicStopReason(s.finish)
if err != nil {
return err
}
if stopReason == nil {
fallback := "end_turn"
stopReason = &fallback
}
if err := writeAnthropicSSEEvent(s.w, "message_delta", map[string]any{
"type": "message_delta", "delta": map[string]any{"stop_reason": *stopReason, "stop_sequence": nil},
"usage": s.usage,
}); err != nil {
return err
}
if err := writeAnthropicSSEEvent(s.w, "message_stop", map[string]any{"type": "message_stop"}); err != nil {
return err
}
s.stopped = true
return nil
}
func (s *anthropicBridgeStream) Error(errorType, message string) error {
if s.stopped {
return nil
}
if strings.TrimSpace(errorType) == "" {
errorType = "api_error"
}
s.stopped = true
return writeAnthropicSSEEvent(s.w, "error", anthropicErrorResponse{
Type: "error", Error: errorBody{Type: errorType, Message: message},
})
}
func writeAnthropicSSEEvent(w http.ResponseWriter, event string, payload any) error {
encoded, err := json.Marshal(payload)
if err != nil {
return err
}
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, encoded); err != nil {
return err
}
return nil
}
// anthropicHotPathCodec is the caller-facing Messages codec for one preset
// HTTP turn. It consumes only the normalized outer-turn accumulator and
// release log; selected-provider wire decoding remains in the shared stage
// decoders. The codec owns exactly one caller envelope and terminal.
type anthropicHotPathCodec struct {
mu sync.Mutex
w http.ResponseWriter
model string
stream bool
requestID string
maxTokens int
outer *hotPathOuterTurn
flusher http.Flusher
started bool
terminal bool
releaseAttached bool
progressiveTools bool
nextBlock int
openBlock bool
openKind string
openToolID string
emittedTools map[string]struct{}
}
type hotPathAnthropicCodecContextKey struct{}
type anthropicHotPathBlock struct {
kind string
id string
name string
signature string
fragments []string
toolIndex int
}
func newAnthropicHotPathCodec(
w http.ResponseWriter,
model string,
stream bool,
requestID string,
maxTokens int,
) *anthropicHotPathCodec {
return &anthropicHotPathCodec{
w: w, model: model, stream: stream, requestID: requestID, maxTokens: maxTokens,
}
}
func withHotPathAnthropicCodec(r *http.Request, codec *anthropicHotPathCodec) *http.Request {
if r == nil || codec == nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), hotPathAnthropicCodecContextKey{}, codec))
}
func hotPathAnthropicCodecFromRequest(r *http.Request) *anthropicHotPathCodec {
if r == nil {
return nil
}
codec, _ := r.Context().Value(hotPathAnthropicCodecContextKey{}).(*anthropicHotPathCodec)
return codec
}
func (c *anthropicHotPathCodec) callerOuterTurn(responseID string, outputCapTokens int) *hotPathOuterTurn {
if c == nil {
return newHotPathCallerCappedOuterTurn(responseID, outputCapTokens)
}
c.mu.Lock()
defer c.mu.Unlock()
if c.outer == nil {
capTokens := c.maxTokens
if capTokens <= 0 {
capTokens = outputCapTokens
}
c.outer = newHotPathCallerCappedOuterTurn(responseID, capTokens)
}
return c.outer
}
func (c *anthropicHotPathCodec) currentOuterTurn() *hotPathOuterTurn {
if c == nil {
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
return c.outer
}
// prepareProgressiveWriter connects the normalized outer-turn release seam to
// the caller-facing Messages codec. Initial-selector tool fragments stay held
// until structural classification; later, already-classified Light stages may
// release tool fragments as well as text and reasoning.
func (c *anthropicHotPathCodec) prepareProgressiveWriter(w http.ResponseWriter, outer *hotPathOuterTurn, releaseTools bool) error {
if c == nil || !c.stream || outer == nil {
return nil
}
flusher, ok := w.(http.Flusher)
if !ok {
return fmt.Errorf("response writer does not support flushing")
}
c.mu.Lock()
c.w = w
c.flusher = flusher
c.outer = outer
c.progressiveTools = releaseTools
if c.emittedTools == nil {
c.emittedTools = make(map[string]struct{})
}
attached := c.releaseAttached
if !attached {
c.releaseAttached = true
}
c.mu.Unlock()
if attached {
return nil
}
if err := outer.setReleaseCallback(func(delta hotPathReleasedDelta) error {
return c.writeProgressiveDelta(outer, delta)
}); err != nil {
c.mu.Lock()
c.releaseAttached = false
c.mu.Unlock()
return err
}
return nil
}
func (c *anthropicHotPathCodec) writeProgressiveDelta(outer *hotPathOuterTurn, delta hotPathReleasedDelta) error {
responseID, ok := outer.publicResponseIdentity()
if !ok {
return fmt.Errorf("Anthropic Hot Path response is missing provider identity")
}
c.mu.Lock()
defer c.mu.Unlock()
if c.terminal {
return errHotPathTurnTerminal
}
if delta.Kind == streamgate.EventKindToolCallFragment && !c.progressiveTools {
return nil
}
usage := c.previewUsageLocked(outer)
if _, err := c.startStreamLocked(responseID, usage); err != nil {
return err
}
switch delta.Kind {
case streamgate.EventKindReasoningDelta:
if err := c.ensureProgressiveBlockLocked(outer, "thinking", "", ""); err != nil {
return err
}
return c.writeProgressiveBlockDeltaLocked(map[string]any{"type": "thinking_delta", "thinking": delta.Text})
case streamgate.EventKindTextDelta:
if err := c.ensureProgressiveBlockLocked(outer, "text", "", ""); err != nil {
return err
}
return c.writeProgressiveBlockDeltaLocked(map[string]any{"type": "text_delta", "text": delta.Text})
case streamgate.EventKindToolCallFragment:
if strings.TrimSpace(delta.PublicID) == "" || strings.TrimSpace(delta.Name) == "" {
return fmt.Errorf("Anthropic Hot Path tool block is missing id or name")
}
if err := c.ensureProgressiveBlockLocked(outer, "tool_use", delta.PublicID, delta.Name); err != nil {
return err
}
c.emittedTools[delta.PublicID] = struct{}{}
return c.writeProgressiveBlockDeltaLocked(map[string]any{"type": "input_json_delta", "partial_json": delta.Args})
default:
return fmt.Errorf("unsupported progressive Anthropic delta kind %q", delta.Kind)
}
}
func (c *anthropicHotPathCodec) previewUsageLocked(outer *hotPathOuterTurn) json.RawMessage {
usage, ok := outer.currentPreviewUsage()
if !ok {
return nil
}
raw, _ := json.Marshal(anthropicUsage{
InputTokens: usage.InputTokens, OutputTokens: usage.OutputTokens,
CacheReadInputTokens: usage.CachedInputTokens,
})
return raw
}
func (c *anthropicHotPathCodec) ensureProgressiveBlockLocked(outer *hotPathOuterTurn, kind, toolID, toolName string) error {
if c.openBlock && c.openKind == kind && (kind != "tool_use" || c.openToolID == toolID) {
return nil
}
if err := c.closeProgressiveBlockLocked(outer, ""); err != nil {
return err
}
block := map[string]any{"type": kind}
switch kind {
case "thinking":
block["thinking"], block["signature"] = "", ""
case "text":
block["text"] = ""
case "tool_use":
block["id"], block["name"], block["input"] = toolID, toolName, map[string]any{}
default:
return fmt.Errorf("unsupported Anthropic content block kind %q", kind)
}
if err := writeDirectAnthropicEvent(c.w, c.flusher, "content_block_start", map[string]any{
"type": "content_block_start", "index": c.nextBlock, "content_block": block,
}); err != nil {
return err
}
c.openBlock = true
c.openKind = kind
c.openToolID = toolID
return nil
}
func (c *anthropicHotPathCodec) writeProgressiveBlockDeltaLocked(delta map[string]any) error {
return writeDirectAnthropicEvent(c.w, c.flusher, "content_block_delta", map[string]any{
"type": "content_block_delta", "index": c.nextBlock, "delta": delta,
})
}
func (c *anthropicHotPathCodec) closeProgressiveBlockLocked(outer *hotPathOuterTurn, finalSignature string) error {
if !c.openBlock {
return nil
}
if c.openKind == "thinking" {
signature := finalSignature
if signature == "" && outer != nil {
signature = outer.currentReasoningSignature()
}
if signature != "" {
if err := c.writeProgressiveBlockDeltaLocked(map[string]any{"type": "signature_delta", "signature": signature}); err != nil {
return err
}
}
}
if err := writeDirectAnthropicEvent(c.w, c.flusher, "content_block_stop", map[string]any{
"type": "content_block_stop", "index": c.nextBlock,
}); err != nil {
return err
}
c.nextBlock++
c.openBlock = false
c.openKind = ""
c.openToolID = ""
return nil
}
func (c *anthropicHotPathCodec) runInitialPresetTurn(
s *Server,
w http.ResponseWriter,
r *http.Request,
dispatch routeDispatch,
runMeta map[string]string,
result *edgeservice.ProviderPoolDispatchResult,
) (normalizedStageOutput, bool, error) {
var (
stage normalizedStageOutput
gate hotPathSelectorGate
err error
)
if c.stream {
outer := c.callerOuterTurn("", hotPathOutputTokenCap(runMeta))
if err := c.prepareProgressiveWriter(w, outer, false); err != nil {
return stage, false, err
}
stage, gate, err = s.runLivePresetSelectorResult(
r.Context(), dispatch, "anthropic", runMeta["iop_stage_id"], result, outer,
)
} else {
stage, gate, err = s.collectPresetSelectorResult(r.Context(), dispatch, "anthropic", result)
}
if err != nil {
if contextErr := r.Context().Err(); contextErr != nil {
// Exact active-run cancellation is complete; caller cancellation is
// intentionally wire-silent.
return stage, true, contextErr
}
return stage, false, err
}
err = s.dispatchPresetTurn(w, r, dispatch, "anthropic", c.stream, runMeta, stage, gate)
return stage, true, err
}
func writeHotPathAnthropicOuterResponse(turn *hotPathTurn, output normalizedStageOutput) (bool, error) {
if turn == nil {
return false, nil
}
codec := hotPathAnthropicCodecFromRequest(turn.Request)
if codec == nil {
return false, nil
}
codec.w = turn.Writer
if codec.model == "" {
codec.model = directPublicModel(turn)
}
return true, codec.write(output)
}
func writeHotPathAnthropicOuterError(turn *hotPathTurn, status int, errorType, message string) bool {
if turn == nil {
return false
}
codec := hotPathAnthropicCodecFromRequest(turn.Request)
if codec == nil {
return false
}
codec.w = turn.Writer
disposition := hotPathTerminalDisposition{
Kind: hotPathDispositionProviderError, Cause: message, Source: "anthropic_outer_error",
}
selected := false
if turn.OuterTurn != nil {
if terminalDisposition, ok := turn.OuterTurn.terminalDisposition(); ok {
disposition = terminalDisposition
selected = true
}
}
if !selected && strings.Contains(strings.ToLower(errorType), "invalid") {
disposition.Kind = hotPathDispositionValidationError
}
_ = codec.writeDisposition(disposition, status, errorType, message)
return true
}
func (c *anthropicHotPathCodec) bindResponseID(responseID string) error {
responseID = strings.TrimSpace(responseID)
if responseID == "" {
return fmt.Errorf("Anthropic Hot Path response is missing provider identity")
}
if c == nil {
return nil
}
c.mu.Lock()
outer := c.outer
c.mu.Unlock()
if outer != nil {
return outer.bindPublicResponseID(responseID)
}
return nil
}
func (c *anthropicHotPathCodec) write(output normalizedStageOutput) error {
if c == nil || c.w == nil {
return fmt.Errorf("Anthropic Hot Path codec is unavailable")
}
if err := c.bindResponseID(output.ResponseID); err != nil {
return err
}
responseID := strings.TrimSpace(output.ResponseID)
outer := c.currentOuterTurn()
if outer != nil {
var ok bool
responseID, ok = outer.publicResponseIdentity()
if !ok {
return fmt.Errorf("Anthropic Hot Path response is missing provider identity")
}
}
blocks, err := c.blocks(output)
if err != nil {
return err
}
stopReason := anthropicDirectStopReason(output.TerminalReason)
if outer != nil {
if disposition, ok := outer.terminalDisposition(); ok {
policy := anthropicHotPathPolicy(disposition)
switch {
case policy.silent && outer.isTerminalCommitted():
return c.writeDisposition(disposition, 0, "", "")
case policy.errorTerminal && outer.isTerminalCommitted():
return c.writeDisposition(disposition, policy.status, policy.errorType, disposition.Cause)
case policy.stopReason != "":
stopReason = policy.stopReason
}
}
}
if stopReason == "" {
if len(output.ToolCalls) > 0 {
stopReason = "tool_use"
} else {
stopReason = "end_turn"
}
}
usage := anthropicHotPathUsage(output)
if c.stream {
return c.writeStream(responseID, blocks, stopReason, usage)
}
return c.writeJSON(responseID, blocks, stopReason, usage)
}
func (c *anthropicHotPathCodec) blocks(output normalizedStageOutput) ([]anthropicHotPathBlock, error) {
var released []hotPathReleasedDelta
if c.outer != nil && !output.CallerStageOnly {
released = c.outer.releasedDeltas()
}
if len(released) == 0 {
if output.Reasoning != "" {
released = append(released, hotPathReleasedDelta{Kind: streamgate.EventKindReasoningDelta, Text: output.Reasoning})
}
if output.Content != "" {
released = append(released, hotPathReleasedDelta{Kind: streamgate.EventKindTextDelta, Text: output.Content})
}
for _, call := range output.ToolCalls {
released = append(released, hotPathReleasedDelta{
Kind: streamgate.EventKindToolCallFragment, PublicID: call.ID,
Name: call.Name, Args: directToolArguments(call),
})
}
}
blocks := make([]anthropicHotPathBlock, 0, len(released))
toolBlocks := make(map[string]int)
toolOrdinal := 0
for _, delta := range released {
switch delta.Kind {
case streamgate.EventKindReasoningDelta, streamgate.EventKindTextDelta:
kind := "text"
if delta.Kind == streamgate.EventKindReasoningDelta {
kind = "thinking"
}
if len(blocks) == 0 || blocks[len(blocks)-1].kind != kind {
blocks = append(blocks, anthropicHotPathBlock{kind: kind, toolIndex: -1})
}
blocks[len(blocks)-1].fragments = append(blocks[len(blocks)-1].fragments, delta.Text)
case streamgate.EventKindToolCallFragment:
key := delta.PublicID
if key == "" {
key = fmt.Sprintf("tool-%d", toolOrdinal)
}
blockIndex, ok := toolBlocks[key]
if !ok {
block := anthropicHotPathBlock{kind: "tool_use", id: delta.PublicID, name: delta.Name, toolIndex: toolOrdinal}
if toolOrdinal < len(output.ToolCalls) {
call := output.ToolCalls[toolOrdinal]
block.id = call.ID
block.name = call.Name
}
blocks = append(blocks, block)
blockIndex = len(blocks) - 1
toolBlocks[key] = blockIndex
toolOrdinal++
}
blocks[blockIndex].fragments = append(blocks[blockIndex].fragments, delta.Args)
}
}
for toolOrdinal < len(output.ToolCalls) {
call := output.ToolCalls[toolOrdinal]
blocks = append(blocks, anthropicHotPathBlock{
kind: "tool_use", id: call.ID, name: call.Name,
fragments: []string{directToolArguments(call)}, toolIndex: toolOrdinal,
})
toolOrdinal++
}
for index := range blocks {
block := &blocks[index]
if block.kind == "tool_use" {
if strings.TrimSpace(block.id) == "" || strings.TrimSpace(block.name) == "" {
return nil, fmt.Errorf("Anthropic Hot Path tool block is missing id or name")
}
arguments := strings.Join(block.fragments, "")
if block.toolIndex >= 0 && block.toolIndex < len(output.ToolCalls) {
expected := directToolArguments(output.ToolCalls[block.toolIndex])
if arguments == "" {
arguments = expected
block.fragments = []string{expected}
} else if expected != "" && arguments != expected {
return nil, fmt.Errorf("Anthropic Hot Path tool fragments do not match the issued call")
}
}
if arguments == "" {
arguments = "{}"
block.fragments = []string{arguments}
}
if !json.Valid([]byte(arguments)) {
return nil, fmt.Errorf("Anthropic Hot Path tool input is not valid JSON")
}
}
}
for index := len(blocks) - 1; index >= 0; index-- {
if blocks[index].kind == "thinking" {
blocks[index].signature = output.ReasoningSignature
break
}
}
return blocks, nil
}
func anthropicHotPathUsage(output normalizedStageOutput) json.RawMessage {
if len(output.Usage) > 0 {
var fields map[string]json.RawMessage
if json.Unmarshal(output.Usage, &fields) == nil {
if _, ok := fields["input_tokens"]; ok {
return cloneRawJSON(output.Usage)
}
}
}
if output.OpenAIUsage != nil {
raw, _ := json.Marshal(output.OpenAIUsage)
return openAIUsageToAnthropic(raw)
}
return openAIUsageToAnthropic(output.Usage)
}
func (c *anthropicHotPathCodec) writeJSON(responseID string, blocks []anthropicHotPathBlock, stopReason string, usage json.RawMessage) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.terminal {
return errHotPathTurnTerminal
}
content := make([]map[string]any, 0, len(blocks))
for _, block := range blocks {
switch block.kind {
case "thinking":
content = append(content, map[string]any{
"type": "thinking", "thinking": strings.Join(block.fragments, ""), "signature": block.signature,
})
case "text":
content = append(content, map[string]any{"type": "text", "text": strings.Join(block.fragments, "")})
case "tool_use":
var input any
if err := json.Unmarshal([]byte(strings.Join(block.fragments, "")), &input); err != nil {
return err
}
content = append(content, map[string]any{
"type": "tool_use", "id": block.id, "name": block.name, "input": input,
})
}
}
response := map[string]any{
"id": responseID, "type": "message", "role": "assistant", "model": c.model,
"content": content, "stop_reason": stopReason, "stop_sequence": nil,
}
if len(usage) > 0 {
response["usage"] = usage
}
c.terminal = true
return writeDirectJSON(c.w, http.StatusOK, response)
}
func (c *anthropicHotPathCodec) startStreamLocked(responseID string, usage json.RawMessage) (http.Flusher, error) {
flusher := c.flusher
if flusher == nil {
var ok bool
flusher, ok = c.w.(http.Flusher)
if !ok {
return nil, fmt.Errorf("response writer does not support flushing")
}
c.flusher = flusher
}
if c.started {
return flusher, nil
}
c.w.Header().Set("Content-Type", "text/event-stream")
c.w.Header().Set("Cache-Control", "no-cache")
c.w.WriteHeader(http.StatusOK)
message := map[string]any{
"id": responseID, "type": "message", "role": "assistant", "model": c.model,
"content": []any{}, "stop_reason": nil, "stop_sequence": nil,
}
if startUsage := anthropicStartUsage(usage); len(startUsage) > 0 {
message["usage"] = startUsage
}
if err := writeDirectAnthropicEvent(c.w, flusher, "message_start", map[string]any{
"type": "message_start", "message": message,
}); err != nil {
return nil, err
}
c.started = true
return flusher, nil
}
func (c *anthropicHotPathCodec) writeStream(responseID string, blocks []anthropicHotPathBlock, stopReason string, usage json.RawMessage) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.terminal {
return errHotPathTurnTerminal
}
flusher, err := c.startStreamLocked(responseID, usage)
if err != nil {
return err
}
if c.started && c.openBlock {
if err := c.closeProgressiveBlockLocked(c.outer, outputReasoningSignature(blocks)); err != nil {
return err
}
}
for _, block := range blocks {
if c.releaseAttached {
if block.kind != "tool_use" {
continue
}
if _, emitted := c.emittedTools[block.id]; emitted {
continue
}
}
if err := c.writeCompleteBlockLocked(block); err != nil {
return err
}
}
delta := map[string]any{
"type": "message_delta", "delta": map[string]any{"stop_reason": stopReason, "stop_sequence": nil},
}
if len(usage) > 0 {
delta["usage"] = usage
}
if err := writeDirectAnthropicEvent(c.w, flusher, "message_delta", delta); err != nil {
return err
}
if err := writeDirectAnthropicEvent(c.w, flusher, "message_stop", map[string]any{"type": "message_stop"}); err != nil {
return err
}
c.terminal = true
return nil
}
func outputReasoningSignature(blocks []anthropicHotPathBlock) string {
for index := len(blocks) - 1; index >= 0; index-- {
if blocks[index].kind == "thinking" {
return blocks[index].signature
}
}
return ""
}
func (c *anthropicHotPathCodec) writeCompleteBlockLocked(block anthropicHotPathBlock) error {
index := c.nextBlock
start := map[string]any{"type": block.kind}
switch block.kind {
case "thinking":
start["thinking"], start["signature"] = "", ""
case "text":
start["text"] = ""
case "tool_use":
start["id"], start["name"], start["input"] = block.id, block.name, map[string]any{}
}
if err := writeDirectAnthropicEvent(c.w, c.flusher, "content_block_start", map[string]any{
"type": "content_block_start", "index": index, "content_block": start,
}); err != nil {
return err
}
for _, fragment := range block.fragments {
delta := map[string]any{"type": "text_delta", "text": fragment}
switch block.kind {
case "thinking":
delta = map[string]any{"type": "thinking_delta", "thinking": fragment}
case "tool_use":
delta = map[string]any{"type": "input_json_delta", "partial_json": fragment}
}
if err := writeDirectAnthropicEvent(c.w, c.flusher, "content_block_delta", map[string]any{
"type": "content_block_delta", "index": index, "delta": delta,
}); err != nil {
return err
}
}
if block.kind == "thinking" && block.signature != "" {
if err := writeDirectAnthropicEvent(c.w, c.flusher, "content_block_delta", map[string]any{
"type": "content_block_delta", "index": index,
"delta": map[string]any{"type": "signature_delta", "signature": block.signature},
}); err != nil {
return err
}
}
if err := writeDirectAnthropicEvent(c.w, c.flusher, "content_block_stop", map[string]any{
"type": "content_block_stop", "index": index,
}); err != nil {
return err
}
c.nextBlock++
return nil
}
func (c *anthropicHotPathCodec) writeError(status int, errorType, message string) error {
disposition := hotPathTerminalDisposition{
Kind: hotPathDispositionProviderError, Cause: message, Source: "anthropic_codec_error",
}
if strings.Contains(strings.ToLower(errorType), "invalid") {
disposition.Kind = hotPathDispositionValidationError
}
if outer := c.currentOuterTurn(); outer != nil {
if selected, ok := outer.terminalDisposition(); ok {
disposition = selected
}
}
return c.writeDisposition(disposition, status, errorType, message)
}
func (c *anthropicHotPathCodec) writeDisposition(
disposition hotPathTerminalDisposition,
status int,
errorType, message string,
) error {
if c == nil || c.w == nil {
return fmt.Errorf("Anthropic Hot Path codec is unavailable")
}
policy := anthropicHotPathPolicy(disposition)
if policy.status != 0 {
status = policy.status
}
if policy.errorType != "" {
errorType = policy.errorType
}
if strings.TrimSpace(message) == "" {
message = hotPathFirstNonEmpty(disposition.Cause, "hot path stage failed")
}
c.mu.Lock()
defer c.mu.Unlock()
if c.terminal {
return errHotPathTurnTerminal
}
if policy.silent {
c.terminal = true
return nil
}
if !policy.errorTerminal {
return fmt.Errorf("Anthropic disposition %q is not an error terminal", disposition.Kind)
}
if c.stream && c.started {
flusher := c.flusher
if flusher == nil {
var ok bool
flusher, ok = c.w.(http.Flusher)
if !ok {
return fmt.Errorf("response writer does not support flushing")
}
}
c.terminal = true
return writeDirectAnthropicEvent(c.w, flusher, "error", anthropicErrorResponse{
Type: "error", Error: errorBody{Type: errorType, Message: message},
})
}
c.terminal = true
writeAnthropicError(c.w, status, errorType, message)
return nil
}
func (s *Server) writeAnthropicChatBridgeResponse(w http.ResponseWriter, r *http.Request, handle edgeservice.ProviderTunnelResult, envelope anthropicRequestEnvelope) {
frames := handle.Stream().Frames
if frames == nil {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel is unavailable")
return
}
timer := time.NewTimer(handle.WaitTimeout())
defer timer.Stop()
status := http.StatusOK
headers := make(map[string]string)
started := false
committed := false
var body []byte
var stream *anthropicBridgeStream
if envelope.Stream {
stream = newAnthropicBridgeStream(w, envelope.Model)
}
flush := func() {
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
commitStream := func() {
if committed {
return
}
copyAnthropicResponseHeaders(w.Header(), headers)
w.Header().Del("Content-Length")
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(status)
committed = true
flush()
}
for {
select {
case <-r.Context().Done():
s.cancelRunOnHTTPGiveUp(handle.Dispatch(), r.Context().Err())
return
case <-timer.C:
s.cancelRunOnHTTPGiveUp(handle.Dispatch(), errRunTimedOut)
if !committed {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider response timed out")
} else {
_ = stream.Error("api_error", "provider response timed out")
flush()
}
return
case frame, ok := <-frames:
if !ok {
if !committed {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel closed before a response")
} else {
_ = stream.Error("api_error", "provider tunnel closed before a response")
flush()
}
return
}
switch frame.GetKind() {
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START:
if started {
continue
}
started = true
status = int(frame.GetStatusCode())
if status == 0 {
status = http.StatusOK
}
if status >= http.StatusBadRequest {
s.observeAnthropicChatBridgeRejection(status)
}
for key, value := range frame.GetHeaders() {
headers[key] = value
}
if envelope.Stream && status < http.StatusBadRequest {
commitStream()
}
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY:
if envelope.Stream && status < http.StatusBadRequest {
commitStream()
if err := stream.Feed(frame.GetBody()); err != nil {
_ = stream.Error("api_error", "upstream stream could not be translated")
flush()
return
}
flush()
} else {
body = append(body, frame.GetBody()...)
}
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR:
if committed {
_ = stream.Error("api_error", "provider tunnel failed")
flush()
} else {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel failed")
}
return
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END:
if envelope.Stream && status < http.StatusBadRequest {
commitStream()
if err := stream.Finish(); err != nil {
_ = stream.Error("api_error", "upstream stream could not be translated")
}
flush()
return
}
copyAnthropicResponseHeaders(w.Header(), headers)
w.Header().Del("Content-Length")
if status >= http.StatusBadRequest {
writeJSON(w, status, convertChatErrorToAnthropic(body))
return
}
converted, err := convertChatResponseToAnthropic(body, envelope.Model)
if err != nil {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "upstream response could not be translated")
return
}
writeJSON(w, http.StatusOK, converted)
return
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE:
continue
}
}
}
}
// writeAnthropicResponsesBridgeResponse translates an OpenAI Responses
// provider result back to the Messages surface. Streaming input is currently
// buffered until the Responses terminal so the caller still receives one
// valid Anthropic SSE lifecycle without exposing provider-specific events.
func (s *Server) writeAnthropicResponsesBridgeResponse(w http.ResponseWriter, r *http.Request, handle edgeservice.ProviderTunnelResult, envelope anthropicRequestEnvelope) {
frames := handle.Stream().Frames
if frames == nil {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel is unavailable")
return
}
timer := time.NewTimer(handle.WaitTimeout())
defer timer.Stop()
status := http.StatusOK
headers := make(map[string]string)
var body []byte
for {
select {
case <-r.Context().Done():
s.cancelRunOnHTTPGiveUp(handle.Dispatch(), r.Context().Err())
return
case <-timer.C:
s.cancelRunOnHTTPGiveUp(handle.Dispatch(), errRunTimedOut)
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider response timed out")
return
case frame, ok := <-frames:
if !ok {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel closed before a response")
return
}
switch frame.GetKind() {
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START:
status = int(frame.GetStatusCode())
if status == 0 {
status = http.StatusOK
}
for key, value := range frame.GetHeaders() {
headers[key] = value
}
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY:
body = append(body, frame.GetBody()...)
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR:
writeAnthropicError(w, http.StatusBadGateway, "api_error", "provider tunnel failed")
return
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END:
copyAnthropicResponseHeaders(w.Header(), headers)
w.Header().Del("Content-Length")
if status >= http.StatusBadRequest {
writeAnthropicError(w, status, "api_error", "upstream provider rejected the request")
return
}
responseBody := body
if envelope.Stream {
var terminal struct {
Type string `json:"type"`
Response json.RawMessage `json:"response"`
}
for _, event := range splitOpenAIResponsesSSE(body) {
if json.Unmarshal(event, &terminal) == nil && terminal.Type == "response.completed" && len(terminal.Response) > 0 {
responseBody = terminal.Response
}
}
}
converted, err := convertResponsesResponseToAnthropic(responseBody, envelope.Model)
if err != nil {
writeAnthropicError(w, http.StatusBadGateway, "api_error", "upstream response could not be translated")
return
}
if !envelope.Stream {
writeJSON(w, http.StatusOK, converted)
return
}
writeBufferedAnthropicMessageStream(w, converted)
return
}
}
}
}
func splitOpenAIResponsesSSE(body []byte) [][]byte {
var payloads [][]byte
normalized := bytes.ReplaceAll(body, []byte("\r\n"), []byte("\n"))
for _, event := range bytes.Split(normalized, []byte("\n\n")) {
var data [][]byte
for _, line := range bytes.Split(event, []byte("\n")) {
line = bytes.TrimSpace(line)
if bytes.HasPrefix(line, []byte("data:")) {
part := bytes.TrimSpace(bytes.TrimPrefix(line, []byte("data:")))
if !bytes.Equal(part, []byte("[DONE]")) {
data = append(data, part)
}
}
}
if len(data) > 0 {
payloads = append(payloads, bytes.Join(data, []byte("\n")))
}
}
return payloads
}
func writeBufferedAnthropicMessageStream(w http.ResponseWriter, message anthropicMessageResponse) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
start := message
start.Content = []map[string]any{}
start.StopReason = nil
start.Usage.OutputTokens = 0
_ = writeAnthropicSSEEvent(w, "message_start", map[string]any{"type": "message_start", "message": start})
for index, block := range message.Content {
blockType, _ := block["type"].(string)
opening := map[string]any{"type": blockType}
var delta map[string]any
switch blockType {
case "text":
opening["text"] = ""
delta = map[string]any{"type": "text_delta", "text": block["text"]}
case "thinking":
opening["thinking"], opening["signature"] = "", ""
delta = map[string]any{"type": "thinking_delta", "thinking": block["thinking"]}
case "tool_use":
opening["id"], opening["name"], opening["input"] = block["id"], block["name"], map[string]any{}
encodedInput, _ := json.Marshal(block["input"])
delta = map[string]any{"type": "input_json_delta", "partial_json": string(encodedInput)}
default:
continue
}
_ = writeAnthropicSSEEvent(w, "content_block_start", map[string]any{"type": "content_block_start", "index": index, "content_block": opening})
_ = writeAnthropicSSEEvent(w, "content_block_delta", map[string]any{"type": "content_block_delta", "index": index, "delta": delta})
_ = writeAnthropicSSEEvent(w, "content_block_stop", map[string]any{"type": "content_block_stop", "index": index})
}
_ = writeAnthropicSSEEvent(w, "message_delta", map[string]any{
"type": "message_delta", "delta": map[string]any{"stop_reason": message.StopReason, "stop_sequence": nil},
"usage": message.Usage,
})
_ = writeAnthropicSSEEvent(w, "message_stop", map[string]any{"type": "message_stop"})
}