143 lines
3.4 KiB
Go
143 lines
3.4 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"nub/internal/llm"
|
|
)
|
|
|
|
const systemSeparator = "--- system ---\n"
|
|
|
|
func buildRequest(req llm.Request, caps llm.Caps) wireRequest {
|
|
wr := wireRequest{
|
|
Model: req.Model,
|
|
MaxTokens: req.MaxTokens,
|
|
Temperature: req.Temp,
|
|
Stream: true,
|
|
}
|
|
if caps.UsageInStream {
|
|
wr.StreamOptions = &streamOptions{IncludeUsage: true}
|
|
}
|
|
|
|
var messages []wireMessage
|
|
for _, m := range req.Messages {
|
|
messages = append(messages, mapMessage(m)...)
|
|
}
|
|
|
|
// System-Rolle je nach Endpoint-Capability (5.1 Punkt 4): "system" oder
|
|
// "developer" als eigene Message-Rolle, sonst als erste User-Message mit
|
|
// Trennmarker (Endpoint ohne System-Rollen-Unterstützung).
|
|
if len(req.System) > 0 {
|
|
text := blocksToText(req.System)
|
|
switch caps.SystemRole {
|
|
case "system", "developer":
|
|
messages = append([]wireMessage{{Role: caps.SystemRole, Content: textContent(text)}}, messages...)
|
|
default:
|
|
messages = prependSystemAsUser(messages, text)
|
|
}
|
|
}
|
|
wr.Messages = messages
|
|
|
|
// "tools" weglassen statt leer senden (5.1 Punkt 2).
|
|
if len(req.Tools) > 0 {
|
|
wr.Tools = make([]wireToolDef, len(req.Tools))
|
|
for i, t := range req.Tools {
|
|
wr.Tools[i] = wireToolDef{
|
|
Type: "function",
|
|
Function: wireFunctionDef{
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
Parameters: t.Schema,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
return wr
|
|
}
|
|
|
|
func mapMessage(m llm.Message) []wireMessage {
|
|
var out []wireMessage
|
|
role := string(m.Role)
|
|
|
|
var textParts []string
|
|
var toolCalls []wireToolCall
|
|
|
|
flushAssistant := func() {
|
|
if len(textParts) == 0 && len(toolCalls) == 0 {
|
|
return
|
|
}
|
|
wm := wireMessage{Role: role, ToolCalls: toolCalls}
|
|
if len(textParts) > 0 {
|
|
wm.Content = textContent(joinText(textParts))
|
|
}
|
|
out = append(out, wm)
|
|
textParts = nil
|
|
toolCalls = nil
|
|
}
|
|
|
|
for _, b := range m.Content {
|
|
switch b.Kind {
|
|
case llm.KindText:
|
|
textParts = append(textParts, b.Text)
|
|
case llm.KindThinking:
|
|
// Reasoning-Blöcke werden beim Zurücksenden nicht erneut mitgeschickt;
|
|
// sie sind rein empfangsseitig relevant (5.1 Punkt 5).
|
|
case llm.KindToolUse:
|
|
toolCalls = append(toolCalls, wireToolCall{
|
|
ID: b.ID,
|
|
Type: "function",
|
|
Function: wireToolCallFunc{
|
|
Name: b.Name,
|
|
Arguments: string(b.Input),
|
|
},
|
|
})
|
|
case llm.KindToolResult:
|
|
flushAssistant()
|
|
out = append(out, wireMessage{
|
|
Role: "tool",
|
|
ToolCallID: b.ToolUseID,
|
|
Content: textContent(blocksToText(b.Result)),
|
|
})
|
|
case llm.KindImage:
|
|
// v1: Bild-Content wird nicht gemappt (SupportsImages-Endpoints folgen später).
|
|
}
|
|
}
|
|
flushAssistant()
|
|
return out
|
|
}
|
|
|
|
func blocksToText(blocks []llm.Block) string {
|
|
var parts []string
|
|
for _, b := range blocks {
|
|
if b.Kind == llm.KindText || b.Kind == llm.KindThinking {
|
|
parts = append(parts, b.Text)
|
|
}
|
|
}
|
|
return joinText(parts)
|
|
}
|
|
|
|
func joinText(parts []string) string {
|
|
out := ""
|
|
for i, p := range parts {
|
|
if i > 0 {
|
|
out += "\n"
|
|
}
|
|
out += p
|
|
}
|
|
return out
|
|
}
|
|
|
|
func textContent(s string) json.RawMessage {
|
|
b, _ := json.Marshal(s)
|
|
return b
|
|
}
|
|
|
|
// prependSystemAsUser wird verwendet, wenn Caps.SystemRole == "none" gesetzt
|
|
// ist (Endpoint ohne System-Rolle): System-Text als erste User-Message mit
|
|
// Trennmarker.
|
|
func prependSystemAsUser(messages []wireMessage, systemText string) []wireMessage {
|
|
marker := fmt.Sprintf("%s%s", systemSeparator, systemText)
|
|
return append([]wireMessage{{Role: "user", Content: textContent(marker)}}, messages...)
|
|
}
|