Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
80 kB · 2322 lines
Go
at commit 291cfb87
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323package executor
import ( "bufio" "bytes" "compress/flate" "compress/gzip" "compress/zlib" "context" "fmt" "io" "net/http" "net/url" "sort" "strings" "time"
"github.com/andybalholm/brotli" "github.com/google/uuid" "github.com/klauspost/compress/zstd" claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v7/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson"
"github.com/gin-gonic/gin")
const ( claudeTokenCountingBeta = "token-counting-2024-11-01" claudeFastModeBeta = "fast-mode-2026-02-01" claudeOAuthBeta = "oauth-2025-04-20" claudeCodeBeta = "claude-code-20250219" claudeContext1MBeta = "context-1m-2025-08-07" claudeMidConvSystemBeta = "mid-conversation-system-2026-04-07" claudeAdvisorToolBeta = "advisor-tool-2026-03-01" claudeAdvancedToolUseBeta = "advanced-tool-use-2025-11-20" claudeEffortBeta = "effort-2025-11-24" claudeServerSideFallbackBeta = "server-side-fallback-2026-06-01" claudeFallbackCreditBeta = "fallback-credit-2026-06-01" claudeStructuredOutputsBeta = "structured-outputs-2025-12-15" claudeExtendedCacheTTLBeta = "extended-cache-ttl-2025-04-11" claudeCacheDiagnosisBeta = "cache-diagnosis-2026-04-07" claudeRedactThinkingBeta = "redact-thinking-2026-02-12")
// claudeCodeCLIConstantBetas are the betas Claude Code 2.1.220 sends on every// /v1/messages request from the "cli" entrypoint, in wire order, excluding the// leading claude-code-20250219.//// redact-thinking-2026-02-12 belongs here because cloaked requests always claim// cc_entrypoint=cli; the "sdk-cli" entrypoint omits it. It is still dropped for// requests that carry thinking.display, see claudeThinkingDisplaySet.var claudeCodeCLIConstantBetas = []string{ "interleaved-thinking-2025-05-14", claudeRedactThinkingBeta, "thinking-token-count-2026-05-13", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05",}
// claudeCodeTrailingBetas are caller-supplied betas that real Claude Code emits// after effort-2025-11-24, in that relative order. They are forwarded when the// caller asks for them and dropped otherwise.var claudeCodeTrailingBetas = []string{ claudeServerSideFallbackBeta, claudeFallbackCreditBeta, claudeStructuredOutputsBeta,}
// claudeCodeCLIBetas assembles the Anthropic-Beta baseline the way Claude Code// 2.1.220 does: the list is per-request, not a fixed string. requested holds the// betas the caller asked for, which decide the capability flags below.//// Verified against api.anthropic.com with isolated 2.1.220 profiles on both// API-key and OAuth paths. A 2026-08-03 A/B capture with two distinct OAuth// accounts confirmed the current tool beta and OAuth trailer below.// The full observed order is://// 1 claude-code-20250219// 2 oauth-2025-04-20 OAuth credentials only// 3 context-1m-2025-08-07 [1m] model variants only// 4 interleaved-thinking-2025-05-14// 5 redact-thinking-2026-02-12 cli entrypoint, no thinking.display// 6 thinking-token-count-2026-05-13// 7 context-management-2025-06-27// 8 prompt-caching-scope-2026-01-05// 9 mid-conversation-system-2026-04-07 models accepting a role=system turn// 10 advisor-tool-2026-03-01 requests declaring advisor tools or requesting advisor beta// 11 advanced-tool-use-2025-11-20 requests with tools// 12 effort-2025-11-24// 13 server-side-fallback-2026-06-01// 14 fallback-credit-2026-06-01// 15 fast-mode-2026-02-01 speed:fast requests only// 16 extended-cache-ttl-2025-04-11 OAuth credentials only// 17 cache-diagnosis-2026-04-07 requests with diagnostics only//// An empty body keeps the optimistic role=system default, matching the cloaking// policy for unknown and future model IDs.func claudeCodeCLIBetas(body []byte, requested map[string]bool, oauthToken bool) string { betas := make([]string, 0, len(claudeCodeCLIConstantBetas)+len(claudeCodeTrailingBetas)+7) betas = append(betas, claudeCodeBeta) if oauthToken { betas = append(betas, claudeOAuthBeta) } if requested[claudeContext1MBeta] { betas = append(betas, claudeContext1MBeta) } redactThinking := !claudeThinkingDisplaySet(body) for _, beta := range claudeCodeCLIConstantBetas { if beta == claudeRedactThinkingBeta && !redactThinking { continue } betas = append(betas, beta) } if !claudeUsesLegacySystemReminder(body) { betas = append(betas, claudeMidConvSystemBeta) } if requested[claudeAdvisorToolBeta] || claudeBodyHasAdvisorTool(body) { betas = append(betas, claudeAdvisorToolBeta) } if tools := gjson.GetBytes(body, "tools"); tools.IsArray() && len(tools.Array()) > 0 { betas = append(betas, claudeAdvancedToolUseBeta) } betas = append(betas, claudeEffortBeta) if oauthToken && !requested[claudeFallbackCreditBeta] { betas = append(betas, claudeFallbackCreditBeta) } for _, beta := range claudeCodeTrailingBetas { if requested[beta] { betas = append(betas, beta) } } if claudeRequestUsesFastMode(body, requested) { betas = append(betas, claudeFastModeBeta) } if oauthToken { betas = append(betas, claudeExtendedCacheTTLBeta) } if diagnostics := gjson.GetBytes(body, "diagnostics"); diagnostics.IsObject() { betas = append(betas, claudeCacheDiagnosisBeta) } return strings.Join(betas, ",")}
// claudeBodyHasAdvisorTool reports whether the request body declares an// advisor server tool.func claudeBodyHasAdvisorTool(body []byte) bool { tools := gjson.GetBytes(body, "tools") if !tools.IsArray() { return false } for _, tool := range tools.Array() { toolType := strings.ToLower(strings.TrimSpace(tool.Get("type").String())) if strings.HasPrefix(toolType, "advisor_") { return true } } return false}
// claudeThinkingDisplaySet reports whether the request carries a thinking.display// value. Claude Code 2.1.220 and redact-thinking-2026-02-12 are mutually// exclusive by construction: the beta is only appended while thinking summaries// are off, and the request builder removes it again whenever a display value is// attached. Sending both makes Anthropic honour the redaction and return thinking// blocks with an empty thinking field, so the caller's summary request would be// answered with a signature and no text. Verified on api.anthropic.com with// claude-opus-4-8: display=summarized yields thinking text only when the beta is// absent, and a native 2.1.220 CLI run with showThinkingSummaries enabled sends// display=summarized without the beta.func claudeThinkingDisplaySet(body []byte) bool { display := gjson.GetBytes(body, "thinking.display") return display.Type == gjson.String && strings.TrimSpace(display.String()) != ""}
// claudeRequestUsesFastMode reports whether the request selects the fast service// tier. Anthropic rejects the body's speed field with "Extra inputs are not// permitted" unless fast-mode-2026-02-01 is declared, so the beta has to follow// the body. Deriving it here rather than at the call sites is deliberate: the// streaming and non-streaming paths previously disagreed and streaming silently// dropped the beta, turning every fast request into a 400.func claudeRequestUsesFastMode(body []byte, requested map[string]bool) bool { if requested[claudeFastModeBeta] { return true } speed := gjson.GetBytes(body, "speed") return speed.Type == gjson.String && strings.EqualFold(strings.TrimSpace(speed.String()), "fast")}
// claudeCountTokensBetas is the fixed profile Claude Code 2.1.220 sends to// /v1/messages/count_tokens. It is far smaller than the inference baseline:// redact-thinking, thinking-token-count, prompt-caching-scope, effort and every// conditional beta are absent. Verified identical across 37 captured calls.var claudeCountTokensBetas = []string{ claudeCodeBeta, "interleaved-thinking-2025-05-14", "context-management-2025-06-27", claudeTokenCountingBeta,}
func claudeCountTokensBetasForCredential(oauthToken bool) string { betas := make([]string, 0, len(claudeCountTokensBetas)+1) betas = append(betas, claudeCodeBeta) if oauthToken { betas = append(betas, claudeOAuthBeta) } betas = append(betas, claudeCountTokensBetas[1:]...) return strings.Join(betas, ",")}
func withClaudeCountTokensOAuthBeta(betas string) string { parts := make([]string, 0, len(claudeCountTokensBetas)+1) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if seen[claudeOAuthBeta] { return strings.Join(parts, ",") } insertAt := 0 if len(parts) > 0 && parts[0] == claudeCodeBeta { insertAt = 1 } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeOAuthBeta return strings.Join(parts, ",")}
// withClaudeOAuthCredentialBetas restores the credential-scoped betas that// describe the selected upstream OAuth account rather than caller capability.//// A confirmed native client authenticates to CPA with whatever key the user// configured and cannot know that CPA will select an OAuth credential upstream,// so its header never carries the OAuth betas. Passing it through verbatim ships// a Bearer request that declares neither oauth-2025-04-20 nor// extended-cache-ttl-2025-04-11, which no real OAuth client ever does. Passthrough// governs what the caller expressed; the credential is CPA's own choice and has to// be described accurately.//// Betas already present are left exactly where the caller put them.func withClaudeOAuthCredentialBetas(betas string) string { parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } if !seen[claudeOAuthBeta] { // Captured position 2, directly after claude-code-20250219. insertAt := 0 if len(parts) > 0 && parts[0] == claudeCodeBeta { insertAt = 1 } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeOAuthBeta } if !seen[claudeExtendedCacheTTLBeta] { parts = append(parts, claudeExtendedCacheTTLBeta) } return strings.Join(parts, ",")}
// withClaudeAdvisorToolBeta ensures advisor-tool-2026-03-01 is present when// the body declares an advisor server tool, placed at the observed wire position// before advanced-tool-use-2025-11-20 or effort-2025-11-24.func withClaudeAdvisorToolBeta(betas string) string { if strings.TrimSpace(betas) == "" { return claudeAdvisorToolBeta } parts := make([]string, 0, 16) seen := make(map[string]bool) for _, beta := range strings.Split(betas, ",") { if beta = strings.TrimSpace(beta); beta != "" && beta != claudeAdvisorToolBeta && !seen[beta] { parts = append(parts, beta) seen[beta] = true } } insertAt := len(parts) for index, beta := range parts { if beta == claudeAdvancedToolUseBeta || beta == claudeEffortBeta || beta == claudeServerSideFallbackBeta || beta == claudeFallbackCreditBeta || beta == claudeStructuredOutputsBeta || beta == claudeFastModeBeta || beta == claudeExtendedCacheTTLBeta || beta == claudeCacheDiagnosisBeta { insertAt = index break } } parts = append(parts, "") copy(parts[insertAt+1:], parts[insertAt:]) parts[insertAt] = claudeAdvisorToolBeta return strings.Join(parts, ",")}
// claudeEntitlementError marks an upstream refusal that is a property of the// request shape combined with the account's entitlements, not of the credential's// health. The auth manager must neither rotate nor cool down on these.type claudeEntitlementError struct { statusErr}
func (claudeEntitlementError) IsRequestScoped() bool { return true}
func (claudeEntitlementError) IsCredentialScoped() bool { return false}
type claudeRateLimitError struct { statusErr credentialScoped bool}
func (e claudeRateLimitError) IsCredentialScoped() bool { return e.credentialScoped}
func (e claudeRateLimitError) IsRequestScoped() bool { return false}
// classifyClaudeUpstreamError promotes upstream refusals that no other credential// can satisfy into request-scoped errors.//// Anthropic answers a fast-mode request from an account without the matching// usage credits with 429 rate_limit_error "Usage credits are required for fast// mode". The generic pipeline reads 429 as quota exhaustion: it marks the// credential Quota.Exceeded, applies an exponential cooldown and rotates to the// next one, which returns the same 429. A single speed:"fast" request would walk// the whole Claude pool and cool down every credential, all of which remain// perfectly healthy for ordinary traffic. The refusal belongs to the request.func classifyClaudeUpstreamError(statusCode int, headers http.Header, body []byte) error { var retryAfter *time.Duration if statusCode == http.StatusTooManyRequests || (statusCode >= 400 && statusCode < 600) { retryAfter = helps.ParseClaudeRateLimitReset(headers, time.Now()) } err := statusErr{code: statusCode, msg: string(body), retryAfter: retryAfter} if statusCode == http.StatusTooManyRequests { if helps.ClaudeHeadersIndicateUnifiedRateLimitRejection(headers) { return claudeRateLimitError{statusErr: err, credentialScoped: true} } if claudeBodyIndicatesFastModeCredits(body) { return claudeEntitlementError{err} } // Ordinary model-level Claude 429 (not a unified 5h/7d rejection) return claudeRateLimitError{statusErr: err, credentialScoped: false} } return err}
// claudeBodyIndicatesFastModeCredits matches Anthropic's fast-mode entitlement// refusal without matching a genuine rate limit, which never mentions fast mode.func claudeBodyIndicatesFastModeCredits(body []byte) bool { message := strings.ToLower(gjson.GetBytes(body, "error.message").String()) if message == "" { message = strings.ToLower(string(body)) } return strings.Contains(message, "fast request rejected") || (strings.Contains(message, "fast") && (strings.Contains(message, "usage credits") || strings.Contains(message, "credits are required")))}
// claudeRequestedBetas collects every beta the caller asked for, from the// Anthropic-Beta header and from betas lifted out of the request body.func claudeRequestedBetas(incomingBetas string, extraBetas []string) map[string]bool { requested := make(map[string]bool) for _, beta := range strings.Split(incomingBetas, ",") { if beta = strings.TrimSpace(beta); beta != "" { requested[beta] = true } } for _, beta := range extraBetas { if beta = strings.TrimSpace(beta); beta != "" { requested[beta] = true } } return requested}
// isAnthropicUpstreamURL reports whether a resolved request targets Anthropic's// first-party API.//// Every rule that reconstructs Claude Code's identity must key on this rather// than on the cloaked flag. Kimi rewrites base_url to api.kimi.com and custom// gateways set their own host, yet both delegate to ClaudeExecutor and are// therefore cloaked; a cloak-keyed rule silently rewrites their traffic too.func isAnthropicUpstreamURL(u *url.URL) bool { return helps.IsAnthropicUpstreamURL(u)}
// isAnthropicUpstreamBase reports whether a configured base URL targets Anthropic's// first-party API. Used before the outgoing request exists.func isAnthropicUpstreamBase(baseURL string) bool { parsed, err := url.Parse(strings.TrimSpace(baseURL)) if err != nil { return false } return isAnthropicUpstreamURL(parsed)}
// extractAndRemoveBetas extracts the "betas" array from the body and removes it.// Returns the extracted betas as a string slice and the modified body.func extractAndRemoveBetas(body []byte) ([]string, []byte) { betasResult := gjson.GetBytes(body, "betas") if !betasResult.Exists() { return nil, body } var betas []string if betasResult.IsArray() { for _, item := range betasResult.Array() { if s := strings.TrimSpace(item.String()); s != "" { betas = append(betas, s) } } } else if s := strings.TrimSpace(betasResult.String()); s != "" { betas = append(betas, s) } body, _ = sjson.DeleteBytes(body, "betas") return betas, body}
// disableThinkingIfToolChoiceForced checks if tool_choice forces tool use and disables thinking.// Anthropic API does not allow thinking when tool_choice is set to "any" or a specific tool.// See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerationsfunc disableThinkingIfToolChoiceForced(body []byte) []byte { toolChoiceType := gjson.GetBytes(body, "tool_choice.type").String() // "auto" is allowed with thinking, but "any" or "tool" (specific tool) are not if toolChoiceType == "any" || toolChoiceType == "tool" { // Remove thinking configuration entirely to avoid API error body, _ = sjson.DeleteBytes(body, "thinking") // Adaptive thinking may also set output_config.effort; remove it to avoid // leaking thinking controls when tool_choice forces tool use. body, _ = sjson.DeleteBytes(body, "output_config.effort") if oc := gjson.GetBytes(body, "output_config"); oc.Exists() && oc.IsObject() && len(oc.Map()) == 0 { body, _ = sjson.DeleteBytes(body, "output_config") } } return body}
// normalizeClaudeSamplingForUpstream keeps Anthropic message requests valid.//// Translated and cloaked callers keep the conservative normalization: their// sampling knobs come from a protocol that was not written for Anthropic, and// Anthropic rejects several combinations outright, so neither temperature nor// top_p is worth forwarding.//// A confirmed native Claude Code client owns its own wire, exactly like// cache_control placement. The measured structured Haiku helper sends// "temperature":1 and claudeCodeHelperShapeStructured keys on it, so stripping// it would emit a shape no native client ever produces. Keep what the caller// sent and drop only what Anthropic actually rejects (verified live):// - thinking active: temperature must be 1, top_p must be >= 0.95, top_k unset// - otherwise: temperature and top_p cannot both be specifiedfunc normalizeClaudeSamplingForUpstream(body []byte, nativeOwned bool) []byte { thinkingActive := false switch strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) { case "enabled", "adaptive", "auto": thinkingActive = true }
if !nativeOwned { body, _ = sjson.DeleteBytes(body, "temperature") body, _ = sjson.DeleteBytes(body, "top_p") if thinkingActive { body, _ = sjson.DeleteBytes(body, "top_k") } return body }
if thinkingActive { if temperature := gjson.GetBytes(body, "temperature"); temperature.Exists() && temperature.Num != 1 { body, _ = sjson.DeleteBytes(body, "temperature") } if topP := gjson.GetBytes(body, "top_p"); topP.Exists() && topP.Num < 0.95 { body, _ = sjson.DeleteBytes(body, "top_p") } body, _ = sjson.DeleteBytes(body, "top_k") return body } // Anthropic accepts either one but not both; temperature is the knob native // Claude Code actually sends, so top_p is the one that gives way. if gjson.GetBytes(body, "temperature").Exists() && gjson.GetBytes(body, "top_p").Exists() { body, _ = sjson.DeleteBytes(body, "top_p") } return body}
type compositeReadCloser struct { io.Reader closers []func() error}
func (c *compositeReadCloser) Close() error { var firstErr error for i := range c.closers { if c.closers[i] == nil { continue } if err := c.closers[i](); err != nil && firstErr == nil { firstErr = err } } return firstErr}
// peekableBody wraps a bufio.Reader around the original ReadCloser so that// magic bytes can be inspected without consuming them from the stream.type peekableBody struct { *bufio.Reader closer io.Closer}
func (p *peekableBody) Close() error { return p.closer.Close()}
func claudeResponseContentEncoding(header http.Header) string { return strings.Join(header.Values("Content-Encoding"), ",")}
func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadCloser, error) { if body == nil { return nil, fmt.Errorf("response body is nil") } if contentEncoding == "" { // No Content-Encoding header. Attempt best-effort magic-byte detection to // handle misbehaving upstreams that compress without setting the header. // Only gzip (1f 8b) and zstd (28 b5 2f fd) have reliable magic sequences; // br and deflate have none and are left as-is. // The bufio wrapper preserves unread bytes so callers always see the full // stream regardless of whether decompression was applied. pb := &peekableBody{Reader: bufio.NewReader(body), closer: body} magic, peekErr := pb.Peek(4) if peekErr == nil || (peekErr == io.EOF && len(magic) >= 2) { switch { case len(magic) >= 2 && magic[0] == 0x1f && magic[1] == 0x8b: gzipReader, gzErr := gzip.NewReader(pb) if gzErr != nil { _ = pb.Close() return nil, fmt.Errorf("magic-byte gzip: failed to create reader: %w", gzErr) } return &compositeReadCloser{ Reader: gzipReader, closers: []func() error{ gzipReader.Close, pb.Close, }, }, nil case len(magic) >= 4 && magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd: decoder, zdErr := zstd.NewReader(pb) if zdErr != nil { _ = pb.Close() return nil, fmt.Errorf("magic-byte zstd: failed to create reader: %w", zdErr) } return &compositeReadCloser{ Reader: decoder, closers: []func() error{ func() error { decoder.Close(); return nil }, pb.Close, }, }, nil } } return pb, nil } encodings := strings.Split(contentEncoding, ",") reader := io.Reader(body) decoderClosers := make([]func() error, 0, len(encodings)) cleanup := func() { for i := len(decoderClosers) - 1; i >= 0; i-- { _ = decoderClosers[i]() } _ = body.Close() } for index := len(encodings) - 1; index >= 0; index-- { encoding := strings.TrimSpace(strings.ToLower(encodings[index])) switch encoding { case "", "identity": continue case "gzip": gzipReader, errGzip := gzip.NewReader(reader) if errGzip != nil { cleanup() return nil, fmt.Errorf("failed to create gzip reader: %w", errGzip) } reader = gzipReader decoderClosers = append(decoderClosers, gzipReader.Close) case "deflate": deflateReader, errDeflate := newClaudeDeflateReader(reader) if errDeflate != nil { cleanup() return nil, errDeflate } reader = deflateReader decoderClosers = append(decoderClosers, deflateReader.Close) case "br": reader = brotli.NewReader(reader) case "zstd": decoder, errZstd := zstd.NewReader(reader) if errZstd != nil { cleanup() return nil, fmt.Errorf("failed to create zstd reader: %w", errZstd) } reader = decoder decoderClosers = append(decoderClosers, func() error { decoder.Close() return nil }) default: cleanup() return nil, fmt.Errorf("unsupported content encoding %q", encoding) } } if len(decoderClosers) == 0 && reader == body { return body, nil } closers := make([]func() error, 0, len(decoderClosers)+1) for index := len(decoderClosers) - 1; index >= 0; index-- { closers = append(closers, decoderClosers[index]) } closers = append(closers, body.Close) return &compositeReadCloser{Reader: reader, closers: closers}, nil}
func newClaudeDeflateReader(reader io.Reader) (io.ReadCloser, error) { buffered := bufio.NewReader(reader) header, errPeek := buffered.Peek(2) if errPeek == nil && isZlibHeader(header) { zlibReader, errZlib := zlib.NewReader(buffered) if errZlib != nil { return nil, fmt.Errorf("failed to create zlib deflate reader: %w", errZlib) } return zlibReader, nil } return flate.NewReader(buffered), nil}
func isZlibHeader(header []byte) bool { if len(header) < 2 { return false } cmf, flg := header[0], header[1] return cmf&0x0f == 8 && cmf>>4 <= 7 && (uint16(cmf)<<8|uint16(flg))%31 == 0}
// claudeCredentialUsesOAuth classifies the selected upstream credential. It is the// single authority for every decision that has to agree with the OAuth beta// profile, including the extended-cache-ttl beta and the matching body cache ttl.func claudeCredentialUsesOAuth(auth *cliproxyauth.Auth, apiKey string) bool { if isClaudeOAuthToken(apiKey) { return true } if auth != nil && auth.AuthKind() == cliproxyauth.AuthKindAPIKey { return false } hasAPIKeyAttr := auth != nil && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["api_key"]) != "" return !hasAPIKeyAttr}
func copyClaudeCallerFingerprintHeaders(dst, src http.Header) { if dst == nil || src == nil { return } for name, values := range src { lowerName := strings.ToLower(strings.TrimSpace(name)) if lowerName != "accept" && lowerName != "accept-encoding" && lowerName != "user-agent" && lowerName != "x-app" && lowerName != "x-client-request-id" && !strings.HasPrefix(lowerName, "anthropic-") && !strings.HasPrefix(lowerName, "x-stainless-") && !strings.HasPrefix(lowerName, "x-claude-code-") && !strings.HasPrefix(lowerName, "x-claude-remote-") && lowerName != "x-client-app" && lowerName != "x-anthropic-additional-protection" { continue } dst.Del(name) for _, value := range values { dst.Add(name, value) } }}
func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool, extraBetas []string, body []byte, cfg *config.Config, incomingHeaders http.Header, confirmedClaudeCode bool, sessionIDs ...string) error { return applyClaudeHeadersWithNativeProfile( r, auth, apiKey, stream, extraBetas, body, cfg, incomingHeaders, confirmedClaudeCode, false, sessionIDs..., )}
func applyClaudeHeadersWithNativeProfile( r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool, extraBetas []string, body []byte, cfg *config.Config, incomingHeaders http.Header, confirmedClaudeCode bool, helperProfile bool, sessionIDs ...string,) error { if r == nil { return nil } hdrDefault := func(cfgVal, fallback string) string { if cfgVal != "" { return cfgVal } return fallback }
var hd config.ClaudeHeaderDefaults if cfg != nil { hd = cfg.ClaudeHeaderDefaults }
// Authentication and wire fingerprint are separate authorities. File-backed // delegated providers still use Bearer auth, but only real Claude OAuth and // explicit fingerprint-profile opt-ins receive the CLI wire profile. credentialUsesBearer := claudeCredentialUsesOAuth(auth, apiKey) useAPIKey := !credentialUsesBearer fp := resolveClaudeFingerprintPolicy(cfg, auth, apiKey) wirePolicy, _ := resolveClaudeWirePolicy(cfg, auth, apiKey, confirmedClaudeCode) applyCLIFingerprint := fp.ProfileClaudeCodeCLI || wirePolicy.Cloak preserveCallerFingerprint := !applyCLIFingerprint && !confirmedClaudeCode useOAuthBetas := fp.UseOAuthBetas isAnthropicBase := isAnthropicUpstreamURL(r.URL) if strings.TrimSpace(apiKey) != "" { if isAnthropicBase && useAPIKey { r.Header.Del("Authorization") r.Header.Set("x-api-key", apiKey) } else { r.Header.Del("x-api-key") r.Header.Set("Authorization", "Bearer "+apiKey) } } else { r.Header.Del("Authorization") r.Header.Del("x-api-key") } r.Header.Set("Content-Type", "application/json")
if incomingHeaders == nil { if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { incomingHeaders = ginCtx.Request.Header } } stabilizeDeviceProfile := helps.ClaudeDeviceProfileStabilizationEnabled(cfg) var deviceProfile helps.ClaudeDeviceProfile if stabilizeDeviceProfile && confirmedClaudeCode { var errDeviceProfile error deviceProfile, errDeviceProfile = helps.ResolveClaudeDeviceProfileRequired(r.Context(), auth, apiKey, incomingHeaders, cfg) if errDeviceProfile != nil { return errDeviceProfile } }
incomingBetas := strings.TrimSpace(strings.Join(incomingHeaders.Values("Anthropic-Beta"), ",")) countTokens := r.URL != nil && strings.HasSuffix(r.URL.Path, "/count_tokens") requestedMap := claudeRequestedBetas(incomingBetas, extraBetas) advisorNeeded := requestedMap[claudeAdvisorToolBeta] || claudeBodyHasAdvisorTool(body)
baseBetas := incomingBetas if !preserveCallerFingerprint { baseBetas = claudeCodeCLIBetas(body, requestedMap, useOAuthBetas) if countTokens { baseBetas = claudeCountTokensBetasForCredential(useOAuthBetas) if advisorNeeded { baseBetas = withClaudeAdvisorToolBeta(baseBetas) } } } if confirmedClaudeCode && incomingBetas != "" { baseBetas = incomingBetas if advisorNeeded { baseBetas = withClaudeAdvisorToolBeta(baseBetas) } // Measured Haiku helper requests already carry the exact credential // beta profile and intentionally omit extended-cache-ttl. if useOAuthBetas && !helperProfile { if countTokens { baseBetas = withClaudeCountTokensOAuthBeta(baseBetas) } else { baseBetas = withClaudeOAuthCredentialBetas(baseBetas) } } } if preserveCallerFingerprint && advisorNeeded { baseBetas = withClaudeAdvisorToolBeta(baseBetas) } existingSet := make(map[string]bool) for _, beta := range strings.Split(baseBetas, ",") { if beta = strings.TrimSpace(beta); beta != "" { existingSet[beta] = true } } appendBeta := func(beta string) { beta = strings.TrimSpace(beta) if beta == "" || existingSet[beta] { return } if strings.TrimSpace(baseBetas) == "" { baseBetas = beta } else { baseBetas += "," + beta } existingSet[beta] = true } if preserveCallerFingerprint { // Caller-owned mode preserves both header and body-lifted betas verbatim. // The explicit speed=fast request still needs its protocol beta. if strings.EqualFold(strings.TrimSpace(gjson.GetBytes(body, "speed").String()), "fast") { appendBeta(claudeFastModeBeta) } for _, beta := range extraBetas { appendBeta(beta) } } else { // On direct Anthropic an unconfirmed CLI-profile caller's own betas are // dropped: appending them to the measured baseline produces a shape real // Claude Code never sends. Custom gateways keep caller extensions. if !confirmedClaudeCode && incomingBetas != "" && !isAnthropicBase { for _, beta := range strings.Split(incomingBetas, ",") { appendBeta(beta) } } if !isAnthropicBase { for _, beta := range extraBetas { appendBeta(beta) } } } applyBetaHeader := func() { if strings.TrimSpace(baseBetas) == "" { r.Header.Del("Anthropic-Beta") return } r.Header.Set("Anthropic-Beta", baseBetas) } applyBetaHeader()
if preserveCallerFingerprint { defaultAccept := "application/json" defaultAcceptEncoding := "gzip, deflate, br, zstd" if stream && !isAnthropicBase { defaultAccept = "text/event-stream" defaultAcceptEncoding = "identity" } copyClaudeCallerFingerprintHeaders(r.Header, incomingHeaders) misc.EnsureHeader(r.Header, incomingHeaders, "Anthropic-Version", "2023-06-01") misc.EnsureHeader(r.Header, incomingHeaders, "Accept", defaultAccept) misc.EnsureHeader(r.Header, incomingHeaders, "Accept-Encoding", defaultAcceptEncoding) // Caller-owned mode forwards the caller's own User-Agent, but a caller that // sent none must not fall through to Go's transport default // ("Go-http-client/1.1"), which upstreams read as a bot signature. Identify // as CPA instead: honest about the hop, and not a fabricated client. misc.EnsureHeader(r.Header, incomingHeaders, "User-Agent", "CLIProxyAPI/"+buildinfo.Version) applyBetaHeader() var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs, incomingHeaders) // Scope the custom-header escape hatch exactly like the CLI path below, which // claws overrides back on api.anthropic.com (an operator Anthropic-Beta reaches // a first-party API that rejects unknown values) and on any streaming request // (an Accept override silently disables event negotiation), while letting a // non-streaming third-party gateway keep them. Restoring here means restoring // the caller's own choice, not CPA's default: this mode is caller-owned. restoreCallerTransport := func() { resetHeader := func(name, fallback string) { if value := strings.TrimSpace(incomingHeaders.Get(name)); value != "" { r.Header.Set(name, value) return } r.Header.Set(name, fallback) } resetHeader("Accept", defaultAccept) resetHeader("Accept-Encoding", defaultAcceptEncoding) } if isAnthropicBase { applyBetaHeader() restoreCallerTransport() } else if stream { restoreCallerTransport() } return nil }
identityHeader := func(name, fallback string) { if confirmedClaudeCode { misc.EnsureHeader(r.Header, incomingHeaders, name, fallback) return } r.Header.Set(name, fallback) } identityHeader("Anthropic-Version", "2023-06-01") identityHeader("Anthropic-Dangerous-Direct-Browser-Access", "true") identityHeader("X-App", "cli") // Values below match Claude Code 2.1.258 / @anthropic-ai/sdk 0.112.1. identityHeader("X-Stainless-Retry-Count", "0") identityHeader("X-Stainless-Runtime", "node") identityHeader("X-Stainless-Lang", "js") // Native async SDK helpers add this header independently of body.stream. // Preserve it only after the complete native-client detector succeeds. if confirmedClaudeCode && incomingHeaders.Get("X-Stainless-Async") == "async" { r.Header.Set("X-Stainless-Async", "async") } // Claude Code omits X-Stainless-Timeout on count_tokens; only a confirmed // native client that sent one of its own keeps it there. if !countTokens { identityHeader("X-Stainless-Timeout", hdrDefault(hd.Timeout, "600")) } else if confirmedClaudeCode { if incomingTimeout := incomingHeaders.Get("X-Stainless-Timeout"); incomingTimeout != "" { r.Header.Set("X-Stainless-Timeout", incomingTimeout) } } // Selected-credential OAuth identity is an explicit native passthrough // exception. Callers pass the same agent-conversation UUID written to // metadata.user_id; legacy paths retain their previous cached fallback. sessionID := "" for _, candidate := range sessionIDs { if candidate = strings.TrimSpace(candidate); candidate != "" { sessionID = candidate break } } if sessionID != "" { r.Header.Set("X-Claude-Code-Session-Id", sessionID) } else { var errSessionID error sessionID, errSessionID = helps.CachedSessionIDRequired(r.Context(), apiKey) if errSessionID != nil { return errSessionID } identityHeader("X-Claude-Code-Session-Id", sessionID) } // Preserve native Claude Code subagent and environment headers when present in the incoming request. for _, hdr := range []string{ "X-Claude-Code-Agent-Id", "X-Claude-Code-Parent-Agent-Id", "X-Claude-Remote-Container-Id", "X-Claude-Remote-Session-Id", "X-Client-App", "X-Anthropic-Additional-Protection", } { if val := helps.HeaderValueCaseInsensitive(incomingHeaders, hdr); val != "" { r.Header.Set(hdr, val) } } // Per-request UUID, matches Claude Code's x-client-request-id for first-party API. // identityHeader prefers the incoming value for a confirmed client, so a confirmed // helper keeps its own native request ID and this fresh UUID only covers a caller // that sent none. Helpers opt in on custom gateways too. if isAnthropicBase || helperProfile { identityHeader("x-client-request-id", uuid.New().String()) } r.Header.Set("Connection", "keep-alive") // Regular Claude Code requests negotiate transport identically for streaming // and non-streaming requests. Measured Haiku helpers are the exception: their // minimal non-stream request offers gzip only, while the structured streaming // helper offers the full compression set. Confirmed helpers preserve the // incoming native values. applyTransportNegotiation := func() { if helperProfile { identityHeader("Accept", "application/json") identityHeader("Accept-Encoding", "gzip") return } if stream && !isAnthropicBase { // Other Anthropic-compatible upstreams (Kimi, custom gateways) may select // SSE from Accept and need not compress predictably, so they keep the // conservative contract. r.Header.Set("Accept", "text/event-stream") r.Header.Set("Accept-Encoding", "identity") return } r.Header.Set("Accept", "application/json") r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") } applyTransportNegotiation() // Confirmed Claude Code requests may contribute their real software profile. // Unconfirmed clients always receive the CLI baseline instead of being // allowed to populate or reuse another client's software profile. if stabilizeDeviceProfile { if confirmedClaudeCode { helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile) } else { helps.ApplyClaudeDefaultDeviceProfileHeaders(r, cfg) } } else { helps.ApplyClaudeLegacyDeviceHeaders(r, incomingHeaders, cfg, confirmedClaudeCode) } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs, incomingHeaders) // Custom credential headers are a configuration escape hatch for third-party // gateways, so they keep the last word there. On api.anthropic.com they must // not rewrite the reconstructed identity: an overridden Anthropic-Beta yields a // combination real Claude Code never sends and the API rejects, and an // overridden Accept-Encoding contradicts the negotiated transport. Both were // reachable because this ran after the whole header set was assembled. if isAnthropicBase { r.Header.Set("Anthropic-Beta", baseBetas) applyTransportNegotiation() } else if stream { // Elsewhere only streaming is protected, so an Accept override cannot // silently disable event negotiation. applyTransportNegotiation() } return nil}
// doClaudeUpstreamRequest is the single send boundary for every Claude upstream// call. Folding the wire-casing pass in here makes it structurally impossible// for one of the three request paths to drift away from the others, which is// exactly how the streaming and non-streaming beta sets diverged before.func doClaudeUpstreamRequest(client *http.Client, req *http.Request) (*http.Response, error) { applyClaudeWireHeaderCasing(req) cliproxyexecutor.MarkUpstreamAttempt(req.Context()) return client.Do(req)}
// claudeWireHeaderCasing maps Go's canonical header name to the exact casing// Claude Code 2.1.220 puts on the wire. Only the names that differ are listed;// the other twelve already survive canonicalisation unchanged.var claudeWireHeaderCasing = map[string]string{ "X-Stainless-Os": "X-Stainless-OS", "Anthropic-Beta": "anthropic-beta", "Anthropic-Version": "anthropic-version", "X-App": "x-app", "X-Client-Request-Id": "x-client-request-id",
"Anthropic-Dangerous-Direct-Browser-Access": "anthropic-dangerous-direct-browser-access",}
// applyClaudeWireHeaderCasing restores the header name casing of the real client.//// CPA negotiates ALPN http/1.1 with Anthropic, so header names reach the server// verbatim rather than lowercased by HPACK, which makes casing observable. Go// canonicalises every name passed through Header.Set, turning the client's// anthropic-beta and x-app into Anthropic-Beta and X-App. Writing the map keys// directly is the only way to keep the original casing.//// This also fixes ordering for free: Go sorts header names bytewise when it// serialises them, and the real client's order is exactly that same bytewise// sort, so correct casing reproduces the correct order. Host, User-Agent and// Content-Length remain misplaced because Go writes them ahead of the sorted// block; that needs transport-level surgery and is out of scope here.//// Call this immediately before handing the request to the client and nowhere// else. The rewritten keys are unreachable through Header.Get, which// canonicalises its argument, so running it any earlier would silently hide// these headers from the rest of the pipeline.func applyClaudeWireHeaderCasing(r *http.Request) { if r == nil || r.Header == nil || !isAnthropicUpstreamURL(r.URL) { return } for canonical, wire := range claudeWireHeaderCasing { values, ok := r.Header[canonical] if !ok { continue } delete(r.Header, canonical) r.Header[wire] = values }}
func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) { if a == nil { return "", "" } if a.Attributes != nil { apiKey = a.Attributes["api_key"] baseURL = a.Attributes["base_url"] } if apiKey == "" { apiKey = claudeauth.ReadMetadataString(&a.Metadata, "access_token") } return}
// claudePayloadHasMidSystemMessage reports whether the caller placed a// {"role":"system"} turn inside messages.func claudePayloadHasMidSystemMessage(payload []byte) bool { messages := gjson.GetBytes(payload, "messages") if !messages.IsArray() { return false } found := false messages.ForEach(func(_, message gjson.Result) bool { if strings.EqualFold(strings.TrimSpace(message.Get("role").String()), "system") { found = true return false } return true }) return found}
func rebuildMidSystemMessagesToTopLevel(payload []byte) []byte { messages := gjson.GetBytes(payload, "messages") if !messages.IsArray() { return payload }
var movedSystemParts []string keptMessages := make([]string, 0, int(messages.Get("#").Int())) messages.ForEach(func(_, message gjson.Result) bool { if strings.EqualFold(strings.TrimSpace(message.Get("role").String()), "system") { movedSystemParts = append(movedSystemParts, claudeSystemTextParts(message.Get("content"))...) return true } keptMessages = append(keptMessages, message.Raw) return true }) if len(movedSystemParts) == 0 { return payload }
systemParts := claudeSystemTextParts(gjson.GetBytes(payload, "system")) systemParts = append(systemParts, movedSystemParts...) if len(systemParts) > 0 { if updated, errSetSystem := sjson.SetRawBytes(payload, "system", rawJSONArray(systemParts)); errSetSystem == nil { payload = updated } } if updated, errSetMessages := sjson.SetRawBytes(payload, "messages", rawJSONArray(keptMessages)); errSetMessages == nil { payload = updated } return payload}
func claudeSystemTextParts(content gjson.Result) []string { if !content.Exists() { return nil } if content.Type == gjson.String { text := content.String() if strings.TrimSpace(text) == "" { return nil } block := []byte(`{"type":"text","text":""}`) block, _ = sjson.SetBytes(block, "text", text) return []string{string(block)} } if !content.IsArray() { return nil }
var parts []string content.ForEach(func(_, item gjson.Result) bool { if item.Type == gjson.String { text := item.String() if strings.TrimSpace(text) != "" { block := []byte(`{"type":"text","text":""}`) block, _ = sjson.SetBytes(block, "text", text) parts = append(parts, string(block)) } return true } if item.IsObject() && item.Get("type").String() == "text" && strings.TrimSpace(item.Get("text").String()) != "" { parts = append(parts, item.Raw) } return true }) return parts}
func rawJSONArray(items []string) []byte { if len(items) == 0 { return []byte("[]") } var builder strings.Builder builder.WriteByte('[') for i, item := range items { if i > 0 { builder.WriteByte(',') } builder.WriteString(item) } builder.WriteByte(']') return []byte(builder.String())}
func isClaudeOAuthToken(apiKey string) bool { return strings.Contains(apiKey, "sk-ant-oat")}
type claudeMCPAliasOptions struct { secret string}
func resolveClaudeMCPAliasOptions(ctx context.Context) claudeMCPAliasOptions { // Alias identity belongs to the downstream caller, not to the selected // upstream credential. This keeps names stable across OAuth refresh and auth // failover while giving one caller a shared virtual MCP server component. secret := strings.TrimSpace(helps.APIKeyFromContext(ctx)) if secret == "" { secret = "cpa-claude-mcp-default-caller" } return claudeMCPAliasOptions{secret: secret}}
// prepareClaudeOAuthToolNamesForUpstream applies one request-local MCP symbol// table across every Claude OAuth request path.func prepareClaudeOAuthToolNamesForUpstream(body []byte, mcpAliases claudeMCPAliasOptions) ([]byte, map[string]string) { return remapOAuthToolNamesWithOptions(body, mcpAliases)}
func restoreClaudeOAuthToolNamesFromResponse(body []byte, reverseMap map[string]string) ([]byte, error) { return reverseRemapOAuthToolNames(body, reverseMap)}
func restoreClaudeOAuthToolNamesFromStreamLine(line []byte, reverseMap map[string]string) ([]byte, error) { return reverseRemapOAuthToolNamesFromStreamLine(line, reverseMap)}
// remapOAuthToolNames represents every declared third-party client tool as a// semantic Claude Code MCP extension. Existing valid MCP names and explicit// typed Anthropic tools remain unchanged.//// It operates on tools[].name, tool_choice.name, and all declared// tool_use/tool_reference references in messages.//// The returned map is keyed on the upstream name and maps to the client-supplied// original name. Callers MUST pass this map to the reverse// functions so only aliases allocated for this request are restored on the// response. A global reverse map would mix symbols from unrelated callers.func remapOAuthToolNames(body []byte) ([]byte, map[string]string) { return remapOAuthToolNamesWithOptions(body, claudeMCPAliasOptions{secret: "cpa-claude-mcp-default-caller"})}
type claudeRawJSONEdit struct { start int end int replacement string}
func remapOAuthToolNamesWithOptions(body []byte, mcpAliases claudeMCPAliasOptions) ([]byte, map[string]string) { remapped, reverseMap, ok := remapOAuthToolNamesWithBatchedEdits(body, mcpAliases) if ok { return remapped, reverseMap } return remapOAuthToolNamesWithOptionsLegacy(body, mcpAliases)}
// remapOAuthToolNamesWithBatchedEdits records offsets from the original JSON// and applies every rename in one copy. Repeated sjson.SetBytes calls copy most// of the request for every historical tool reference, turning this path into// O(body size * reference count) allocation growth.func remapOAuthToolNamesWithBatchedEdits(body []byte, mcpAliases claudeMCPAliasOptions) ([]byte, map[string]string, bool) { if !gjson.ValidBytes(body) { return nil, nil, false }
reverseMap := make(map[string]string) recordRename := func(original, renamed string) { // Preserve the first-seen original name if the same upstream name is // produced from multiple call sites; they all map back identically. if _, exists := reverseMap[renamed]; !exists { reverseMap[renamed] = original } }
// Build one request-specific forward map from declarations. Every client // tool, including typed custom declarations and names resembling Claude // built-ins, gets an MCP alias. Historical references use this same map. tools := gjson.GetBytes(body, "tools") forwardMap := make(map[string]string) protectedNames := make(map[string]bool) reservedNames := helps.AugmentClaudeBuiltinToolRegistry(body, nil) if tools.Exists() && tools.IsArray() { tools.ForEach(func(_, tool gjson.Result) bool { name := tool.Get("name").String() if name != "" { reservedNames[name] = true } if helps.IsClaudeServerToolType(tool.Get("type").String()) { protectedNames[name] = true } return true }) passthroughMCPTools := make([]string, 0, 4) tools.ForEach(func(_, tool gjson.Result) bool { if helps.IsClaudeServerToolType(tool.Get("type").String()) { return true } name := tool.Get("name").String() if name == "" { return true } if helps.IsClaudeMCPToolName(name) { passthroughMCPTools = append(passthroughMCPTools, name) return true } if _, exists := forwardMap[name]; exists { return true } alias, allocated := helps.AllocateClaudeMCPToolAlias(mcpAliases.secret, name, reservedNames) if !allocated { log.Warnf("claude oauth mcp alias: no free alias left for tool %q, forwarding the original name", name) return true } forwardMap[name] = alias reservedNames[alias] = true return true }) recordPassthroughMCPTools(recordRename, forwardMap, passthroughMCPTools) }
rewriteName := func(name string) (string, bool) { if name == "" || protectedNames[name] || helps.IsClaudeMCPToolName(name) { return name, false } if newName, ok := forwardMap[name]; ok && newName != name { return newName, true } return name, false }
edits := make([]claudeRawJSONEdit, 0, len(forwardMap)+1) appendRawEdit := func(result gjson.Result, replacement string) bool { start := result.Index end := start + len(result.Raw) if result.Raw == "" || start < 0 || end < start || end > len(body) || !bytes.Equal(body[start:end], []byte(result.Raw)) { return false } edits = append(edits, claudeRawJSONEdit{start: start, end: end, replacement: replacement}) return true } appendStringEdit := func(result gjson.Result, replacement string) bool { // Generated aliases only emit [A-Za-z0-9_-], so adding quotes is // byte-identical to sjson's encoding without another allocation. return appendRawEdit(result, `"`+replacement+`"`) }
// 1. Rebuild typed custom tools exactly as before, but replace the original // tools array only after all offsets have been collected. toolsNeedRewrite := false if tools.Exists() && tools.IsArray() { tools.ForEach(func(_, tool gjson.Result) bool { toolType := tool.Get("type").String() if helps.IsClaudeServerToolType(toolType) { return true } if strings.TrimSpace(toolType) != "" { toolsNeedRewrite = true return false } name := tool.Get("name").String() _, toolsNeedRewrite = rewriteName(name) return !toolsNeedRewrite }) } if toolsNeedRewrite { var toolsJSON strings.Builder toolsJSON.WriteByte('[') toolCount := 0 tools.ForEach(func(_, tool gjson.Result) bool { if helps.IsClaudeServerToolType(tool.Get("type").String()) { if toolCount > 0 { toolsJSON.WriteByte(',') } toolsJSON.WriteString(tool.Raw) toolCount++ return true }
name := tool.Get("name").String() toolJSON := tool.Raw if strings.TrimSpace(tool.Get("type").String()) != "" { if updatedTool, errDelete := sjson.Delete(toolJSON, "type"); errDelete == nil { toolJSON = updatedTool } } if newName, renamed := rewriteName(name); renamed { updatedTool, err := sjson.Set(toolJSON, "name", newName) if err == nil { toolJSON = updatedTool recordRename(name, newName) } }
if toolCount > 0 { toolsJSON.WriteByte(',') } toolsJSON.WriteString(toolJSON) toolCount++ return true }) toolsJSON.WriteByte(']') if !appendRawEdit(tools, toolsJSON.String()) { return nil, nil, false } }
// 2. Rename tool_choice if it references a declared client tool. toolChoice := gjson.GetBytes(body, "tool_choice") if toolChoice.Get("type").String() == "tool" { nameResult := toolChoice.Get("name") tcName := nameResult.String() if newName, renamed := rewriteName(tcName); renamed { if !appendStringEdit(nameResult, newName) { return nil, nil, false } recordRename(tcName, newName) } }
// 3. Rename tool references in messages while every Result.Index still // points into the original request bytes. messages := gjson.GetBytes(body, "messages") validOffsets := true if messages.Exists() && messages.IsArray() { messages.ForEach(func(_, msg gjson.Result) bool { content := msg.Get("content") if !content.Exists() || !content.IsArray() { return true } content.ForEach(func(_, part gjson.Result) bool { switch part.Get("type").String() { case "tool_use": nameResult := part.Get("name") name := nameResult.String() if newName, renamed := rewriteName(name); renamed { if !appendStringEdit(nameResult, newName) { validOffsets = false return false } recordRename(name, newName) } case "tool_reference": nameResult := part.Get("tool_name") toolName := nameResult.String() if newName, renamed := rewriteName(toolName); renamed { if !appendStringEdit(nameResult, newName) { validOffsets = false return false } recordRename(toolName, newName) } case "tool_result": nestedContent := part.Get("content") if nestedContent.Exists() && nestedContent.IsArray() { nestedContent.ForEach(func(_, nestedPart gjson.Result) bool { if nestedPart.Get("type").String() != "tool_reference" { return true } nameResult := nestedPart.Get("tool_name") nestedToolName := nameResult.String() if newName, renamed := rewriteName(nestedToolName); renamed { if !appendStringEdit(nameResult, newName) { validOffsets = false return false } recordRename(nestedToolName, newName) } return true }) } case "tool_search_tool_result": toolRefs := part.Get("content.tool_references") if toolRefs.Exists() && toolRefs.IsArray() { toolRefs.ForEach(func(_, refPart gjson.Result) bool { if refPart.Get("type").String() != "tool_reference" { return true } nameResult := refPart.Get("tool_name") refToolName := nameResult.String() if newName, renamed := rewriteName(refToolName); renamed { if !appendStringEdit(nameResult, newName) { validOffsets = false return false } recordRename(refToolName, newName) } return true }) } } return validOffsets }) return validOffsets }) } if !validOffsets { return nil, nil, false }
remapped, ok := applyClaudeRawJSONEdits(body, edits) if !ok { return nil, nil, false } return remapped, reverseMap, true}
func applyClaudeRawJSONEdits(body []byte, edits []claudeRawJSONEdit) ([]byte, bool) { if len(edits) == 0 { return body, true } sort.Slice(edits, func(i, j int) bool { return edits[i].start < edits[j].start })
finalSize := len(body) cursor := 0 for _, edit := range edits { if edit.start < cursor || edit.start < 0 || edit.end < edit.start || edit.end > len(body) { return nil, false } finalSize += len(edit.replacement) - (edit.end - edit.start) if finalSize < 0 { return nil, false } cursor = edit.end }
out := make([]byte, 0, finalSize) cursor = 0 for _, edit := range edits { out = append(out, body[cursor:edit.start]...) out = append(out, edit.replacement...) cursor = edit.end } out = append(out, body[cursor:]...) return out, true}
// remapOAuthToolNamesWithOptionsLegacy is the byte-for-byte compatibility// fallback for malformed JSON or an unexpected GJSON offset. Keep it available// as a differential-test oracle for the batched implementation.func remapOAuthToolNamesWithOptionsLegacy(body []byte, mcpAliases claudeMCPAliasOptions) ([]byte, map[string]string) { reverseMap := make(map[string]string) recordRename := func(original, renamed string) { // Preserve the first-seen original name if the same upstream name is // produced from multiple call sites; they all map back identically. if _, exists := reverseMap[renamed]; !exists { reverseMap[renamed] = original } }
// Build one request-specific forward map from declarations. Every client // tool, including typed custom declarations and names resembling Claude // built-ins, gets an MCP alias. Historical references use this same map. tools := gjson.GetBytes(body, "tools") forwardMap := make(map[string]string) protectedNames := make(map[string]bool) reservedNames := helps.AugmentClaudeBuiltinToolRegistry(body, nil) if tools.Exists() && tools.IsArray() { tools.ForEach(func(_, tool gjson.Result) bool { name := tool.Get("name").String() if name != "" { reservedNames[name] = true } if helps.IsClaudeServerToolType(tool.Get("type").String()) { protectedNames[name] = true } return true }) passthroughMCPTools := make([]string, 0, 4) tools.ForEach(func(_, tool gjson.Result) bool { if helps.IsClaudeServerToolType(tool.Get("type").String()) { return true } name := tool.Get("name").String() if name == "" { return true } if helps.IsClaudeMCPToolName(name) { passthroughMCPTools = append(passthroughMCPTools, name) return true } if _, exists := forwardMap[name]; exists { return true } alias, allocated := helps.AllocateClaudeMCPToolAlias(mcpAliases.secret, name, reservedNames) if !allocated { log.Warnf("claude oauth mcp alias: no free alias left for tool %q, forwarding the original name", name) return true } forwardMap[name] = alias reservedNames[alias] = true return true }) recordPassthroughMCPTools(recordRename, forwardMap, passthroughMCPTools) }
rewriteName := func(name string) (string, bool) { if name == "" || protectedNames[name] || helps.IsClaudeMCPToolName(name) { return name, false } if newName, ok := forwardMap[name]; ok && newName != name { return newName, true } return name, false }
// 1. Rewrite the tools array without rebuilding from a stale gjson snapshot. toolsNeedRewrite := false if tools.Exists() && tools.IsArray() { tools.ForEach(func(_, tool gjson.Result) bool { toolType := tool.Get("type").String() if helps.IsClaudeServerToolType(toolType) { return true } if strings.TrimSpace(toolType) != "" { toolsNeedRewrite = true return false } name := tool.Get("name").String() _, toolsNeedRewrite = rewriteName(name) return !toolsNeedRewrite }) } if toolsNeedRewrite { var toolsJSON strings.Builder toolsJSON.WriteByte('[') toolCount := 0 tools.ForEach(func(_, tool gjson.Result) bool { if helps.IsClaudeServerToolType(tool.Get("type").String()) { if toolCount > 0 { toolsJSON.WriteByte(',') } toolsJSON.WriteString(tool.Raw) toolCount++ return true }
name := tool.Get("name").String() toolJSON := tool.Raw if strings.TrimSpace(tool.Get("type").String()) != "" { if updatedTool, errDelete := sjson.Delete(toolJSON, "type"); errDelete == nil { toolJSON = updatedTool } } if newName, renamed := rewriteName(name); renamed { updatedTool, err := sjson.Set(toolJSON, "name", newName) if err == nil { toolJSON = updatedTool recordRename(name, newName) } }
if toolCount > 0 { toolsJSON.WriteByte(',') } toolsJSON.WriteString(toolJSON) toolCount++ return true }) toolsJSON.WriteByte(']') body, _ = sjson.SetRawBytes(body, "tools", []byte(toolsJSON.String())) }
// 2. Rename tool_choice if it references a declared client tool. toolChoiceType := gjson.GetBytes(body, "tool_choice.type").String() if toolChoiceType == "tool" { tcName := gjson.GetBytes(body, "tool_choice.name").String() if newName, renamed := rewriteName(tcName); renamed { body, _ = sjson.SetBytes(body, "tool_choice.name", newName) recordRename(tcName, newName) } }
// 3. Rename tool references in messages messages := gjson.GetBytes(body, "messages") if messages.Exists() && messages.IsArray() { messages.ForEach(func(msgIndex, msg gjson.Result) bool { content := msg.Get("content") if !content.Exists() || !content.IsArray() { return true } content.ForEach(func(contentIndex, part gjson.Result) bool { partType := part.Get("type").String() switch partType { case "tool_use": name := part.Get("name").String() if newName, renamed := rewriteName(name); renamed { path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int()) body, _ = sjson.SetBytes(body, path, newName) recordRename(name, newName) } case "tool_reference": toolName := part.Get("tool_name").String() if newName, renamed := rewriteName(toolName); renamed { path := fmt.Sprintf("messages.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int()) body, _ = sjson.SetBytes(body, path, newName) recordRename(toolName, newName) } case "tool_result": // Handle nested tool_reference blocks inside tool_result.content[] toolID := part.Get("tool_use_id").String() _ = toolID // tool_use_id stays as-is nestedContent := part.Get("content") if nestedContent.Exists() && nestedContent.IsArray() { nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool { if nestedPart.Get("type").String() == "tool_reference" { nestedToolName := nestedPart.Get("tool_name").String() if newName, renamed := rewriteName(nestedToolName); renamed { nestedPath := fmt.Sprintf("messages.%d.content.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int(), nestedIndex.Int()) body, _ = sjson.SetBytes(body, nestedPath, newName) recordRename(nestedToolName, newName) } } return true }) } case "tool_search_tool_result": toolRefs := part.Get("content.tool_references") if toolRefs.Exists() && toolRefs.IsArray() { toolRefs.ForEach(func(refIndex, refPart gjson.Result) bool { if refPart.Get("type").String() == "tool_reference" { refToolName := refPart.Get("tool_name").String() if newName, renamed := rewriteName(refToolName); renamed { refPath := fmt.Sprintf("messages.%d.content.%d.content.tool_references.%d.tool_name", msgIndex.Int(), contentIndex.Int(), refIndex.Int()) body, _ = sjson.SetBytes(body, refPath, newName) recordRename(refToolName, newName) } } return true }) } } return true }) return true }) }
return body, reverseMap}
type claudeMCPAliasParts struct { server string toolID string semantic string}
type claudeMCPAliasEntry struct { alias string original string parts claudeMCPAliasParts}
type claudeMCPAliasResolver struct { exact map[string]string aliases []claudeMCPAliasEntry servers map[string]struct{}}
type claudeMCPAliasRestoreError struct { error}
func (e claudeMCPAliasRestoreError) Unwrap() error { return e.error}
func (claudeMCPAliasRestoreError) IsRequestScoped() bool { return true}
func newClaudeMCPAliasResolver(reverseMap map[string]string) claudeMCPAliasResolver { resolver := claudeMCPAliasResolver{ exact: reverseMap, aliases: make([]claudeMCPAliasEntry, 0, len(reverseMap)), servers: make(map[string]struct{}), } for alias, original := range reverseMap { if alias == original { // Caller-owned MCP tool recorded for exact passthrough only. It must not // register a virtual server or take part in fuzzy alias recovery. continue } parts, ok := parseClaudeMCPAlias(alias) if !ok { continue } resolver.aliases = append(resolver.aliases, claudeMCPAliasEntry{ alias: alias, original: original, parts: parts, }) resolver.servers[parts.server] = struct{}{} } return resolver}
func parseClaudeMCPAlias(name string) (claudeMCPAliasParts, bool) { if !helps.IsClaudeMCPToolName(name) { return claudeMCPAliasParts{}, false } rest, ok := strings.CutPrefix(name, "mcp__") if !ok { return claudeMCPAliasParts{}, false } server, tool, ok := strings.Cut(rest, "__") if !ok || server == "" { return claudeMCPAliasParts{}, false } toolID, semantic, ok := strings.Cut(tool, "_") if !ok || toolID == "" || semantic == "" { return claudeMCPAliasParts{}, false } return claudeMCPAliasParts{server: server, toolID: toolID, semantic: semantic}, true}
func claudeMCPAliasServer(name string) string { rest, ok := strings.CutPrefix(name, "mcp__") if !ok { return "" } server, _, ok := strings.Cut(rest, "__") if !ok { return "" } return server}
// recordPassthroughMCPTools remembers caller-owned MCP tool names that were left// untouched. Without this the response resolver would treat such a name as a// drifted alias whenever the derived two-word virtual server happens to equal a// real MCP server name, and would either restore the wrong tool or fail the// request. Recording is skipped when nothing was aliased so an untouched request// keeps an empty reverse map and the restore path stays a no-op.func recordPassthroughMCPTools(recordRename func(original, renamed string), forwardMap map[string]string, passthrough []string) { if len(forwardMap) == 0 { return } for _, name := range passthrough { recordRename(name, name) }}
func (resolver claudeMCPAliasResolver) resolve(name string) (string, bool, error) { if original, ok := resolver.exact[name]; ok { if original == name { // Caller-owned MCP tool: forward it exactly as the client declared it. return "", false, nil } return original, true, nil }
server := claudeMCPAliasServer(name) if _, known := resolver.servers[server]; !known { return "", false, nil }
canonicalServerPrefix := "mcp__" + server + "__" normalizedName := name suffix := strings.TrimPrefix(name, canonicalServerPrefix) for { strippedSuffix, repeatedServer := strings.CutPrefix(suffix, server+"__") if !repeatedServer { break } suffix = strippedSuffix normalizedName = canonicalServerPrefix + suffix if original, exact := resolver.exact[normalizedName]; exact { return original, true, nil } }
matchedOriginal := "" matchCount := 0 for _, entry := range resolver.aliases { if entry.parts.server == server && strings.HasSuffix(name, entry.alias) { matchedOriginal = entry.original matchCount++ } } if matchCount == 1 { return matchedOriginal, true, nil } if matchCount > 1 { return "", false, claudeMCPAliasRestoreError{fmt.Errorf("cannot restore Claude OAuth MCP tool alias %q: matched multiple declared aliases", name)} }
parts, validAlias := parseClaudeMCPAlias(normalizedName) if validAlias { for _, entry := range resolver.aliases { if entry.parts.server == parts.server && entry.parts.semantic == parts.semantic { matchedOriginal = entry.original matchCount++ } } } // Extra words in the tool component still parse, but the semantic field // is then wrong. Fall through to an unambiguous suffix match so word-level // repeats do not become restore 500s. if matchCount == 0 { var suffixMatches []claudeMCPAliasEntry for _, entry := range resolver.aliases { if entry.parts.server == server && strings.HasSuffix(normalizedName, "_"+entry.parts.semantic) { suffixMatches = append(suffixMatches, entry) } } if len(suffixMatches) == 1 { matchedOriginal = suffixMatches[0].original matchCount = 1 } else if len(suffixMatches) > 1 { // If multiple candidates match (e.g. "_file" and "_read_file"), // choose the strictly longest semantic match when unambiguous. longest := suffixMatches[0] tie := false for _, candidate := range suffixMatches[1:] { if len(candidate.parts.semantic) > len(longest.parts.semantic) { longest = candidate tie = false } else if len(candidate.parts.semantic) == len(longest.parts.semantic) { tie = true } } if !tie { matchedOriginal = longest.original matchCount = 1 } else { matchCount = len(suffixMatches) } } if matchCount == 1 { // This path guesses instead of failing, so leave a trace: it is the only // way to tell a silent wrong-tool restore from a healthy request. log.Debugf("claude oauth mcp alias: recovered drifted tool name %q as %q via semantic suffix", name, matchedOriginal) } } if matchCount == 1 { return matchedOriginal, true, nil } if matchCount > 1 { return "", false, claudeMCPAliasRestoreError{fmt.Errorf("cannot restore Claude OAuth MCP tool alias %q: semantic suffix matches multiple declared tools", name)} }
return "", false, claudeMCPAliasRestoreError{fmt.Errorf("cannot restore Claude OAuth MCP tool alias %q: no unique request-local match", name)}}
// reverseRemapOAuthToolNames reverses the tool name mapping for non-stream responses// using the per-request map produced by remapOAuthToolNames. Names outside the// request-local generated MCP server are passed through unchanged.func reverseRemapOAuthToolNames(body []byte, reverseMap map[string]string) ([]byte, error) { if len(reverseMap) == 0 { return body, nil } content := gjson.GetBytes(body, "content") if !content.Exists() || !content.IsArray() { return body, nil } resolver := newClaudeMCPAliasResolver(reverseMap) var resolveErr error content.ForEach(func(index, part gjson.Result) bool { partType := part.Get("type").String() switch partType { case "tool_use": name := part.Get("name").String() origName, matched, errResolve := resolver.resolve(name) if errResolve != nil { resolveErr = errResolve return false } if matched { path := fmt.Sprintf("content.%d.name", index.Int()) body, _ = sjson.SetBytes(body, path, origName) } case "tool_reference": toolName := part.Get("tool_name").String() origName, matched, errResolve := resolver.resolve(toolName) if errResolve != nil { resolveErr = errResolve return false } if matched { path := fmt.Sprintf("content.%d.tool_name", index.Int()) body, _ = sjson.SetBytes(body, path, origName) } case "tool_result": nestedContent := part.Get("content") if nestedContent.Exists() && nestedContent.IsArray() { nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool { if nestedPart.Get("type").String() != "tool_reference" { return true } toolName := nestedPart.Get("tool_name").String() origName, matched, errResolve := resolver.resolve(toolName) if errResolve != nil { resolveErr = errResolve return false } if matched { path := fmt.Sprintf("content.%d.content.%d.tool_name", index.Int(), nestedIndex.Int()) body, _ = sjson.SetBytes(body, path, origName) } return true }) } case "tool_search_tool_result": toolRefs := part.Get("content.tool_references") if toolRefs.Exists() && toolRefs.IsArray() { toolRefs.ForEach(func(refIndex, refPart gjson.Result) bool { if refPart.Get("type").String() != "tool_reference" { return true } toolName := refPart.Get("tool_name").String() origName, matched, errResolve := resolver.resolve(toolName) if errResolve != nil { resolveErr = errResolve return false } if matched { path := fmt.Sprintf("content.%d.content.tool_references.%d.tool_name", index.Int(), refIndex.Int()) body, _ = sjson.SetBytes(body, path, origName) } return true }) } } return resolveErr == nil }) return body, resolveErr}
// reverseRemapOAuthToolNamesFromStreamLine reverses the tool name mapping for SSE// stream lines, using the per-request reverseMap produced by remapOAuthToolNames.func reverseRemapOAuthToolNamesFromStreamLine(line []byte, reverseMap map[string]string) ([]byte, error) { if len(reverseMap) == 0 { return line, nil } payload := helps.JSONPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return line, nil }
contentBlock := gjson.GetBytes(payload, "content_block") if !contentBlock.Exists() { return line, nil }
resolver := newClaudeMCPAliasResolver(reverseMap) blockType := contentBlock.Get("type").String() var updated []byte var err error
switch blockType { case "tool_use": name := contentBlock.Get("name").String() origName, matched, errResolve := resolver.resolve(name) if errResolve != nil { return line, errResolve } if !matched { return line, nil } updated, err = sjson.SetBytes(payload, "content_block.name", origName) case "tool_reference": toolName := contentBlock.Get("tool_name").String() origName, matched, errResolve := resolver.resolve(toolName) if errResolve != nil { return line, errResolve } if !matched { return line, nil } updated, err = sjson.SetBytes(payload, "content_block.tool_name", origName) case "tool_search_tool_result": toolRefs := contentBlock.Get("content.tool_references") if !toolRefs.Exists() || !toolRefs.IsArray() { return line, nil } updatedPayload := payload var resolveErr error hasChange := false toolRefs.ForEach(func(refIndex, refPart gjson.Result) bool { if refPart.Get("type").String() != "tool_reference" { return true } toolName := refPart.Get("tool_name").String() origName, matched, errResolve := resolver.resolve(toolName) if errResolve != nil { resolveErr = errResolve return false } if matched { path := fmt.Sprintf("content_block.content.tool_references.%d.tool_name", refIndex.Int()) updatedPayload, err = sjson.SetBytes(updatedPayload, path, origName) if err != nil { return false } hasChange = true } return true }) if resolveErr != nil { return line, resolveErr } if err != nil { return line, fmt.Errorf("rewrite Claude OAuth MCP tool alias: %w", err) } if !hasChange { return line, nil } updated = updatedPayload default: return line, nil } if err != nil { return line, fmt.Errorf("rewrite Claude OAuth MCP tool alias: %w", err) }
trimmed := bytes.TrimSpace(line) if bytes.HasPrefix(trimmed, []byte("data:")) { return append([]byte("data: "), updated...), nil } return updated, nil}
func applyClaudeToolPrefix(body []byte, prefix string) []byte { if prefix == "" { return body }
// Collect built-in tool names from the authoritative fallback seed list and // augment it with any typed built-ins present in the current request body. builtinTools := helps.AugmentClaudeBuiltinToolRegistry(body, nil)
if tools := gjson.GetBytes(body, "tools"); tools.Exists() && tools.IsArray() { tools.ForEach(func(index, tool gjson.Result) bool { // Skip built-in tools (web_search, code_execution, etc.) which have // a "type" field and require their name to remain unchanged. if tool.Get("type").Exists() && tool.Get("type").String() != "" { if n := tool.Get("name").String(); n != "" { builtinTools[n] = true } return true } name := tool.Get("name").String() if name == "" || strings.HasPrefix(name, prefix) || helps.IsClaudeMCPToolName(name) { return true } path := fmt.Sprintf("tools.%d.name", index.Int()) body, _ = sjson.SetBytes(body, path, prefix+name) return true }) }
if gjson.GetBytes(body, "tool_choice.type").String() == "tool" { name := gjson.GetBytes(body, "tool_choice.name").String() if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] && !helps.IsClaudeMCPToolName(name) { body, _ = sjson.SetBytes(body, "tool_choice.name", prefix+name) } }
if messages := gjson.GetBytes(body, "messages"); messages.Exists() && messages.IsArray() { messages.ForEach(func(msgIndex, msg gjson.Result) bool { content := msg.Get("content") if !content.Exists() || !content.IsArray() { return true } content.ForEach(func(contentIndex, part gjson.Result) bool { partType := part.Get("type").String() switch partType { case "tool_use": name := part.Get("name").String() if name == "" || strings.HasPrefix(name, prefix) || builtinTools[name] || helps.IsClaudeMCPToolName(name) { return true } path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int()) body, _ = sjson.SetBytes(body, path, prefix+name) case "tool_reference": toolName := part.Get("tool_name").String() if toolName == "" || strings.HasPrefix(toolName, prefix) || builtinTools[toolName] || helps.IsClaudeMCPToolName(toolName) { return true } path := fmt.Sprintf("messages.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int()) body, _ = sjson.SetBytes(body, path, prefix+toolName) case "tool_result": // Handle nested tool_reference blocks inside tool_result.content[] nestedContent := part.Get("content") if nestedContent.Exists() && nestedContent.IsArray() { nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool { if nestedPart.Get("type").String() == "tool_reference" { nestedToolName := nestedPart.Get("tool_name").String() if nestedToolName != "" && !strings.HasPrefix(nestedToolName, prefix) && !builtinTools[nestedToolName] && !helps.IsClaudeMCPToolName(nestedToolName) { nestedPath := fmt.Sprintf("messages.%d.content.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int(), nestedIndex.Int()) body, _ = sjson.SetBytes(body, nestedPath, prefix+nestedToolName) } } return true }) } } return true }) return true }) }
return body}
func stripClaudeToolPrefixFromResponse(body []byte, prefix string) []byte { if prefix == "" { return body } content := gjson.GetBytes(body, "content") if !content.Exists() || !content.IsArray() { return body } content.ForEach(func(index, part gjson.Result) bool { partType := part.Get("type").String() switch partType { case "tool_use": name := part.Get("name").String() if !strings.HasPrefix(name, prefix) { return true } path := fmt.Sprintf("content.%d.name", index.Int()) body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(name, prefix)) case "tool_reference": toolName := part.Get("tool_name").String() if !strings.HasPrefix(toolName, prefix) { return true } path := fmt.Sprintf("content.%d.tool_name", index.Int()) body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(toolName, prefix)) case "tool_result": // Handle nested tool_reference blocks inside tool_result.content[] nestedContent := part.Get("content") if nestedContent.Exists() && nestedContent.IsArray() { nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool { if nestedPart.Get("type").String() == "tool_reference" { nestedToolName := nestedPart.Get("tool_name").String() if strings.HasPrefix(nestedToolName, prefix) { nestedPath := fmt.Sprintf("content.%d.content.%d.tool_name", index.Int(), nestedIndex.Int()) body, _ = sjson.SetBytes(body, nestedPath, strings.TrimPrefix(nestedToolName, prefix)) } } return true }) } } return true }) return body}
func stripClaudeToolPrefixFromStreamLine(line []byte, prefix string) []byte { if prefix == "" { return line } payload := helps.JSONPayload(line) if len(payload) == 0 || !gjson.ValidBytes(payload) { return line } contentBlock := gjson.GetBytes(payload, "content_block") if !contentBlock.Exists() { return line }
blockType := contentBlock.Get("type").String() var updated []byte var err error
switch blockType { case "tool_use": name := contentBlock.Get("name").String() if !strings.HasPrefix(name, prefix) { return line } updated, err = sjson.SetBytes(payload, "content_block.name", strings.TrimPrefix(name, prefix)) if err != nil { return line } case "tool_reference": toolName := contentBlock.Get("tool_name").String() if !strings.HasPrefix(toolName, prefix) { return line } updated, err = sjson.SetBytes(payload, "content_block.tool_name", strings.TrimPrefix(toolName, prefix)) if err != nil { return line } default: return line }
trimmed := bytes.TrimSpace(line) if bytes.HasPrefix(trimmed, []byte("data:")) { return append([]byte("data: "), updated...) } return updated}