mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 21:10:51 +08:00
Add full Amp CLI support to enable routing AI model requests through the proxy
while maintaining Amp-specific features like thread management, user info, and
telemetry. Includes complete documentation and pull bot configuration.
Features:
- Modular architecture with RouteModule interface for clean integration
- Reverse proxy for Amp management routes (thread/user/meta/ads/telemetry)
- Provider-specific route aliases (/api/provider/{provider}/*)
- Secret management with precedence: config > env > file
- 5-minute secret caching to reduce file I/O
- Automatic gzip decompression for responses
- Proper connection cleanup to prevent leaks
- Localhost-only restriction for management routes (configurable)
- CORS protection for management endpoints
Documentation:
- Complete setup guide (USING_WITH_FACTORY_AND_AMP.md)
- OAuth setup for OpenAI (ChatGPT Plus/Pro) and Anthropic (Claude Pro/Max)
- Factory CLI config examples with all model variants
- Amp CLI/IDE configuration examples
- tmux setup for remote server deployment
- Screenshots and diagrams
Configuration:
- Pull bot disabled for this repo (manual rebase workflow)
- Config fields: AmpUpstreamURL, AmpUpstreamAPIKey, AmpRestrictManagementToLocalhost
- Compatible with upstream DisableCooling and other features
Technical details:
- internal/api/modules/amp/: Complete Amp routing module
- sdk/api/httpx/: HTTP utilities for gzip/transport
- 94.6% test coverage with 34 comprehensive test cases
- Clean integration minimizes merge conflict risk
Security:
- Management routes restricted to localhost by default
- Configurable via amp-restrict-management-to-localhost
- Prevents drive-by browser attacks on user data
This provides a production-ready foundation for Amp CLI integration while
maintaining clean separation from upstream code for easy rebasing.
Amp-Thread-ID: https://ampcode.com/threads/T-9e2befc5-f969-41c6-890c-5b779d58cf18
156 lines
4.0 KiB
Go
156 lines
4.0 KiB
Go
package amp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SecretSource provides Amp API keys with configurable precedence and caching
|
|
type SecretSource interface {
|
|
Get(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// cachedSecret holds a secret value with expiration
|
|
type cachedSecret struct {
|
|
value string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// MultiSourceSecret implements precedence-based secret lookup:
|
|
// 1. Explicit config value (highest priority)
|
|
// 2. Environment variable AMP_API_KEY
|
|
// 3. File-based secret (lowest priority)
|
|
type MultiSourceSecret struct {
|
|
explicitKey string
|
|
envKey string
|
|
filePath string
|
|
cacheTTL time.Duration
|
|
|
|
mu sync.RWMutex
|
|
cache *cachedSecret
|
|
}
|
|
|
|
// NewMultiSourceSecret creates a secret source with precedence and caching
|
|
func NewMultiSourceSecret(explicitKey string, cacheTTL time.Duration) *MultiSourceSecret {
|
|
if cacheTTL == 0 {
|
|
cacheTTL = 5 * time.Minute // Default 5 minute cache
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
filePath := filepath.Join(home, ".local", "share", "amp", "secrets.json")
|
|
|
|
return &MultiSourceSecret{
|
|
explicitKey: strings.TrimSpace(explicitKey),
|
|
envKey: "AMP_API_KEY",
|
|
filePath: filePath,
|
|
cacheTTL: cacheTTL,
|
|
}
|
|
}
|
|
|
|
// NewMultiSourceSecretWithPath creates a secret source with a custom file path (for testing)
|
|
func NewMultiSourceSecretWithPath(explicitKey string, filePath string, cacheTTL time.Duration) *MultiSourceSecret {
|
|
if cacheTTL == 0 {
|
|
cacheTTL = 5 * time.Minute
|
|
}
|
|
|
|
return &MultiSourceSecret{
|
|
explicitKey: strings.TrimSpace(explicitKey),
|
|
envKey: "AMP_API_KEY",
|
|
filePath: filePath,
|
|
cacheTTL: cacheTTL,
|
|
}
|
|
}
|
|
|
|
// Get retrieves the Amp API key using precedence: config > env > file
|
|
// Results are cached for cacheTTL duration to avoid excessive file reads
|
|
func (s *MultiSourceSecret) Get(ctx context.Context) (string, error) {
|
|
// Precedence 1: Explicit config key (highest priority, no caching needed)
|
|
if s.explicitKey != "" {
|
|
return s.explicitKey, nil
|
|
}
|
|
|
|
// Precedence 2: Environment variable
|
|
if envValue := strings.TrimSpace(os.Getenv(s.envKey)); envValue != "" {
|
|
return envValue, nil
|
|
}
|
|
|
|
// Precedence 3: File-based secret (lowest priority, cached)
|
|
// Check cache first
|
|
s.mu.RLock()
|
|
if s.cache != nil && time.Now().Before(s.cache.expiresAt) {
|
|
value := s.cache.value
|
|
s.mu.RUnlock()
|
|
return value, nil
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
// Cache miss or expired - read from file
|
|
key, err := s.readFromFile()
|
|
if err != nil {
|
|
// Cache empty result to avoid repeated file reads on missing files
|
|
s.updateCache("")
|
|
return "", err
|
|
}
|
|
|
|
// Cache the result
|
|
s.updateCache(key)
|
|
return key, nil
|
|
}
|
|
|
|
// readFromFile reads the Amp API key from the secrets file
|
|
func (s *MultiSourceSecret) readFromFile() (string, error) {
|
|
content, err := os.ReadFile(s.filePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil // Missing file is not an error, just no key available
|
|
}
|
|
return "", fmt.Errorf("failed to read amp secrets from %s: %w", s.filePath, err)
|
|
}
|
|
|
|
var secrets map[string]string
|
|
if err := json.Unmarshal(content, &secrets); err != nil {
|
|
return "", fmt.Errorf("failed to parse amp secrets from %s: %w", s.filePath, err)
|
|
}
|
|
|
|
key := strings.TrimSpace(secrets["apiKey@https://ampcode.com/"])
|
|
return key, nil
|
|
}
|
|
|
|
// updateCache updates the cached secret value
|
|
func (s *MultiSourceSecret) updateCache(value string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.cache = &cachedSecret{
|
|
value: value,
|
|
expiresAt: time.Now().Add(s.cacheTTL),
|
|
}
|
|
}
|
|
|
|
// InvalidateCache clears the cached secret, forcing a fresh read on next Get
|
|
func (s *MultiSourceSecret) InvalidateCache() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.cache = nil
|
|
}
|
|
|
|
// StaticSecretSource returns a fixed API key (for testing)
|
|
type StaticSecretSource struct {
|
|
key string
|
|
}
|
|
|
|
// NewStaticSecretSource creates a secret source with a fixed key
|
|
func NewStaticSecretSource(key string) *StaticSecretSource {
|
|
return &StaticSecretSource{key: strings.TrimSpace(key)}
|
|
}
|
|
|
|
// Get returns the static API key
|
|
func (s *StaticSecretSource) Get(ctx context.Context) (string, error) {
|
|
return s.key, nil
|
|
}
|