mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Amp CLI sends 'context-1m-2025-08-07' in Anthropic-Beta header which
requires a special 1M context window subscription. After upstream rebase
to v6.3.7 (commit 38cfbac), CLIProxyAPI now respects client-provided
Anthropic-Beta headers instead of always using defaults.
When users configure local OAuth providers (Claude, etc), requests bypass
the ampcode.com proxy and use their own API subscriptions. These personal
subscriptions typically don't include the 1M context beta feature, causing
'long context beta not available' errors.
Changes:
- Add filterBetaFeatures() helper to strip specific beta features
- Filter context-1m-2025-08-07 in fallback handler when using local providers
- Preserve full headers when proxying to ampcode.com (paid users get all features)
- Add 7 test cases covering all edge cases
This fix is isolated to the Amp module and only affects the local provider
path. Users proxying through ampcode.com are unaffected and receive full
1M context support as part of their paid service.
117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
package amp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http/httputil"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// FallbackHandler wraps a standard handler with fallback logic to ampcode.com
|
|
// when the model's provider is not available in CLIProxyAPI
|
|
type FallbackHandler struct {
|
|
getProxy func() *httputil.ReverseProxy
|
|
}
|
|
|
|
// NewFallbackHandler creates a new fallback handler wrapper
|
|
// The getProxy function allows lazy evaluation of the proxy (useful when proxy is created after routes)
|
|
func NewFallbackHandler(getProxy func() *httputil.ReverseProxy) *FallbackHandler {
|
|
return &FallbackHandler{
|
|
getProxy: getProxy,
|
|
}
|
|
}
|
|
|
|
// WrapHandler wraps a gin.HandlerFunc with fallback logic
|
|
// If the model's provider is not configured in CLIProxyAPI, it forwards to ampcode.com
|
|
func (fh *FallbackHandler) WrapHandler(handler gin.HandlerFunc) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Read the request body to extract the model name
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
log.Errorf("amp fallback: failed to read request body: %v", err)
|
|
handler(c)
|
|
return
|
|
}
|
|
|
|
// Restore the body for the handler to read
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
|
|
// Try to extract model from request body or URL path (for Gemini)
|
|
modelName := extractModelFromRequest(bodyBytes, c)
|
|
if modelName == "" {
|
|
// Can't determine model, proceed with normal handler
|
|
handler(c)
|
|
return
|
|
}
|
|
|
|
// Normalize model (handles Gemini thinking suffixes)
|
|
normalizedModel, _ := util.NormalizeGeminiThinkingModel(modelName)
|
|
|
|
// Check if we have providers for this model
|
|
providers := util.GetProviderName(normalizedModel)
|
|
|
|
if len(providers) == 0 {
|
|
// No providers configured - check if we have a proxy for fallback
|
|
proxy := fh.getProxy()
|
|
if proxy != nil {
|
|
// Fallback to ampcode.com
|
|
log.Infof("amp fallback: model %s has no configured provider, forwarding to ampcode.com", modelName)
|
|
|
|
// Restore body again for the proxy
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
|
|
// Forward to ampcode.com
|
|
proxy.ServeHTTP(c.Writer, c.Request)
|
|
return
|
|
}
|
|
|
|
// No proxy available, let the normal handler return the error
|
|
log.Debugf("amp fallback: model %s has no configured provider and no proxy available", modelName)
|
|
}
|
|
|
|
// Providers available or no proxy for fallback, restore body and use normal handler
|
|
// Filter Anthropic-Beta header to remove features requiring special subscription
|
|
// This is needed when using local providers (bypassing the Amp proxy)
|
|
if betaHeader := c.Request.Header.Get("Anthropic-Beta"); betaHeader != "" {
|
|
filtered := filterBetaFeatures(betaHeader, "context-1m-2025-08-07")
|
|
if filtered != "" {
|
|
c.Request.Header.Set("Anthropic-Beta", filtered)
|
|
} else {
|
|
c.Request.Header.Del("Anthropic-Beta")
|
|
}
|
|
}
|
|
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
handler(c)
|
|
}
|
|
}
|
|
|
|
// extractModelFromRequest attempts to extract the model name from various request formats
|
|
func extractModelFromRequest(body []byte, c *gin.Context) string {
|
|
// First try to parse from JSON body (OpenAI, Claude, etc.)
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(body, &payload); err == nil {
|
|
// Check common model field names
|
|
if model, ok := payload["model"].(string); ok {
|
|
return model
|
|
}
|
|
}
|
|
|
|
// For Gemini requests, model is in the URL path: /models/{model}:generateContent
|
|
// Extract from :action parameter (e.g., "gemini-pro:generateContent")
|
|
if action := c.Param("action"); action != "" {
|
|
// Split by colon to get model name (e.g., "gemini-pro:generateContent" -> "gemini-pro")
|
|
parts := strings.Split(action, ":")
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
return parts[0]
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|