mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4415220555 | |||
| a3a04d8a54 | |||
| 792e2b164b | |||
| 5859dcdc00 | |||
| 3c2b0f4dd0 |
@@ -54,19 +54,6 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
|
||||
|
||||
func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Content {
|
||||
var history []*genai.Content
|
||||
|
||||
// Add system message first
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.Text(g.providerOptions.systemMessage)},
|
||||
Role: "user",
|
||||
})
|
||||
|
||||
// Add a system response to acknowledge the system message
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.Text("I'll help you with that.")},
|
||||
Role: "model",
|
||||
})
|
||||
|
||||
for _, msg := range messages {
|
||||
switch msg.Role {
|
||||
case message.User:
|
||||
@@ -132,7 +119,8 @@ func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Cont
|
||||
}
|
||||
|
||||
func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool {
|
||||
geminiTools := make([]*genai.Tool, 0, len(tools))
|
||||
geminiTool := &genai.Tool{}
|
||||
geminiTool.FunctionDeclarations = make([]*genai.FunctionDeclaration, 0, len(tools))
|
||||
|
||||
for _, tool := range tools {
|
||||
info := tool.Info()
|
||||
@@ -146,23 +134,18 @@ func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool {
|
||||
},
|
||||
}
|
||||
|
||||
geminiTools = append(geminiTools, &genai.Tool{
|
||||
FunctionDeclarations: []*genai.FunctionDeclaration{declaration},
|
||||
})
|
||||
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, declaration)
|
||||
}
|
||||
|
||||
return geminiTools
|
||||
return []*genai.Tool{geminiTool}
|
||||
}
|
||||
|
||||
func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishReason {
|
||||
reasonStr := reason.String()
|
||||
switch {
|
||||
case reasonStr == "STOP":
|
||||
case reason == genai.FinishReasonStop:
|
||||
return message.FinishReasonEndTurn
|
||||
case reasonStr == "MAX_TOKENS":
|
||||
case reason == genai.FinishReasonMaxTokens:
|
||||
return message.FinishReasonMaxTokens
|
||||
case strings.Contains(reasonStr, "FUNCTION") || strings.Contains(reasonStr, "TOOL"):
|
||||
return message.FinishReasonToolUse
|
||||
default:
|
||||
return message.FinishReasonUnknown
|
||||
}
|
||||
@@ -171,7 +154,11 @@ func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishRea
|
||||
func (g *geminiClient) send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
|
||||
model := g.client.GenerativeModel(g.providerOptions.model.APIModel)
|
||||
model.SetMaxOutputTokens(int32(g.providerOptions.maxTokens))
|
||||
|
||||
model.SystemInstruction = &genai.Content{
|
||||
Parts: []genai.Part{
|
||||
genai.Text(g.providerOptions.systemMessage),
|
||||
},
|
||||
}
|
||||
// Convert tools
|
||||
if len(tools) > 0 {
|
||||
model.Tools = g.convertTools(tools)
|
||||
@@ -189,19 +176,13 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
attempts := 0
|
||||
for {
|
||||
attempts++
|
||||
var toolCalls []message.ToolCall
|
||||
chat := model.StartChat()
|
||||
chat.History = geminiMessages[:len(geminiMessages)-1] // All but last message
|
||||
|
||||
lastMsg := geminiMessages[len(geminiMessages)-1]
|
||||
var lastText string
|
||||
for _, part := range lastMsg.Parts {
|
||||
if text, ok := part.(genai.Text); ok {
|
||||
lastText = string(text)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := chat.SendMessage(ctx, genai.Text(lastText))
|
||||
resp, err := chat.SendMessage(ctx, lastMsg.Parts...)
|
||||
// If there is an error we are going to see if we can retry the call
|
||||
if err != nil {
|
||||
retry, after, retryErr := g.shouldRetry(attempts, err)
|
||||
@@ -221,7 +202,6 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
}
|
||||
|
||||
content := ""
|
||||
var toolCalls []message.ToolCall
|
||||
|
||||
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
@@ -232,20 +212,28 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
toolCalls = append(toolCalls, message.ToolCall{
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
Finished: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
finishReason := message.FinishReasonEndTurn
|
||||
if len(resp.Candidates) > 0 {
|
||||
finishReason = g.finishReason(resp.Candidates[0].FinishReason)
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
finishReason = message.FinishReasonToolUse
|
||||
}
|
||||
|
||||
return &ProviderResponse{
|
||||
Content: content,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: g.usage(resp),
|
||||
FinishReason: g.finishReason(resp.Candidates[0].FinishReason),
|
||||
FinishReason: finishReason,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -253,7 +241,11 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
func (g *geminiClient) stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
|
||||
model := g.client.GenerativeModel(g.providerOptions.model.APIModel)
|
||||
model.SetMaxOutputTokens(int32(g.providerOptions.maxTokens))
|
||||
|
||||
model.SystemInstruction = &genai.Content{
|
||||
Parts: []genai.Part{
|
||||
genai.Text(g.providerOptions.systemMessage),
|
||||
},
|
||||
}
|
||||
// Convert tools
|
||||
if len(tools) > 0 {
|
||||
model.Tools = g.convertTools(tools)
|
||||
@@ -277,18 +269,10 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
for {
|
||||
attempts++
|
||||
chat := model.StartChat()
|
||||
chat.History = geminiMessages[:len(geminiMessages)-1] // All but last message
|
||||
|
||||
chat.History = geminiMessages[:len(geminiMessages)-1]
|
||||
lastMsg := geminiMessages[len(geminiMessages)-1]
|
||||
var lastText string
|
||||
for _, part := range lastMsg.Parts {
|
||||
if text, ok := part.(genai.Text); ok {
|
||||
lastText = string(text)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
iter := chat.SendMessageStream(ctx, genai.Text(lastText))
|
||||
iter := chat.SendMessageStream(ctx, lastMsg.Parts...)
|
||||
|
||||
currentContent := ""
|
||||
toolCalls := []message.ToolCall{}
|
||||
@@ -331,23 +315,23 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
switch p := part.(type) {
|
||||
case genai.Text:
|
||||
newText := string(p)
|
||||
delta := newText[len(currentContent):]
|
||||
delta := string(p)
|
||||
if delta != "" {
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventContentDelta,
|
||||
Content: delta,
|
||||
}
|
||||
currentContent = newText
|
||||
currentContent += delta
|
||||
}
|
||||
case genai.FunctionCall:
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
newCall := message.ToolCall{
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
Finished: true,
|
||||
}
|
||||
|
||||
isNew := true
|
||||
@@ -369,37 +353,26 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
eventChan <- ProviderEvent{Type: EventContentStop}
|
||||
|
||||
if finalResp != nil {
|
||||
|
||||
finishReason := message.FinishReasonEndTurn
|
||||
if len(finalResp.Candidates) > 0 {
|
||||
finishReason = g.finishReason(finalResp.Candidates[0].FinishReason)
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
finishReason = message.FinishReasonToolUse
|
||||
}
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventComplete,
|
||||
Response: &ProviderResponse{
|
||||
Content: currentContent,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: g.usage(finalResp),
|
||||
FinishReason: g.finishReason(finalResp.Candidates[0].FinishReason),
|
||||
FinishReason: finishReason,
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we get here, we need to retry
|
||||
if attempts > maxRetries {
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventError,
|
||||
Error: fmt.Errorf("maximum retry attempts reached: %d retries", maxRetries),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
eventChan <- ProviderEvent{Type: EventError, Error: ctx.Err()}
|
||||
}
|
||||
return
|
||||
case <-time.After(time.Duration(2000*(1<<(attempts-1))) * time.Millisecond):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -132,14 +134,73 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
|
||||
}
|
||||
|
||||
func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
|
||||
if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
|
||||
if !strings.HasSuffix(searchPath, "/") {
|
||||
searchPath += "/"
|
||||
}
|
||||
pattern = searchPath + pattern
|
||||
matches, err := globWithRipgrep(pattern, searchPath, limit)
|
||||
if err == nil {
|
||||
return matches, len(matches) >= limit, nil
|
||||
}
|
||||
|
||||
fsys := os.DirFS("/")
|
||||
return globWithDoublestar(pattern, searchPath, limit)
|
||||
}
|
||||
|
||||
func globWithRipgrep(
|
||||
pattern, searchRoot string,
|
||||
limit int,
|
||||
) ([]string, error) {
|
||||
if searchRoot == "" {
|
||||
searchRoot = "."
|
||||
}
|
||||
|
||||
rgBin, err := exec.LookPath("rg")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ripgrep not found in $PATH: %w", err)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(pattern) && !strings.HasPrefix(pattern, "/") {
|
||||
pattern = "/" + pattern
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--files",
|
||||
"--null",
|
||||
"--glob", pattern,
|
||||
"-L",
|
||||
}
|
||||
|
||||
cmd := exec.Command(rgBin, args...)
|
||||
cmd.Dir = searchRoot
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("ripgrep: %w\n%s", err, out)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
for _, p := range bytes.Split(out, []byte{0}) {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
abs := filepath.Join(searchRoot, string(p))
|
||||
if skipHidden(abs) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, abs)
|
||||
}
|
||||
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return len(matches[i]) < len(matches[j])
|
||||
})
|
||||
|
||||
if len(matches) > limit {
|
||||
matches = matches[:limit]
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func globWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
|
||||
fsys := os.DirFS(searchPath)
|
||||
|
||||
relPattern := strings.TrimPrefix(pattern, "/")
|
||||
|
||||
@@ -158,7 +219,11 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
|
||||
return nil // Skip files we can't access
|
||||
}
|
||||
|
||||
absPath := "/" + path // Restore absolute path
|
||||
absPath := path // Restore absolute path
|
||||
if !strings.HasPrefix(absPath, searchPath) {
|
||||
absPath = filepath.Join(searchPath, absPath)
|
||||
}
|
||||
|
||||
matches = append(matches, fileInfo{
|
||||
path: absPath,
|
||||
modTime: info.ModTime(),
|
||||
|
||||
Reference in New Issue
Block a user