Skip to content
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
11 changes: 11 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Config struct {
BlacklistPatterns []string `mapstructure:"blacklist_patterns"`
OpenRouter OpenRouterConfig `mapstructure:"openrouter"`
AzureOpenAI AzureOpenAIConfig `mapstructure:"azure_openai"`
Copilot CopilotConfig `mapstructure:"copilot"`
Prompts PromptsConfig `mapstructure:"prompts"`
}

Expand All @@ -41,6 +42,12 @@ type AzureOpenAIConfig struct {
DeploymentName string `mapstructure:"deployment_name"`
}

// CopilotConfig holds GitHub Copilot configuration
type CopilotConfig struct {
Enabled bool `mapstructure:"enabled"`
Model string `mapstructure:"model"`
}

// PromptsConfig holds customizable prompt templates
type PromptsConfig struct {
BaseSystem string `mapstructure:"base_system"`
Expand All @@ -66,6 +73,10 @@ func DefaultConfig() *Config {
Model: "google/gemini-2.5-flash-preview",
},
AzureOpenAI: AzureOpenAIConfig{},
Copilot: CopilotConfig{
Enabled: true,
Model: "gpt-4o",
},
Prompts: PromptsConfig{
BaseSystem: ``,
ChatAssistant: ``,
Expand Down
251 changes: 251 additions & 0 deletions copilot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# GitHub Copilot Integration Implementation Plan

## Overview
This document outlines the implementation plan for adding GitHub Copilot support to TmuxAI, allowing users to leverage Copilot's AI models (GPT-4o, Claude 3.5 Sonnet, etc.) directly within their tmux sessions.

## Key Features
- OAuth device flow authentication (no manual token hunting)
- Automatic token refresh for Copilot's short-lived API tokens
- Model discovery and selection
- Seamless provider switching between Copilot, OpenRouter, and Azure OpenAI
- Zero-config startup (no API key required to launch TmuxAI)

## Architecture Changes

### 1. Remove Hard API Key Requirement
**File**: `internal/manager.go`
- Modify `NewManager()` to allow initialization without API keys
- Add `HasConfiguredProvider()` method to check if any provider is available
- Track provider status with new field: `ProviderStatus string`

### 2. Provider Priority System
Order of preference:
1. GitHub Copilot (if authenticated)
2. Azure OpenAI (if configured)
3. OpenRouter (if configured)
4. None (show setup instructions)

## Implementation Steps

### Step 1: Config Structure Extension
**File**: `config/config.go`
```go
type CopilotConfig struct {
Enabled bool `mapstructure:"enabled"`
Model string `mapstructure:"model"`
AuthToken string // Loaded from file, not config
}
```

Add to main Config struct:
```go
type Config struct {
// ... existing fields
Copilot CopilotConfig `mapstructure:"copilot"`
}
```

### Step 2: Copilot Authentication Module
**New File**: `internal/copilot_auth.go`

Key components:
- `RequestDeviceCode()` - Initiate OAuth device flow
- `PollForAuth(deviceCode)` - Poll GitHub for auth completion
- `SaveAuthToken(token)` - Store in `~/.config/tmuxai/.copilot-auth-token`
- `LoadAuthToken()` - Read stored auth token
- `GetCopilotAPIToken(authToken)` - Exchange for short-lived API token
- Token cache with 1-hour TTL

OAuth endpoints:
- Device code: `https://github.com/login/device/code`
- Token exchange: `https://github.com/login/oauth/access_token`
- Copilot token: `https://api.github.com/copilot_internal/v2/token`

Client ID: `Iv1.b507a08c87ecfe98`

### Step 3: Extend AI Client
**File**: `internal/ai_client.go`

Modifications:
```go
type AiClient struct {
config *config.Config
client *http.Client
copilotAuthToken string
copilotAPIToken *CopilotToken // Cached with expiry
}

type CopilotToken struct {
Token string
ExpiresAt time.Time
}
```

Update `ChatCompletion()`:
- Check provider priority
- If Copilot: refresh API token if expired
- Set endpoint to `https://api.githubcopilot.com`
- Use Copilot token in Authorization header

### Step 4: Command Interface
**File**: `internal/chat_command.go`

New commands:
- `/copilot login` - Start OAuth device flow
- `/copilot logout` - Clear stored tokens
- `/copilot status` - Show auth status
- `/copilot models` - List available models

Implementation:
```go
case prefixMatch(commandPrefix, "/copilot"):
m.processCopilotCommand(parts)
return
```

### Step 5: Message Interception
**File**: `internal/process_message.go`

Before processing messages, check:
```go
if !m.HasConfiguredProvider() {
m.ShowProviderSetupInstructions()
return
}
```

Setup instructions:
```
No API provider configured. Please configure one:

• GitHub Copilot (recommended):
/copilot login

• OpenRouter:
export TMUXAI_OPENROUTER_API_KEY="your-key"

• Azure OpenAI:
Set credentials in ~/.config/tmuxai/config.yaml

Run /help for more information.
```

### Step 6: Model Discovery
**Endpoint**: `GET https://api.githubcopilot.com/models`

Headers:
```
Authorization: Bearer {copilot_api_token}
Content-Type: application/json
Copilot-Integration-Id: vscode-chat
```

Parse response and cache available models.

## User Experience Flow

### First-Time Setup
1. User runs `tmuxai` (no API key required)
2. User sends message → sees provider setup instructions
3. User runs `/copilot login`
4. TmuxAI shows:
```
First copy your one-time code:
XXXX-XXXX

Then visit: https://github.com/login/device

Waiting for authentication...
Press Enter to check status...
```
5. User completes GitHub auth
6. TmuxAI confirms: "Authentication successful!"
7. Ready to use

### Subsequent Usage
1. TmuxAI loads saved auth token on startup
2. Automatically refreshes Copilot API token as needed
3. Seamless experience with no manual intervention

## Configuration Example

```yaml
# ~/.config/tmuxai/config.yaml
copilot:
enabled: true
model: gpt-4o # or claude-3-5-sonnet, o1-mini, etc.

# Copilot takes precedence if enabled and authenticated
openrouter:
api_key: ${OPENROUTER_API_KEY}
model: gpt-4o-mini
```

## Testing Plan

1. **Auth Flow Testing**
- Test device code request
- Test auth polling with timeout
- Test token storage and retrieval
- Test invalid/expired tokens

2. **Provider Switching**
- Test Copilot → OpenRouter fallback
- Test missing all providers
- Test config override via `/config set`

3. **Model Discovery**
- Test listing available models
- Test model selection
- Test invalid model handling

4. **Token Refresh**
- Test automatic refresh on expiry
- Test handling of refresh failures
- Test concurrent request handling

## Error Handling

1. **Auth Failures**
- Network errors → retry with backoff
- Invalid device code → restart flow
- Expired auth token → prompt re-login

2. **API Failures**
- Token refresh fails → fall back to other providers
- Model not available → suggest alternatives
- Rate limits → show clear error message

## Security Considerations

1. **Token Storage**
- Store auth token with 0600 permissions
- Never log tokens
- Clear tokens on `/copilot logout`

2. **Token Transmission**
- Always use HTTPS
- Include proper headers
- No token in URL parameters

## Migration Path for Existing Users

1. Existing users continue working with current providers
2. Copilot becomes available via `/copilot login`
3. No breaking changes to existing configs
4. Can disable Copilot with `copilot.enabled: false`

## Implementation Timeline

- Phase 1: Core auth flow and token management
- Phase 2: AI client integration
- Phase 3: Command interface and UX
- Phase 4: Model discovery and advanced features
- Phase 5: Testing and documentation

## References

- [GitHub Device Flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)
- [Copilot REST API](https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line)
- [Shell-ask implementation](https://github.com/egoist/shell-ask)
- [Aider Copilot docs](https://aider.chat/docs/llms/copilot.html)
62 changes: 56 additions & 6 deletions internal/ai_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import (
"github.com/alvinunreal/tmuxai/logger"
)

// AiClient represents an AI client for interacting with OpenAI-compatible APIs including Azure OpenAI
// AiClient represents an AI client for interacting with OpenAI-compatible APIs including Azure OpenAI and Copilot
type AiClient struct {
config *config.Config
client *http.Client
config *config.Config
client *http.Client
copilotAuth *CopilotAuth
}

// Message represents a chat message
Expand Down Expand Up @@ -49,11 +50,16 @@ type ChatCompletionResponse struct {

func NewAiClient(cfg *config.Config) *AiClient {
return &AiClient{
config: cfg,
client: &http.Client{},
config: cfg,
client: &http.Client{},
}
}

// SetCopilotAuth sets the CopilotAuth instance to use
func (c *AiClient) SetCopilotAuth(auth *CopilotAuth) {
c.copilotAuth = auth
}

// GetResponseFromChatMessages gets a response from the AI based on chat messages
func (c *AiClient) GetResponseFromChatMessages(ctx context.Context, chatMessages []ChatMessage, model string) (string, error) {
// Convert chat messages to AI client format
Expand Down Expand Up @@ -98,6 +104,41 @@ func (c *AiClient) ChatCompletion(ctx context.Context, messages []Message, model
var url string
var apiKeyHeader string
var apiKey string
var additionalHeaders map[string]string

// Check provider priority: Copilot -> Azure -> OpenRouter
if c.config.Copilot.Enabled && c.copilotAuth != nil {
logger.Debug("Copilot enabled, checking authentication...")
if c.copilotAuth.IsAuthenticated() {
logger.Debug("Copilot authenticated, getting API token...")
// Use GitHub Copilot endpoint
copilotToken, err := c.copilotAuth.GetCopilotAPIToken()
if err != nil {
logger.Error("Failed to get Copilot token: %v", err)
// Don't fall back, return the actual error
return "", fmt.Errorf("failed to get Copilot API token: %w", err)
}
url = "https://api.githubcopilot.com/chat/completions"
apiKeyHeader = "Authorization"
apiKey = "Bearer " + copilotToken
additionalHeaders = map[string]string{
"User-Agent": "GithubCopilot/1.155.0",
"Editor-Plugin-Version": "copilot/1.155.0",
"Editor-Version": "vscode/1.85.1",
"Copilot-Integration-Id": "vscode-chat",
}
// Use Copilot model if specified, otherwise use default
if c.config.Copilot.Model != "" {
reqBody.Model = c.config.Copilot.Model
}
logger.Debug("Using GitHub Copilot with model: %s", reqBody.Model)
goto sendRequest
} else {
logger.Debug("Copilot not authenticated")
}
} else {
logger.Debug("Copilot not enabled or copilotAuth is nil: enabled=%v, copilotAuth=%v", c.config.Copilot.Enabled, c.copilotAuth != nil)
}

if c.config.AzureOpenAI.APIKey != "" {
// Use Azure OpenAI endpoint
Expand All @@ -111,14 +152,18 @@ func (c *AiClient) ChatCompletion(ctx context.Context, messages []Message, model

// Azure endpoint doesn't expect model in body
reqBody.Model = ""
} else {
} else if c.config.OpenRouter.APIKey != "" {
// default OpenRouter/OpenAI compatible endpoint
baseURL := strings.TrimSuffix(c.config.OpenRouter.BaseURL, "/")
url = baseURL + "/chat/completions"
apiKeyHeader = "Authorization"
apiKey = "Bearer " + c.config.OpenRouter.APIKey
} else {
return "", fmt.Errorf("no API provider configured")
}

sendRequest:

reqJSON, err := json.Marshal(reqBody)
if err != nil {
logger.Error("Failed to marshal request: %v", err)
Expand All @@ -138,6 +183,11 @@ func (c *AiClient) ChatCompletion(ctx context.Context, messages []Message, model
req.Header.Set("HTTP-Referer", "https://github.com/alvinunreal/tmuxai")
req.Header.Set("X-Title", "TmuxAI")

// Add any additional headers (e.g., for Copilot)
for k, v := range additionalHeaders {
req.Header.Set(k, v)
}

// Log the request details for debugging before sending
logger.Debug("Sending API request to: %s with model: %s", url, model)

Expand Down
Loading
Loading