Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
|
|
@ -0,0 +1,517 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue