iop/apps/edge/internal/openai/hot_path_direct.go

385 lines
12 KiB
Go

package openai
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"iop/packages/go/config"
)
type hotPathTurn struct {
RequestID string
StageID string
CallID string
OwnerEdgeID string
PrincipalRef string
Preset config.ExecutionPreset
Dispatch routeDispatch
Protocol string // "openai" or "anthropic"
Stream bool
PublicModelID string
Writer http.ResponseWriter
Request *http.Request
}
func (s *Server) runDirectTurn(_ context.Context, turn *hotPathTurn, output normalizedStageOutput) error {
for _, call := range output.ToolCalls {
if len(reservedPathsFromToolCall(call)) > 0 {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectError(turn, http.StatusBadRequest, "invalid_request_error", "direct flow violation: reserved artifact path .iop/job/ emitted in direct turn")
}
}
if strings.TrimSpace(output.ResponseID) == "" {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectError(turn, http.StatusBadGateway, "api_error", "direct response is missing provider execution identity")
}
if len(output.ToolCalls) > 0 {
expected := make([]logicalRequestExpectedTool, 0, len(output.ToolCalls))
for _, call := range output.ToolCalls {
providerID := strings.TrimSpace(call.ProviderCallID)
if providerID == "" {
providerID = call.ID
}
expected = append(expected, logicalRequestExpectedTool{PublicCallID: call.ID, ProviderCallID: providerID})
}
issuedHash, err := directIssuedCallHash(turn.Protocol, output)
if err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectError(turn, http.StatusBadGateway, "api_error", err.Error())
}
if turn.RequestID != "" {
if _, err := s.requestCoordinator.awaitToolResults(turn.RequestID, turn.OwnerEdgeID, turn.StageID, expected, issuedHash); err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return s.writeDirectError(turn, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("failed to await tool results: %v", err))
}
}
if err := s.writeDirectResponse(turn, output); err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return err
}
return nil
}
if err := s.writeDirectResponse(turn, output); err != nil {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
return err
}
if turn.RequestID != "" {
s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID)
}
return nil
}
func directIssuedCallHash(protocol string, output normalizedStageOutput) (string, error) {
if protocol == "anthropic" {
return fingerprintCanonicalJSON(logicalRequestEndpointAnthropic, map[string]any{
"role": "assistant", "content": anthropicDirectBlocks(output),
})
}
return fingerprintCanonicalJSON(logicalRequestEndpointChat, openAIDirectMessage(output))
}
func (s *Server) writeDirectError(turn *hotPathTurn, status int, errorType, message string) error {
if turn.Protocol == "anthropic" {
writeAnthropicError(turn.Writer, status, errorType, message)
} else {
writeError(turn.Writer, status, errorType, message)
}
return fmt.Errorf("%s: %s", errorType, message)
}
func (s *Server) writeDirectResponse(turn *hotPathTurn, output normalizedStageOutput) error {
if turn.Protocol == "anthropic" {
return writeAnthropicDirectResponse(turn, output)
}
return writeOpenAIDirectResponse(turn, output)
}
func directPublicModel(turn *hotPathTurn) string {
if model := strings.TrimSpace(turn.PublicModelID); model != "" {
return model
}
if model := strings.TrimSpace(turn.Dispatch.ExternalModelID); model != "" {
return model
}
return turn.Dispatch.Target
}
func openAIDirectMessage(output normalizedStageOutput) chatMessage {
message := chatMessage{Role: "assistant", Content: output.Content, ReasoningContent: output.Reasoning}
for _, call := range output.ToolCalls {
message.ToolCalls = append(message.ToolCalls, openAIDirectToolCall(call))
}
return message
}
func openAIDirectToolCall(call normalizedToolCall) map[string]any {
return map[string]any{
"id": call.ID, "type": "function",
"function": map[string]any{"name": call.Name, "arguments": directToolArguments(call)},
}
}
func directToolArguments(call normalizedToolCall) string {
if strings.TrimSpace(call.RawArgs) != "" {
return call.RawArgs
}
raw, _ := json.Marshal(call.Arguments)
return string(raw)
}
func writeOpenAIDirectResponse(turn *hotPathTurn, output normalizedStageOutput) error {
model := directPublicModel(turn)
finishReason := strings.TrimSpace(output.TerminalReason)
if finishReason == "" {
if len(output.ToolCalls) > 0 {
finishReason = "tool_calls"
} else {
finishReason = "stop"
}
}
if turn.Stream {
return writeOpenAIDirectStream(turn, output, model, finishReason)
}
response := map[string]any{
"id": output.ResponseID, "object": "chat.completion", "created": output.Created, "model": model,
"choices": []any{map[string]any{
"index": 0, "message": openAIDirectMessage(output), "finish_reason": finishReason,
}},
}
if len(output.Usage) > 0 {
response["usage"] = output.Usage
}
return writeDirectJSON(turn.Writer, http.StatusOK, response)
}
func writeOpenAIDirectStream(turn *hotPathTurn, output normalizedStageOutput, model, finishReason string) error {
flusher, ok := turn.Writer.(http.Flusher)
if !ok {
return fmt.Errorf("response writer does not support flushing")
}
w := turn.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
emit := func(delta map[string]any, reason string, usage json.RawMessage) error {
choice := map[string]any{"index": 0, "delta": delta, "finish_reason": nil}
if reason != "" {
choice["finish_reason"] = reason
}
chunk := map[string]any{
"id": output.ResponseID, "object": "chat.completion.chunk", "created": output.Created,
"model": model, "choices": []any{choice},
}
if len(usage) > 0 {
chunk["usage"] = usage
}
return writeDirectSSEData(w, flusher, chunk)
}
if err := emit(map[string]any{"role": "assistant"}, "", nil); err != nil {
return err
}
if output.Reasoning != "" {
if err := emit(map[string]any{"reasoning_content": output.Reasoning}, "", nil); err != nil {
return err
}
}
if output.Content != "" {
if err := emit(map[string]any{"content": output.Content}, "", nil); err != nil {
return err
}
}
if len(output.ToolCalls) > 0 {
calls := make([]any, 0, len(output.ToolCalls))
for index, call := range output.ToolCalls {
value := openAIDirectToolCall(call)
value["index"] = index
calls = append(calls, value)
}
if err := emit(map[string]any{"tool_calls": calls}, "", nil); err != nil {
return err
}
}
if err := emit(map[string]any{}, finishReason, output.Usage); err != nil {
return err
}
if _, err := fmt.Fprint(w, "data: [DONE]\n\n"); err != nil {
return err
}
flusher.Flush()
return nil
}
func anthropicDirectBlocks(output normalizedStageOutput) []map[string]any {
blocks := make([]map[string]any, 0, 2+len(output.ToolCalls))
if output.Reasoning != "" {
blocks = append(blocks, map[string]any{"type": "thinking", "thinking": output.Reasoning, "signature": output.ReasoningSignature})
}
if output.Content != "" {
blocks = append(blocks, map[string]any{"type": "text", "text": output.Content})
}
for _, call := range output.ToolCalls {
var input any
if json.Unmarshal([]byte(directToolArguments(call)), &input) != nil {
input = map[string]any{}
}
blocks = append(blocks, map[string]any{"type": "tool_use", "id": call.ID, "name": call.Name, "input": input})
}
return blocks
}
func writeAnthropicDirectResponse(turn *hotPathTurn, output normalizedStageOutput) error {
model := directPublicModel(turn)
stopReason := strings.TrimSpace(output.TerminalReason)
if stopReason == "" {
if len(output.ToolCalls) > 0 {
stopReason = "tool_use"
} else {
stopReason = "end_turn"
}
}
if turn.Stream {
return writeAnthropicDirectStream(turn, output, model, stopReason)
}
response := map[string]any{
"id": output.ResponseID, "type": "message", "role": "assistant", "model": model,
"content": anthropicDirectBlocks(output), "stop_reason": stopReason, "stop_sequence": nil,
}
if len(output.Usage) > 0 {
response["usage"] = output.Usage
}
return writeDirectJSON(turn.Writer, http.StatusOK, response)
}
func writeAnthropicDirectStream(turn *hotPathTurn, output normalizedStageOutput, model, stopReason string) error {
flusher, ok := turn.Writer.(http.Flusher)
if !ok {
return fmt.Errorf("response writer does not support flushing")
}
w := turn.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
startUsage := anthropicStartUsage(output.Usage)
message := map[string]any{
"id": output.ResponseID, "type": "message", "role": "assistant", "model": model,
"content": []any{}, "stop_reason": nil, "stop_sequence": nil,
}
if len(startUsage) > 0 {
message["usage"] = startUsage
}
if err := writeDirectAnthropicEvent(w, flusher, "message_start", map[string]any{"type": "message_start", "message": message}); err != nil {
return err
}
for index, block := range anthropicDirectBlocks(output) {
blockType, _ := block["type"].(string)
startBlock := make(map[string]any, len(block))
for key, value := range block {
startBlock[key] = value
}
switch blockType {
case "text":
startBlock["text"] = ""
case "thinking":
startBlock["thinking"] = ""
startBlock["signature"] = ""
case "tool_use":
startBlock["input"] = map[string]any{}
}
if err := writeDirectAnthropicEvent(w, flusher, "content_block_start", map[string]any{
"type": "content_block_start", "index": index, "content_block": startBlock,
}); err != nil {
return err
}
var delta map[string]any
switch blockType {
case "text":
delta = map[string]any{"type": "text_delta", "text": block["text"]}
case "thinking":
delta = map[string]any{"type": "thinking_delta", "thinking": block["thinking"]}
case "tool_use":
raw, _ := json.Marshal(block["input"])
delta = map[string]any{"type": "input_json_delta", "partial_json": string(raw)}
}
if err := writeDirectAnthropicEvent(w, flusher, "content_block_delta", map[string]any{
"type": "content_block_delta", "index": index, "delta": delta,
}); err != nil {
return err
}
if blockType == "thinking" && block["signature"] != "" {
if err := writeDirectAnthropicEvent(w, 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(w, flusher, "content_block_stop", map[string]any{
"type": "content_block_stop", "index": index,
}); err != nil {
return err
}
}
delta := map[string]any{
"type": "message_delta", "delta": map[string]any{"stop_reason": stopReason, "stop_sequence": nil},
}
if len(output.Usage) > 0 {
delta["usage"] = output.Usage
}
if err := writeDirectAnthropicEvent(w, flusher, "message_delta", delta); err != nil {
return err
}
return writeDirectAnthropicEvent(w, flusher, "message_stop", map[string]any{"type": "message_stop"})
}
func anthropicStartUsage(raw json.RawMessage) json.RawMessage {
if len(raw) == 0 {
return nil
}
var usage map[string]any
if json.Unmarshal(raw, &usage) != nil {
return nil
}
for key := range usage {
if key == "output_tokens" {
delete(usage, key)
}
}
encoded, _ := json.Marshal(usage)
return encoded
}
func writeDirectJSON(w http.ResponseWriter, status int, value any) error {
body, err := json.Marshal(value)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, err = w.Write(append(body, '\n'))
return err
}
func writeDirectSSEData(w http.ResponseWriter, flusher http.Flusher, value any) error {
body, err := json.Marshal(value)
if err != nil {
return err
}
if _, err := fmt.Fprintf(w, "data: %s\n\n", body); err != nil {
return err
}
flusher.Flush()
return nil
}
func writeDirectAnthropicEvent(w http.ResponseWriter, flusher http.Flusher, event string, value any) error {
if err := writeAnthropicSSEEvent(w, event, value); err != nil {
return err
}
flusher.Flush()
return nil
}