Skip to content

feat: mcp support, openai update, refactor #486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
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
54 changes: 32 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,40 +132,50 @@ Check the [`./features.md`](./features.md) for more details.

## Usage

- `-m`, `--model`: Specify Large Language Model to use.
- `-f`, `--format`: Ask the LLM to format the response in a given format.
- `--format-as`: Specify the format for the output (used with `--format`).
- `-P`, `--prompt` Include the prompt from the arguments and stdin, truncate stdin to specified number of lines.
- `-p`, `--prompt-args`: Include the prompt from the arguments in the response.
- `-q`, `--quiet`: Only output errors to standard err.
- `-r`, `--raw`: Print raw response without syntax highlighting.
- `--settings`: Open settings.
- `-x`, `--http-proxy`: Use HTTP proxy to connect to the API endpoints.
- `--max-retries`: Maximum number of retries.
- `--max-tokens`: Specify maximum tokens with which to respond.
- `--no-limit`: Do not limit the response tokens.
- `--role`: Specify the role to use (See [custom roles](#custom-roles)).
- `-m`, `--model`: Specify Large Language Model to use
- `-M`, `--ask-model`: Ask which model to use via interactive prompt
- `-f`, `--format`: Ask the LLM to format the response in a given format
- `--format-as`: Specify the format for the output (used with `--format`)
- `-P`, `--prompt` Include the prompt from the arguments and stdin, truncate stdin to specified number of lines
- `-p`, `--prompt-args`: Include the prompt from the arguments in the response
- `-q`, `--quiet`: Only output errors to standard err
- `-r`, `--raw`: Print raw response without syntax highlighting
- `--settings`: Open settings
- `-x`, `--http-proxy`: Use HTTP proxy to connect to the API endpoints
- `--max-retries`: Maximum number of retries
- `--max-tokens`: Specify maximum tokens with which to respond
- `--no-limit`: Do not limit the response tokens
- `--role`: Specify the role to use (See [custom roles](#custom-roles))
- `--word-wrap`: Wrap output at width (defaults to 80)
- `--reset-settings`: Restore settings to default.
- `--reset-settings`: Restore settings to default
- `--theme`: Theme to use in the forms; valid choices are: `charm`, `catppuccin`, `dracula`, and `base16`
- `--status-text`: Text to show while generating

#### Conversations

- `-t`, `--title`: Set the title for the conversation.
- `-l`, `--list`: List saved conversations.
- `-c`, `--continue`: Continue from last response or specific title or SHA-1.
- `-C`, `--continue-last`: Continue the last conversation.
- `-s`, `--show`: Show saved conversation for the given title or SHA-1.
- `-S`, `--show-last`: Show previous conversation.
- `-s`, `--show`: Show saved conversation for the given title or SHA-1
- `-S`, `--show-last`: Show previous conversation
- `--delete-older-than=<duration>`: Deletes conversations older than given duration (`10d`, `1mo`).
- `--delete`: Deletes the saved conversations for the given titles or SHA-1s.
- `--no-cache`: Do not save conversations.
- `--delete`: Deletes the saved conversations for the given titles or SHA-1s
- `--no-cache`: Do not save conversations

#### MCP

- `--mcp-servers`: MCP Servers configurations
- `--mcp-disable`: Disable specific MCP servers
- `--mcp-list`: List all available MCP servers
- `--mcp-list-tools`: List all available tools from enabled MCP servers

#### Advanced

- `--fanciness`: Level of fanciness.
- `--temp`: Sampling temperature.
- `--topp`: Top P value.
- `--topk`: Top K value.
- `--fanciness`: Level of fanciness
- `--temp`: Sampling temperature
- `--topp`: Top P value
- `--topk`: Top K value

## Custom Roles

Expand Down
122 changes: 99 additions & 23 deletions anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/anthropics/anthropic-sdk-go/packages/ssestream"
openai "github.com/sashabaranov/go-openai"
"github.com/mark3labs/mcp-go/mcp"
"github.com/openai/openai-go"
)

// AnthropicClientConfig represents the configuration for the Anthropic API client.
Expand Down Expand Up @@ -40,7 +42,7 @@ func NewAnthropicClientWithConfig(config AnthropicClientConfig) *AnthropicClient
option.WithHTTPClient(config.HTTPClient),
}
if config.BaseURL != "" {
opts = append(opts, option.WithBaseURL(config.BaseURL))
opts = append(opts, option.WithBaseURL(strings.TrimSuffix(config.BaseURL, "/v1")))
}
client := anthropic.NewClient(opts...)
return &AnthropicClient{
Expand All @@ -54,11 +56,16 @@ func (c *AnthropicClient) CreateChatCompletionStream(
ctx context.Context,
request anthropic.MessageNewParams,
) *AnthropicChatCompletionStream {
return &AnthropicChatCompletionStream{
anthropicStreamReader: &anthropicStreamReader{
Stream: c.Messages.NewStreaming(ctx, request),
},
s := &AnthropicChatCompletionStream{
stream: c.Messages.NewStreaming(ctx, request),
request: request,
}

s.factory = func() *ssestream.Stream[anthropic.MessageStreamEventUnion] {
return c.Messages.NewStreaming(ctx, s.request)
}

return s
}

func makeAnthropicSystem(system string) []anthropic.TextBlockParam {
Expand All @@ -74,45 +81,114 @@ func makeAnthropicSystem(system string) []anthropic.TextBlockParam {

// AnthropicChatCompletionStream represents a stream for chat completion.
type AnthropicChatCompletionStream struct {
*anthropicStreamReader
}

type anthropicStreamReader struct {
*ssestream.Stream[anthropic.MessageStreamEventUnion]
stream *ssestream.Stream[anthropic.MessageStreamEventUnion]
request anthropic.MessageNewParams
factory func() *ssestream.Stream[anthropic.MessageStreamEventUnion]
message anthropic.Message
}

// Recv reads the next response from the stream.
func (r *anthropicStreamReader) Recv() (response openai.ChatCompletionStreamResponse, err error) {
if err := r.Err(); err != nil {
return openai.ChatCompletionStreamResponse{}, fmt.Errorf("anthropic: %w", err)
func (r *AnthropicChatCompletionStream) Recv() (response openai.ChatCompletionChunk, err error) {
if r.stream == nil {
r.stream = r.factory()
r.message = anthropic.Message{}
}
for r.Next() {
event := r.Current()

if r.stream.Next() {
event := r.stream.Current()
if err := r.message.Accumulate(event); err != nil {
return openai.ChatCompletionChunk{}, fmt.Errorf("anthropic: %w", err)
}
switch eventVariant := event.AsAny().(type) {
case anthropic.ContentBlockDeltaEvent:
switch deltaVariant := eventVariant.Delta.AsAny().(type) {
case anthropic.TextDelta:
return openai.ChatCompletionStreamResponse{
Choices: []openai.ChatCompletionStreamChoice{
return openai.ChatCompletionChunk{
Choices: []openai.ChatCompletionChunkChoice{
{
Index: 0,
Delta: openai.ChatCompletionStreamChoiceDelta{
Delta: openai.ChatCompletionChunkChoiceDelta{
Content: deltaVariant.Text,
Role: "assistant",
Role: roleAssistant,
},
},
},
}, nil
}
}
return openai.ChatCompletionChunk{}, errNoContent
}
if err := r.stream.Err(); err != nil {
return openai.ChatCompletionChunk{}, fmt.Errorf("anthropic: %w", err)
}
if err := r.stream.Close(); err != nil {
return openai.ChatCompletionChunk{}, fmt.Errorf("anthropic: %w", err)
}
r.request.Messages = append(r.request.Messages, r.message.ToParam())
r.stream = nil

toolResults := []anthropic.ContentBlockParamUnion{}
var sb strings.Builder
for _, block := range r.message.Content {
switch variant := block.AsAny().(type) {
case anthropic.ToolUseBlock:
content, err := toolCall(variant.Name, []byte(variant.JSON.Input.Raw()))
toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, content, err != nil))
_, _ = sb.WriteString("\n> Ran: `" + variant.Name + "`")
if err != nil {
_, _ = sb.WriteString(" (failed: `" + err.Error() + "`)")
}
_, _ = sb.WriteString("\n")
}
}
_, _ = sb.WriteString("\n")

if len(toolResults) == 0 {
return openai.ChatCompletionChunk{}, io.EOF
}
return openai.ChatCompletionStreamResponse{}, io.EOF

msg := anthropic.NewUserMessage(toolResults...)
r.request.Messages = append(r.request.Messages, msg)

return openai.ChatCompletionChunk{
Choices: []openai.ChatCompletionChunkChoice{
{
Index: 0,
Delta: openai.ChatCompletionChunkChoiceDelta{
Content: sb.String(),
Role: roleTool,
},
},
},
}, nil
}

// Close closes the stream.
func (r *anthropicStreamReader) Close() error {
if err := r.Stream.Close(); err != nil {
func (r *AnthropicChatCompletionStream) Close() error {
if r.stream == nil {
return nil
}
if err := r.stream.Close(); err != nil {
return fmt.Errorf("anthropic: %w", err)
}
r.stream = nil
return nil
}

func makeAnthropicMCPTools(mcps map[string][]mcp.Tool) []anthropic.ToolUnionParam {
var tools []anthropic.ToolUnionParam
for name, serverTools := range mcps {
for _, tool := range serverTools {
tools = append(tools, anthropic.ToolUnionParam{
OfTool: &anthropic.ToolParam{
InputSchema: anthropic.ToolInputSchemaParam{
Properties: tool.InputSchema.Properties,
},
Name: fmt.Sprintf("%s_%s", name, tool.Name),
Description: anthropic.String(tool.Description),
},
})
}
}
return tools
}
32 changes: 16 additions & 16 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"sync"
"time"

openai "github.com/sashabaranov/go-openai"
"github.com/openai/openai-go"
)

// CacheType represents the type of cache being used.
Expand Down Expand Up @@ -95,11 +95,11 @@
}

type convoCache struct {
cache *Cache[[]openai.ChatCompletionMessage]
cache *Cache[[]modsMessage]
}

func newCache(dir string) *convoCache {
cache, err := NewCache[[]openai.ChatCompletionMessage](dir, ConversationCache)
cache, err := NewCache[[]modsMessage](dir, ConversationCache)
if err != nil {
return nil
}
Expand All @@ -108,13 +108,13 @@
}
}

func (c *convoCache) read(id string, messages *[]openai.ChatCompletionMessage) error {
func (c *convoCache) read(id string, messages *[]modsMessage) error {
return c.cache.Read(id, func(r io.Reader) error {
return decode(r, messages)
})
}

func (c *convoCache) write(id string, messages *[]openai.ChatCompletionMessage) error {
func (c *convoCache) write(id string, messages *[]modsMessage) error {
return c.cache.Write(id, func(w io.Writer) error {
return encode(w, messages)
})
Expand All @@ -134,38 +134,38 @@

func (c *cachedCompletionStream) Close() error { return nil }

func (c *cachedCompletionStream) Recv() (openai.ChatCompletionStreamResponse, error) {
func (c *cachedCompletionStream) Recv() (openai.ChatCompletionChunk, error) {
c.m.Lock()
defer c.m.Unlock()

if c.read == len(c.messages) {
return openai.ChatCompletionStreamResponse{}, io.EOF
return openai.ChatCompletionChunk{}, io.EOF
}

msg := c.messages[c.read]
prefix := ""

switch msg.Role {
case openai.ChatMessageRoleSystem:
case "system":

Check failure on line 149 in cache.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

string `system` has 6 occurrences, but such constant `roleSystem` already exists (goconst)
prefix += "\n**System**: "
case openai.ChatMessageRoleUser:
case "user":

Check failure on line 151 in cache.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

string `user` has 6 occurrences, but such constant `roleUser` already exists (goconst)
prefix += "\n**Prompt**: "
case openai.ChatMessageRoleAssistant:
case "assistant":

Check failure on line 153 in cache.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

string `assistant` has 4 occurrences, but such constant `roleAssistant` already exists (goconst)
prefix += "\n**Assistant**: "
case openai.ChatMessageRoleFunction:
case "function":
prefix += "\n**Function**: "
case openai.ChatMessageRoleTool:
case "tool":

Check failure on line 157 in cache.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

string `tool` has 3 occurrences, but such constant `roleTool` already exists (goconst)
prefix += "\n**Tool**: "
}

c.read++

return openai.ChatCompletionStreamResponse{
Choices: []openai.ChatCompletionStreamChoice{
return openai.ChatCompletionChunk{
Choices: []openai.ChatCompletionChunkChoice{
{
Delta: openai.ChatCompletionStreamChoiceDelta{
Delta: openai.ChatCompletionChunkChoiceDelta{
Content: prefix + msg.Content + "\n",
Role: msg.Role,
Role: string(msg.Role),
},
},
},
Expand Down
Loading
Loading