mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
721 lines
20 KiB
Go
721 lines
20 KiB
Go
package llm_client
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/mark3labs/mcp-go/mcp"
|
||
|
||
"yunion.io/x/pkg/errors"
|
||
|
||
api "yunion.io/x/onecloud/pkg/apis/llm"
|
||
"yunion.io/x/onecloud/pkg/llm/models"
|
||
)
|
||
|
||
func init() {
|
||
models.RegisterLLMClientDriver(newOpenAI())
|
||
}
|
||
|
||
type openai struct{}
|
||
|
||
func newOpenAI() models.ILLMClient {
|
||
return new(openai)
|
||
}
|
||
|
||
func (o *openai) GetType() api.LLMClientType {
|
||
return api.LLM_CLIENT_OPENAI
|
||
}
|
||
|
||
func buildOpenAIModelsURL(endpoint string) (string, error) {
|
||
endpoint = strings.TrimSpace(endpoint)
|
||
if endpoint == "" {
|
||
return "", errors.Error("endpoint is empty")
|
||
}
|
||
|
||
baseURL, err := url.Parse(endpoint)
|
||
if err != nil {
|
||
return "", errors.Wrapf(err, "invalid endpoint URL %s", endpoint)
|
||
}
|
||
baseURL.RawQuery = ""
|
||
baseURL.Fragment = ""
|
||
|
||
path := strings.TrimRight(baseURL.Path, "/")
|
||
switch {
|
||
case path == "":
|
||
baseURL.Path = "/v1/models"
|
||
case strings.HasSuffix(path, "/v1/models"):
|
||
baseURL.Path = path
|
||
case strings.HasSuffix(path, "/v1"):
|
||
baseURL.Path = path + "/models"
|
||
default:
|
||
baseURL.Path = path + "/v1/models"
|
||
}
|
||
|
||
return baseURL.String(), nil
|
||
}
|
||
|
||
func listOpenAIModelsWithClient(ctx context.Context, client *http.Client, endpoint string) ([]string, error) {
|
||
modelsURL, err := buildOpenAIModelsURL(endpoint)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, modelsURL, nil)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "create request")
|
||
}
|
||
req.Header.Set("Accept", "application/json")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "do request")
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "read response body")
|
||
}
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, errors.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
var modelResp OpenAIModelsResponse
|
||
if err := json.Unmarshal(body, &modelResp); err != nil {
|
||
return nil, errors.Wrapf(err, "decode response: %s", string(body))
|
||
}
|
||
|
||
ret := make([]string, 0, len(modelResp.Data))
|
||
for _, model := range modelResp.Data {
|
||
name := strings.TrimSpace(model.ID)
|
||
if name == "" {
|
||
continue
|
||
}
|
||
ret = append(ret, name)
|
||
}
|
||
return ret, nil
|
||
}
|
||
|
||
func (o *openai) ListModels(ctx context.Context, endpoint string) ([]string, error) {
|
||
client := &http.Client{
|
||
Timeout: 30 * time.Second,
|
||
}
|
||
return listOpenAIModelsWithClient(ctx, client, endpoint)
|
||
}
|
||
|
||
func (o *openai) Chat(ctx context.Context, mcpAgent *models.SMCPAgent, messages interface{}, tools interface{}) (models.ILLMChatResponse, error) {
|
||
// 转换 messages
|
||
var openaiMessages []OpenAIChatMessage
|
||
if msgs, ok := messages.([]OpenAIChatMessage); ok {
|
||
openaiMessages = msgs
|
||
} else if msgs, ok := messages.([]models.ILLMChatMessage); ok {
|
||
openaiMessages = make([]OpenAIChatMessage, len(msgs))
|
||
for i, msg := range msgs {
|
||
// Check if it's an OpenAIChatMessage to preserve ToolCallID and ReasoningContent
|
||
if om, ok := msg.(*OpenAIChatMessage); ok {
|
||
openaiMessages[i] = *om
|
||
} else {
|
||
// General conversion
|
||
openaiMessages[i] = OpenAIChatMessage{
|
||
Role: msg.GetRole(),
|
||
Content: msg.GetContent(),
|
||
}
|
||
// 转换工具调用
|
||
if toolCalls := msg.GetToolCalls(); len(toolCalls) > 0 {
|
||
openaiMessages[i].ToolCalls = make([]OpenAIToolCall, len(toolCalls))
|
||
for j, tc := range toolCalls {
|
||
fc := tc.GetFunction()
|
||
argsBytes, _ := json.Marshal(fc.GetArguments())
|
||
openaiMessages[i].ToolCalls[j] = OpenAIToolCall{
|
||
ID: tc.GetId(),
|
||
Type: "function",
|
||
Function: OpenAIFunctionCall{
|
||
Name: fc.GetName(),
|
||
Arguments: string(argsBytes),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
return nil, errors.Error("invalid messages type")
|
||
}
|
||
|
||
// 转换 tools
|
||
var openaiTools []OpenAITool
|
||
if ts, ok := tools.([]OpenAITool); ok {
|
||
openaiTools = ts
|
||
} else if ts, ok := tools.([]models.ILLMTool); ok {
|
||
openaiTools = make([]OpenAITool, len(ts))
|
||
for i, t := range ts {
|
||
tf := t.GetFunction()
|
||
openaiTools[i] = OpenAITool{
|
||
Type: t.GetType(),
|
||
Function: OpenAIToolFunction{
|
||
Name: tf.GetName(),
|
||
Description: tf.GetDescription(),
|
||
Parameters: tf.GetParameters(),
|
||
},
|
||
}
|
||
}
|
||
} else if tools == nil {
|
||
openaiTools = nil
|
||
}
|
||
|
||
return o.doChatRequest(ctx, mcpAgent, openaiMessages, openaiTools)
|
||
}
|
||
|
||
type OpenAIModelsResponse struct {
|
||
Object string `json:"object,omitempty"`
|
||
Data []OpenAIModelEntry `json:"data"`
|
||
}
|
||
|
||
type OpenAIModelEntry struct {
|
||
ID string `json:"id"`
|
||
Object string `json:"object,omitempty"`
|
||
OwnedBy string `json:"owned_by,omitempty"`
|
||
}
|
||
|
||
func (o *openai) ChatStream(ctx context.Context, mcpAgent *models.SMCPAgent, messages interface{}, tools interface{}, onChunk func(models.ILLMChatResponse) error) error {
|
||
// 转换 messages
|
||
var openaiMessages []OpenAIChatMessage
|
||
|
||
if msgs, ok := messages.([]OpenAIChatMessage); ok {
|
||
openaiMessages = msgs
|
||
} else {
|
||
var ilMsgs []models.ILLMChatMessage
|
||
if msgs, ok := messages.([]models.ILLMChatMessage); ok {
|
||
ilMsgs = msgs
|
||
} else if msg, ok := messages.(models.ILLMChatMessage); ok {
|
||
ilMsgs = []models.ILLMChatMessage{msg}
|
||
} else {
|
||
return errors.Error("invalid messages type")
|
||
}
|
||
|
||
openaiMessages = make([]OpenAIChatMessage, len(ilMsgs))
|
||
for i, msg := range ilMsgs {
|
||
// Check if it's an OpenAIChatMessage to preserve ToolCallID and ReasoningContent
|
||
if om, ok := msg.(*OpenAIChatMessage); ok {
|
||
openaiMessages[i] = *om
|
||
} else {
|
||
// General conversion
|
||
openaiMessages[i] = OpenAIChatMessage{
|
||
Role: msg.GetRole(),
|
||
Content: msg.GetContent(),
|
||
}
|
||
// 转换工具调用
|
||
if toolCalls := msg.GetToolCalls(); len(toolCalls) > 0 {
|
||
openaiMessages[i].ToolCalls = make([]OpenAIToolCall, len(toolCalls))
|
||
for j, tc := range toolCalls {
|
||
fc := tc.GetFunction()
|
||
argsBytes, _ := json.Marshal(fc.GetArguments())
|
||
openaiMessages[i].ToolCalls[j] = OpenAIToolCall{
|
||
ID: tc.GetId(),
|
||
Type: "function",
|
||
Function: OpenAIFunctionCall{
|
||
Name: fc.GetName(),
|
||
Arguments: string(argsBytes),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换 tools
|
||
var openaiTools []OpenAITool
|
||
if ts, ok := tools.([]OpenAITool); ok {
|
||
openaiTools = ts
|
||
} else if ts, ok := tools.([]models.ILLMTool); ok {
|
||
openaiTools = make([]OpenAITool, len(ts))
|
||
for i, t := range ts {
|
||
tf := t.GetFunction()
|
||
openaiTools[i] = OpenAITool{
|
||
Type: t.GetType(),
|
||
Function: OpenAIToolFunction{
|
||
Name: tf.GetName(),
|
||
Description: tf.GetDescription(),
|
||
Parameters: tf.GetParameters(),
|
||
},
|
||
}
|
||
}
|
||
} else if tools == nil {
|
||
openaiTools = nil
|
||
}
|
||
|
||
return o.doChatStreamRequest(ctx, mcpAgent, openaiMessages, openaiTools, onChunk)
|
||
}
|
||
|
||
func (o *openai) doChatStreamRequest(ctx context.Context, mcpAgent *models.SMCPAgent, messages []OpenAIChatMessage, tools []OpenAITool, onChunk func(models.ILLMChatResponse) error) error {
|
||
req := OpenAIChatRequest{
|
||
Model: mcpAgent.Model,
|
||
Messages: messages,
|
||
Tools: tools,
|
||
Stream: true,
|
||
}
|
||
|
||
reqBody, err := json.Marshal(req)
|
||
if err != nil {
|
||
return errors.Wrap(err, "marshal request")
|
||
}
|
||
|
||
endpoint := strings.TrimSuffix(mcpAgent.LLMUrl, "/")
|
||
// Default to /v1/chat/completions if not specified and not a custom path
|
||
if !strings.Contains(endpoint, "/chat/completions") {
|
||
if strings.HasSuffix(endpoint, "/v1") {
|
||
endpoint = endpoint + "/chat/completions"
|
||
} else {
|
||
endpoint = endpoint + "/v1/chat/completions"
|
||
}
|
||
}
|
||
|
||
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(reqBody))
|
||
if err != nil {
|
||
return errors.Wrap(err, "create request")
|
||
}
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
apiKey, err := mcpAgent.GetApiKey()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if apiKey != "" {
|
||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||
}
|
||
|
||
client := &http.Client{
|
||
// Stream request no timeout
|
||
Timeout: 0,
|
||
}
|
||
|
||
resp, err := client.Do(httpReq)
|
||
if err != nil {
|
||
return errors.Wrap(err, "do request")
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return formatLLMHTTPError(resp.StatusCode, mcpAgent.Model, body)
|
||
}
|
||
|
||
scanner := bufio.NewScanner(resp.Body)
|
||
// 工具调用场景下单行 SSE 可能很大,提高 buffer
|
||
scanner.Buffer(make([]byte, 64*1024), 2*1024*1024)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
if !strings.HasPrefix(line, "data: ") {
|
||
continue
|
||
}
|
||
|
||
data := strings.TrimPrefix(line, "data: ")
|
||
if data == "[DONE]" {
|
||
break
|
||
}
|
||
|
||
var chunk OpenAIChatStreamResponse
|
||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||
return errors.Wrapf(err, "decode stream chunk: %s", data)
|
||
}
|
||
|
||
if onChunk != nil {
|
||
if err := onChunk(&chunk); err != nil {
|
||
return errors.Wrap(err, "process chunk")
|
||
}
|
||
}
|
||
}
|
||
|
||
if err := scanner.Err(); err != nil {
|
||
return errors.Wrap(err, "read stream")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (o *openai) doChatRequest(ctx context.Context, mcpAgent *models.SMCPAgent, messages []OpenAIChatMessage, tools []OpenAITool) (*OpenAIChatResponse, error) {
|
||
req := OpenAIChatRequest{
|
||
Model: mcpAgent.Model,
|
||
Messages: messages,
|
||
Tools: tools,
|
||
}
|
||
|
||
reqBody, err := json.Marshal(req)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "marshal request")
|
||
}
|
||
|
||
endpoint := strings.TrimSuffix(mcpAgent.LLMUrl, "/")
|
||
// Default to /v1/chat/completions if not specified and not a custom path
|
||
if !strings.Contains(endpoint, "/chat/completions") {
|
||
if strings.HasSuffix(endpoint, "/v1") {
|
||
endpoint = endpoint + "/chat/completions"
|
||
} else {
|
||
endpoint = endpoint + "/v1/chat/completions"
|
||
}
|
||
}
|
||
|
||
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(reqBody))
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "create request")
|
||
}
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
apiKey, err := mcpAgent.GetApiKey()
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "get apiKey")
|
||
}
|
||
if apiKey != "" {
|
||
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
|
||
}
|
||
|
||
client := &http.Client{
|
||
Timeout: 300 * time.Second,
|
||
}
|
||
|
||
resp, err := client.Do(httpReq)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "do request")
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "read response body")
|
||
}
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, formatLLMHTTPError(resp.StatusCode, mcpAgent.Model, body)
|
||
}
|
||
|
||
var chatResp OpenAIChatResponse
|
||
if err := json.Unmarshal(body, &chatResp); err != nil {
|
||
return nil, errors.Wrapf(err, "decode response: %s", string(body))
|
||
}
|
||
|
||
if len(chatResp.Choices) == 0 {
|
||
return nil, errors.Error("no choices in response")
|
||
}
|
||
|
||
return &chatResp, nil
|
||
}
|
||
|
||
func (o *openai) NewUserMessage(content string) models.ILLMChatMessage {
|
||
return &OpenAIChatMessage{
|
||
Role: "user",
|
||
Content: content,
|
||
}
|
||
}
|
||
|
||
func (o *openai) NewAssistantMessage(content string) models.ILLMChatMessage {
|
||
return &OpenAIChatMessage{
|
||
Role: "assistant",
|
||
Content: content,
|
||
}
|
||
}
|
||
|
||
func (o *openai) NewAssistantMessageWithToolCalls(toolCalls []models.ILLMToolCall) models.ILLMChatMessage {
|
||
openaiToolCalls := make([]OpenAIToolCall, len(toolCalls))
|
||
for i, tc := range toolCalls {
|
||
if otc, ok := tc.(*OpenAIToolCall); ok {
|
||
openaiToolCalls[i] = *otc
|
||
} else {
|
||
fc := tc.GetFunction()
|
||
argsBytes, _ := json.Marshal(fc.GetArguments())
|
||
openaiToolCalls[i] = OpenAIToolCall{
|
||
ID: tc.GetId(),
|
||
Type: "function",
|
||
Function: OpenAIFunctionCall{
|
||
Name: fc.GetName(),
|
||
Arguments: string(argsBytes),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
return &OpenAIChatMessage{
|
||
Role: "assistant",
|
||
ToolCalls: openaiToolCalls,
|
||
}
|
||
}
|
||
|
||
func (o *openai) NewAssistantMessageWithToolCallsAndReasoning(reasoningContent, content string, toolCalls []models.ILLMToolCall) models.ILLMChatMessage {
|
||
openaiToolCalls := make([]OpenAIToolCall, len(toolCalls))
|
||
for i, tc := range toolCalls {
|
||
if otc, ok := tc.(*OpenAIToolCall); ok {
|
||
openaiToolCalls[i] = *otc
|
||
} else {
|
||
fc := tc.GetFunction()
|
||
argsBytes, _ := json.Marshal(fc.GetArguments())
|
||
openaiToolCalls[i] = OpenAIToolCall{
|
||
ID: tc.GetId(),
|
||
Type: "function",
|
||
Function: OpenAIFunctionCall{
|
||
Name: fc.GetName(),
|
||
Arguments: string(argsBytes),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
return &OpenAIChatMessage{
|
||
Role: "assistant",
|
||
Content: content,
|
||
ReasoningContent: reasoningContent,
|
||
ToolCalls: openaiToolCalls,
|
||
}
|
||
}
|
||
|
||
func (o *openai) NewToolMessage(toolId string, toolName string, content string) models.ILLMChatMessage {
|
||
return &OpenAIChatMessage{
|
||
Role: "tool",
|
||
ToolCallID: toolId,
|
||
Content: content,
|
||
}
|
||
}
|
||
|
||
func (o *openai) NewSystemMessage(content string) models.ILLMChatMessage {
|
||
return &OpenAIChatMessage{
|
||
Role: "system",
|
||
Content: content,
|
||
}
|
||
}
|
||
|
||
func (o *openai) ConvertMCPTools(mcpTools []mcp.Tool) []models.ILLMTool {
|
||
tools := make([]models.ILLMTool, len(mcpTools))
|
||
for i, t := range mcpTools {
|
||
var params map[string]interface{}
|
||
if t.RawInputSchema != nil {
|
||
_ = json.Unmarshal(t.RawInputSchema, ¶ms)
|
||
} else {
|
||
schemaBytes, _ := json.Marshal(t.InputSchema)
|
||
_ = json.Unmarshal(schemaBytes, ¶ms)
|
||
}
|
||
tools[i] = &OpenAITool{
|
||
Type: "function",
|
||
Function: OpenAIToolFunction{
|
||
Name: t.Name,
|
||
Description: t.Description,
|
||
Parameters: params,
|
||
},
|
||
}
|
||
}
|
||
return tools
|
||
}
|
||
|
||
// formatLLMHTTPError 把上游 JSON 错误整理成可读单行提示。
|
||
func formatLLMHTTPError(status int, model string, body []byte) error {
|
||
msg := extractLLMErrorMessage(body)
|
||
if msg == "" {
|
||
msg = strings.TrimSpace(string(body))
|
||
msg = strings.ReplaceAll(msg, "\n", " ")
|
||
msg = strings.Join(strings.Fields(msg), " ")
|
||
}
|
||
if msg == "" {
|
||
msg = http.StatusText(status)
|
||
}
|
||
|
||
lower := strings.ToLower(msg)
|
||
switch {
|
||
case strings.Contains(lower, "unsupported model"):
|
||
return errors.Errorf("模型不支持:当前配置为 %q(%s)", model, msg)
|
||
case status == http.StatusUnauthorized || strings.Contains(lower, "invalid api key") || strings.Contains(lower, "incorrect api key"):
|
||
return errors.Errorf("鉴权失败:请检查 Agent 的 API Key(%s)", msg)
|
||
case status == http.StatusTooManyRequests:
|
||
return errors.Errorf("请求过于频繁,请稍后重试(%s)", msg)
|
||
default:
|
||
return errors.Errorf("大模型接口返回 %d:%s", status, msg)
|
||
}
|
||
}
|
||
|
||
func extractLLMErrorMessage(body []byte) string {
|
||
body = bytes.TrimSpace(body)
|
||
if len(body) == 0 {
|
||
return ""
|
||
}
|
||
var payload map[string]interface{}
|
||
if err := json.Unmarshal(body, &payload); err != nil {
|
||
return ""
|
||
}
|
||
if errObj, ok := payload["error"].(map[string]interface{}); ok {
|
||
if m, ok := errObj["message"].(string); ok && strings.TrimSpace(m) != "" {
|
||
return strings.TrimSpace(m)
|
||
}
|
||
if m, ok := errObj["msg"].(string); ok && strings.TrimSpace(m) != "" {
|
||
return strings.TrimSpace(m)
|
||
}
|
||
}
|
||
if m, ok := payload["message"].(string); ok && strings.TrimSpace(m) != "" {
|
||
return strings.TrimSpace(m)
|
||
}
|
||
if m, ok := payload["msg"].(string); ok && strings.TrimSpace(m) != "" {
|
||
return strings.TrimSpace(m)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// Structures
|
||
|
||
type OpenAIChatMessage struct {
|
||
Role string `json:"role"`
|
||
Content string `json:"content"`
|
||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
|
||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||
}
|
||
|
||
func (m *OpenAIChatMessage) GetRole() string { return m.Role }
|
||
func (m *OpenAIChatMessage) GetContent() string { return m.Content }
|
||
func (m *OpenAIChatMessage) GetToolCalls() []models.ILLMToolCall {
|
||
if len(m.ToolCalls) == 0 {
|
||
return nil
|
||
}
|
||
toolCalls := make([]models.ILLMToolCall, len(m.ToolCalls))
|
||
for i := range m.ToolCalls {
|
||
tc := m.ToolCalls[i]
|
||
toolCalls[i] = &tc
|
||
}
|
||
return toolCalls
|
||
}
|
||
|
||
type OpenAIToolCall struct {
|
||
Index int `json:"index"`
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Function OpenAIFunctionCall `json:"function"`
|
||
}
|
||
|
||
func (tc *OpenAIToolCall) GetFunction() models.ILLMFunctionCall { return &tc.Function }
|
||
func (tc *OpenAIToolCall) GetId() string { return tc.ID }
|
||
func (tc *OpenAIToolCall) GetIndex() int { return tc.Index }
|
||
|
||
type OpenAIFunctionCall struct {
|
||
Name string `json:"name"`
|
||
Arguments string `json:"arguments"`
|
||
}
|
||
|
||
func (fc *OpenAIFunctionCall) GetName() string { return fc.Name }
|
||
func (fc *OpenAIFunctionCall) GetRawArguments() string { return fc.Arguments }
|
||
func (fc *OpenAIFunctionCall) GetArguments() map[string]interface{} {
|
||
var args map[string]interface{}
|
||
_ = json.Unmarshal([]byte(fc.Arguments), &args)
|
||
return args
|
||
}
|
||
|
||
type OpenAITool struct {
|
||
Type string `json:"type"`
|
||
Function OpenAIToolFunction `json:"function"`
|
||
}
|
||
|
||
func (t *OpenAITool) GetType() string { return t.Type }
|
||
func (t *OpenAITool) GetFunction() models.ILLMToolFunction { return &t.Function }
|
||
|
||
type OpenAIToolFunction struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
Parameters map[string]interface{} `json:"parameters"`
|
||
}
|
||
|
||
func (tf *OpenAIToolFunction) GetName() string { return tf.Name }
|
||
func (tf *OpenAIToolFunction) GetDescription() string { return tf.Description }
|
||
func (tf *OpenAIToolFunction) GetParameters() map[string]interface{} { return tf.Parameters }
|
||
|
||
type OpenAIChatRequest struct {
|
||
Model string `json:"model"`
|
||
Messages []OpenAIChatMessage `json:"messages"`
|
||
Tools []OpenAITool `json:"tools,omitempty"`
|
||
Stream bool `json:"stream,omitempty"`
|
||
}
|
||
|
||
type OpenAIChatResponse struct {
|
||
ID string `json:"id"`
|
||
Choices []OpenAIChoice `json:"choices"`
|
||
}
|
||
|
||
type OpenAIChoice struct {
|
||
Message OpenAIChatMessage `json:"message"`
|
||
FinishReason string `json:"finish_reason"`
|
||
}
|
||
|
||
func (r *OpenAIChatResponse) GetContent() string {
|
||
if len(r.Choices) > 0 {
|
||
return r.Choices[0].Message.Content
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (r *OpenAIChatResponse) GetReasoningContent() string {
|
||
if len(r.Choices) > 0 {
|
||
return r.Choices[0].Message.ReasoningContent
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (r *OpenAIChatResponse) HasToolCalls() bool {
|
||
return len(r.Choices) > 0 && len(r.Choices[0].Message.ToolCalls) > 0
|
||
}
|
||
|
||
func (r *OpenAIChatResponse) GetToolCalls() []models.ILLMToolCall {
|
||
if len(r.Choices) == 0 {
|
||
return nil
|
||
}
|
||
return r.Choices[0].Message.GetToolCalls()
|
||
}
|
||
|
||
type OpenAIChatStreamResponse struct {
|
||
ID string `json:"id"`
|
||
Choices []OpenAIChatStreamChoice `json:"choices"`
|
||
}
|
||
|
||
type OpenAIChatStreamChoice struct {
|
||
Delta OpenAIChatStreamDelta `json:"delta"`
|
||
FinishReason string `json:"finish_reason"`
|
||
}
|
||
|
||
type OpenAIChatStreamDelta struct {
|
||
Role string `json:"role,omitempty"`
|
||
Content string `json:"content,omitempty"`
|
||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
|
||
}
|
||
|
||
func (r *OpenAIChatStreamResponse) GetContent() string {
|
||
if len(r.Choices) > 0 {
|
||
return r.Choices[0].Delta.Content
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (r *OpenAIChatStreamResponse) GetReasoningContent() string {
|
||
if len(r.Choices) > 0 {
|
||
return r.Choices[0].Delta.ReasoningContent
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (r *OpenAIChatStreamResponse) HasToolCalls() bool {
|
||
return len(r.Choices) > 0 && len(r.Choices[0].Delta.ToolCalls) > 0
|
||
}
|
||
|
||
func (r *OpenAIChatStreamResponse) GetToolCalls() []models.ILLMToolCall {
|
||
if len(r.Choices) == 0 {
|
||
return nil
|
||
}
|
||
toolCalls := make([]models.ILLMToolCall, len(r.Choices[0].Delta.ToolCalls))
|
||
for i := range r.Choices[0].Delta.ToolCalls {
|
||
tc := r.Choices[0].Delta.ToolCalls[i]
|
||
toolCalls[i] = &tc
|
||
}
|
||
return toolCalls
|
||
}
|