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 OuterTurn *hotPathOuterTurn } func (s *Server) runDirectTurn(ctx context.Context, turn *hotPathTurn, output normalizedStageOutput) error { directTerminal := hotPathTerminalDispositionSuccess reachedTerminal := false defer func() { // Emit the single direct-mode terminal observation exactly once. The // tool-turn path leaves reachedTerminal false so an agent round-trip is // not mistaken for a logical terminal. Disposition is normalized before // projection so raw error text never reaches logs or labels (SDD S15). if reachedTerminal { s.observeHotPathTerminal(ctx, hotPathModeDirect, directTerminal, turn.RequestID, turn.StageID, turn.Preset.ID) } }() for _, call := range output.ToolCalls { if len(reservedPathsFromToolCall(call)) > 0 { directTerminal = hotPathTerminalDispositionValidationError reachedTerminal = true 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) == "" { directTerminal = hotPathTerminalDispositionProviderError reachedTerminal = true s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID) return s.writeDirectError(turn, http.StatusBadGateway, "api_error", "direct response is missing provider execution identity") } visible := cloneNormalizedStageOutput(output) if turn.OuterTurn != nil { if !output.ProgressivelyReleased { if err := runHotPathCollectedStage(ctx, turn.OuterTurn, turn.StageID, output); err != nil { directTerminal = hotPathTerminalDispositionProviderError reachedTerminal = true s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID) return s.writeDirectError(turn, http.StatusBadGateway, "api_error", fmt.Sprintf("direct outer turn failed: %v", err)) } } visible = hotPathCompatibilityOutput(turn.OuterTurn, output, turn.Protocol) } if len(visible.ToolCalls) > 0 { expected := make([]logicalRequestExpectedTool, 0, len(visible.ToolCalls)) for _, call := range visible.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, visible) if err != nil { directTerminal = hotPathTerminalDispositionProviderError reachedTerminal = true 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 { directTerminal = hotPathTerminalDispositionValidationError reachedTerminal = true 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 turn.OuterTurn != nil { turn.OuterTurn.commitTerminalSuccess(output.TerminalReason) visible = hotPathCompatibilityOutput(turn.OuterTurn, visible, turn.Protocol) } if err := s.writeDirectResponse(turn, visible); err != nil { // Classify the response-write failure through the closed error mapper // so a caller-canceled or timed-out endpoint write wins over // provider_error, matching the cleanup post-write ownership rule. directTerminal = hotPathTerminalDispositionFromKind(hotPathDispositionForError(err)) reachedTerminal = true s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID) return err } // Tool turn: the logical request is still waiting for agent tool // results, so this HTTP turn is not a logical terminal. return nil } if turn.OuterTurn != nil { turn.OuterTurn.commitTerminalSuccess(output.TerminalReason) visible = hotPathCompatibilityOutput(turn.OuterTurn, visible, turn.Protocol) } if err := s.writeDirectResponse(turn, visible); err != nil { // The final direct response also resolves cancellation/timeout through the // closed error mapper before the deferred exact-once terminal emission. directTerminal = hotPathTerminalDispositionFromKind(hotPathDispositionForError(err)) reachedTerminal = true if turn.RequestID != "" { s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID) } return err } if turn.RequestID != "" { s.terminalPresetRequest(turn.RequestID, turn.OwnerEdgeID) } if hotPathIsProviderLengthTerminal(output.TerminalReason) { directTerminal = hotPathTerminalDispositionLength } reachedTerminal = true 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 { disposition := hotPathTerminalDisposition{ Kind: hotPathDispositionProviderError, Cause: message, Source: "direct_error", } if turn != nil && turn.OuterTurn != nil { turn.OuterTurn.commitTerminalError(errorType, errorType) if selected, ok := turn.OuterTurn.terminalDisposition(); ok { disposition = selected } } else if strings.Contains(strings.ToLower(errorType), "invalid") { disposition.Kind = hotPathDispositionValidationError } if turn.Protocol == "anthropic" { if !writeHotPathAnthropicOuterError(turn, status, errorType, message) { policy := anthropicHotPathPolicy(disposition) if !policy.silent { writeAnthropicError(turn.Writer, policy.status, policy.errorType, message) } } } else { if !writeHotPathChatOuterError(turn, status, errorType, message, disposition) { policy := chatHotPathPolicy(disposition) if !policy.silent { writeError(turn.Writer, policy.status, policy.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 { if handled, err := writeHotPathChatOuterResponse(turn, output); handled { return err } model := directPublicModel(turn) finishReason := openAIDirectFinishReason(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 openAIDirectFinishReason(reason string) string { switch strings.TrimSpace(reason) { case "end_turn": return "stop" case "tool_use": return "tool_calls" case "max_tokens": return "length" default: return strings.TrimSpace(reason) } } 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 { if handled, err := writeHotPathAnthropicOuterResponse(turn, output); handled { return err } codec := newAnthropicHotPathCodec( turn.Writer, directPublicModel(turn), turn.Stream, turn.RequestID, 0, ) codec.outer = turn.OuterTurn return codec.write(output) } func anthropicDirectStopReason(reason string) string { switch strings.TrimSpace(reason) { case "length": return "max_tokens" case "tool_calls": return "tool_use" case "stop": return "end_turn" default: return strings.TrimSpace(reason) } } 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 }