feat: Add Gemini adapter, improve API key fallback for Gemini AI Studio compatibility, and enhance OpenAI tool call streaming.

This commit is contained in:
CJACK
2026-02-22 01:26:08 +08:00
parent 7a4e994f3a
commit 920767f486
16 changed files with 1041 additions and 21 deletions

View File

@@ -44,12 +44,11 @@ func BuildChatCompletion(completionID, model, finalPrompt, finalThinking, finalT
}
func BuildResponseObject(responseID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
// Responses output should only be treated as tool calls when the model
// produced a standalone structured payload. This prevents accidental
// empty output_text on normal prose that merely contains tool_call-like text.
detected := util.ParseStandaloneToolCalls(finalText, toolNames)
// Align responses tool-call semantics with chat/completions:
// mixed prose + tool_call payloads should still be interpreted as tool calls.
detected := util.ParseToolCalls(finalText, toolNames)
if len(detected) == 0 && strings.TrimSpace(finalThinking) != "" {
detected = util.ParseStandaloneToolCalls(finalThinking, toolNames)
detected = util.ParseToolCalls(finalThinking, toolNames)
}
exposedOutputText := finalText
output := make([]any, 0, 2)

View File

@@ -70,7 +70,7 @@ func TestBuildResponseObjectToolCallsFollowChatShape(t *testing.T) {
}
}
func TestBuildResponseObjectKeepsOutputTextForMixedProse(t *testing.T) {
func TestBuildResponseObjectTreatsMixedProseToolPayloadAsToolCall(t *testing.T) {
obj := BuildResponseObject(
"resp_test",
"gpt-4o",
@@ -81,17 +81,41 @@ func TestBuildResponseObjectKeepsOutputTextForMixedProse(t *testing.T) {
)
outputText, _ := obj["output_text"].(string)
if outputText == "" {
t.Fatalf("expected output_text to be preserved for mixed prose")
if outputText != "" {
t.Fatalf("expected output_text hidden once tool calls are detected, got %q", outputText)
}
output, _ := obj["output"].([]any)
if len(output) != 2 {
t.Fatalf("expected function_call + tool_calls wrapper, got %#v", obj["output"])
}
first, _ := output[0].(map[string]any)
if first["type"] != "function_call" {
t.Fatalf("expected first output type function_call, got %#v", first["type"])
}
}
func TestBuildResponseObjectFencedToolPayloadRemainsText(t *testing.T) {
obj := BuildResponseObject(
"resp_test",
"gpt-4o",
"prompt",
"",
"```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"golang\"}}]}\n```",
[]string{"search"},
)
outputText, _ := obj["output_text"].(string)
if outputText == "" {
t.Fatalf("expected output_text preserved for fenced example")
}
output, _ := obj["output"].([]any)
if len(output) != 1 {
t.Fatalf("expected one output item, got %#v", obj["output"])
t.Fatalf("expected one message output item, got %#v", obj["output"])
}
first, _ := output[0].(map[string]any)
if first["type"] != "message" {
t.Fatalf("expected output type message, got %#v", first["type"])
t.Fatalf("expected message output type, got %#v", first["type"])
}
}