mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +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
112 lines
2.8 KiB
Go
112 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
gin "github.com/gin-gonic/gin"
|
|
proxyconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
|
)
|
|
|
|
func newTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
tmpDir := t.TempDir()
|
|
authDir := filepath.Join(tmpDir, "auth")
|
|
if err := os.MkdirAll(authDir, 0o700); err != nil {
|
|
t.Fatalf("failed to create auth dir: %v", err)
|
|
}
|
|
|
|
cfg := &proxyconfig.Config{
|
|
SDKConfig: sdkconfig.SDKConfig{
|
|
APIKeys: []string{"test-key"},
|
|
},
|
|
Port: 0,
|
|
AuthDir: authDir,
|
|
Debug: true,
|
|
LoggingToFile: false,
|
|
UsageStatisticsEnabled: false,
|
|
}
|
|
|
|
authManager := auth.NewManager(nil, nil, nil)
|
|
accessManager := sdkaccess.NewManager()
|
|
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
return NewServer(cfg, authManager, accessManager, configPath)
|
|
}
|
|
|
|
func TestAmpProviderModelRoutes(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
path string
|
|
wantStatus int
|
|
wantContains string
|
|
}{
|
|
{
|
|
name: "openai root models",
|
|
path: "/api/provider/openai/models",
|
|
wantStatus: http.StatusOK,
|
|
wantContains: `"object":"list"`,
|
|
},
|
|
{
|
|
name: "groq root models",
|
|
path: "/api/provider/groq/models",
|
|
wantStatus: http.StatusOK,
|
|
wantContains: `"object":"list"`,
|
|
},
|
|
{
|
|
name: "openai models",
|
|
path: "/api/provider/openai/v1/models",
|
|
wantStatus: http.StatusOK,
|
|
wantContains: `"object":"list"`,
|
|
},
|
|
{
|
|
name: "anthropic models",
|
|
path: "/api/provider/anthropic/v1/models",
|
|
wantStatus: http.StatusOK,
|
|
wantContains: `"data"`,
|
|
},
|
|
{
|
|
name: "google models v1",
|
|
path: "/api/provider/google/v1/models",
|
|
wantStatus: http.StatusOK,
|
|
wantContains: `"models"`,
|
|
},
|
|
{
|
|
name: "google models v1beta",
|
|
path: "/api/provider/google/v1beta/models",
|
|
wantStatus: http.StatusOK,
|
|
wantContains: `"models"`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server := newTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
|
req.Header.Set("Authorization", "Bearer test-key")
|
|
|
|
rr := httptest.NewRecorder()
|
|
server.engine.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != tc.wantStatus {
|
|
t.Fatalf("unexpected status code for %s: got %d want %d; body=%s", tc.path, rr.Code, tc.wantStatus, rr.Body.String())
|
|
}
|
|
if body := rr.Body.String(); !strings.Contains(body, tc.wantContains) {
|
|
t.Fatalf("response body for %s missing %q: %s", tc.path, tc.wantContains, body)
|
|
}
|
|
})
|
|
}
|
|
}
|