From 054039ea9bfbafe6e6a89c7800e8b7ef9ea3df2b Mon Sep 17 00:00:00 2001 From: pdfinn Date: Fri, 23 Jan 2026 09:32:59 +0700 Subject: [PATCH] feat: Add CLI backend for Claude Max subscription Add support for using Claude Code CLI as an alternative backend, allowing users with Claude Max subscriptions to use llm9p without API tokens. New files: - internal/llm/backend.go: Backend interface for swappable LLM providers - internal/llm/cli_client.go: CLI-based client using `claude` command Changes: - Add -backend flag: 'api' (default) or 'cli' - Refactor llmfs to use Backend interface instead of concrete Client - Model names normalized for CLI (opus, sonnet, haiku) Usage: ./llm9p -backend cli # Uses Claude Max subscription ./llm9p -backend api # Uses Anthropic API (default) Limitations of CLI backend: - Token counting not available (always 0) - Streaming is simulated (full response as single chunk) - Uses short model names (opus, sonnet, haiku) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 22 ++- README.md | 29 ++- cmd/llm9p/main.go | 39 +++- internal/llm/backend.go | 41 +++++ internal/llm/cli_client.go | 368 +++++++++++++++++++++++++++++++++++++ internal/llmfs/ask.go | 4 +- internal/llmfs/context.go | 4 +- internal/llmfs/new.go | 4 +- internal/llmfs/root.go | 2 +- internal/llmfs/state.go | 8 +- internal/llmfs/stream.go | 8 +- internal/llmfs/tokens.go | 4 +- 12 files changed, 504 insertions(+), 29 deletions(-) create mode 100644 internal/llm/backend.go create mode 100644 internal/llm/cli_client.go diff --git a/CLAUDE.md b/CLAUDE.md index 47e77383332715a6ad035ceb44fde25838cdeaf4..7f29831093d762d817c167c03250dae4faa043b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,9 +10,12 @@ This guide is for Claude Code and developers working on the llm9p codebase. # Build go build -o llm9p ./cmd/llm9p -# Run +# Run with Anthropic API ANTHROPIC_API_KEY=sk-... ./llm9p -addr :5640 +# Run with Claude Max subscription (via CLI) +./llm9p -addr :5640 -backend cli + # Run with debug logging ANTHROPIC_API_KEY=sk-... ./llm9p -addr :5640 -debug ``` @@ -86,11 +89,13 @@ llm9p/ - No external dependencies (stdlib only) - `File` and `Dir` interfaces define the filesystem abstraction -2. **LLM Client (`internal/llm/client.go`)** - - Wraps Anthropic SDK +2. **LLM Client (`internal/llm/`)** + - `backend.go` - Backend interface for swappable LLM providers + - `client.go` - Anthropic API client (requires API key) + - `cli_client.go` - Claude Code CLI client (uses Max subscription) - Manages conversation state - Supports both sync and streaming responses - - Tracks token usage + - Tracks token usage (API only) 3. **LLM Filesystem (`internal/llmfs/`)** - Implements each file in the LLM filesystem @@ -322,6 +327,15 @@ The following scenarios have been tested and verified working: - [x] Short response ("Write a haiku") streams correctly - [x] Long response ("Count 1 to 20") streams all content +### CLI Backend (Claude Max subscription) +- [x] Server starts with `-backend cli` flag +- [x] Model returns "sonnet" (normalized name) +- [x] `echo "What is 2+2?" | 9p write ask` returns correct answer +- [x] Multi-turn conversation maintains context +- [x] Model switching works (haiku, sonnet, opus) +- [x] Conversation reset works via `new` file +- [x] System messages work via `context` file + ## Future Enhancements - [ ] Multiple conversation support (via subdirectories) diff --git a/README.md b/README.md index d617983b8e8feed5f5a86160e1cd40e01781a177..d7f5d61a5e604dd82b9b6474361df200d3fc2299 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,27 @@ go build -o llm9p ./cmd/llm9p ### Start the Server +**Option A: Using Anthropic API** (requires API key) + ```bash export ANTHROPIC_API_KEY=sk-ant-... ./llm9p -addr :5640 ``` +**Option B: Using Claude Max Subscription** (via Claude Code CLI) + +If you have a Claude Max subscription and the Claude Code CLI installed: + +```bash +./llm9p -addr :5640 -backend cli +``` + +This uses your Claude Max subscription instead of API tokens. No API key required. + +**Requirements for CLI backend:** +- Claude Code CLI installed and authenticated (`claude` command available) +- Active Claude Max subscription + ### Mount the Filesystem There are several ways to mount the filesystem depending on your environment. @@ -219,6 +235,7 @@ cat /mnt/llm/ask | Flag | Default | Description | |------|---------|-------------| | `-addr` | `:5640` | Address to listen on | +| `-backend` | `api` | Backend: `api` (Anthropic API) or `cli` (Claude Code CLI) | | `-debug` | `false` | Enable debug logging | ### Environment Variables @@ -229,10 +246,20 @@ cat /mnt/llm/ask ## Default Settings -- **Model**: `claude-sonnet-4-20250514` +- **Model**: `claude-sonnet-4-20250514` (API) or `sonnet` (CLI) - **Temperature**: `0.7` - **Max Tokens**: `4096` +### Backend Differences + +| Feature | API Backend | CLI Backend | +|---------|-------------|-------------| +| Authentication | API key required | Claude Max subscription | +| Token counting | Accurate | Not available (always 0) | +| Model names | Full names | Aliases (opus, sonnet, haiku) | +| Streaming | True streaming | Simulated (full response) | +| Rate limits | API limits apply | Subscription limits apply | + ## Requirements - Go 1.21+ diff --git a/cmd/llm9p/main.go b/cmd/llm9p/main.go index 62583d54cd8c7034d519524e9a7b859fa512ee7a..5431ba6be0f1656c91a7d59f313d9b6577da9637 100644 --- a/cmd/llm9p/main.go +++ b/cmd/llm9p/main.go @@ -4,6 +4,10 @@ // // ANTHROPIC_API_KEY=sk-... llm9p -addr :5640 // +// Or with Claude Max subscription (via Claude Code CLI): +// +// llm9p -addr :5640 -backend cli +// // Mount with: // // 9pfuse localhost:5640 /mnt/llm @@ -21,6 +25,7 @@ import ( "log" "net" "os" + "os/exec" "os/signal" "syscall" @@ -32,18 +37,38 @@ import ( func main() { addr := flag.String("addr", ":5640", "Address to listen on") debug := flag.Bool("debug", false, "Enable debug logging") + backend := flag.String("backend", "api", "Backend to use: 'api' (Anthropic API) or 'cli' (Claude Code CLI for Max subscription)") flag.Parse() - // Get API key from environment - apiKey := os.Getenv("ANTHROPIC_API_KEY") - if apiKey == "" { - fmt.Fprintln(os.Stderr, "Error: ANTHROPIC_API_KEY environment variable not set") + var client llm.Backend + + switch *backend { + case "cli": + // Check that claude CLI is available + if _, err := exec.LookPath("claude"); err != nil { + fmt.Fprintln(os.Stderr, "Error: 'claude' CLI not found in PATH") + fmt.Fprintln(os.Stderr, "Install Claude Code CLI or use -backend api with ANTHROPIC_API_KEY") + os.Exit(1) + } + client = llm.NewCLIClient() + log.Println("Using Claude Code CLI backend (Claude Max subscription)") + + case "api": + // Get API key from environment + apiKey := os.Getenv("ANTHROPIC_API_KEY") + if apiKey == "" { + fmt.Fprintln(os.Stderr, "Error: ANTHROPIC_API_KEY environment variable not set") + fmt.Fprintln(os.Stderr, "Set ANTHROPIC_API_KEY or use -backend cli for Claude Max subscription") + os.Exit(1) + } + client = llm.NewClient(apiKey) + log.Println("Using Anthropic API backend") + + default: + fmt.Fprintf(os.Stderr, "Error: unknown backend '%s' (use 'api' or 'cli')\n", *backend) os.Exit(1) } - // Create LLM client - client := llm.NewClient(apiKey) - // Create filesystem root := llmfs.NewRoot(client) diff --git a/internal/llm/backend.go b/internal/llm/backend.go new file mode 100644 index 0000000000000000000000000000000000000000..3df5776591e3e7957b8c4ab90256b901ce9fd6eb --- /dev/null +++ b/internal/llm/backend.go @@ -0,0 +1,41 @@ +// Package llm provides LLM backends for the 9P filesystem. +package llm + +import "context" + +// Backend defines the interface for LLM backends. +// Both API and CLI clients implement this interface. +type Backend interface { + // Model returns the current model name + Model() string + // SetModel sets the model for subsequent requests + SetModel(model string) + // Temperature returns the current temperature + Temperature() float64 + // SetTemperature sets the temperature (0.0-2.0) + SetTemperature(temp float64) error + // LastTokens returns token count from last response + LastTokens() int + // Messages returns conversation history + Messages() []Message + // MessagesJSON returns conversation history as JSON + MessagesJSON() ([]byte, error) + // AddSystemMessage adds a system message + AddSystemMessage(content string) + // Reset clears conversation history + Reset() + // Ask sends a prompt and returns the response (blocking) + Ask(ctx context.Context, prompt string) (string, error) + // StartStream begins streaming a response + StartStream(ctx context.Context, prompt string) error + // ReadStreamChunk reads the next streaming chunk + ReadStreamChunk() (string, bool) + // IsStreaming returns whether a stream is in progress + IsStreaming() bool + // WaitStream waits for stream to complete + WaitStream() +} + +// Verify that both clients implement Backend +var _ Backend = (*Client)(nil) +var _ Backend = (*CLIClient)(nil) diff --git a/internal/llm/cli_client.go b/internal/llm/cli_client.go new file mode 100644 index 0000000000000000000000000000000000000000..5748ad17eab5d46be22be511d194d006e2b138ba --- /dev/null +++ b/internal/llm/cli_client.go @@ -0,0 +1,368 @@ +// CLI backend for Claude Max subscription via Claude Code CLI. +package llm + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "os/exec" + "strings" + "sync" +) + +// CLIClient uses the Claude Code CLI for LLM requests. +// This allows using a Claude Max subscription instead of API tokens. +type CLIClient struct { + mu sync.RWMutex + model string + temperature float64 + messages []Message + lastTokens int + streaming bool + streamChan chan string + streamDone chan struct{} +} + +// cliResponse represents the JSON response from claude CLI +type cliResponse struct { + Type string `json:"type"` + Result string `json:"result"` +} + +// NewCLIClient creates a new CLI-based LLM client +func NewCLIClient() *CLIClient { + return &CLIClient{ + model: "sonnet", // CLI uses short model names + temperature: 0.7, + messages: make([]Message, 0), + } +} + +// normalizeModel converts full model names to CLI aliases +func normalizeModel(model string) string { + model = strings.ToLower(model) + switch { + case strings.Contains(model, "opus"): + return "opus" + case strings.Contains(model, "haiku"): + return "haiku" + default: + return "sonnet" + } +} + +// Model returns the current model name +func (c *CLIClient) Model() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.model +} + +// SetModel sets the model for subsequent requests +func (c *CLIClient) SetModel(model string) { + c.mu.Lock() + defer c.mu.Unlock() + c.model = normalizeModel(model) +} + +// Temperature returns the current temperature +func (c *CLIClient) Temperature() float64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.temperature +} + +// SetTemperature sets the temperature for subsequent requests +func (c *CLIClient) SetTemperature(temp float64) error { + if temp < 0.0 || temp > 2.0 { + return fmt.Errorf("temperature must be between 0.0 and 2.0") + } + c.mu.Lock() + defer c.mu.Unlock() + c.temperature = temp + return nil +} + +// LastTokens returns the token count from the last response +// Note: CLI doesn't provide token counts, so this is always 0 +func (c *CLIClient) LastTokens() int { + c.mu.RLock() + defer c.mu.RUnlock() + return c.lastTokens +} + +// Messages returns a copy of the conversation history +func (c *CLIClient) Messages() []Message { + c.mu.RLock() + defer c.mu.RUnlock() + result := make([]Message, len(c.messages)) + copy(result, c.messages) + return result +} + +// MessagesJSON returns the conversation history as JSON +func (c *CLIClient) MessagesJSON() ([]byte, error) { + c.mu.RLock() + defer c.mu.RUnlock() + return json.MarshalIndent(c.messages, "", " ") +} + +// AddSystemMessage adds a system message to the context +func (c *CLIClient) AddSystemMessage(content string) { + c.mu.Lock() + defer c.mu.Unlock() + c.messages = append([]Message{{Role: "system", Content: content}}, c.messages...) +} + +// Reset clears the conversation history +func (c *CLIClient) Reset() { + c.mu.Lock() + defer c.mu.Unlock() + c.messages = make([]Message, 0) + c.lastTokens = 0 +} + +// buildPrompt builds a full prompt string from conversation history +func (c *CLIClient) buildPrompt() string { + var parts []string + for _, msg := range c.messages { + switch msg.Role { + case "user": + parts = append(parts, fmt.Sprintf("Human: %s", msg.Content)) + case "assistant": + parts = append(parts, fmt.Sprintf("Assistant: %s", msg.Content)) + } + } + return strings.Join(parts, "\n\n") +} + +// getSystemPrompt extracts system messages as a single string +func (c *CLIClient) getSystemPrompt() string { + var systems []string + for _, msg := range c.messages { + if msg.Role == "system" { + systems = append(systems, msg.Content) + } + } + return strings.Join(systems, "\n\n") +} + +// Ask sends a prompt to the LLM via CLI and returns the response +func (c *CLIClient) Ask(ctx context.Context, prompt string) (string, error) { + c.mu.Lock() + c.messages = append(c.messages, Message{Role: "user", Content: prompt}) + fullPrompt := c.buildPrompt() + systemPrompt := c.getSystemPrompt() + model := c.model + c.mu.Unlock() + + // Build claude CLI command + args := []string{ + "--print", + "--output-format", "json", + "--model", model, + "--dangerously-skip-permissions", + "--allowedTools", "", + } + + if systemPrompt != "" { + args = append(args, "--system-prompt", systemPrompt) + } + + args = append(args, "-") // Read from stdin + + cmd := exec.CommandContext(ctx, "claude", args...) + cmd.Stdin = bytes.NewBufferString(fullPrompt) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + // Remove user message on error + c.mu.Lock() + if len(c.messages) > 0 { + c.messages = c.messages[:len(c.messages)-1] + } + c.mu.Unlock() + return "", fmt.Errorf("claude CLI error: %w (stderr: %s)", err, stderr.String()) + } + + // Parse JSON response + responseText, err := parseJSONResponse(stdout.String()) + if err != nil { + // Remove user message on error + c.mu.Lock() + if len(c.messages) > 0 { + c.messages = c.messages[:len(c.messages)-1] + } + c.mu.Unlock() + return "", fmt.Errorf("failed to parse CLI response: %w", err) + } + + // Update state + c.mu.Lock() + c.messages = append(c.messages, Message{Role: "assistant", Content: responseText}) + c.lastTokens = 0 // CLI doesn't provide token counts + c.mu.Unlock() + + return responseText, nil +} + +// parseJSONResponse extracts the result from claude CLI JSON output +func parseJSONResponse(output string) (string, error) { + // Try parsing each line as JSON (CLI may output multiple JSON objects) + scanner := bufio.NewScanner(strings.NewReader(output)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + + var resp cliResponse + if err := json.Unmarshal([]byte(line), &resp); err != nil { + continue // Not valid JSON, try next line + } + + if resp.Type == "result" && resp.Result != "" { + return resp.Result, nil + } + } + + // Fallback: return raw output if no JSON result found + output = strings.TrimSpace(output) + if output != "" { + return output, nil + } + + return "", fmt.Errorf("no result in CLI output") +} + +// StartStream begins streaming a response for the given prompt +// Note: CLI streaming is simulated - we run the command and feed output progressively +func (c *CLIClient) StartStream(ctx context.Context, prompt string) error { + c.mu.Lock() + if c.streaming { + c.mu.Unlock() + return fmt.Errorf("stream already in progress") + } + + c.messages = append(c.messages, Message{Role: "user", Content: prompt}) + fullPrompt := c.buildPrompt() + systemPrompt := c.getSystemPrompt() + model := c.model + + c.streaming = true + c.streamChan = make(chan string, 100) + c.streamDone = make(chan struct{}) + c.mu.Unlock() + + go func() { + defer func() { + c.mu.Lock() + c.streaming = false + close(c.streamChan) + close(c.streamDone) + c.mu.Unlock() + }() + + // Build command + args := []string{ + "--print", + "--output-format", "json", + "--model", model, + "--dangerously-skip-permissions", + "--allowedTools", "", + } + + if systemPrompt != "" { + args = append(args, "--system-prompt", systemPrompt) + } + + args = append(args, "-") + + cmd := exec.CommandContext(ctx, "claude", args...) + cmd.Stdin = bytes.NewBufferString(fullPrompt) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + select { + case c.streamChan <- fmt.Sprintf("[Error: %v]", err): + case <-ctx.Done(): + } + c.mu.Lock() + if len(c.messages) > 0 { + c.messages = c.messages[:len(c.messages)-1] + } + c.mu.Unlock() + return + } + + // Parse and send response + responseText, err := parseJSONResponse(stdout.String()) + if err != nil { + select { + case c.streamChan <- fmt.Sprintf("[Error: %v]", err): + case <-ctx.Done(): + } + c.mu.Lock() + if len(c.messages) > 0 { + c.messages = c.messages[:len(c.messages)-1] + } + c.mu.Unlock() + return + } + + // Send response as a single chunk (CLI doesn't truly stream) + select { + case c.streamChan <- responseText: + case <-ctx.Done(): + return + } + + // Update state + c.mu.Lock() + c.messages = append(c.messages, Message{Role: "assistant", Content: responseText}) + c.lastTokens = 0 + c.mu.Unlock() + }() + + return nil +} + +// ReadStreamChunk reads the next chunk from the stream +func (c *CLIClient) ReadStreamChunk() (string, bool) { + c.mu.RLock() + streamChan := c.streamChan + c.mu.RUnlock() + + if streamChan == nil { + return "", false + } + + chunk, ok := <-streamChan + return chunk, ok +} + +// IsStreaming returns whether a stream is currently in progress +func (c *CLIClient) IsStreaming() bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.streaming +} + +// WaitStream waits for the current stream to complete +func (c *CLIClient) WaitStream() { + c.mu.RLock() + done := c.streamDone + c.mu.RUnlock() + + if done != nil { + <-done + } +} diff --git a/internal/llmfs/ask.go b/internal/llmfs/ask.go index 00b723f3b3c53ac67fbd7a8cf6d5baba465b8dc2..f598e274c8786b2480ae9c22fe42fb954bb23a12 100644 --- a/internal/llmfs/ask.go +++ b/internal/llmfs/ask.go @@ -13,13 +13,13 @@ import ( // AskFile is the main interaction file - write a prompt, read the response type AskFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend mu sync.RWMutex lastResponse string } // NewAskFile creates the ask file -func NewAskFile(client *llm.Client) *AskFile { +func NewAskFile(client llm.Backend) *AskFile { return &AskFile{ BaseFile: protocol.NewBaseFile("ask", 0666), client: client, diff --git a/internal/llmfs/context.go b/internal/llmfs/context.go index 9cbf83a9975b27a1bb40bd5ba79f361acd5ac4b2..d7b3bf2b5ec3bf89d188927b63ad50dae63b0eed 100644 --- a/internal/llmfs/context.go +++ b/internal/llmfs/context.go @@ -13,11 +13,11 @@ import ( // Write: appends a system message to context type ContextFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewContextFile creates the context file -func NewContextFile(client *llm.Client) *ContextFile { +func NewContextFile(client llm.Backend) *ContextFile { return &ContextFile{ BaseFile: protocol.NewBaseFile("context", 0666), client: client, diff --git a/internal/llmfs/new.go b/internal/llmfs/new.go index 9b5900528b5297fe3637e5a1c9bd333feef52ca0..4093880fd2430557b55d71e5c881b4ec6a8dc8f4 100644 --- a/internal/llmfs/new.go +++ b/internal/llmfs/new.go @@ -8,11 +8,11 @@ import ( // NewFile is a write-only file that resets the conversation when written to type NewFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewNewFile creates the new file -func NewNewFile(client *llm.Client) *NewFile { +func NewNewFile(client llm.Backend) *NewFile { return &NewFile{ BaseFile: protocol.NewBaseFile("new", 0222), client: client, diff --git a/internal/llmfs/root.go b/internal/llmfs/root.go index e0b5d445be4de249f40b5546e3049abaafceea1d..26f92f023ea5ba647b19f4801e220bc52fbe3d78 100644 --- a/internal/llmfs/root.go +++ b/internal/llmfs/root.go @@ -7,7 +7,7 @@ import ( ) // NewRoot creates the root directory of the LLM filesystem -func NewRoot(client *llm.Client) protocol.Dir { +func NewRoot(client llm.Backend) protocol.Dir { root := protocol.NewStaticDir("llm") // Add all files diff --git a/internal/llmfs/state.go b/internal/llmfs/state.go index ddd1c30b480756081ac1e00c743285a5a980c25d..b58a41e327aa5be6d0c328a75d40847191884b05 100644 --- a/internal/llmfs/state.go +++ b/internal/llmfs/state.go @@ -13,11 +13,11 @@ import ( // ModelFile exposes the current model name (read/write) type ModelFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewModelFile creates the model file -func NewModelFile(client *llm.Client) *ModelFile { +func NewModelFile(client llm.Backend) *ModelFile { return &ModelFile{ BaseFile: protocol.NewBaseFile("model", 0666), client: client, @@ -51,11 +51,11 @@ func (f *ModelFile) Stat() protocol.Stat { // TemperatureFile exposes the current temperature (read/write) type TemperatureFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewTemperatureFile creates the temperature file -func NewTemperatureFile(client *llm.Client) *TemperatureFile { +func NewTemperatureFile(client llm.Backend) *TemperatureFile { return &TemperatureFile{ BaseFile: protocol.NewBaseFile("temperature", 0666), client: client, diff --git a/internal/llmfs/stream.go b/internal/llmfs/stream.go index 63647f9430730272c070082b87705ee331e105b6..3053b2cd4b35e51a7752efc222304e055bdcb6c9 100644 --- a/internal/llmfs/stream.go +++ b/internal/llmfs/stream.go @@ -14,11 +14,11 @@ import ( // Returns EOF when the stream is complete type ChunkFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewChunkFile creates the stream/chunk file -func NewChunkFile(client *llm.Client) *ChunkFile { +func NewChunkFile(client llm.Backend) *ChunkFile { return &ChunkFile{ BaseFile: protocol.NewBaseFile("chunk", 0444), client: client, @@ -58,11 +58,11 @@ func (f *ChunkFile) Stat() protocol.Stat { // Write a prompt to start streaming, then read chunks from stream/chunk type StreamAskFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewStreamAskFile creates the stream/ask file -func NewStreamAskFile(client *llm.Client) *StreamAskFile { +func NewStreamAskFile(client llm.Backend) *StreamAskFile { return &StreamAskFile{ BaseFile: protocol.NewBaseFile("ask", 0222), // write-only client: client, diff --git a/internal/llmfs/tokens.go b/internal/llmfs/tokens.go index 0e46917cf4e821a950cd8f501ff0afe369e445a5..c4dda1dc5f2a1b0ee3476ad64cb0e4926b282e55 100644 --- a/internal/llmfs/tokens.go +++ b/internal/llmfs/tokens.go @@ -11,11 +11,11 @@ import ( // TokensFile exposes the last response token count (read-only) type TokensFile struct { *protocol.BaseFile - client *llm.Client + client llm.Backend } // NewTokensFile creates the tokens file -func NewTokensFile(client *llm.Client) *TokensFile { +func NewTokensFile(client llm.Backend) *TokensFile { return &TokensFile{ BaseFile: protocol.NewBaseFile("tokens", 0444), client: client,