Go: add tools for a lot of providers (#17341)

### Summary

As title

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Haruko386
2026-07-24 19:33:38 +08:00
committed by GitHub
parent 0471fd5695
commit c575164695
69 changed files with 2553 additions and 669 deletions

View File

@@ -166,7 +166,8 @@ func (a *AI302Model) ChatWithMessages(ctx context.Context, modelName string, mes
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -185,6 +186,7 @@ func (a *AI302Model) ChatWithMessages(ctx context.Context, modelName string, mes
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -263,6 +265,7 @@ func (a *AI302Model) ChatStreamlyWithSender(ctx context.Context, modelName strin
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
accumulatedToolCalls := make(map[int]map[string]any)
if _, err = ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -279,6 +282,8 @@ func (a *AI302Model) ChatStreamlyWithSender(ctx context.Context, modelName strin
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err = sender(nil, &reasoningContent); err != nil {
@@ -298,6 +303,8 @@ func (a *AI302Model) ChatStreamlyWithSender(ctx context.Context, modelName strin
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"
return sender(&endOfStream, nil)

View File

@@ -139,7 +139,8 @@ func (a *AstraflowModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -151,6 +152,7 @@ func (a *AstraflowModel) ChatWithMessages(ctx context.Context, modelName string,
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -205,6 +207,7 @@ func (a *AstraflowModel) ChatStreamlyWithSender(ctx context.Context, modelName s
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
if apiErr, ok := event["error"]; ok {
return fmt.Errorf("astraflow: upstream stream error: %v", apiErr)
@@ -219,6 +222,7 @@ func (a *AstraflowModel) ChatStreamlyWithSender(ctx context.Context, modelName s
return nil
}
if delta, ok := firstChoice["delta"].(map[string]interface{}); ok {
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if r, ok := delta["reasoning_content"].(string); ok && r != "" {
rr := r
if err := sender(nil, &rr); err != nil {
@@ -240,6 +244,7 @@ func (a *AstraflowModel) ChatStreamlyWithSender(ctx context.Context, modelName s
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("astraflow: stream ended before [DONE] or finish_reason")
}

View File

@@ -198,31 +198,39 @@ func (a *AvianModel) ChatStreamlyWithSender(ctx context.Context, modelName strin
}
sawTerminal := false
done, err := ParseSSEStream[avianChatResponse](resp.Body, func(event avianChatResponse) error {
if event.Error != nil {
return fmt.Errorf("avian: upstream stream error: %v", event.Error)
}
if len(event.Choices) == 0 {
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]any](resp.Body, func(event map[string]any) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil
}
choice := event.Choices[0]
if reasoning := choice.Delta.ReasoningContent; reasoning != "" {
if err := sender(nil, &reasoning); err != nil {
return err
}
} else if reasoning := choice.Delta.Reasoning; reasoning != "" {
if err := sender(nil, &reasoning); err != nil {
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
return nil
}
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err = sender(nil, &reasoningContent); err != nil {
return err
}
}
if choice.Delta.Content != "" {
if err := sender(&choice.Delta.Content, nil); err != nil {
content, ok := delta["content"].(string)
if ok && content != "" {
if err = sender(&content, nil); err != nil {
return err
}
}
if choice.FinishReason != "" || event.FinishReason != "" {
sawTerminal = true
}
return nil
})
if err != nil {
@@ -232,6 +240,8 @@ func (a *AvianModel) ChatStreamlyWithSender(ctx context.Context, modelName strin
return fmt.Errorf("avian: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
return sender(&endOfStream, nil)
}

View File

@@ -117,7 +117,8 @@ func (b *BaichuanModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("no message in response")
}
@@ -126,6 +127,7 @@ func (b *BaichuanModel) ChatWithMessages(ctx context.Context, modelName string,
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &emptyReason,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -177,6 +179,7 @@ func (b *BaichuanModel) ChatStreamlyWithSender(ctx context.Context, modelName st
// SSE parsing: read line by line
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -195,6 +198,8 @@ func (b *BaichuanModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
@@ -215,6 +220,8 @@ func (b *BaichuanModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return fmt.Errorf("baichuan: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"
return sender(&endOfStream, nil)

View File

@@ -224,6 +224,7 @@ func (c *CoHereModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
eventType, ok := event["type"].(string)
if !ok {
@@ -240,6 +241,9 @@ func (c *CoHereModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
if !ok {
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
msg, ok := delta["message"].(map[string]interface{})
if !ok {
return nil
@@ -270,6 +274,8 @@ func (c *CoHereModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
return fmt.Errorf("Cohere: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
return sender(&endOfStream, nil)
}

View File

@@ -287,29 +287,43 @@ func (c *CometAPIModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}
sawTerminal := false
done, err := ParseSSEStream[cometapiChatResponsePayload](resp.Body, func(event cometapiChatResponsePayload) error {
if len(event.Choices) == 0 {
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil
}
choice := event.Choices[0]
reasoningContent := choice.Delta.ReasoningContent
content := choice.Delta.Content
if reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
return nil
}
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err = sender(nil, &reasoningContent); err != nil {
return err
}
}
if content != "" {
if err := sender(&content, nil); err != nil {
content, ok := delta["content"].(string)
if ok && content != "" {
if err = sender(&content, nil); err != nil {
return err
}
}
if choice.FinishReason != "" {
if finishReason, ok := firstChoice["finish_reason"].(string); ok && finishReason != "" {
sawTerminal = true
}
return nil
})
if err != nil {
@@ -319,6 +333,8 @@ func (c *CometAPIModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return fmt.Errorf("cometapi: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
if err := sender(&endOfStream, nil); err != nil {
return err

View File

@@ -137,7 +137,8 @@ func (d *DeepInfraModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -216,6 +217,7 @@ func (d *DeepInfraModel) ChatStreamlyWithSender(ctx context.Context, modelName s
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -234,6 +236,8 @@ func (d *DeepInfraModel) ChatStreamlyWithSender(ctx context.Context, modelName s
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -257,6 +261,7 @@ func (d *DeepInfraModel) ChatStreamlyWithSender(ctx context.Context, modelName s
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("deepinfra: stream ended before [DONE] or finish_reason")
}

View File

@@ -221,35 +221,45 @@ func (f *FuturMixModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return fmt.Errorf("futurmix chat stream API error: %s, body: %s", resp.Status, string(body))
}
sawTerminal := false
done, err := ParseSSEStream[futurmixChatResponse](resp.Body, func(event futurmixChatResponse) error {
if len(event.Choices) == 0 {
accumulatedToolCalls := make(map[int]map[string]any)
if _, err = ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil
}
choice := event.Choices[0]
if choice.Delta.ReasoningContent != "" {
r := choice.Delta.ReasoningContent
if err := sender(nil, &r); err != nil {
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
return nil
}
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err = sender(nil, &reasoningContent); err != nil {
return err
}
}
if choice.Delta.Content != "" {
c := choice.Delta.Content
if err := sender(&c, nil); err != nil {
content, ok := delta["content"].(string)
if ok && content != "" {
if err = sender(&content, nil); err != nil {
return err
}
}
if choice.FinishReason != "" {
sawTerminal = true
}
return nil
})
if err != nil {
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
if !done && !sawTerminal {
return fmt.Errorf("futurmix: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
return sender(&endOfStream, nil)

View File

@@ -25,7 +25,6 @@ import (
"mime/multipart"
"net/http"
"ragflow/internal/common"
"sort"
"strings"
"time"
)
@@ -264,6 +263,8 @@ func (g *GiteeModel) ChatStreamlyWithSender(ctx context.Context, modelName strin
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
common.Info(content)
@@ -296,56 +297,6 @@ func (g *GiteeModel) ChatStreamlyWithSender(ctx context.Context, modelName strin
}
}
if tcs, ok := delta["tool_calls"].([]interface{}); ok {
for _, tc := range tcs {
tcMap, ok := tc.(map[string]interface{})
if !ok {
continue
}
idxF, ok := tcMap["index"].(float64)
if !ok {
continue
}
idx := int(idxF)
existing, hasExisting := accumulatedToolCalls[idx]
if !hasExisting {
accumulatedToolCalls[idx] = cloneMap(tcMap)
continue
}
if id, ok := tcMap["id"].(string); ok && id != "" {
if eid, ok := existing["id"].(string); ok {
existing["id"] = eid + id
} else {
existing["id"] = id
}
}
if typ, ok := tcMap["type"].(string); ok && typ != "" {
existing["type"] = typ
}
if fn, ok := tcMap["function"].(map[string]interface{}); ok {
ef, ok := existing["function"].(map[string]interface{})
if !ok {
ef = make(map[string]interface{})
existing["function"] = ef
}
if name, ok := fn["name"].(string); ok && name != "" {
if en, ok := ef["name"].(string); ok {
ef["name"] = en + name
} else {
ef["name"] = name
}
}
if args, ok := fn["arguments"].(string); ok && args != "" {
if ea, ok := ef["arguments"].(string); ok {
ef["arguments"] = ea + args
} else {
ef["arguments"] = args
}
}
}
}
}
finishReason, ok := firstChoice["finish_reason"].(string)
if ok && finishReason != "" {
sawTerminal = true
@@ -365,18 +316,7 @@ func (g *GiteeModel) ChatStreamlyWithSender(ctx context.Context, modelName strin
}
}
if len(accumulatedToolCalls) > 0 && chatModelConfig != nil {
indices := make([]int, 0, len(accumulatedToolCalls))
for idx := range accumulatedToolCalls {
indices = append(indices, idx)
}
sort.Ints(indices)
tcs := make([]map[string]interface{}, 0, len(accumulatedToolCalls))
for _, idx := range indices {
tcs = append(tcs, accumulatedToolCalls[idx])
}
chatModelConfig.ToolCallsResult = &tcs
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -18,6 +18,7 @@ package models
import (
"context"
"encoding/json"
"fmt"
"ragflow/internal/common"
"strings"
@@ -119,6 +120,240 @@ func (g *GoogleModel) baseURL(apiConfig *APIConfig) string {
return strings.TrimSpace(baseURL)
}
func googleChatContents(messages []Message) []*genai.Content {
var contents []*genai.Content
toolCallNames := make(map[string]string)
for _, msg := range messages {
switch msg.Role {
case "tool":
name := toolCallNames[msg.ToolCallID]
if name == "" {
name = msg.ToolCallID
}
contents = append(contents, &genai.Content{
Role: genai.RoleUser,
Parts: []*genai.Part{{
FunctionResponse: &genai.FunctionResponse{
ID: msg.ToolCallID,
Name: name,
Response: googleFunctionResponse(msg.Content),
},
}},
})
continue
}
var role genai.Role
switch msg.Role {
case "model", "assistant":
role = genai.RoleModel
default:
role = genai.RoleUser
}
parts := googleMessageParts(msg.Content)
for _, toolCall := range msg.ToolCalls {
id, _ := toolCall["id"].(string)
fn, _ := toolCall["function"].(map[string]interface{})
name, _ := fn["name"].(string)
if name == "" {
continue
}
args := map[string]any{}
if arguments, ok := fn["arguments"].(string); ok && strings.TrimSpace(arguments) != "" {
_ = json.Unmarshal([]byte(arguments), &args)
}
if id != "" {
toolCallNames[id] = name
}
parts = append(parts, &genai.Part{FunctionCall: &genai.FunctionCall{
ID: id,
Name: name,
Args: args,
}})
}
if len(parts) > 0 {
contents = append(contents, genai.NewContentFromParts(parts, role))
}
}
return contents
}
func googleMessageParts(content interface{}) []*genai.Part {
switch c := content.(type) {
case string:
if c == "" {
return nil
}
return []*genai.Part{genai.NewPartFromText(c)}
case []interface{}:
var parts []*genai.Part
for _, item := range c {
itemMap, ok := item.(map[string]interface{})
if !ok {
continue
}
contentType, _ := itemMap["type"].(string)
switch contentType {
case "text":
if text, ok := itemMap["text"].(string); ok && text != "" {
parts = append(parts, genai.NewPartFromText(text))
}
case "image_url":
if imgMap, ok := itemMap["image_url"].(map[string]interface{}); ok {
if url, ok := imgMap["url"].(string); ok && url != "" {
parts = append(parts, genai.NewPartFromURI(url, "image/jpeg"))
}
}
}
}
return parts
default:
return nil
}
}
func googleFunctionResponse(content interface{}) map[string]any {
switch c := content.(type) {
case map[string]any:
return c
case string:
var response map[string]any
if err := json.Unmarshal([]byte(c), &response); err == nil && response != nil {
return response
}
return map[string]any{"output": c}
default:
return map[string]any{"output": c}
}
}
func googleGenerateContentConfig(chatModelConfig *ChatConfig) *genai.GenerateContentConfig {
if chatModelConfig == nil {
return nil
}
cfg := &genai.GenerateContentConfig{}
if chatModelConfig.Temperature != nil {
value := float32(*chatModelConfig.Temperature)
cfg.Temperature = &value
}
if chatModelConfig.TopP != nil {
value := float32(*chatModelConfig.TopP)
cfg.TopP = &value
}
if chatModelConfig.MaxTokens != nil {
cfg.MaxOutputTokens = int32(*chatModelConfig.MaxTokens)
}
if chatModelConfig.Stop != nil {
cfg.StopSequences = *chatModelConfig.Stop
}
if tools := googleTools(chatModelConfig.Tools); len(tools) > 0 {
cfg.Tools = tools
cfg.ToolConfig = &genai.ToolConfig{
FunctionCallingConfig: &genai.FunctionCallingConfig{Mode: googleFunctionCallingMode(chatModelConfig.ToolChoice)},
}
}
if cfg.Temperature == nil && cfg.TopP == nil && cfg.MaxOutputTokens == 0 && len(cfg.StopSequences) == 0 && len(cfg.Tools) == 0 {
return nil
}
return cfg
}
func googleFunctionCallingMode(toolChoice *string) genai.FunctionCallingConfigMode {
if toolChoice == nil {
return genai.FunctionCallingConfigModeAuto
}
switch strings.ToLower(strings.TrimSpace(*toolChoice)) {
case "none":
return genai.FunctionCallingConfigModeNone
case "required", "any":
return genai.FunctionCallingConfigModeAny
default:
return genai.FunctionCallingConfigModeAuto
}
}
func googleTools(rawTools interface{}) []*genai.Tool {
var declarations []*genai.FunctionDeclaration
for _, rawTool := range normalizeToolList(rawTools) {
toolMap, ok := rawTool.(map[string]interface{})
if !ok {
continue
}
fn, ok := toolMap["function"].(map[string]interface{})
if !ok {
fn = toolMap
}
name, _ := fn["name"].(string)
if name == "" {
continue
}
description, _ := fn["description"].(string)
declaration := &genai.FunctionDeclaration{
Name: name,
Description: description,
}
if parameters, ok := fn["parameters"]; ok {
declaration.ParametersJsonSchema = parameters
}
declarations = append(declarations, declaration)
}
if len(declarations) == 0 {
return nil
}
return []*genai.Tool{{FunctionDeclarations: declarations}}
}
func normalizeToolList(rawTools interface{}) []interface{} {
switch tools := rawTools.(type) {
case nil:
return nil
case []interface{}:
return tools
case []map[string]interface{}:
result := make([]interface{}, 0, len(tools))
for _, tool := range tools {
result = append(result, tool)
}
return result
default:
return nil
}
}
func googleToolCalls(functionCalls []*genai.FunctionCall) []map[string]interface{} {
if len(functionCalls) == 0 {
return nil
}
toolCalls := make([]map[string]interface{}, 0, len(functionCalls))
for idx, functionCall := range functionCalls {
if functionCall == nil || functionCall.Name == "" {
continue
}
id := functionCall.ID
if id == "" {
id = fmt.Sprintf("gemini-call-%d", idx)
}
arguments, err := json.Marshal(functionCall.Args)
if err != nil {
arguments = []byte("{}")
}
toolCalls = append(toolCalls, map[string]interface{}{
"id": id,
"type": "function",
"function": map[string]interface{}{
"name": functionCall.Name,
"arguments": string(arguments),
},
})
}
return toolCalls
}
func (g *GoogleModel) ChatWithMessages(ctx context.Context, modelName string, messages []Message, apiConfig *APIConfig, chatModelConfig *ChatConfig, modelUsage *common.ModelUsage) (*ChatResponse, error) {
if err := g.baseModel.APIConfigCheck(apiConfig); err != nil {
return nil, err
@@ -136,51 +371,10 @@ func (g *GoogleModel) ChatWithMessages(ctx context.Context, modelName string, me
return nil, err
}
// Convert messages to Google SDK format
var contents []*genai.Content
for _, msg := range messages {
var role genai.Role
switch msg.Role {
case "user":
role = genai.RoleUser
case "model", "assistant":
role = genai.RoleModel
default:
role = genai.RoleUser
}
// Handle content based on type
switch c := msg.Content.(type) {
case string:
contents = append(contents, genai.NewContentFromText(c, role))
case []interface{}:
// Multimodal content - group parts within a single content
var parts []*genai.Part
for _, item := range c {
if itemMap, ok := item.(map[string]interface{}); ok {
contentType, _ := itemMap["type"].(string)
switch contentType {
case "text":
if text, ok := itemMap["text"].(string); ok {
parts = append(parts, genai.NewPartFromText(text))
}
case "image_url":
if imgMap, ok := itemMap["image_url"].(map[string]interface{}); ok {
if url, ok := imgMap["url"].(string); ok {
parts = append(parts, genai.NewPartFromURI(url, "image/jpeg"))
}
}
}
}
}
if len(parts) > 0 {
contents = append(contents, genai.NewContentFromParts(parts, role))
}
}
}
contents := googleChatContents(messages)
// Generate content (non-streaming)
response, err := client.Models.GenerateContent(ctx, modelName, contents, nil)
response, err := client.Models.GenerateContent(ctx, modelName, contents, googleGenerateContentConfig(chatModelConfig))
if err != nil {
return nil, err
}
@@ -188,7 +382,7 @@ func (g *GoogleModel) ChatWithMessages(ctx context.Context, modelName string, me
// Extract text from response
answer := response.Text()
return &ChatResponse{Answer: &answer}, nil
return &ChatResponse{Answer: &answer, ToolCalls: googleToolCalls(response.FunctionCalls())}, nil
}
// ChatStreamlyWithSender sends messages and streams response via sender function (best performance, no channel)
@@ -212,59 +406,21 @@ func (g *GoogleModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
return err
}
// Convert messages to Google SDK format
var contents []*genai.Content
for _, msg := range messages {
var role genai.Role
switch msg.Role {
case "user":
role = genai.RoleUser
case "model", "assistant":
role = genai.RoleModel
default:
role = genai.RoleUser
}
// Handle content based on type
switch c := msg.Content.(type) {
case string:
contents = append(contents, genai.NewContentFromText(c, role))
case []interface{}:
// Multimodal content - group parts within a single content
var parts []*genai.Part
for _, item := range c {
if itemMap, ok := item.(map[string]interface{}); ok {
contentType, _ := itemMap["type"].(string)
switch contentType {
case "text":
if text, ok := itemMap["text"].(string); ok {
parts = append(parts, genai.NewPartFromText(text))
}
case "image_url":
if imgMap, ok := itemMap["image_url"].(map[string]interface{}); ok {
if url, ok := imgMap["url"].(string); ok {
parts = append(parts, genai.NewPartFromURI(url, "image/jpeg"))
}
}
}
}
}
if len(parts) > 0 {
contents = append(contents, genai.NewContentFromParts(parts, role))
}
}
}
contents := googleChatContents(messages)
var toolCalls []map[string]interface{}
for response, err := range client.Models.GenerateContentStream(
ctx,
modelName,
contents,
nil,
googleGenerateContentConfig(chatModelConfig),
) {
if err != nil {
return err
}
toolCalls = append(toolCalls, googleToolCalls(response.FunctionCalls())...)
content := response.Text()
var responseContent string
@@ -291,6 +447,10 @@ func (g *GoogleModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
}
}
if chatModelConfig != nil && len(toolCalls) > 0 {
chatModelConfig.ToolCallsResult = &toolCalls
}
return err
}

View File

@@ -2,6 +2,7 @@ package models
import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"
@@ -391,6 +392,103 @@ func TestCollectGoogleModelNamesReturnsPageError(t *testing.T) {
}
}
func TestGoogleGenerateContentConfigConvertsTools(t *testing.T) {
toolChoice := "required"
cfg := googleGenerateContentConfig(&ChatConfig{
Tools: []map[string]interface{}{{
"type": "function",
"function": map[string]interface{}{
"name": "search_my_dataset",
"description": "Search dataset.",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{"type": "string"},
},
"required": []string{"query"},
},
},
}},
ToolChoice: &toolChoice,
})
if cfg == nil || len(cfg.Tools) != 1 || len(cfg.Tools[0].FunctionDeclarations) != 1 {
t.Fatalf("tools = %#v, want one function declaration", cfg)
}
declaration := cfg.Tools[0].FunctionDeclarations[0]
if declaration.Name != "search_my_dataset" || declaration.Description != "Search dataset." {
t.Fatalf("declaration = %#v", declaration)
}
if declaration.ParametersJsonSchema == nil {
t.Fatal("ParametersJsonSchema is nil")
}
if cfg.ToolConfig == nil || cfg.ToolConfig.FunctionCallingConfig == nil {
t.Fatalf("ToolConfig = %#v", cfg.ToolConfig)
}
if cfg.ToolConfig.FunctionCallingConfig.Mode != genai.FunctionCallingConfigModeAny {
t.Fatalf("mode = %s, want ANY", cfg.ToolConfig.FunctionCallingConfig.Mode)
}
}
func TestGoogleChatContentsConvertsToolHistory(t *testing.T) {
contents := googleChatContents([]Message{
{
Role: "assistant",
Content: nil,
ToolCalls: []map[string]interface{}{{
"id": "call-1",
"type": "function",
"function": map[string]interface{}{
"name": "search_my_dataset",
"arguments": `{"query":"marigold"}`,
},
}},
},
{Role: "tool", ToolCallID: "call-1", Content: "flower result"},
})
if len(contents) != 2 {
t.Fatalf("contents len = %d, want 2", len(contents))
}
functionCall := contents[0].Parts[0].FunctionCall
if functionCall == nil || functionCall.ID != "call-1" || functionCall.Name != "search_my_dataset" {
t.Fatalf("function call = %#v", functionCall)
}
if functionCall.Args["query"] != "marigold" {
t.Fatalf("args = %#v", functionCall.Args)
}
functionResponse := contents[1].Parts[0].FunctionResponse
if functionResponse == nil || functionResponse.ID != "call-1" || functionResponse.Name != "search_my_dataset" {
t.Fatalf("function response = %#v", functionResponse)
}
if functionResponse.Response["output"] != "flower result" {
t.Fatalf("response = %#v", functionResponse.Response)
}
}
func TestGoogleToolCallsConvertsFunctionCalls(t *testing.T) {
toolCalls := googleToolCalls([]*genai.FunctionCall{{
ID: "call-1",
Name: "search_my_dataset",
Args: map[string]any{"query": "marigold"},
}})
if len(toolCalls) != 1 {
t.Fatalf("tool calls len = %d, want 1", len(toolCalls))
}
if toolCalls[0]["id"] != "call-1" || toolCalls[0]["type"] != "function" {
t.Fatalf("tool call = %#v", toolCalls[0])
}
function, _ := toolCalls[0]["function"].(map[string]interface{})
if function["name"] != "search_my_dataset" {
t.Fatalf("function = %#v", function)
}
var args map[string]interface{}
if err := json.Unmarshal([]byte(function["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if args["query"] != "marigold" {
t.Fatalf("arguments = %#v", args)
}
}
func stringPtr(value string) *string {
return &value
}

View File

@@ -120,7 +120,8 @@ func (g *GPUStackModel) ChatWithMessages(ctx context.Context, modelName string,
return nil, fmt.Errorf("invalid message format")
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -132,6 +133,7 @@ func (g *GPUStackModel) ChatWithMessages(ctx context.Context, modelName string,
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -189,6 +191,7 @@ func (g *GPUStackModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
if apiErr, ok := event["error"]; ok {
return fmt.Errorf("gpustack: upstream stream error: %v", apiErr)
@@ -207,6 +210,8 @@ func (g *GPUStackModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if r, ok := delta["reasoning_content"].(string); ok && r != "" {
if err := sender(nil, &r); err != nil {
return err
@@ -229,6 +234,7 @@ func (g *GPUStackModel) ChatStreamlyWithSender(ctx context.Context, modelName st
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("gpustack: stream ended before [DONE] or finish_reason")
}

View File

@@ -216,30 +216,51 @@ func (g *GroqModel) ChatStreamlyWithSender(ctx context.Context, modelName string
}
sawTerminal := false
done, err := ParseSSEStream[groqChatResponse](resp.Body, func(event groqChatResponse) error {
if event.Error != nil {
return fmt.Errorf("groq: upstream stream error: %v", event.Error)
accumulatedToolCalls := make(map[int]map[string]interface{})
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
tokenUsage, found, usageErr := decodeOpenAICompatibleStreamUsage(event)
if usageErr != nil {
return usageErr
}
if len(event.Choices) == 0 {
if found {
applyStreamUsage(chatModelConfig, modelUsage, tokenUsage)
}
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil
}
choice := event.Choices[0]
reasoning := choice.Delta.ReasoningContent
if reasoning == "" {
reasoning = choice.Delta.Reasoning
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
return nil
}
if reasoning != "" {
if err := sender(nil, &reasoning); err != nil {
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
return err
}
}
if choice.Delta.Content != "" {
if err := sender(&choice.Delta.Content, nil); err != nil {
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
return err
}
}
if choice.FinishReason != "" || event.FinishReason != "" {
finishReason, ok := firstChoice["finish_reason"].(string)
if ok && finishReason != "" {
sawTerminal = true
}
return nil
@@ -248,9 +269,10 @@ func (g *GroqModel) ChatStreamlyWithSender(ctx context.Context, modelName string
return fmt.Errorf("failed to scan response body: %w", err)
}
if !done && !sawTerminal {
return fmt.Errorf("groq: stream ended before [DONE] or finish_reason")
return fmt.Errorf("deepseek: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
return sender(&endOfStream, nil)
}

View File

@@ -133,7 +133,8 @@ func (h *HuggingFaceModel) ChatWithMessages(ctx context.Context, modelName strin
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -152,6 +153,7 @@ func (h *HuggingFaceModel) ChatWithMessages(ctx context.Context, modelName strin
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -212,6 +214,7 @@ func (h *HuggingFaceModel) ChatStreamlyWithSender(ctx context.Context, modelName
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -230,6 +233,8 @@ func (h *HuggingFaceModel) ChatStreamlyWithSender(ctx context.Context, modelName
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -248,6 +253,7 @@ func (h *HuggingFaceModel) ChatStreamlyWithSender(ctx context.Context, modelName
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -117,7 +117,8 @@ func (h *HunyuanModel) ChatWithMessages(ctx context.Context, modelName string, m
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -129,6 +130,7 @@ func (h *HunyuanModel) ChatWithMessages(ctx context.Context, modelName string, m
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -180,6 +182,7 @@ func (h *HunyuanModel) ChatStreamlyWithSender(ctx context.Context, modelName str
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
if apiErr, ok := event["error"]; ok {
return fmt.Errorf("hunyuan: upstream stream error: %v", apiErr)
@@ -194,6 +197,7 @@ func (h *HunyuanModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return nil
}
if delta, ok := firstChoice["delta"].(map[string]interface{}); ok {
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if r, ok := delta["reasoning_content"].(string); ok && r != "" {
rr := r
if err := sender(nil, &rr); err != nil {
@@ -215,6 +219,7 @@ func (h *HunyuanModel) ChatStreamlyWithSender(ctx context.Context, modelName str
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("hunyuan: stream ended before [DONE] or finish_reason")
}

View File

@@ -143,7 +143,8 @@ func (j *JieKouAIModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -162,6 +163,7 @@ func (j *JieKouAIModel) ChatWithMessages(ctx context.Context, modelName string,
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -225,6 +227,7 @@ func (j *JieKouAIModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}
// SSE parsing: read line by line
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -241,6 +244,8 @@ func (j *JieKouAIModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -259,6 +264,7 @@ func (j *JieKouAIModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -146,7 +146,8 @@ func (l *LmStudioModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -164,6 +165,7 @@ func (l *LmStudioModel) ChatWithMessages(ctx context.Context, modelName string,
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -233,6 +235,7 @@ func (l *LmStudioModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}
// SSE parsing: read line by line
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -251,6 +254,8 @@ func (l *LmStudioModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -269,6 +274,7 @@ func (l *LmStudioModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -154,7 +154,8 @@ func (l *LocalAIModel) ChatWithMessages(ctx context.Context, modelName string, m
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -165,6 +166,7 @@ func (l *LocalAIModel) ChatWithMessages(ctx context.Context, modelName string, m
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -248,6 +250,7 @@ func (l *LocalAIModel) ChatStreamlyWithSender(ctx context.Context, modelName str
}()
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
streamDone, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
lastActiveMu.Lock()
lastActive = time.Now()
@@ -268,6 +271,8 @@ func (l *LocalAIModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if reasoning := extractLocalAIReasoning(delta); reasoning != "" {
if err := sender(nil, &reasoning); err != nil {
return err
@@ -293,6 +298,7 @@ func (l *LocalAIModel) ChatStreamlyWithSender(ctx context.Context, modelName str
}
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !streamDone && !sawTerminal {
return fmt.Errorf("localai: stream ended before [DONE] or finish_reason")
}

View File

@@ -123,7 +123,8 @@ func (l *LongCatModel) ChatWithMessages(ctx context.Context, modelName string, m
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -140,6 +141,7 @@ func (l *LongCatModel) ChatWithMessages(ctx context.Context, modelName string, m
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -200,6 +202,7 @@ func (l *LongCatModel) ChatStreamlyWithSender(ctx context.Context, modelName str
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
if apiErr, ok := event["error"]; ok {
return fmt.Errorf("longcat: upstream stream error: %v", apiErr)
@@ -220,6 +223,8 @@ func (l *LongCatModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if r, ok := delta["reasoning_content"].(string); ok && r != "" {
if err := sender(nil, &r); err != nil {
return err
@@ -242,6 +247,7 @@ func (l *LongCatModel) ChatStreamlyWithSender(ctx context.Context, modelName str
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("longcat: stream ended before [DONE] or finish_reason")
}

View File

@@ -127,10 +127,12 @@ func (m *MistralModel) ChatWithMessages(ctx context.Context, modelName string, m
if err != nil {
return nil, err
}
toolCalls := extractToolCalls(messageMap)
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -226,6 +228,7 @@ func (m *MistralModel) ChatStreamlyWithSender(ctx context.Context, modelName str
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -242,6 +245,8 @@ func (m *MistralModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
@@ -258,6 +263,7 @@ func (m *MistralModel) ChatStreamlyWithSender(ctx context.Context, modelName str
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("mistral: stream ended before [DONE] or finish_reason")
}

View File

@@ -252,6 +252,7 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(ctx context.Context, modelName
}()
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
streamDone, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
lastActiveMu.Lock()
lastActive = time.Now()
@@ -267,6 +268,7 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(ctx context.Context, modelName
}
if delta, ok := firstChoice["delta"].(map[string]interface{}); ok {
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if reasoning := modelscopeReasoningFromMap(delta); reasoning != "" {
if err := sender(nil, &reasoning); err != nil {
return err
@@ -294,6 +296,8 @@ func (m *ModelScopeModel) ChatStreamlyWithSender(ctx context.Context, modelName
return fmt.Errorf("modelscope: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
return sender(&endOfStream, nil)
}

View File

@@ -233,24 +233,51 @@ func (n *N1NModel) ChatStreamlyWithSender(ctx context.Context, modelName string,
}
sawTerminal := false
done, err := ParseSSEStream[n1nChatResponse](resp.Body, func(event n1nChatResponse) error {
if len(event.Choices) == 0 {
accumulatedToolCalls := make(map[int]map[string]interface{})
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
tokenUsage, found, usageErr := decodeOpenAICompatibleStreamUsage(event)
if usageErr != nil {
return usageErr
}
if found {
applyStreamUsage(chatModelConfig, modelUsage, tokenUsage)
}
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
return nil
}
choice := event.Choices[0]
if choice.Delta.ReasoningContent != "" {
r := choice.Delta.ReasoningContent
if err := sender(nil, &r); err != nil {
firstChoice, ok := choices[0].(map[string]interface{})
if !ok {
return nil
}
delta, ok := firstChoice["delta"].(map[string]interface{})
if !ok {
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
return err
}
}
if choice.Delta.Content != "" {
c := choice.Delta.Content
if err := sender(&c, nil); err != nil {
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
return err
}
}
if choice.FinishReason != "" {
finishReason, ok := firstChoice["finish_reason"].(string)
if ok && finishReason != "" {
sawTerminal = true
}
return nil
@@ -259,9 +286,11 @@ func (n *N1NModel) ChatStreamlyWithSender(ctx context.Context, modelName string,
return fmt.Errorf("failed to scan response body: %w", err)
}
if !done && !sawTerminal {
return fmt.Errorf("n1n: stream ended before [DONE] or finish_reason")
return fmt.Errorf("deepseek: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
if err := sender(&endOfStream, nil); err != nil {
return err

View File

@@ -256,7 +256,8 @@ func (n *NovitaModel) ChatWithMessages(ctx context.Context, modelName string, me
}
rawContent, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -269,18 +270,22 @@ func (n *NovitaModel) ChatWithMessages(ctx context.Context, modelName string, me
// `reasoning_content` field, with `content` already cleaned.
// Handle both so the visible Answer is always tag-free and any
// reasoning the upstream supplied is preserved.
visible, reasoning := splitNovitaThink(rawContent)
if r, ok := messageMap["reasoning_content"].(string); ok && r != "" {
if reasoning != "" {
reasoning += "\n" + r
} else {
reasoning = r
visible, reasoning := "", ""
if ok {
visible, reasoning = splitNovitaThink(rawContent)
if r, ok := messageMap["reasoning_content"].(string); ok && r != "" {
if reasoning != "" {
reasoning += "\n" + r
} else {
reasoning = r
}
}
}
return &ChatResponse{
Answer: &visible,
ReasonContent: &reasoning,
ToolCalls: toolCalls,
}, nil
}

View File

@@ -133,7 +133,8 @@ func (n *NvidiaModel) ChatWithMessages(ctx context.Context, modelName string, me
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -151,6 +152,7 @@ func (n *NvidiaModel) ChatWithMessages(ctx context.Context, modelName string, me
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -215,6 +217,7 @@ func (n *NvidiaModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -231,6 +234,8 @@ func (n *NvidiaModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -250,6 +255,8 @@ func (n *NvidiaModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
if err = sender(&endOfStream, nil); err != nil {
return err

View File

@@ -130,7 +130,8 @@ func (o *OpenRouterModel) ChatWithMessages(ctx context.Context, modelName string
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("no message in response")
}
@@ -148,6 +149,7 @@ func (o *OpenRouterModel) ChatWithMessages(ctx context.Context, modelName string
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -210,6 +212,7 @@ func (o *OpenRouterModel) ChatStreamlyWithSender(ctx context.Context, modelName
return fmt.Errorf("invalid status code: %d, body: %s", resp.StatusCode, string(body))
}
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -228,6 +231,8 @@ func (o *OpenRouterModel) ChatStreamlyWithSender(ctx context.Context, modelName
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -246,6 +251,7 @@ func (o *OpenRouterModel) ChatStreamlyWithSender(ctx context.Context, modelName
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -188,6 +188,7 @@ func (o *OrcaRouterModel) ChatStreamlyWithSender(ctx context.Context, modelName
// SSE parsing: read line by line
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -206,6 +207,8 @@ func (o *OrcaRouterModel) ChatStreamlyWithSender(ctx context.Context, modelName
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
@@ -222,6 +225,7 @@ func (o *OrcaRouterModel) ChatStreamlyWithSender(ctx context.Context, modelName
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
_ = done
_ = sawTerminal

View File

@@ -122,7 +122,8 @@ func (s *StepFunModel) ChatWithMessages(ctx context.Context, modelName string, m
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -180,6 +181,7 @@ func (s *StepFunModel) ChatStreamlyWithSender(ctx context.Context, modelName str
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -196,6 +198,8 @@ func (s *StepFunModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
content, ok := delta["content"].(string)
if ok && content != "" {
if err := sender(&content, nil); err != nil {
@@ -212,6 +216,7 @@ func (s *StepFunModel) ChatStreamlyWithSender(ctx context.Context, modelName str
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("stepfun: stream ended before [DONE] or finish_reason")
}

View File

@@ -127,7 +127,8 @@ func (t *TokenHubModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -145,6 +146,7 @@ func (t *TokenHubModel) ChatWithMessages(ctx context.Context, modelName string,
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -199,6 +201,7 @@ func (t *TokenHubModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -215,6 +218,8 @@ func (t *TokenHubModel) ChatStreamlyWithSender(ctx context.Context, modelName st
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if !ok || reasoningContent == "" {
reasoningContent, _ = delta["reasoning"].(string)
@@ -237,6 +242,7 @@ func (t *TokenHubModel) ChatStreamlyWithSender(ctx context.Context, modelName st
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -118,7 +118,8 @@ func (t *TokenPonyModel) ChatWithMessages(ctx context.Context, modelName string,
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -130,6 +131,7 @@ func (t *TokenPonyModel) ChatWithMessages(ctx context.Context, modelName string,
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -181,6 +183,7 @@ func (t *TokenPonyModel) ChatStreamlyWithSender(ctx context.Context, modelName s
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
if apiErr, ok := event["error"]; ok {
return fmt.Errorf("tokenpony: upstream stream error: %v", apiErr)
@@ -196,6 +199,7 @@ func (t *TokenPonyModel) ChatStreamlyWithSender(ctx context.Context, modelName s
}
if delta, ok := firstChoice["delta"].(map[string]interface{}); ok {
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if r, ok := delta["reasoning_content"].(string); ok && r != "" {
rr := r
if err := sender(nil, &rr); err != nil {
@@ -217,6 +221,7 @@ func (t *TokenPonyModel) ChatStreamlyWithSender(ctx context.Context, modelName s
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("tokenpony: stream ended before [DONE] or finish_reason")
}

View File

@@ -127,7 +127,8 @@ func (u *UpstageModel) ChatWithMessages(ctx context.Context, modelName string, m
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -139,6 +140,7 @@ func (u *UpstageModel) ChatWithMessages(ctx context.Context, modelName string, m
return &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}, nil
}
@@ -199,6 +201,7 @@ func (u *UpstageModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -215,6 +218,8 @@ func (u *UpstageModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
if r, ok := delta["reasoning"].(string); ok && r != "" {
if err := sender(nil, &r); err != nil {
return err
@@ -241,6 +246,8 @@ func (u *UpstageModel) ChatStreamlyWithSender(ctx context.Context, modelName str
return fmt.Errorf("upstage: stream ended before [DONE] or finish_reason")
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
endOfStream := "[DONE]"
if err := sender(&endOfStream, nil); err != nil {
return err

View File

@@ -146,7 +146,8 @@ func (v *VllmModel) ChatWithMessages(ctx context.Context, modelName string, mess
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -164,6 +165,7 @@ func (v *VllmModel) ChatWithMessages(ctx context.Context, modelName string, mess
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -234,6 +236,7 @@ func (v *VllmModel) ChatStreamlyWithSender(ctx context.Context, modelName string
}
// SSE parsing: read line by line
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
common.Info(fmt.Sprintf("%v", event))
@@ -252,6 +255,8 @@ func (v *VllmModel) ChatStreamlyWithSender(ctx context.Context, modelName string
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -270,6 +275,7 @@ func (v *VllmModel) ChatStreamlyWithSender(ctx context.Context, modelName string
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"

View File

@@ -133,7 +133,8 @@ func (x *XAIModel) ChatWithMessages(ctx context.Context, modelName string, messa
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("invalid content format")
}
@@ -150,6 +151,7 @@ func (x *XAIModel) ChatWithMessages(ctx context.Context, modelName string, messa
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -201,6 +203,7 @@ func (x *XAIModel) ChatStreamlyWithSender(ctx context.Context, modelName string,
}
sawTerminal := false
accumulatedToolCalls := make(map[int]map[string]any)
done, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
choices, ok := event["choices"].([]interface{})
if !ok || len(choices) == 0 {
@@ -217,6 +220,8 @@ func (x *XAIModel) ChatStreamlyWithSender(ctx context.Context, modelName string,
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -240,6 +245,7 @@ func (x *XAIModel) ChatStreamlyWithSender(ctx context.Context, modelName string,
if err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(chatModelConfig, accumulatedToolCalls)
if !done && !sawTerminal {
return fmt.Errorf("xai: stream ended before [DONE] or finish_reason")
}

View File

@@ -127,7 +127,8 @@ func (x *XunFeiModel) ChatWithMessages(ctx context.Context, modelName string, me
}
content, ok := messageMap["content"].(string)
if !ok {
toolCalls := extractToolCalls(messageMap)
if !ok && len(toolCalls) == 0 {
return nil, fmt.Errorf("no message in response")
}
@@ -145,6 +146,7 @@ func (x *XunFeiModel) ChatWithMessages(ctx context.Context, modelName string, me
chatResponse := &ChatResponse{
Answer: &content,
ReasonContent: &reasonContent,
ToolCalls: toolCalls,
}
return chatResponse, nil
@@ -209,6 +211,7 @@ func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
}
// SSE parsing: read line by line
accumulatedToolCalls := make(map[int]map[string]any)
if _, err := ParseSSEStream[map[string]interface{}](resp.Body, func(event map[string]interface{}) error {
if data, marshalErr := json.Marshal(event); marshalErr == nil {
common.Info(string(data))
@@ -229,6 +232,8 @@ func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
return nil
}
accumulateToolCallDeltas(delta, accumulatedToolCalls)
reasoningContent, ok := delta["reasoning_content"].(string)
if ok && reasoningContent != "" {
if err := sender(nil, &reasoningContent); err != nil {
@@ -247,6 +252,7 @@ func (x *XunFeiModel) ChatStreamlyWithSender(ctx context.Context, modelName stri
}); err != nil {
return fmt.Errorf("failed to scan response body: %w", err)
}
setSortedToolCallsResult(modelConfig, accumulatedToolCalls)
// Send [DONE] marker for OpenAI compatibility
endOfStream := "[DONE]"