ccpoad / internal /protocol /builtin /request_prompt_normalize.go
anyalerob's picture
Upload folder using huggingface_hub
2986042 verified
Raw
History Blame Contribute Delete
13.4 kB
package builtin
import (
"encoding/json"
"fmt"
"strings"
"ccLoad/internal/protocol"
"github.com/bytedance/sonic"
)
func normalizeOpenAIConversation(req openAIChatRequest) (conversation, error) {
conv := conversation{Turns: make([]conversationTurn, 0, len(req.Messages))}
var err error
conv.Tools, err = parseFunctionTools(req.Tools, "openai")
if err != nil {
return conversation{}, err
}
conv.ToolChoice, err = parseToolChoice(req.ToolChoice, "openai")
if err != nil {
return conversation{}, err
}
// 顶层 parallel_tool_calls=false 等价 Anthropic tool_choice.disable_parallel_tool_use。
if req.ParallelToolCalls != nil && !*req.ParallelToolCalls {
conv.ToolChoice.DisableParallel = true
}
conv.Sampling = buildOpenAISampling(req)
if thinking := openAIReasoningEffortToThinking(req.ReasoningEffort); thinking != nil {
conv.Thinking = thinking
}
for i, msg := range req.Messages {
role := normalizeRole(msg.Role)
parts, err := extractOpenAIContentParts(msg.Content)
if err != nil {
return conversation{}, fmt.Errorf("openai message %d: %w", i, err)
}
toolParts, err := extractOpenAIToolCallParts(msg.ToolCalls)
if err != nil {
return conversation{}, fmt.Errorf("openai message %d: %w", i, err)
}
parts = append(parts, toolParts...)
switch role {
case "system", "developer", "user", "assistant":
if len(parts) == 0 {
continue
}
conv.Turns = append(conv.Turns, conversationTurn{Role: role, Parts: parts})
case "tool":
if strings.TrimSpace(msg.ToolCallID) == "" {
return conversation{}, fmt.Errorf("%w: openai tool message missing tool_call_id", protocol.ErrUnsupportedRequestShape)
}
conv.Turns = append(conv.Turns, conversationTurn{Role: role, Parts: []conversationPart{{
Kind: partKindToolResult,
ToolResult: &conversationToolResult{
CallID: strings.TrimSpace(msg.ToolCallID),
Name: strings.TrimSpace(msg.Name),
Parts: parts,
},
}}})
default:
return conversation{}, fmt.Errorf("%w: unsupported openai message role %q", protocol.ErrUnsupportedRequestShape, msg.Role)
}
}
resolveToolResultNames(&conv)
if len(conv.Turns) == 0 && len(conv.Tools) == 0 {
return conversation{}, fmt.Errorf("%w: no convertible openai messages", protocol.ErrUnsupportedRequestShape)
}
return conv, nil
}
func normalizeAnthropicConversation(req anthropicMessagesRequest) (conversation, error) {
conv := conversation{Turns: make([]conversationTurn, 0, len(req.Messages)+1)}
var err error
conv.Tools, err = parseFunctionTools(req.Tools, "anthropic")
if err != nil {
return conversation{}, err
}
conv.ToolChoice, err = parseToolChoice(req.ToolChoice, "anthropic")
if err != nil {
return conversation{}, err
}
if disable, ok := extractAnthropicDisableParallel(req.ToolChoice); ok {
conv.ToolChoice.DisableParallel = disable
}
if req.Thinking != nil && strings.TrimSpace(req.Thinking.Type) != "" {
conv.Thinking = req.Thinking
}
if req.System != nil {
systemParts, err := extractAnthropicContentParts(req.System)
if err != nil {
return conversation{}, fmt.Errorf("anthropic system: %w", err)
}
systemParts = dropReasoningParts(systemParts)
if len(systemParts) > 0 {
conv.Turns = append(conv.Turns, conversationTurn{Role: "system", Parts: systemParts})
}
}
for i, msg := range req.Messages {
role := normalizeRole(msg.Role)
parts, err := extractAnthropicContentParts(msg.Content)
if err != nil {
return conversation{}, fmt.Errorf("anthropic message %d: %w", i, err)
}
if role != "assistant" {
parts = dropReasoningParts(parts)
}
switch role {
case "user", "assistant":
if len(parts) == 0 {
continue
}
conv.Turns = append(conv.Turns, conversationTurn{Role: role, Parts: parts})
default:
return conversation{}, fmt.Errorf("%w: unsupported anthropic message role %q", protocol.ErrUnsupportedRequestShape, msg.Role)
}
}
resolveToolResultNames(&conv)
if len(conv.Turns) == 0 && len(conv.Tools) == 0 {
return conversation{}, fmt.Errorf("%w: no convertible anthropic messages", protocol.ErrUnsupportedRequestShape)
}
return conv, nil
}
func normalizeCodexConversation(req codexRequest) (conversation, error) {
conv := conversation{Turns: make([]conversationTurn, 0, len(req.Input)+1)}
var err error
conv.Tools, err = parseFunctionTools(req.Tools, "codex")
if err != nil {
return conversation{}, err
}
conv.ToolChoice, err = parseToolChoice(req.ToolChoice, "codex")
if err != nil {
return conversation{}, err
}
if req.ParallelToolCalls != nil && !*req.ParallelToolCalls {
conv.ToolChoice.DisableParallel = true
}
conv.Sampling = buildCodexSampling(req)
if conv.Sampling != nil {
if thinking := openAIReasoningEffortToThinking(conv.Sampling.ReasoningEffort); thinking != nil {
conv.Thinking = thinking
}
}
if strings.TrimSpace(req.Instructions) != "" {
conv.Turns = append(conv.Turns, conversationTurn{Role: "system", Parts: []conversationPart{{Kind: partKindText, Text: req.Instructions}}})
}
for i, rawItem := range req.Input {
var item map[string]any
if err := sonic.Unmarshal(rawItem, &item); err != nil {
return conversation{}, fmt.Errorf("codex input %d: %w", i, err)
}
typ := normalizeRole(stringValue(item["type"]))
if typ == "" {
role := normalizeRole(stringValue(item["role"]))
if role != "" {
if _, hasContent := item["content"]; hasContent {
typ = "message"
}
}
}
switch typ {
case "message":
role := normalizeRole(stringValue(item["role"]))
parts, err := extractCodexContentParts(item["content"])
if err != nil {
return conversation{}, fmt.Errorf("codex input %d: %w", i, err)
}
switch role {
case "system", "developer", "user", "assistant":
if len(parts) == 0 {
continue
}
conv.Turns = append(conv.Turns, conversationTurn{Role: role, Parts: parts})
default:
return conversation{}, fmt.Errorf("%w: unsupported codex message role %q", protocol.ErrUnsupportedRequestShape, role)
}
case "function_call":
call, err := decodeCodexToolCall(item)
if err != nil {
return conversation{}, fmt.Errorf("codex input %d: %w", i, err)
}
conv.Turns = append(conv.Turns, conversationTurn{Role: "assistant", Parts: []conversationPart{{Kind: partKindToolCall, ToolCall: &call}}})
case "function_call_output":
result, err := decodeCodexToolResult(item)
if err != nil {
return conversation{}, fmt.Errorf("codex input %d: %w", i, err)
}
conv.Turns = append(conv.Turns, conversationTurn{Role: "tool", Parts: []conversationPart{{Kind: partKindToolResult, ToolResult: &result}}})
case "input_text", "output_text", "text", "input_image", "image", "input_file", "file":
part, err := decodeCodexContentPart(item)
if err != nil {
return conversation{}, fmt.Errorf("codex input %d: %w", i, err)
}
conv.Turns = append(conv.Turns, conversationTurn{Role: "user", Parts: []conversationPart{part}})
default:
return conversation{}, fmt.Errorf("%w: unsupported codex input item type %q", protocol.ErrUnsupportedRequestShape, typ)
}
}
resolveToolResultNames(&conv)
if len(conv.Turns) == 0 && len(conv.Tools) == 0 {
return conversation{}, fmt.Errorf("%w: no convertible codex input messages", protocol.ErrUnsupportedRequestShape)
}
return conv, nil
}
func normalizeGeminiConversation(req geminiRequestPayload) (conversation, error) {
conv := conversation{Turns: make([]conversationTurn, 0, len(req.Contents)+1)}
var err error
conv.Tools, err = parseGeminiTools(req.Tools)
if err != nil {
return conversation{}, err
}
conv.ToolChoice, err = parseGeminiToolChoice(req.ToolConfig)
if err != nil {
return conversation{}, err
}
if req.SystemInstruction != nil {
systemParts, err := extractGeminiParts(req.SystemInstruction.Parts, nil, nil)
if err != nil {
return conversation{}, fmt.Errorf("gemini system_instruction: %w", err)
}
if len(systemParts) > 0 {
conv.Turns = append(conv.Turns, conversationTurn{Role: "system", Parts: systemParts})
}
}
pendingToolCallIDs := make([]string, 0)
nextToolCallID := 1
for i, content := range req.Contents {
role, err := normalizeGeminiRole(content.Role)
if err != nil {
return conversation{}, fmt.Errorf("gemini content %d: %w", i, err)
}
parts, err := extractGeminiParts(content.Parts, &pendingToolCallIDs, &nextToolCallID)
if err != nil {
return conversation{}, fmt.Errorf("gemini content %d: %w", i, err)
}
if len(parts) == 0 {
continue
}
conv.Turns = append(conv.Turns, conversationTurn{Role: role, Parts: parts})
}
resolveToolResultNames(&conv)
if len(conv.Turns) == 0 && len(conv.Tools) == 0 {
return conversation{}, fmt.Errorf("%w: no convertible gemini contents", protocol.ErrUnsupportedRequestShape)
}
return conv, nil
}
func splitConversationForSystem(conv conversation) ([]conversationPart, []conversationTurn, error) {
systemParts := make([]conversationPart, 0)
turns := make([]conversationTurn, 0, len(conv.Turns))
for _, turn := range conv.Turns {
role := normalizeRole(turn.Role)
if role == "system" || role == "developer" {
systemParts = append(systemParts, turn.Parts...)
continue
}
turns = append(turns, turn)
}
return systemParts, turns, nil
}
func collectSystemText(conv conversation) (string, []conversationTurn, error) {
systemParts, turns, err := splitConversationForSystem(conv)
if err != nil {
return "", nil, err
}
var builder strings.Builder
for _, part := range systemParts {
if part.Kind != partKindText {
return "", nil, fmt.Errorf("%w: codex instructions only support text system content", protocol.ErrUnsupportedRequestShape)
}
if builder.Len() > 0 {
builder.WriteString("\n\n")
}
builder.WriteString(part.Text)
}
return builder.String(), turns, nil
}
func parseFunctionTools(raw json.RawMessage, source string) ([]conversationTool, error) {
if !hasJSONValue(raw) {
return nil, nil
}
var items []map[string]any
if err := sonic.Unmarshal(raw, &items); err != nil {
return nil, fmt.Errorf("%s tools: %w", source, err)
}
tools := make([]conversationTool, 0, len(items))
for i, item := range items {
typ, err := normalizeConversationToolType(stringValue(item["type"]))
if err != nil {
return nil, fmt.Errorf("%w: unsupported %s tool type %q at index %d", protocol.ErrUnsupportedRequestShape, source, normalizeRole(stringValue(item["type"])), i)
}
if typ != "function" {
tool := conversationTool{
Type: typ,
Options: cloneMapWithoutKeys(item, "type"),
}
tools = append(tools, tool)
continue
}
fn := item
if normalizeRole(stringValue(item["type"])) == "function" {
nested, ok := item["function"].(map[string]any)
if ok && len(nested) > 0 {
fn = nested
}
}
name := strings.TrimSpace(stringValue(fn["name"]))
if name == "" {
return nil, fmt.Errorf("%w: %s tool %d missing name", protocol.ErrUnsupportedRequestShape, source, i)
}
schema, err := rawJSONFromFields(fn, "parameters", "input_schema")
if err != nil {
return nil, err
}
tools = append(tools, conversationTool{
Type: "function",
Name: name,
Description: stringValue(fn["description"]),
InputSchema: schema,
})
}
return tools, nil
}
func parseToolChoice(raw json.RawMessage, source string) (conversationToolChoice, error) {
if !hasJSONValue(raw) {
return conversationToolChoice{}, nil
}
var text string
if err := sonic.Unmarshal(raw, &text); err == nil {
switch normalizeRole(text) {
case "", "auto":
return conversationToolChoice{Mode: "auto"}, nil
case "none":
return conversationToolChoice{Mode: "none"}, nil
case "required", "any":
return conversationToolChoice{Mode: "required"}, nil
default:
return conversationToolChoice{}, fmt.Errorf("%w: unsupported %s tool_choice %q", protocol.ErrUnsupportedRequestShape, source, text)
}
}
var obj map[string]any
if err := sonic.Unmarshal(raw, &obj); err != nil {
return conversationToolChoice{}, fmt.Errorf("%s tool_choice: %w", source, err)
}
typ := normalizeRole(stringValue(obj["type"]))
switch typ {
case "auto", "":
if name := nestedNameField(obj, "function", "name"); name != "" {
return conversationToolChoice{Mode: "named", Name: name, ToolType: "function"}, nil
}
return conversationToolChoice{Mode: "auto"}, nil
case "none":
return conversationToolChoice{Mode: "none"}, nil
case "required", "any":
return conversationToolChoice{Mode: "required"}, nil
case "function":
name := nestedNameField(obj, "function", "name")
if name == "" {
name = strings.TrimSpace(stringValue(obj["name"]))
}
if name == "" {
return conversationToolChoice{}, fmt.Errorf("%w: named %s tool_choice missing name", protocol.ErrUnsupportedRequestShape, source)
}
return conversationToolChoice{Mode: "named", Name: name, ToolType: "function"}, nil
case "tool":
name := strings.TrimSpace(stringValue(obj["name"]))
if name == "" {
return conversationToolChoice{}, fmt.Errorf("%w: named %s tool_choice missing name", protocol.ErrUnsupportedRequestShape, source)
}
return conversationToolChoice{Mode: "named", Name: name, ToolType: "function"}, nil
default:
if isBuiltinConversationToolType(typ) {
return conversationToolChoice{Mode: "named", ToolType: typ}, nil
}
return conversationToolChoice{}, fmt.Errorf("%w: unsupported %s tool_choice type %q", protocol.ErrUnsupportedRequestShape, source, typ)
}
}