From 72d82268e574609597d92c5e2a36018be45d4c70 Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Mon, 3 Nov 2025 09:22:00 -0700 Subject: [PATCH] fix(amp): filter context-1m beta header for local OAuth providers 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. --- internal/api/modules/amp/fallback_handlers.go | 11 ++++ internal/api/modules/amp/proxy.go | 19 ++++++ internal/api/modules/amp/proxy_test.go | 61 +++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/internal/api/modules/amp/fallback_handlers.go b/internal/api/modules/amp/fallback_handlers.go index d0ccac56..d8c140ad 100644 --- a/internal/api/modules/amp/fallback_handlers.go +++ b/internal/api/modules/amp/fallback_handlers.go @@ -75,6 +75,17 @@ func (fh *FallbackHandler) WrapHandler(handler gin.HandlerFunc) gin.HandlerFunc } // 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) } diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index 5e267290..d417d068 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -46,6 +46,10 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi // Could generate one here if needed } + // Note: We do NOT filter Anthropic-Beta headers in the proxy path + // Users going through ampcode.com proxy are paying for the service and should get all features + // including 1M context window (context-1m-2025-08-07) + // Inject API key from secret source (precedence: config > env > file) if key, err := secretSource.Get(req.Context()); err == nil && key != "" { req.Header.Set("X-Api-Key", key) @@ -174,3 +178,18 @@ func proxyHandler(proxy *httputil.ReverseProxy) gin.HandlerFunc { proxy.ServeHTTP(c.Writer, c.Request) } } + +// filterBetaFeatures removes a specific beta feature from comma-separated list +func filterBetaFeatures(header, featureToRemove string) string { + features := strings.Split(header, ",") + filtered := make([]string, 0, len(features)) + + for _, feature := range features { + trimmed := strings.TrimSpace(feature) + if trimmed != "" && trimmed != featureToRemove { + filtered = append(filtered, trimmed) + } + } + + return strings.Join(filtered, ",") +} diff --git a/internal/api/modules/amp/proxy_test.go b/internal/api/modules/amp/proxy_test.go index 864ed22c..a9694c01 100644 --- a/internal/api/modules/amp/proxy_test.go +++ b/internal/api/modules/amp/proxy_test.go @@ -437,3 +437,64 @@ func TestIsStreamingResponse(t *testing.T) { }) } } + +func TestFilterBetaFeatures(t *testing.T) { + tests := []struct { + name string + header string + featureToRemove string + expected string + }{ + { + name: "Remove context-1m from middle", + header: "fine-grained-tool-streaming-2025-05-14,context-1m-2025-08-07,oauth-2025-04-20", + featureToRemove: "context-1m-2025-08-07", + expected: "fine-grained-tool-streaming-2025-05-14,oauth-2025-04-20", + }, + { + name: "Remove context-1m from start", + header: "context-1m-2025-08-07,fine-grained-tool-streaming-2025-05-14", + featureToRemove: "context-1m-2025-08-07", + expected: "fine-grained-tool-streaming-2025-05-14", + }, + { + name: "Remove context-1m from end", + header: "fine-grained-tool-streaming-2025-05-14,context-1m-2025-08-07", + featureToRemove: "context-1m-2025-08-07", + expected: "fine-grained-tool-streaming-2025-05-14", + }, + { + name: "Feature not present", + header: "fine-grained-tool-streaming-2025-05-14,oauth-2025-04-20", + featureToRemove: "context-1m-2025-08-07", + expected: "fine-grained-tool-streaming-2025-05-14,oauth-2025-04-20", + }, + { + name: "Only feature to remove", + header: "context-1m-2025-08-07", + featureToRemove: "context-1m-2025-08-07", + expected: "", + }, + { + name: "Empty header", + header: "", + featureToRemove: "context-1m-2025-08-07", + expected: "", + }, + { + name: "Header with spaces", + header: "fine-grained-tool-streaming-2025-05-14, context-1m-2025-08-07 , oauth-2025-04-20", + featureToRemove: "context-1m-2025-08-07", + expected: "fine-grained-tool-streaming-2025-05-14,oauth-2025-04-20", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterBetaFeatures(tt.header, tt.featureToRemove) + if result != tt.expected { + t.Errorf("filterBetaFeatures() = %q, want %q", result, tt.expected) + } + }) + } +}