package convert import ( "encoding/json" "strings" "openteam/server/internal/proxy/claude" "openteam/server/internal/proxy/openai" "openteam/server/internal/proxy/responses" ) // CanonicalRequest is the gateway-internal standard form (OpenAI chat shape). // Every protocol is converted into this before being emitted to a channel. type CanonicalRequest struct { Model string System string Messages []openai.ChatMessage Stream bool Temperature *float64 TopP *float64 MaxTokens *int Stop []string Tools []openai.Tool ToolChoice json.RawMessage ResponseFormat json.RawMessage // Extra passthrough-only fields for OpenAI channels. RawOpenAIExtras map[string]json.RawMessage } // contentString returns a plain-text representation of a message content, // handling both string and structured (Claude-style blocks) content. func contentString(raw json.RawMessage) string { if len(raw) == 0 { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return s } var blocks []struct { Type string `json:"type"` Text string `json:"text"` } if err := json.Unmarshal(raw, &blocks); err == nil { out := "" for _, b := range blocks { if b.Text != "" { out += b.Text } } return out } return "" } // requestFromOpenAIChat parses an OpenAI chat body into the canonical form. func requestFromOpenAIChat(body []byte) (*CanonicalRequest, error) { var r openai.ChatRequest if err := json.Unmarshal(body, &r); err != nil { return nil, err } var msgs []openai.ChatMessage if err := json.Unmarshal(r.Messages, &msgs); err != nil { return nil, err } req := &CanonicalRequest{ Model: r.Model, Messages: msgs, Stream: r.Stream, Temperature: r.Temperature, TopP: r.TopP, Stop: parseStop(r.Stop), ToolChoice: r.ToolChoice, } if r.MaxTokens != nil { req.MaxTokens = r.MaxTokens } else if r.MaxCompl != nil { req.MaxTokens = r.MaxCompl } if len(r.Tools) > 0 { _ = json.Unmarshal(r.Tools, &req.Tools) } req.ResponseFormat = r.ResponseFmt req.System = extractSystem(msgs) req.RawOpenAIExtras = rawExtras(body, "model", "messages", "stream", "temperature", "top_p", "max_tokens", "max_completion_tokens", "stop", "tools", "tool_choice", "response_format", "user") return req, nil } func requestFromClaude(body []byte) (*CanonicalRequest, error) { var r claude.Request if err := json.Unmarshal(body, &r); err != nil { return nil, err } var msgs []openai.ChatMessage // Claude messages: content can be a string or blocks; tool_use/tool_result // blocks map to assistant.tool_calls and role=tool messages. if err := claudeMessagesToChat(r.Messages, &msgs); err != nil { return nil, err } req := &CanonicalRequest{ Model: r.Model, Messages: msgs, Stream: r.Stream, Temperature: r.Temperature, TopP: r.TopP, Stop: parseStop(r.StopSequences), } if r.MaxTokens > 0 { mt := r.MaxTokens req.MaxTokens = &mt } if len(r.Tools) > 0 { _ = json.Unmarshal(r.Tools, &req.Tools) } req.ToolChoice = r.ToolChoice // Claude system can be a string or array of blocks. req.System = contentString(r.System) return req, nil } func requestFromResponses(body []byte) (*CanonicalRequest, error) { var r responses.Request if err := json.Unmarshal(body, &r); err != nil { return nil, err } req := &CanonicalRequest{ Model: r.Model, Stream: r.Stream, } // instructions → system. req.System = contentString(r.Instructions) // Parse input items into chat messages. msgs, err := responsesInputToChat(r.Input) if err != nil { return nil, err } req.Messages = msgs if r.MaxOutputTokens != nil { req.MaxTokens = r.MaxOutputTokens } if len(r.Tools) > 0 { _ = json.Unmarshal(r.Tools, &req.Tools) } // output_format / text.format → response_format. if len(r.OutputFormat) > 0 { req.ResponseFormat = r.OutputFormat } else if len(r.Text) > 0 { var t struct { Format json.RawMessage `json:"format"` } if json.Unmarshal(r.Text, &t) == nil && len(t.Format) > 0 { req.ResponseFormat = t.Format } } return req, nil } func extractSystem(msgs []openai.ChatMessage) string { var parts []string for _, m := range msgs { if m.Role == "system" { if s := contentString(m.Content); s != "" { parts = append(parts, s) } } } return strings.Join(parts, "\n") } func parseStop(raw json.RawMessage) []string { if len(raw) == 0 { return nil } var one string if err := json.Unmarshal(raw, &one); err == nil { return []string{one} } var many []string if err := json.Unmarshal(raw, &many); err == nil { return many } return nil } // rawExtras captures fields not otherwise modeled so they can be re-emitted // on OpenAI passthrough-style conversions. func rawExtras(body []byte, skip ...string) map[string]json.RawMessage { var obj map[string]json.RawMessage if json.Unmarshal(body, &obj) != nil { return nil } for _, k := range skip { delete(obj, k) } return obj }