Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 2 additions & 126 deletions internal/providers/gemini/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import (
"strings"
"time"

"github.com/google/uuid"

"gomodel/internal/core"
"gomodel/internal/llmclient"
"gomodel/internal/providers"
Expand Down Expand Up @@ -198,134 +196,12 @@ func (p *Provider) ListModels(ctx context.Context) (*core.ModelsResponse, error)
}, nil
}

// convertResponsesRequestToChat converts a ResponsesRequest to ChatRequest for Gemini
func convertResponsesRequestToChat(req *core.ResponsesRequest) *core.ChatRequest {
chatReq := &core.ChatRequest{
Model: req.Model,
Messages: make([]core.Message, 0),
Temperature: req.Temperature,
Stream: req.Stream,
}

if req.MaxOutputTokens != nil {
chatReq.MaxTokens = req.MaxOutputTokens
}

// Add system instruction if provided
if req.Instructions != "" {
chatReq.Messages = append(chatReq.Messages, core.Message{
Role: "system",
Content: req.Instructions,
})
}

// Convert input to messages
switch input := req.Input.(type) {
case string:
chatReq.Messages = append(chatReq.Messages, core.Message{
Role: "user",
Content: input,
})
case []interface{}:
for _, item := range input {
if msgMap, ok := item.(map[string]interface{}); ok {
role, _ := msgMap["role"].(string)
content := extractContentFromInput(msgMap["content"])
if role != "" && content != "" {
chatReq.Messages = append(chatReq.Messages, core.Message{
Role: role,
Content: content,
})
}
}
}
}

return chatReq
}

// extractContentFromInput extracts text content from responses input
func extractContentFromInput(content interface{}) string {
switch c := content.(type) {
case string:
return c
case []interface{}:
// Array of content parts - extract text
var texts []string
for _, part := range c {
if partMap, ok := part.(map[string]interface{}); ok {
if text, ok := partMap["text"].(string); ok {
texts = append(texts, text)
}
}
}
return strings.Join(texts, " ")
}
return ""
}

// convertChatResponseToResponses converts a ChatResponse to ResponsesResponse
func convertChatResponseToResponses(resp *core.ChatResponse) *core.ResponsesResponse {
content := ""
if len(resp.Choices) > 0 {
content = resp.Choices[0].Message.Content
}

return &core.ResponsesResponse{
ID: resp.ID,
Object: "response",
CreatedAt: resp.Created,
Model: resp.Model,
Provider: resp.Provider,
Status: "completed",
Output: []core.ResponsesOutputItem{
{
ID: "msg_" + uuid.New().String(),
Type: "message",
Role: "assistant",
Status: "completed",
Content: []core.ResponsesContentItem{
{
Type: "output_text",
Text: content,
Annotations: []string{},
},
},
},
},
Usage: &core.ResponsesUsage{
InputTokens: resp.Usage.PromptTokens,
OutputTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.TotalTokens,
},
}
}

// Responses sends a Responses API request to Gemini (converted to chat format)
func (p *Provider) Responses(ctx context.Context, req *core.ResponsesRequest) (*core.ResponsesResponse, error) {
// Convert ResponsesRequest to ChatRequest
chatReq := convertResponsesRequestToChat(req)

// Use the existing ChatCompletion method
chatResp, err := p.ChatCompletion(ctx, chatReq)
if err != nil {
return nil, err
}

return convertChatResponseToResponses(chatResp), nil
return providers.ResponsesViaChat(ctx, p, req)
}

// StreamResponses returns a raw response body for streaming Responses API (caller must close)
func (p *Provider) StreamResponses(ctx context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) {
// Convert ResponsesRequest to ChatRequest
chatReq := convertResponsesRequestToChat(req)

// Get the streaming response from chat completions
stream, err := p.StreamChatCompletion(ctx, chatReq)
if err != nil {
return nil, err
}

// Wrap the stream to convert chat completion format to Responses API format
return providers.NewOpenAIResponsesStreamConverter(stream, req.Model, "gemini"), nil
return providers.StreamResponsesViaChat(ctx, p, req, "gemini")
}
127 changes: 0 additions & 127 deletions internal/providers/gemini/gemini_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,130 +479,3 @@ data: [DONE]
t.Error("response should end with [DONE]")
}
}

func TestConvertResponsesRequestToChat(t *testing.T) {
temp := 0.7
maxTokens := 1024

tests := []struct {
name string
input *core.ResponsesRequest
checkFn func(*testing.T, *core.ChatRequest)
}{
{
name: "string input",
input: &core.ResponsesRequest{
Model: "gemini-2.0-flash",
Input: "Hello",
},
checkFn: func(t *testing.T, req *core.ChatRequest) {
if req.Model != "gemini-2.0-flash" {
t.Errorf("Model = %q, want %q", req.Model, "gemini-2.0-flash")
}
if len(req.Messages) != 1 {
t.Errorf("len(Messages) = %d, want 1", len(req.Messages))
}
if req.Messages[0].Role != "user" {
t.Errorf("Messages[0].Role = %q, want %q", req.Messages[0].Role, "user")
}
},
},
{
name: "with instructions",
input: &core.ResponsesRequest{
Model: "gemini-2.0-flash",
Input: "Hello",
Instructions: "Be helpful",
},
checkFn: func(t *testing.T, req *core.ChatRequest) {
if len(req.Messages) < 2 {
t.Fatalf("len(Messages) = %d, want at least 2", len(req.Messages))
}
if req.Messages[0].Role != "system" {
t.Errorf("Messages[0].Role = %q, want %q", req.Messages[0].Role, "system")
}
if req.Messages[0].Content != "Be helpful" {
t.Errorf("Messages[0].Content = %q, want %q", req.Messages[0].Content, "Be helpful")
}
},
},
{
name: "with parameters",
input: &core.ResponsesRequest{
Model: "gemini-2.0-flash",
Input: "Hello",
Temperature: &temp,
MaxOutputTokens: &maxTokens,
},
checkFn: func(t *testing.T, req *core.ChatRequest) {
if req.Temperature == nil || *req.Temperature != 0.7 {
t.Errorf("Temperature = %v, want 0.7", req.Temperature)
}
if req.MaxTokens == nil || *req.MaxTokens != 1024 {
t.Errorf("MaxTokens = %v, want 1024", req.MaxTokens)
}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := convertResponsesRequestToChat(tt.input)
tt.checkFn(t, result)
})
}
}

func TestConvertChatResponseToResponses(t *testing.T) {
resp := &core.ChatResponse{
ID: "gemini-123",
Object: "chat.completion",
Model: "gemini-2.0-flash",
Created: 1677652288,
Choices: []core.Choice{
{
Index: 0,
Message: core.Message{
Role: "assistant",
Content: "Hello! How can I help you today?",
},
FinishReason: "stop",
},
},
Usage: core.Usage{
PromptTokens: 10,
CompletionTokens: 20,
TotalTokens: 30,
},
}

result := convertChatResponseToResponses(resp)

if result.ID != "gemini-123" {
t.Errorf("ID = %q, want %q", result.ID, "gemini-123")
}
if result.Object != "response" {
t.Errorf("Object = %q, want %q", result.Object, "response")
}
if result.Model != "gemini-2.0-flash" {
t.Errorf("Model = %q, want %q", result.Model, "gemini-2.0-flash")
}
if result.Status != "completed" {
t.Errorf("Status = %q, want %q", result.Status, "completed")
}
if len(result.Output) != 1 {
t.Fatalf("len(Output) = %d, want 1", len(result.Output))
}
if result.Usage == nil {
t.Fatal("Usage should not be nil")
}
if result.Usage.InputTokens != 10 {
t.Errorf("InputTokens = %d, want 10", result.Usage.InputTokens)
}
if result.Usage.OutputTokens != 20 {
t.Errorf("OutputTokens = %d, want 20", result.Usage.OutputTokens)
}
if result.Usage.TotalTokens != 30 {
t.Errorf("TotalTokens = %d, want 30", result.Usage.TotalTokens)
}
}
Loading