mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 08:55:28 +08:00
309 lines
9.0 KiB
Go
309 lines
9.0 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"ds2api/internal/util"
|
|
)
|
|
|
|
func BuildChatCompletion(completionID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
|
|
detected := util.ParseToolCalls(finalText, toolNames)
|
|
finishReason := "stop"
|
|
messageObj := map[string]any{"role": "assistant", "content": finalText}
|
|
if strings.TrimSpace(finalThinking) != "" {
|
|
messageObj["reasoning_content"] = finalThinking
|
|
}
|
|
if len(detected) > 0 {
|
|
finishReason = "tool_calls"
|
|
messageObj["tool_calls"] = util.FormatOpenAIToolCalls(detected)
|
|
messageObj["content"] = nil
|
|
}
|
|
promptTokens := util.EstimateTokens(finalPrompt)
|
|
reasoningTokens := util.EstimateTokens(finalThinking)
|
|
completionTokens := util.EstimateTokens(finalText)
|
|
|
|
return map[string]any{
|
|
"id": completionID,
|
|
"object": "chat.completion",
|
|
"created": time.Now().Unix(),
|
|
"model": model,
|
|
"choices": []map[string]any{{"index": 0, "message": messageObj, "finish_reason": finishReason}},
|
|
"usage": map[string]any{
|
|
"prompt_tokens": promptTokens,
|
|
"completion_tokens": reasoningTokens + completionTokens,
|
|
"total_tokens": promptTokens + reasoningTokens + completionTokens,
|
|
"completion_tokens_details": map[string]any{
|
|
"reasoning_tokens": reasoningTokens,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func BuildResponseObject(responseID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
|
|
// Responses output should only be treated as tool calls when the model
|
|
// produced a standalone structured payload. This prevents accidental
|
|
// empty output_text on normal prose that merely contains tool_call-like text.
|
|
detected := util.ParseStandaloneToolCalls(finalText, toolNames)
|
|
if len(detected) == 0 && strings.TrimSpace(finalThinking) != "" {
|
|
detected = util.ParseStandaloneToolCalls(finalThinking, toolNames)
|
|
}
|
|
exposedOutputText := finalText
|
|
output := make([]any, 0, 2)
|
|
if len(detected) > 0 {
|
|
exposedOutputText = ""
|
|
if strings.TrimSpace(finalThinking) != "" {
|
|
output = append(output, map[string]any{
|
|
"type": "reasoning",
|
|
"text": finalThinking,
|
|
})
|
|
}
|
|
formatted := util.FormatOpenAIToolCalls(detected)
|
|
output = append(output, toResponsesFunctionCallItems(formatted)...)
|
|
output = append(output, map[string]any{
|
|
"type": "tool_calls",
|
|
"tool_calls": formatted,
|
|
})
|
|
} else {
|
|
content := make([]any, 0, 2)
|
|
if finalThinking != "" {
|
|
content = append([]any{map[string]any{
|
|
"type": "reasoning",
|
|
"text": finalThinking,
|
|
}}, content...)
|
|
}
|
|
if strings.TrimSpace(finalText) != "" {
|
|
content = append(content, map[string]any{
|
|
"type": "output_text",
|
|
"text": finalText,
|
|
})
|
|
}
|
|
if strings.TrimSpace(finalText) == "" && strings.TrimSpace(finalThinking) != "" {
|
|
exposedOutputText = finalThinking
|
|
}
|
|
output = append(output, map[string]any{
|
|
"type": "message",
|
|
"id": "msg_" + strings.ReplaceAll(uuid.NewString(), "-", ""),
|
|
"role": "assistant",
|
|
"content": content,
|
|
})
|
|
}
|
|
promptTokens := util.EstimateTokens(finalPrompt)
|
|
reasoningTokens := util.EstimateTokens(finalThinking)
|
|
completionTokens := util.EstimateTokens(finalText)
|
|
return map[string]any{
|
|
"id": responseID,
|
|
"type": "response",
|
|
"object": "response",
|
|
"created_at": time.Now().Unix(),
|
|
"status": "completed",
|
|
"model": model,
|
|
"output": output,
|
|
"output_text": exposedOutputText,
|
|
"usage": map[string]any{
|
|
"input_tokens": promptTokens,
|
|
"output_tokens": reasoningTokens + completionTokens,
|
|
"total_tokens": promptTokens + reasoningTokens + completionTokens,
|
|
},
|
|
}
|
|
}
|
|
|
|
func toResponsesFunctionCallItems(toolCalls []map[string]any) []any {
|
|
if len(toolCalls) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]any, 0, len(toolCalls))
|
|
for _, tc := range toolCalls {
|
|
callID, _ := tc["id"].(string)
|
|
if strings.TrimSpace(callID) == "" {
|
|
callID = "call_" + strings.ReplaceAll(uuid.NewString(), "-", "")
|
|
}
|
|
name := ""
|
|
args := "{}"
|
|
if fn, ok := tc["function"].(map[string]any); ok {
|
|
if n, _ := fn["name"].(string); strings.TrimSpace(n) != "" {
|
|
name = n
|
|
}
|
|
if a, _ := fn["arguments"].(string); strings.TrimSpace(a) != "" {
|
|
args = a
|
|
}
|
|
}
|
|
out = append(out, map[string]any{
|
|
"id": "fc_" + strings.ReplaceAll(uuid.NewString(), "-", ""),
|
|
"type": "function_call",
|
|
"call_id": callID,
|
|
"name": name,
|
|
"arguments": normalizeJSONString(args),
|
|
"status": "completed",
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeJSONString(raw string) string {
|
|
s := strings.TrimSpace(raw)
|
|
if s == "" {
|
|
return "{}"
|
|
}
|
|
var v any
|
|
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
|
return raw
|
|
}
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func BuildChatStreamDeltaChoice(index int, delta map[string]any) map[string]any {
|
|
return map[string]any{
|
|
"delta": delta,
|
|
"index": index,
|
|
}
|
|
}
|
|
|
|
func BuildChatStreamFinishChoice(index int, finishReason string) map[string]any {
|
|
return map[string]any{
|
|
"delta": map[string]any{},
|
|
"index": index,
|
|
"finish_reason": finishReason,
|
|
}
|
|
}
|
|
|
|
func BuildChatStreamChunk(completionID string, created int64, model string, choices []map[string]any, usage map[string]any) map[string]any {
|
|
out := map[string]any{
|
|
"id": completionID,
|
|
"object": "chat.completion.chunk",
|
|
"created": created,
|
|
"model": model,
|
|
"choices": choices,
|
|
}
|
|
if len(usage) > 0 {
|
|
out["usage"] = usage
|
|
}
|
|
return out
|
|
}
|
|
|
|
func BuildChatUsage(finalPrompt, finalThinking, finalText string) map[string]any {
|
|
promptTokens := util.EstimateTokens(finalPrompt)
|
|
reasoningTokens := util.EstimateTokens(finalThinking)
|
|
completionTokens := util.EstimateTokens(finalText)
|
|
return map[string]any{
|
|
"prompt_tokens": promptTokens,
|
|
"completion_tokens": reasoningTokens + completionTokens,
|
|
"total_tokens": promptTokens + reasoningTokens + completionTokens,
|
|
"completion_tokens_details": map[string]any{
|
|
"reasoning_tokens": reasoningTokens,
|
|
},
|
|
}
|
|
}
|
|
|
|
func BuildResponsesCreatedPayload(responseID, model string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.created",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"object": "response",
|
|
"model": model,
|
|
"status": "in_progress",
|
|
}
|
|
}
|
|
|
|
func BuildResponsesTextDeltaPayload(responseID, delta string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.output_text.delta",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"delta": delta,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesReasoningDeltaPayload(responseID, delta string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.reasoning.delta",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"delta": delta,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesReasoningTextDeltaPayload(responseID, itemID string, outputIndex, contentIndex int, delta string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.reasoning_text.delta",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"item_id": itemID,
|
|
"output_index": outputIndex,
|
|
"content_index": contentIndex,
|
|
"delta": delta,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesReasoningTextDonePayload(responseID, itemID string, outputIndex, contentIndex int, text string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.reasoning_text.done",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"item_id": itemID,
|
|
"output_index": outputIndex,
|
|
"content_index": contentIndex,
|
|
"text": text,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesToolCallDeltaPayload(responseID string, toolCalls []map[string]any) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.output_tool_call.delta",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"tool_calls": toolCalls,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesToolCallDonePayload(responseID string, toolCalls []map[string]any) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.output_tool_call.done",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"tool_calls": toolCalls,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesFunctionCallArgumentsDeltaPayload(responseID, itemID string, outputIndex int, callID, delta string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.function_call_arguments.delta",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"item_id": itemID,
|
|
"output_index": outputIndex,
|
|
"call_id": callID,
|
|
"delta": delta,
|
|
}
|
|
}
|
|
|
|
func BuildResponsesFunctionCallArgumentsDonePayload(responseID, itemID string, outputIndex int, callID, name, arguments string) map[string]any {
|
|
return map[string]any{
|
|
"type": "response.function_call_arguments.done",
|
|
"id": responseID,
|
|
"response_id": responseID,
|
|
"item_id": itemID,
|
|
"output_index": outputIndex,
|
|
"call_id": callID,
|
|
"name": name,
|
|
"arguments": normalizeJSONString(arguments),
|
|
}
|
|
}
|
|
|
|
func BuildResponsesCompletedPayload(response map[string]any) map[string]any {
|
|
responseID, _ := response["id"].(string)
|
|
return map[string]any{
|
|
"type": "response.completed",
|
|
"response_id": responseID,
|
|
"response": response,
|
|
}
|
|
}
|