-
-
Notifications
You must be signed in to change notification settings - Fork 84
refactor(server): unify native forwarding dispatch #707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a5bfad0
9f3cfe2
0d06bfc
21f622c
5b1f458
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,8 +33,21 @@ var Registration = providers.Registration{ | |
| const ( | ||
| defaultBaseURL = "https://api.anthropic.com/v1" | ||
| anthropicAPIVersion = "2023-06-01" | ||
|
|
||
| // oauthTokenPrefix identifies Claude subscription OAuth tokens (created | ||
| // with `claude setup-token`). Anthropic only authorizes these credentials | ||
| // for Claude Code-shaped traffic; they authenticate with a Bearer header | ||
| // plus the oauth beta instead of x-api-key. | ||
| oauthTokenPrefix = "sk-ant-oat" | ||
| oauthBetaFlag = "oauth-2025-04-20" | ||
|
|
||
| anthropicBetaHeader = "anthropic-beta" | ||
| ) | ||
|
|
||
| func isOAuthToken(key string) bool { | ||
| return strings.HasPrefix(key, oauthTokenPrefix) | ||
| } | ||
|
|
||
| var allowedAnthropicImageMediaTypes = map[string]struct{}{ | ||
| "image/jpeg": {}, | ||
| "image/png": {}, | ||
|
|
@@ -155,10 +168,29 @@ func (p *Provider) getBatchResultEndpoints(batchID string) map[string]string { | |
| return cloned | ||
| } | ||
|
|
||
| // pinnedKeyContextKey carries a credential selected before the header hook | ||
| // runs. Passthrough pins its key so header adaptation (the oauth beta merge) | ||
| // and the auth header always describe the same credential, even when the | ||
| // keyring mixes OAuth tokens and API keys. | ||
| type pinnedKeyContextKey struct{} | ||
|
|
||
| func withPinnedKey(ctx context.Context, key string) context.Context { | ||
| return context.WithValue(ctx, pinnedKeyContextKey{}, key) | ||
| } | ||
|
|
||
| // setHeaders sets the required headers for Anthropic API requests. It runs once | ||
| // per outbound request; identified sessions resolve to a stable key. | ||
| func (p *Provider) setHeaders(req *http.Request) { | ||
| req.Header.Set("x-api-key", p.keys.NextForContext(req.Context())) | ||
| key, pinned := req.Context().Value(pinnedKeyContextKey{}).(string) | ||
| if !pinned { | ||
| key = p.keys.NextForContext(req.Context()) | ||
| } | ||
| if isOAuthToken(key) { | ||
| req.Header.Set("Authorization", "Bearer "+key) | ||
| req.Header.Set(anthropicBetaHeader, oauthBetaFlag) | ||
| } else { | ||
| req.Header.Set("x-api-key", key) | ||
| } | ||
| req.Header.Set("anthropic-version", anthropicAPIVersion) | ||
|
|
||
| // Forward request ID if present in context | ||
|
|
@@ -167,12 +199,46 @@ func (p *Provider) setHeaders(req *http.Request) { | |
| } | ||
| } | ||
|
|
||
| // ensureOAuthBeta returns headers with the oauth beta flag merged into a | ||
| // client-supplied anthropic-beta value. Forwarded headers override the ones set | ||
| // by setHeaders, so a client that sends its own beta list would otherwise drop | ||
| // the oauth flag subscription tokens require. Headers without an anthropic-beta | ||
| // entry are returned unchanged: setHeaders' value survives in that case. | ||
| func ensureOAuthBeta(headers http.Header) http.Header { | ||
| for name, values := range headers { | ||
| if !strings.EqualFold(strings.TrimSpace(name), anthropicBetaHeader) { | ||
| continue | ||
| } | ||
| for _, value := range values { | ||
| for flag := range strings.SplitSeq(value, ",") { | ||
| if strings.TrimSpace(flag) == oauthBetaFlag { | ||
| return headers | ||
| } | ||
| } | ||
| } | ||
| merged := make(http.Header, len(headers)) | ||
| maps.Copy(merged, headers) | ||
| merged[name] = append(append([]string{}, values...), oauthBetaFlag) | ||
| return merged | ||
| } | ||
| return headers | ||
| } | ||
|
|
||
| // Passthrough forwards an opaque Anthropic-native request without typed translation. | ||
| func (p *Provider) Passthrough(ctx context.Context, req *core.PassthroughRequest) (*core.PassthroughResponse, error) { | ||
| if req == nil { | ||
| return nil, core.NewInvalidRequestError("passthrough request is required", nil) | ||
| } | ||
|
|
||
| // Select the credential once and pin it for setHeaders, so the beta | ||
| // merge below and the auth header are always based on the same key. | ||
| key := p.keys.NextForContext(ctx) | ||
| ctx = withPinnedKey(ctx, key) | ||
|
Comment on lines
+235
to
+236
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ArtifactsFocused Anthropic retry credential-selection harness source
Two-credential Anthropic passthrough retry output
|
||
| headers := req.Headers | ||
| if isOAuthToken(key) { | ||
| headers = ensureOAuthBeta(headers) | ||
| } | ||
|
|
||
| resp, err := p.client.DoPassthrough(ctx, llmclient.Request{ | ||
| Method: req.Method, | ||
| Endpoint: providers.PassthroughEndpoint(req.Endpoint), | ||
|
|
@@ -181,7 +247,7 @@ func (p *Provider) Passthrough(ctx context.Context, req *core.PassthroughRequest | |
| Stream: req.Stream, | ||
| StreamUncertain: req.StreamUncertain, | ||
| RawBodyReader: req.Body, | ||
| Headers: req.Headers, | ||
| Headers: headers, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Make the merge target deterministic when duplicate header spellings exist.
ensureOAuthBetareturns after it finds the first key that matchesanthropic-betacase-insensitively. Go randomizes map iteration order. Ifheaderscarries two non-canonical spellings of the same header (for exampleanthropic-betaandAnthropic-Beta), the chosen merge target varies between requests. The upstream still receives the flag, so this is a consistency nit rather than a defect. Canonicalizing the lookup withhttp.Header.Valuesremoves the ambiguity.♻️ Proposed refactor to use canonical header access
Note:
http.Header.Valuescanonicalizes the key, so it only reads the canonical entry. Confirm that callers always pass canonicalized headers before you adopt this form;buildPassthroughHeadersininternal/server/passthrough_support.gocanonicalizes keys, but direct callers may not.🤖 Prompt for AI Agents