mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-07 00:24:17 +08:00
Claude Code issues measured Haiku helper requests that intentionally omit the claude-code beta and, for the minimal shape, the system field. Treat those profiles as confirmed native clients so CPA does not cloak or rewrite them. Accept the native metadata builder optional parent_session_id, require platform headers only for presence and software baseline rather than exact equality, derive timeout and version from the same header defaults the emitter uses, and keep discriminating signals strict: exact beta allowlist, body shape, lowercase hex CCH, and session binding. Preserve helper transport headers, omit synthetic stream false on markerless helpers, and gate the billing and CCH fallback on system presence so a later pipeline-attached system prompt cannot go upstream unsigned while the measured no-system wire stays intact.
518 lines
19 KiB
Go
518 lines
19 KiB
Go
package helps
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
const (
|
|
// claudeAnthropicVersion is the only Anthropic-Version Claude Code sends.
|
|
claudeAnthropicVersion = "2023-06-01"
|
|
// claudeDefaultStainlessTimeout is the X-Stainless-Timeout every measured
|
|
// native helper sends. It is deliberately NOT read from
|
|
// claude-header-defaults.timeout: applyClaudeHeaders routes a confirmed client
|
|
// through misc.EnsureHeader, which prefers the incoming header and only falls
|
|
// back to the configured value when the caller sent none. A confirmed helper
|
|
// therefore always forwards its own 600, so comparing against the operator
|
|
// value would make any non-600 configuration reject every genuine helper.
|
|
claudeDefaultStainlessTimeout = "600"
|
|
)
|
|
|
|
var (
|
|
claudeCodeUserAgentPattern = regexp.MustCompile(`(?i)^claude-cli/`)
|
|
claudeCodeUserAgentDetailsPattern = regexp.MustCompile(`(?i)^claude-cli/\S+\s+\(external,\s*([^,)]+)(?:,\s*agent-sdk/([^,)]+))?`)
|
|
claudeCodeNativeUserAgentPattern = regexp.MustCompile(`(?i)^claude-cli/[0-9]+\.[0-9]+\.[0-9]+\s+\(external,\s*[^,)]+(?:,\s*agent-sdk/[0-9]+\.[0-9]+\.[0-9]+)?\)$`)
|
|
)
|
|
|
|
var claudeCodeSubclientByEntrypoint = map[string]string{
|
|
"cli": "claude-code-cli",
|
|
"mcp": "claude-code-mcp",
|
|
"bench": "claude-code-bench",
|
|
"sdk-cli": "claude-code-cli-sdk",
|
|
"sdk-ts": "claude-code-sdk-ts",
|
|
"sdk-py": "claude-code-sdk-py",
|
|
"claude-vscode": "claude-code-vscode",
|
|
"claude-code-github-action": "claude-code-gh-action",
|
|
"local-agent": "claude-local-agent",
|
|
"local_agent": "claude-local-agent",
|
|
"claude-desktop": "claude-desktop",
|
|
"claude-desktop-3p": "claude-desktop-3p",
|
|
"remote": "claude-remote",
|
|
"remote_baku": "claude-remote-baku",
|
|
"remote_cowork": "claude-remote-cowork",
|
|
"remote_trigger": "claude-remote-trigger",
|
|
"remote_desktop": "claude-remote-desktop",
|
|
"remote_mobile": "claude-remote-mobile",
|
|
"claude_in_slack": "claude-in-slack",
|
|
"claude-in-slack": "claude-in-slack",
|
|
"claude-in-teams": "claude-in-teams",
|
|
"claude-security": "claude-security",
|
|
"ssh-remote": "claude-ssh-remote",
|
|
"claude-coworker": "claude-coworker",
|
|
"claude-coworker-terminal": "claude-coworker-terminal",
|
|
}
|
|
|
|
// Only product surfaces with verified 2.1.220 wire behavior are eligible for
|
|
// pass-through. Other first-party-looking entrypoints are cloaked until their
|
|
// CPA-reachable request shape has been captured and reviewed.
|
|
var nativeClaudeEntrypoints = map[string]bool{
|
|
"cli": true,
|
|
"sdk-cli": true,
|
|
"claude-vscode": true,
|
|
}
|
|
|
|
type claudeCodeHelperShape uint8
|
|
|
|
const (
|
|
claudeCodeHelperShapeNone claudeCodeHelperShape = iota
|
|
claudeCodeHelperShapeMinimal
|
|
claudeCodeHelperShapeStructured
|
|
|
|
claudeCodeHelperModel = "claude-haiku-4-5-20251001"
|
|
)
|
|
|
|
// These are the six exact beta sequences observed across 14 markerless native
|
|
// Claude Code 2.1.220 Haiku helper requests. Keeping the allowlist exact avoids
|
|
// turning the helper exception into a generic no-claude-code-beta bypass.
|
|
var measuredClaudeCodeHelperBetaProfiles = map[string]claudeCodeHelperShape{
|
|
claudeCodeHelperBetaProfile(true): claudeCodeHelperShapeMinimal,
|
|
claudeCodeHelperBetaProfile(false): claudeCodeHelperShapeMinimal,
|
|
claudeCodeHelperBetaProfile(true,
|
|
"advisor-tool-2026-03-01",
|
|
"structured-outputs-2025-12-15",
|
|
"cache-diagnosis-2026-04-07",
|
|
): claudeCodeHelperShapeStructured,
|
|
claudeCodeHelperBetaProfile(true,
|
|
"structured-outputs-2025-12-15",
|
|
"fallback-credit-2026-06-01",
|
|
): claudeCodeHelperShapeStructured,
|
|
claudeCodeHelperBetaProfile(true,
|
|
"structured-outputs-2025-12-15",
|
|
): claudeCodeHelperShapeStructured,
|
|
claudeCodeHelperBetaProfile(false,
|
|
"structured-outputs-2025-12-15",
|
|
): claudeCodeHelperShapeStructured,
|
|
}
|
|
|
|
// ClaudeCodeRequestDetection records the strong signals and first-party
|
|
// subclient identity used to distinguish an official Claude Code request from
|
|
// a client that only copied its User-Agent.
|
|
type ClaudeCodeRequestDetection struct {
|
|
Confirmed bool
|
|
StrongSignals bool
|
|
NativeClient bool
|
|
XAppCLI bool
|
|
UserAgent bool
|
|
BetasPresent bool
|
|
MetadataUserID bool
|
|
HelperProfile bool
|
|
Entrypoint string
|
|
Subclient string
|
|
AgentSDKVersion string
|
|
}
|
|
|
|
// DetectClaudeCodeRequest first mirrors CCH's strong-signal contract, then
|
|
// applies CPA's native-client policy. Standard Messages requests require all
|
|
// four strong signals; count_tokens omits metadata.user_id. A separate narrow
|
|
// profile recognizes measured native Haiku helper requests that intentionally
|
|
// omit claude-code-20250219. Generic sdk-ts/sdk-py Agent SDK entrypoints remain
|
|
// unconfirmed and receive CLI cloaking.
|
|
func DetectClaudeCodeRequest(headers http.Header, payload []byte, countTokens bool, configs ...*config.Config) ClaudeCodeRequestDetection {
|
|
var cfg *config.Config
|
|
if len(configs) > 0 {
|
|
cfg = configs[0]
|
|
}
|
|
userAgent := headerValue(headers, "User-Agent")
|
|
entrypoint, agentSDKVersion := parseClaudeCodeUserAgentDetails(userAgent)
|
|
detection := ClaudeCodeRequestDetection{
|
|
XAppCLI: headerValue(headers, "X-App") == "cli",
|
|
UserAgent: plausibleClaudeCodeUserAgent(userAgent, cfg),
|
|
BetasPresent: headerContainsClaudeCodeBeta(headers),
|
|
Entrypoint: entrypoint,
|
|
Subclient: claudeCodeSubclientByEntrypoint[entrypoint],
|
|
AgentSDKVersion: agentSDKVersion,
|
|
}
|
|
|
|
metadataUserID := gjson.GetBytes(payload, "metadata.user_id")
|
|
detection.MetadataUserID = metadataUserID.Exists() && metadataUserID.Type == gjson.String && isValidUserID(metadataUserID.String())
|
|
detection.NativeClient = nativeClaudeEntrypoints[entrypoint]
|
|
standardSignals := detection.XAppCLI && detection.UserAgent && detection.BetasPresent && (countTokens || detection.MetadataUserID)
|
|
detection.HelperProfile = detection.NativeClient && matchesMeasuredClaudeCodeHelperProfile(headers, payload, countTokens, detection, cfg)
|
|
detection.StrongSignals = standardSignals || detection.HelperProfile
|
|
detection.Confirmed = detection.StrongSignals && detection.NativeClient
|
|
return detection
|
|
}
|
|
|
|
func claudeCodeHelperBetaProfile(redactThinking bool, trailing ...string) string {
|
|
betas := []string{"oauth-2025-04-20", "interleaved-thinking-2025-05-14"}
|
|
if redactThinking {
|
|
betas = append(betas, "redact-thinking-2026-02-12")
|
|
}
|
|
betas = append(betas,
|
|
"thinking-token-count-2026-05-13",
|
|
"context-management-2025-06-27",
|
|
"prompt-caching-scope-2026-01-05",
|
|
)
|
|
betas = append(betas, trailing...)
|
|
return strings.Join(betas, ",")
|
|
}
|
|
|
|
func matchesMeasuredClaudeCodeHelperProfile(
|
|
headers http.Header,
|
|
payload []byte,
|
|
countTokens bool,
|
|
detection ClaudeCodeRequestDetection,
|
|
cfg *config.Config,
|
|
) bool {
|
|
if countTokens ||
|
|
detection.Entrypoint != "cli" ||
|
|
detection.BetasPresent ||
|
|
!detection.XAppCLI ||
|
|
!detection.UserAgent ||
|
|
!detection.MetadataUserID {
|
|
return false
|
|
}
|
|
|
|
shape := measuredClaudeCodeHelperBetaProfiles[normalizedClaudeBetaHeader(headers)]
|
|
if shape == claudeCodeHelperShapeNone || measuredClaudeCodeHelperBodyShape(payload) != shape {
|
|
return false
|
|
}
|
|
if !measuredClaudeCodeHelperHeadersMatch(headers, cfg, shape) {
|
|
return false
|
|
}
|
|
return measuredClaudeCodeHelperSessionMatches(headers, payload)
|
|
}
|
|
|
|
// normalizedClaudeBetaHeader joins every Anthropic-Beta value in wire order.
|
|
// Values() is tried first so canonical headers keep a deterministic order; the
|
|
// case-insensitive fallback only exists for hand-built header maps that store a
|
|
// non-canonical key, where ranging the map alone would be order-dependent.
|
|
func normalizedClaudeBetaHeader(headers http.Header) string {
|
|
if headers == nil {
|
|
return ""
|
|
}
|
|
values := headers.Values("Anthropic-Beta")
|
|
if len(values) == 0 {
|
|
keys := make([]string, 0, 2)
|
|
for key := range headers {
|
|
if strings.EqualFold(key, "Anthropic-Beta") {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
values = append(values, headers[key]...)
|
|
}
|
|
}
|
|
betas := make([]string, 0, 12)
|
|
for _, value := range values {
|
|
for _, beta := range strings.Split(value, ",") {
|
|
if beta = strings.TrimSpace(beta); beta != "" {
|
|
betas = append(betas, beta)
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(betas, ",")
|
|
}
|
|
|
|
// measuredClaudeCodeHelperHeadersMatch validates the helper transport envelope.
|
|
//
|
|
// Platform and software-version headers are deliberately NOT compared for
|
|
// equality. The device-profile pipeline this detector feeds already pins OS/Arch
|
|
// to the configured baseline and replaces a non-baseline software tuple instead
|
|
// of rejecting it, so demanding equality here would classify a genuine Claude
|
|
// Code helper from Windows/Linux, or from a different Node or SDK build, as a
|
|
// foreign client and cloak it. Values that carry real discriminating power - the
|
|
// exact beta allowlist, the body shape, the billing CCH and the session binding -
|
|
// stay strict.
|
|
func measuredClaudeCodeHelperHeadersMatch(headers http.Header, cfg *config.Config, shape claudeCodeHelperShape) bool {
|
|
profile := defaultClaudeDeviceProfile(cfg)
|
|
expected := map[string]string{
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
"X-Stainless-Lang": "js",
|
|
"X-Stainless-Runtime": "node",
|
|
"X-Stainless-Retry-Count": "0",
|
|
"X-Stainless-Timeout": claudeDefaultStainlessTimeout,
|
|
"Anthropic-Version": claudeAnthropicVersion,
|
|
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
|
}
|
|
for name, want := range expected {
|
|
if headerValue(headers, name) != want {
|
|
return false
|
|
}
|
|
}
|
|
// Presence is still required: the native SDK always sends these.
|
|
for _, name := range []string{
|
|
"X-Stainless-Package-Version",
|
|
"X-Stainless-Runtime-Version",
|
|
"X-Stainless-OS",
|
|
"X-Stainless-Arch",
|
|
} {
|
|
if headerValue(headers, name) == "" {
|
|
return false
|
|
}
|
|
}
|
|
candidate := ClaudeDeviceProfile{
|
|
UserAgent: headerValue(headers, "User-Agent"),
|
|
PackageVersion: headerValue(headers, "X-Stainless-Package-Version"),
|
|
RuntimeVersion: headerValue(headers, "X-Stainless-Runtime-Version"),
|
|
}
|
|
if version, ok := parseClaudeCLIVersion(candidate.UserAgent); ok {
|
|
candidate.version = version
|
|
candidate.hasVersion = true
|
|
}
|
|
if !meetsClaudeDeviceProfileBaseline(candidate, profile) {
|
|
return false
|
|
}
|
|
if async := headerValue(headers, "X-Stainless-Async"); (shape == claudeCodeHelperShapeStructured && async != "async") ||
|
|
(shape == claudeCodeHelperShapeMinimal && async != "") {
|
|
return false
|
|
}
|
|
compression := headerValue(headers, "Accept-Encoding")
|
|
if (shape == claudeCodeHelperShapeStructured && compression != "gzip, deflate, br, zstd") ||
|
|
(shape == claudeCodeHelperShapeMinimal && compression != "gzip") {
|
|
return false
|
|
}
|
|
requestID := headerValue(headers, "X-Client-Request-Id")
|
|
_, errRequestID := uuid.Parse(requestID)
|
|
return errRequestID == nil
|
|
}
|
|
|
|
func measuredClaudeCodeHelperSessionMatches(headers http.Header, payload []byte) bool {
|
|
metadata := gjson.GetBytes(payload, "metadata")
|
|
if !metadata.IsObject() || !claudeJSONObjectHasKeys([]byte(metadata.Raw), []string{"user_id"}) {
|
|
return false
|
|
}
|
|
userID := metadata.Get("user_id")
|
|
if userID.Type != gjson.String || !isValidUserID(userID.String()) {
|
|
return false
|
|
}
|
|
// The native metadata builder is
|
|
// {...extraMetadata, device_id, account_uuid, session_id, ...parentSessionId && {parent_session_id}}
|
|
// in 2.1.220, 2.1.221 and 2.1.227 alike, so parent_session_id is a legitimate
|
|
// optional trailing key for sub-agent and forked sessions. Rejecting it would
|
|
// cloak the helper requests those sessions issue.
|
|
identityRaw := []byte(userID.String())
|
|
if !claudeJSONObjectHasKeys(identityRaw, []string{"device_id", "account_uuid", "session_id"}) &&
|
|
!claudeJSONObjectHasKeys(identityRaw, []string{"device_id", "account_uuid", "session_id", "parent_session_id"}) {
|
|
return false
|
|
}
|
|
return headerValue(headers, ClaudeCodeSessionHeader) == gjson.GetBytes(identityRaw, "session_id").String()
|
|
}
|
|
|
|
func measuredClaudeCodeHelperBodyShape(payload []byte) claudeCodeHelperShape {
|
|
minimalKeys := []string{"model", "max_tokens", "messages", "metadata"}
|
|
structuredKeys := []string{"model", "messages", "system", "tools", "metadata", "max_tokens", "thinking", "temperature", "output_config", "stream"}
|
|
shape := claudeCodeHelperShapeNone
|
|
switch {
|
|
case claudeJSONObjectHasKeys(payload, minimalKeys):
|
|
shape = claudeCodeHelperShapeMinimal
|
|
case claudeJSONObjectHasKeys(payload, structuredKeys):
|
|
shape = claudeCodeHelperShapeStructured
|
|
default:
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
|
|
maxTokens := gjson.GetBytes(payload, "max_tokens")
|
|
if gjson.GetBytes(payload, "model").String() != claudeCodeHelperModel ||
|
|
maxTokens.Type != gjson.Number {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
messages := gjson.GetBytes(payload, "messages")
|
|
if !messages.IsArray() || len(messages.Array()) != 1 {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
message := messages.Get("0")
|
|
if !claudeJSONObjectHasKeys([]byte(message.Raw), []string{"role", "content"}) ||
|
|
message.Get("role").String() != "user" {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
|
|
if shape == claudeCodeHelperShapeMinimal {
|
|
if maxTokens.Raw != "1" || message.Get("content").Type != gjson.String {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
return shape
|
|
}
|
|
|
|
content := message.Get("content")
|
|
if !content.IsArray() || len(content.Array()) != 1 {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
contentBlock := content.Get("0")
|
|
if !claudeJSONObjectHasKeys([]byte(contentBlock.Raw), []string{"type", "text"}) ||
|
|
contentBlock.Get("type").String() != "text" {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
if !measuredClaudeCodeHelperSystemMatches(gjson.GetBytes(payload, "system")) {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
if tools := gjson.GetBytes(payload, "tools"); !tools.IsArray() || len(tools.Array()) != 0 {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
thinking := gjson.GetBytes(payload, "thinking")
|
|
outputConfig := gjson.GetBytes(payload, "output_config")
|
|
if !claudeJSONObjectHasKeys([]byte(thinking.Raw), []string{"type"}) ||
|
|
thinking.Get("type").String() != "disabled" {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
format := outputConfig.Get("format")
|
|
schema := format.Get("schema")
|
|
properties := schema.Get("properties")
|
|
titleProperty := properties.Get("title")
|
|
required := schema.Get("required")
|
|
additionalProperties := schema.Get("additionalProperties")
|
|
if !claudeJSONObjectHasKeys([]byte(outputConfig.Raw), []string{"format"}) ||
|
|
!claudeJSONObjectHasKeys([]byte(format.Raw), []string{"type", "schema"}) ||
|
|
format.Get("type").String() != "json_schema" ||
|
|
!claudeJSONObjectHasKeys([]byte(schema.Raw), []string{"type", "properties", "required", "additionalProperties"}) ||
|
|
schema.Get("type").String() != "object" ||
|
|
!claudeJSONObjectHasKeys([]byte(properties.Raw), []string{"title"}) ||
|
|
!claudeJSONObjectHasKeys([]byte(titleProperty.Raw), []string{"type"}) ||
|
|
titleProperty.Get("type").String() != "string" ||
|
|
!required.IsArray() || len(required.Array()) != 1 || required.Get("0").String() != "title" ||
|
|
additionalProperties.Type != gjson.False {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
temperature := gjson.GetBytes(payload, "temperature")
|
|
if maxTokens.Raw != "32000" ||
|
|
temperature.Raw != "1" ||
|
|
gjson.GetBytes(payload, "stream").Type != gjson.True {
|
|
return claudeCodeHelperShapeNone
|
|
}
|
|
return shape
|
|
}
|
|
|
|
func measuredClaudeCodeHelperSystemMatches(system gjson.Result) bool {
|
|
if !system.IsArray() || len(system.Array()) != 3 {
|
|
return false
|
|
}
|
|
for _, block := range system.Array() {
|
|
if !claudeJSONObjectHasKeys([]byte(block.Raw), []string{"type", "text"}) || block.Get("type").String() != "text" {
|
|
return false
|
|
}
|
|
}
|
|
billing := system.Get("0.text").String()
|
|
identity := system.Get("1.text").String()
|
|
return strings.HasPrefix(billing, "x-anthropic-billing-header:") && measuredClaudeBillingCCH(billing) && strings.HasPrefix(identity, "You are Claude Code")
|
|
}
|
|
|
|
// measuredClaudeBillingCCH validates the five lowercase hexadecimal characters the
|
|
// native billing header carries. It duplicates isLowerHex in
|
|
// internal/runtime/executor/claude_signing.go because the signing side lives in the
|
|
// package that imports this one; keep the two definitions in step.
|
|
func measuredClaudeBillingCCH(billing string) bool {
|
|
marker := strings.Index(billing, " cch=")
|
|
if marker < 0 {
|
|
return false
|
|
}
|
|
valueStart := marker + len(" cch=")
|
|
valueEnd := valueStart + 5
|
|
if valueEnd >= len(billing) || billing[valueEnd] != ';' {
|
|
return false
|
|
}
|
|
for _, character := range billing[valueStart:valueEnd] {
|
|
decimal := character >= '0' && character <= '9'
|
|
lowerHex := character >= 'a' && character <= 'f'
|
|
if !decimal && !lowerHex {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func claudeJSONObjectHasKeys(raw []byte, want []string) bool {
|
|
if !json.Valid(raw) {
|
|
return false
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
opening, errOpening := decoder.Token()
|
|
if errOpening != nil || opening != json.Delim('{') {
|
|
return false
|
|
}
|
|
keyIndex := 0
|
|
for decoder.More() {
|
|
token, errToken := decoder.Token()
|
|
if errToken != nil {
|
|
return false
|
|
}
|
|
key, okKey := token.(string)
|
|
if !okKey || keyIndex >= len(want) || key != want[keyIndex] {
|
|
return false
|
|
}
|
|
keyIndex++
|
|
var value json.RawMessage
|
|
if errValue := decoder.Decode(&value); errValue != nil {
|
|
return false
|
|
}
|
|
}
|
|
closing, errClosing := decoder.Token()
|
|
return errClosing == nil && closing == json.Delim('}') && keyIndex == len(want)
|
|
}
|
|
|
|
func plausibleClaudeCodeUserAgent(userAgent string, cfg *config.Config) bool {
|
|
userAgent = strings.TrimSpace(userAgent)
|
|
if !claudeCodeUserAgentPattern.MatchString(userAgent) || !claudeCodeNativeUserAgentPattern.MatchString(userAgent) {
|
|
return false
|
|
}
|
|
candidate, okCandidate := parseClaudeCLIVersion(userAgent)
|
|
baseline, okBaseline := parseClaudeCLIVersion(defaultClaudeDeviceProfile(cfg).UserAgent)
|
|
return okCandidate && okBaseline && plausibleClaudeCLIVersion(candidate, baseline)
|
|
}
|
|
|
|
func parseClaudeCodeUserAgentDetails(userAgent string) (entrypoint, agentSDKVersion string) {
|
|
matches := claudeCodeUserAgentDetailsPattern.FindStringSubmatch(strings.TrimSpace(userAgent))
|
|
if len(matches) < 2 {
|
|
return "", ""
|
|
}
|
|
entrypoint = strings.ToLower(strings.TrimSpace(matches[1]))
|
|
if len(matches) >= 3 {
|
|
agentSDKVersion = strings.TrimSpace(matches[2])
|
|
}
|
|
return entrypoint, agentSDKVersion
|
|
}
|
|
|
|
func headerValue(headers http.Header, name string) string {
|
|
if headers == nil {
|
|
return ""
|
|
}
|
|
if value := headers.Get(name); value != "" {
|
|
return value
|
|
}
|
|
for key, values := range headers {
|
|
if !strings.EqualFold(key, name) || len(values) == 0 {
|
|
continue
|
|
}
|
|
return values[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func headerContainsClaudeCodeBeta(headers http.Header) bool {
|
|
if headers == nil {
|
|
return false
|
|
}
|
|
for key, values := range headers {
|
|
if !strings.EqualFold(key, "Anthropic-Beta") {
|
|
continue
|
|
}
|
|
for _, value := range values {
|
|
for _, beta := range strings.Split(value, ",") {
|
|
if strings.TrimSpace(beta) == "claude-code-20250219" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|