2026-03-30 14:09:15 +00:00
|
|
|
package openai
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
2026-04-01 00:53:34 +00:00
|
|
|
"fmt"
|
2026-03-30 14:09:15 +00:00
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ChatCompletionRequest struct {
|
|
|
|
|
Model string `json:"model"`
|
|
|
|
|
Messages []interface{} `json:"messages"`
|
|
|
|
|
Stream bool `json:"stream"`
|
|
|
|
|
Tools []interface{} `json:"tools"`
|
|
|
|
|
ToolChoice interface{} `json:"tool_choice"`
|
|
|
|
|
Functions []interface{} `json:"functions"`
|
|
|
|
|
FunctionCall interface{} `json:"function_call"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NormalizeModelID(raw string) string {
|
|
|
|
|
trimmed := strings.TrimSpace(raw)
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(trimmed, "/")
|
|
|
|
|
last := parts[len(parts)-1]
|
|
|
|
|
if last == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return last
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func imageURLToText(imageURL interface{}) string {
|
|
|
|
|
if imageURL == nil {
|
|
|
|
|
return "[Image]"
|
|
|
|
|
}
|
|
|
|
|
var url string
|
|
|
|
|
switch v := imageURL.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
url = v
|
|
|
|
|
case map[string]interface{}:
|
|
|
|
|
if u, ok := v["url"].(string); ok {
|
|
|
|
|
url = u
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if url == "" {
|
|
|
|
|
return "[Image]"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(url, "data:") {
|
|
|
|
|
end := strings.Index(url, ";")
|
|
|
|
|
mime := "image"
|
|
|
|
|
if end > 5 {
|
|
|
|
|
mime = url[5:end]
|
|
|
|
|
}
|
|
|
|
|
return "[Image: base64 " + mime + "]"
|
|
|
|
|
}
|
|
|
|
|
return "[Image: " + url + "]"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MessageContentToText(content interface{}) string {
|
|
|
|
|
if content == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
switch v := content.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return v
|
|
|
|
|
case []interface{}:
|
|
|
|
|
var parts []string
|
|
|
|
|
for _, p := range v {
|
|
|
|
|
if p == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
switch part := p.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
parts = append(parts, part)
|
|
|
|
|
case map[string]interface{}:
|
|
|
|
|
typ, _ := part["type"].(string)
|
|
|
|
|
switch typ {
|
|
|
|
|
case "text":
|
|
|
|
|
if t, ok := part["text"].(string); ok {
|
|
|
|
|
parts = append(parts, t)
|
|
|
|
|
}
|
|
|
|
|
case "image_url":
|
|
|
|
|
parts = append(parts, imageURLToText(part["image_url"]))
|
|
|
|
|
case "image":
|
|
|
|
|
src := part["source"]
|
|
|
|
|
if src == nil {
|
|
|
|
|
src = part["url"]
|
|
|
|
|
}
|
|
|
|
|
parts = append(parts, imageURLToText(src))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, " ")
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ToolsToSystemText(tools []interface{}, functions []interface{}) string {
|
|
|
|
|
var defs []interface{}
|
|
|
|
|
|
|
|
|
|
for _, t := range tools {
|
|
|
|
|
if m, ok := t.(map[string]interface{}); ok {
|
|
|
|
|
if m["type"] == "function" {
|
|
|
|
|
if fn := m["function"]; fn != nil {
|
|
|
|
|
defs = append(defs, fn)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
defs = append(defs, t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
defs = append(defs, functions...)
|
|
|
|
|
|
|
|
|
|
if len(defs) == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var lines []string
|
|
|
|
|
lines = append(lines, "Available tools (respond with a JSON object to call one):", "")
|
|
|
|
|
|
|
|
|
|
for _, raw := range defs {
|
|
|
|
|
fn, ok := raw.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name, _ := fn["name"].(string)
|
|
|
|
|
desc, _ := fn["description"].(string)
|
|
|
|
|
params := "{}"
|
|
|
|
|
if p := fn["parameters"]; p != nil {
|
|
|
|
|
if b, err := json.MarshalIndent(p, "", " "); err == nil {
|
|
|
|
|
params = string(b)
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
} else if p := fn["input_schema"]; p != nil {
|
|
|
|
|
if b, err := json.MarshalIndent(p, "", " "); err == nil {
|
|
|
|
|
params = string(b)
|
|
|
|
|
}
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
lines = append(lines, "Function: "+name+"\nDescription: "+desc+"\nParameters: "+params)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 00:53:34 +00:00
|
|
|
lines = append(lines, "",
|
|
|
|
|
"When you want to call a tool, use this EXACT format:",
|
|
|
|
|
"",
|
|
|
|
|
"<tool_call>",
|
|
|
|
|
`{"name": "function_name", "arguments": {"param1": "value1"}}`,
|
|
|
|
|
"</tool_call>",
|
|
|
|
|
"",
|
|
|
|
|
"Rules:",
|
|
|
|
|
"- Write your reasoning BEFORE the tool call",
|
|
|
|
|
"- You may make multiple tool calls by using multiple <tool_call> blocks",
|
|
|
|
|
"- STOP writing after the last </tool_call> tag",
|
|
|
|
|
"- If no tool is needed, respond normally without <tool_call> tags",
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-30 14:09:15 +00:00
|
|
|
return strings.Join(lines, "\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SimpleMessage struct {
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildPromptFromMessages(messages []interface{}) string {
|
|
|
|
|
var systemParts []string
|
|
|
|
|
var convo []string
|
|
|
|
|
|
|
|
|
|
for _, raw := range messages {
|
|
|
|
|
m, ok := raw.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
role, _ := m["role"].(string)
|
|
|
|
|
text := MessageContentToText(m["content"])
|
2026-04-01 00:53:34 +00:00
|
|
|
|
2026-03-30 14:09:15 +00:00
|
|
|
switch role {
|
|
|
|
|
case "system", "developer":
|
2026-04-01 00:53:34 +00:00
|
|
|
if text != "" {
|
|
|
|
|
systemParts = append(systemParts, text)
|
|
|
|
|
}
|
2026-03-30 14:09:15 +00:00
|
|
|
case "user":
|
2026-04-01 00:53:34 +00:00
|
|
|
if text != "" {
|
|
|
|
|
convo = append(convo, "User: "+text)
|
|
|
|
|
}
|
2026-03-30 14:09:15 +00:00
|
|
|
case "assistant":
|
2026-04-01 00:53:34 +00:00
|
|
|
toolCalls, _ := m["tool_calls"].([]interface{})
|
|
|
|
|
if len(toolCalls) > 0 {
|
|
|
|
|
var parts []string
|
|
|
|
|
if text != "" {
|
|
|
|
|
parts = append(parts, text)
|
|
|
|
|
}
|
|
|
|
|
for _, tc := range toolCalls {
|
|
|
|
|
tcMap, ok := tc.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
fn, _ := tcMap["function"].(map[string]interface{})
|
|
|
|
|
if fn == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name, _ := fn["name"].(string)
|
|
|
|
|
args, _ := fn["arguments"].(string)
|
|
|
|
|
if args == "" {
|
|
|
|
|
args = "{}"
|
|
|
|
|
}
|
|
|
|
|
parts = append(parts, fmt.Sprintf("<tool_call>\n{\"name\": \"%s\", \"arguments\": %s}\n</tool_call>", name, args))
|
|
|
|
|
}
|
|
|
|
|
if len(parts) > 0 {
|
|
|
|
|
convo = append(convo, "Assistant: "+strings.Join(parts, "\n"))
|
|
|
|
|
}
|
|
|
|
|
} else if text != "" {
|
|
|
|
|
convo = append(convo, "Assistant: "+text)
|
|
|
|
|
}
|
2026-03-30 14:09:15 +00:00
|
|
|
case "tool", "function":
|
2026-04-01 00:53:34 +00:00
|
|
|
name, _ := m["name"].(string)
|
|
|
|
|
toolCallID, _ := m["tool_call_id"].(string)
|
|
|
|
|
label := "Tool result"
|
|
|
|
|
if name != "" {
|
|
|
|
|
label = "Tool result (" + name + ")"
|
|
|
|
|
}
|
|
|
|
|
if toolCallID != "" {
|
|
|
|
|
label += " [id=" + toolCallID + "]"
|
|
|
|
|
}
|
|
|
|
|
if text != "" {
|
|
|
|
|
convo = append(convo, label+": "+text)
|
|
|
|
|
}
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
system := ""
|
|
|
|
|
if len(systemParts) > 0 {
|
|
|
|
|
system = "System:\n" + strings.Join(systemParts, "\n\n") + "\n\n"
|
|
|
|
|
}
|
|
|
|
|
transcript := strings.Join(convo, "\n\n")
|
|
|
|
|
return system + transcript + "\n\nAssistant:"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildPromptFromSimpleMessages(messages []SimpleMessage) string {
|
|
|
|
|
ifaces := make([]interface{}, len(messages))
|
|
|
|
|
for i, m := range messages {
|
|
|
|
|
ifaces[i] = map[string]interface{}{"role": m.Role, "content": m.Content}
|
|
|
|
|
}
|
|
|
|
|
return BuildPromptFromMessages(ifaces)
|
|
|
|
|
}
|