package provider import ( "bufio" "encoding/json" "fmt" "io" "net/http" "strings" ) // CanAdaptResponses reports whether responses ingress can be served via chat completions. func CanAdaptResponses(allowsResponses, allowsChat bool) bool { return allowsChat && !allowsResponses } // ResponsesToChatCompletion converts an OpenAI Responses API request to chat completions format. func ResponsesToChatCompletion(body []byte) ([]byte, error) { var req map[string]json.RawMessage if err := json.Unmarshal(body, &req); err != nil { return nil, fmt.Errorf("invalid json: %w", err) } out := make(map[string]any) if raw, ok := req["model"]; ok { var model string if err := json.Unmarshal(raw, &model); err != nil || model == "" { return nil, fmt.Errorf("model is required") } out["model"] = model } messages := make([]map[string]any, 0, 4) if raw, ok := req["instructions"]; ok { var instructions string if err := json.Unmarshal(raw, &instructions); err == nil && instructions != "" { messages = append(messages, map[string]any{"role": "system", "content": instructions}) } else { var instructionParts []json.RawMessage if err := json.Unmarshal(raw, &instructionParts); err == nil { if text := joinContentPartTexts(instructionParts); text != "" { messages = append(messages, map[string]any{"role": "system", "content": text}) } } } } if raw, ok := req["input"]; ok { inputMsgs, err := parseResponsesInput(raw) if err != nil { return nil, err } messages = append(messages, inputMsgs...) } if len(messages) == 0 { return nil, fmt.Errorf("input is required") } out["messages"] = messages copyField(req, out, "stream") copyField(req, out, "temperature") copyField(req, out, "top_p") copyField(req, out, "user") copyField(req, out, "stop") copyField(req, out, "tools") copyField(req, out, "tool_choice") copyField(req, out, "parallel_tool_calls") copyField(req, out, "seed") copyField(req, out, "n") copyField(req, out, "presence_penalty") copyField(req, out, "frequency_penalty") copyField(req, out, "logit_bias") copyField(req, out, "response_format") if raw, ok := req["max_output_tokens"]; ok { var v int if err := json.Unmarshal(raw, &v); err == nil && v > 0 { out["max_tokens"] = v } } else { copyField(req, out, "max_tokens") } if raw, ok := req["text"]; ok { if rf := responsesTextToResponseFormat(raw); rf != nil { out["response_format"] = rf } } return json.Marshal(out) } func copyField(src map[string]json.RawMessage, dst map[string]any, key string) { if raw, ok := src[key]; ok && len(raw) > 0 && string(raw) != "null" { var v any if json.Unmarshal(raw, &v) == nil { dst[key] = v } } } func responsesTextToResponseFormat(raw json.RawMessage) any { var text struct { Format struct { Type string `json:"type"` } `json:"format"` } if err := json.Unmarshal(raw, &text); err != nil || text.Format.Type == "" { return nil } switch text.Format.Type { case "json_object": return map[string]any{"type": "json_object"} case "text": return map[string]any{"type": "text"} default: return map[string]any{"type": text.Format.Type} } } func parseResponsesInput(raw json.RawMessage) ([]map[string]any, error) { var asString string if err := json.Unmarshal(raw, &asString); err == nil { if asString == "" { return nil, fmt.Errorf("input is required") } return []map[string]any{{"role": "user", "content": asString}}, nil } var asArray []json.RawMessage if err := json.Unmarshal(raw, &asArray); err != nil { return nil, fmt.Errorf("invalid input") } out := make([]map[string]any, 0, len(asArray)) for _, item := range asArray { msg, err := parseResponsesInputItem(item) if err != nil { return nil, err } out = append(out, msg) } return out, nil } func parseResponsesInputItem(raw json.RawMessage) (map[string]any, error) { var asString string if err := json.Unmarshal(raw, &asString); err == nil { return map[string]any{"role": "user", "content": asString}, nil } var item map[string]json.RawMessage if err := json.Unmarshal(raw, &item); err != nil { return nil, fmt.Errorf("invalid input item") } itemType := "" if rawType, ok := item["type"]; ok { _ = json.Unmarshal(rawType, &itemType) } role := "user" if rawRole, ok := item["role"]; ok { _ = json.Unmarshal(rawRole, &role) } if role == "" { role = "user" } role = normalizeResponsesRole(role) switch itemType { case "message": if rawContent, ok := item["content"]; ok { content, err := parseResponsesContent(rawContent) if err != nil { return nil, err } return map[string]any{"role": role, "content": content}, nil } case "input_text": var text string if rawText, ok := item["text"]; ok { _ = json.Unmarshal(rawText, &text) } if text != "" { return map[string]any{"role": role, "content": text}, nil } case "function_call_output": return parseFunctionCallOutputItem(item, role) } if rawContent, ok := item["content"]; ok { content, err := parseResponsesContent(rawContent) if err != nil { return nil, err } return map[string]any{"role": role, "content": content}, nil } if rawText, ok := item["text"]; ok { var text string if err := json.Unmarshal(rawText, &text); err == nil { return map[string]any{"role": role, "content": text}, nil } } if rawInput, ok := item["input"]; ok { var text string if err := json.Unmarshal(rawInput, &text); err == nil { return map[string]any{"role": role, "content": text}, nil } } return nil, fmt.Errorf("unsupported input item") } func normalizeResponsesRole(role string) string { switch role { case "developer": return "system" default: return role } } func parseResponsesContent(raw json.RawMessage) (any, error) { var asString string if err := json.Unmarshal(raw, &asString); err == nil { return asString, nil } var parts []json.RawMessage if err := json.Unmarshal(raw, &parts); err != nil { return nil, fmt.Errorf("invalid content") } converted := make([]any, 0, len(parts)) for _, part := range parts { if item, ok := convertResponsesContentPart(part); ok { converted = append(converted, item) } } if len(converted) == 0 { return "", nil } if len(converted) == 1 { if text, ok := converted[0].(string); ok { return text, nil } if m, ok := converted[0].(map[string]any); ok { if m["type"] == "text" { if text, ok := m["text"].(string); ok { return text, nil } } } } return converted, nil } func convertResponsesContentPart(raw json.RawMessage) (any, bool) { var asString string if err := json.Unmarshal(raw, &asString); err == nil { return asString, true } var part map[string]json.RawMessage if err := json.Unmarshal(raw, &part); err != nil { return nil, false } typ := "" if rawType, ok := part["type"]; ok { _ = json.Unmarshal(rawType, &typ) } switch typ { case "input_text", "text", "output_text": var text string if rawText, ok := part["text"]; ok { _ = json.Unmarshal(rawText, &text) } if text == "" { return nil, false } return map[string]any{"type": "text", "text": text}, true case "input_image": out := map[string]any{"type": "image_url", "image_url": map[string]any{}} img := out["image_url"].(map[string]any) if rawURL, ok := part["image_url"]; ok { var url string if err := json.Unmarshal(rawURL, &url); err == nil { img["url"] = url } else { var nested map[string]any if err := json.Unmarshal(rawURL, &nested); err == nil { for k, v := range nested { img[k] = v } } } } if rawDetail, ok := part["detail"]; ok { var detail string if json.Unmarshal(rawDetail, &detail) == nil { img["detail"] = detail } } if img["url"] == nil { return nil, false } return out, true case "image_url": var v any _ = json.Unmarshal(raw, &v) return v, true case "input_file": var fileID, filename string if rawID, ok := part["file_id"]; ok { _ = json.Unmarshal(rawID, &fileID) } if rawName, ok := part["filename"]; ok { _ = json.Unmarshal(rawName, &filename) } label := filename if label == "" { label = fileID } if label == "" { return nil, false } return map[string]any{"type": "text", "text": "[file:" + label + "]"}, true default: return nil, false } } func joinContentPartTexts(parts []json.RawMessage) string { var texts []string for _, part := range parts { if item, ok := convertResponsesContentPart(part); ok { if text, ok := item.(string); ok { texts = append(texts, text) } else if m, ok := item.(map[string]any); ok && m["type"] == "text" { if text, ok := m["text"].(string); ok { texts = append(texts, text) } } } } return strings.Join(texts, "\n") } func parseFunctionCallOutputItem(item map[string]json.RawMessage, role string) (map[string]any, error) { var output, callID string if rawOutput, ok := item["output"]; ok { _ = json.Unmarshal(rawOutput, &output) } if rawCallID, ok := item["call_id"]; ok { _ = json.Unmarshal(rawCallID, &callID) } if output == "" { return nil, fmt.Errorf("unsupported input item") } msg := map[string]any{"role": "tool", "content": output} if callID != "" { msg["tool_call_id"] = callID } if role != "" && role != "user" { msg["role"] = role } return msg, nil } // ChatCompletionToResponses converts a chat completions response to Responses API format. func ChatCompletionToResponses(body []byte) ([]byte, error) { var chat map[string]any if err := json.Unmarshal(body, &chat); err != nil { return nil, err } if _, ok := chat["error"]; ok { return body, nil } content := extractChatContent(chat) chatID, _ := chat["id"].(string) model, _ := chat["model"].(string) created := extractCreatedAt(chat) respID := responsesIDFromChatID(chatID) msgID := messageIDFromResponseID(respID) usage := chatUsageToResponses(chat["usage"]) resp := buildResponseObject(respID, msgID, model, created, content, usage, "completed") return json.Marshal(resp) } func extractCreatedAt(chat map[string]any) int64 { switch v := chat["created"].(type) { case float64: return int64(v) case int64: return v case int: return int64(v) default: return 0 } } func responsesIDFromChatID(chatID string) string { if chatID == "" { return "resp_adapted" } if strings.HasPrefix(chatID, "resp_") { return chatID } if strings.HasPrefix(chatID, "chatcmpl-") { return "resp_" + strings.TrimPrefix(chatID, "chatcmpl-") } return "resp_" + chatID } func messageIDFromResponseID(respID string) string { if strings.HasPrefix(respID, "resp_") { return "msg_" + strings.TrimPrefix(respID, "resp_") } return "msg_" + respID } func buildOutputTextContent(text string) map[string]any { return map[string]any{ "type": "output_text", "text": text, "annotations": []any{}, } } func buildOutputMessage(msgID, content, status string) map[string]any { return map[string]any{ "type": "message", "id": msgID, "role": "assistant", "status": status, "content": []map[string]any{buildOutputTextContent(content)}, } } func buildResponseObject(respID, msgID, model string, created int64, content string, usage map[string]any, status string) map[string]any { msgStatus := status if status == "in_progress" || status == "queued" { msgStatus = "in_progress" } resp := map[string]any{ "id": respID, "object": "response", "created_at": created, "status": status, "model": model, "output_text": content, "error": nil, "incomplete_details": nil, "parallel_tool_calls": true, "output": []map[string]any{buildOutputMessage(msgID, content, msgStatus)}, } if usage != nil { resp["usage"] = usage } return resp } func extractChatContent(chat map[string]any) string { choices, ok := chat["choices"].([]any) if !ok || len(choices) == 0 { return "" } choice, ok := choices[0].(map[string]any) if !ok { return "" } if msg, ok := choice["message"].(map[string]any); ok { if content, ok := msg["content"].(string); ok { return content } } if delta, ok := choice["delta"].(map[string]any); ok { if content, ok := delta["content"].(string); ok { return content } } return "" } func chatUsageToResponses(usage any) map[string]any { u, ok := usage.(map[string]any) if !ok { return nil } in, hasIn := numberAsInt(u["prompt_tokens"]) out, hasOut := numberAsInt(u["completion_tokens"]) total, hasTotal := numberAsInt(u["total_tokens"]) if !hasIn && !hasOut && !hasTotal { return nil } if !hasTotal && hasIn && hasOut { total = in + out } return map[string]any{ "input_tokens": in, "output_tokens": out, "total_tokens": total, "input_tokens_details": map[string]any{ "cached_tokens": 0, }, "output_tokens_details": map[string]any{ "reasoning_tokens": 0, }, } } func numberAsInt(v any) (int, bool) { switch n := v.(type) { case float64: return int(n), true case int: return n, true case int64: return int(n), true default: return 0, false } } // AdaptResponsesStream wraps a chat-completions SSE stream as Responses API SSE events. func AdaptResponsesStream(chatBody []byte, upstream io.Reader, w io.Writer, flusher http.Flusher) (*StreamResult, error) { var req map[string]any _ = json.Unmarshal(chatBody, &req) model, _ := req["model"].(string) result := &StreamResult{StatusCode: http.StatusOK} respID := "resp_adapted" msgID := "msg_adapted" seq := 0 var fullText string var usage map[string]any started := false writeEvent := func(eventType string, payload map[string]any) error { payload["type"] = eventType payload["sequence_number"] = seq seq++ b, err := json.Marshal(payload) if err != nil { return err } if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, b); err != nil { return err } if flusher != nil { flusher.Flush() } return nil } emitCreated := func(status string) error { response := buildResponseObject(respID, msgID, model, 0, "", nil, status) response["output"] = []any{} response["output_text"] = "" return writeEvent("response.created", map[string]any{"response": response}) } scanner := bufio.NewScanner(upstream) scanner.Buffer(make([]byte, 64*1024), 1024*1024) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { break } tokensIn, tokensOut := parseStreamChunk([]byte(data)) result.TokensIn += tokensIn result.TokensOut += tokensOut var chunk map[string]any if err := json.Unmarshal([]byte(data), &chunk); err != nil { continue } if id, ok := chunk["id"].(string); ok && id != "" { respID = responsesIDFromChatID(id) msgID = messageIDFromResponseID(respID) } if m, ok := chunk["model"].(string); ok && m != "" { model = m } if u := chatUsageToResponses(chunk["usage"]); u != nil { usage = u } delta := extractChatContent(chunk) if delta == "" { continue } if !started { started = true if err := emitCreated("in_progress"); err != nil { return result, err } if err := writeEvent("response.in_progress", map[string]any{ "response": buildResponseObject(respID, msgID, model, 0, "", nil, "in_progress"), }); err != nil { return result, err } if err := writeEvent("response.output_item.added", map[string]any{ "output_index": 0, "item": map[string]any{ "type": "message", "id": msgID, "role": "assistant", "status": "in_progress", "content": []any{}, }, }); err != nil { return result, err } } fullText += delta if err := writeEvent("response.output_text.delta", map[string]any{ "item_id": msgID, "output_index": 0, "content_index": 0, "delta": delta, "logprobs": []any{}, }); err != nil { return result, err } } if err := scanner.Err(); err != nil { return result, err } if started { _ = writeEvent("response.output_text.done", map[string]any{ "item_id": msgID, "output_index": 0, "content_index": 0, "text": fullText, "logprobs": []any{}, }) doneItem := buildOutputMessage(msgID, fullText, "completed") _ = writeEvent("response.output_item.done", map[string]any{ "output_index": 0, "item": doneItem, }) final := buildResponseObject(respID, msgID, model, 0, fullText, usage, "completed") _ = writeEvent("response.completed", map[string]any{"response": final}) } return result, nil }