forked from Gitlawb/openclaude
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp.ts
More file actions
170 lines (162 loc) · 6.46 KB
/
http.ts
File metadata and controls
170 lines (162 loc) · 6.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**
* HTTP utility constants and helpers
*/
import axios from 'axios'
import { OAUTH_BETA_HEADER } from '../constants/oauth.js'
import {
getAnthropicApiKey,
getClaudeAIOAuthTokens,
handleOAuth401Error,
isClaudeAISubscriber,
} from './auth.js'
import { getAPIProvider } from './model/providers.js'
import { getClaudeCodeUserAgent, getPublicBuildVersion } from './userAgent.js'
import { getWorkload } from './workloadContext.js'
// WARNING: Anthropic-owned endpoints rely on the `claude-cli` token for
// backend/log filtering compatibility.
// Please do NOT change this without making sure that logging also gets updated!
export function getUserAgent(): string {
const agentSdkVersion = process.env.CLAUDE_AGENT_SDK_VERSION
? `, agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}`
: ''
// SDK consumers can identify their app/library via CLAUDE_AGENT_SDK_CLIENT_APP
// e.g., "my-app/1.0.0" or "my-library/2.1"
const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP
? `, client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`
: ''
// Turn-/process-scoped workload tag for cron-initiated requests. 1P-only
// observability — proxies strip HTTP headers; QoS routing uses cc_workload
// in the billing-header attribution block instead (see constants/system.ts).
// getAnthropicClient (client.ts:98) calls this per-request inside withRetry,
// so the read picks up the same setWorkload() value as getAttributionHeader.
const workload = getWorkload()
const workloadSuffix = workload ? `, workload/${workload}` : ''
return `claude-cli/${MACRO.VERSION} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})`
}
// Provider-routed API requests can use OpenClaude branding as long as
// Anthropic first-party traffic keeps the compatibility token above.
export function getProviderApiUserAgent(
options?: { isFirstParty?: boolean; providerRouteId?: string },
): string {
const agentSdkVersion = process.env.CLAUDE_AGENT_SDK_VERSION
? `, agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}`
: ''
const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP
? `, client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`
: ''
const workload = getWorkload()
const workloadSuffix = workload ? `, workload/${workload}` : ''
const isFirstParty = options?.isFirstParty ?? getAPIProvider() === 'firstParty'
// Kimi Code and GitHub Copilot currently expect the upstream-compatible
// client token. GitHub Copilot whitelists `claude-cli` but may not
// whitelist `openclaude-cli` — keep compatibility until confirmed.
const requiresCompatibilityIdentity =
isFirstParty || options?.providerRouteId === 'kimi-code' || getAPIProvider() === 'github'
const productName = requiresCompatibilityIdentity
? 'claude-cli'
: 'openclaude-cli'
const version = requiresCompatibilityIdentity
? MACRO.VERSION
: getPublicBuildVersion()
return `${productName}/${version} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})`
}
export function getMCPUserAgent(): string {
const parts: string[] = []
if (process.env.CLAUDE_CODE_ENTRYPOINT) {
parts.push(process.env.CLAUDE_CODE_ENTRYPOINT)
}
if (process.env.CLAUDE_AGENT_SDK_VERSION) {
parts.push(`agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}`)
}
if (process.env.CLAUDE_AGENT_SDK_CLIENT_APP) {
parts.push(`client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`)
}
const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : ''
return `claude-code/${getPublicBuildVersion()}${suffix}`
}
// User-Agent for WebFetch requests to arbitrary sites. `Claude-User` is
// The first-party provider's publicly documented agent for user-initiated fetches (what site
// operators match in robots.txt); the claude-code suffix lets them distinguish
// local CLI traffic from claude.ai server-side fetches.
export function getWebFetchUserAgent(): string {
const supportUrl =
getAPIProvider() === 'firstParty'
? 'https://support.anthropic.com/'
: 'https://github.com/Gitlawb/openclaude'
return `Claude-User (${getClaudeCodeUserAgent()}; +${supportUrl})`
}
export type AuthHeaders = {
headers: Record<string, string>
error?: string
}
/**
* Get authentication headers for API requests
* Returns either OAuth headers for Max/Pro users or API key headers for regular users
*/
export function getAuthHeaders(): AuthHeaders {
if (isClaudeAISubscriber()) {
const oauthTokens = getClaudeAIOAuthTokens()
if (!oauthTokens?.accessToken) {
return {
headers: {},
error: 'No OAuth token available',
}
}
return {
headers: {
Authorization: `Bearer ${oauthTokens.accessToken}`,
'anthropic-beta': OAUTH_BETA_HEADER,
},
}
}
// TODO: this will fail if the API key is being set to an LLM Gateway key
// should we try to query keychain / credentials for a valid Anthropic key?
const apiKey = getAnthropicApiKey()
if (!apiKey) {
return {
headers: {},
error: 'No API key available',
}
}
return {
headers: {
'x-api-key': apiKey,
},
}
}
/**
* Wrapper that handles OAuth 401 errors by force-refreshing the token and
* retrying once. Addresses clock drift scenarios where the local expiration
* check disagrees with the server.
*
* The request closure is called again on retry, so it should re-read auth
* (e.g., via getAuthHeaders()) to pick up the refreshed token.
*
* Note: bridgeApi.ts has its own DI-injected version — handleOAuth401Error
* transitively pulls in config.ts (~1300 modules), which breaks the SDK bundle.
*
* @param opts.also403Revoked - Also retry on 403 with "OAuth token has been
* revoked" body (some endpoints signal revocation this way instead of 401).
*/
export async function withOAuth401Retry<T>(
request: () => Promise<T>,
opts?: { also403Revoked?: boolean },
): Promise<T> {
try {
return await request()
} catch (err) {
if (!axios.isAxiosError(err)) throw err
const status = err.response?.status
const isAuthError =
status === 401 ||
(opts?.also403Revoked &&
status === 403 &&
typeof err.response?.data === 'string' &&
err.response.data.includes('OAuth token has been revoked'))
if (!isAuthError) throw err
const failedAccessToken = getClaudeAIOAuthTokens()?.accessToken
if (!failedAccessToken) throw err
await handleOAuth401Error(failedAccessToken)
return await request()
}
}