@oh-my-pi/pi-ai 17.2.0 → 17.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/types/auth-storage.d.ts +12 -0
- package/dist/types/providers/anthropic.d.ts +8 -10
- package/dist/types/providers/claude-code-fingerprint.d.ts +12 -8
- package/dist/types/providers/cowork-fetch.d.ts +3 -0
- package/dist/types/utils/proxy.d.ts +2 -0
- package/package.json +4 -4
- package/src/auth-storage.ts +27 -0
- package/src/providers/anthropic.ts +49 -69
- package/src/providers/claude-code-fingerprint.ts +12 -11
- package/src/providers/cowork-fetch.ts +201 -0
- package/src/stream.ts +12 -4
- package/src/utils/proxy.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.1] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added exact OAuth credential-row resolution by durable credential id. The targeted path refreshes only that row and never ranks, rotates, or falls back to sibling accounts.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Anthropic OAuth requests now reproduce Cowork's current `claude-desktop` request profile, including client/runtime metadata, beta selection, system and billing attestation, the 64K output cap, and stable HTTP/1.1 header ordering.
|
|
14
|
+
|
|
5
15
|
## [17.2.0] - 2026-07-30
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -1083,6 +1083,18 @@ export declare class AuthStorage {
|
|
|
1083
1083
|
* overrides have replaced OAuth with an explicit API key.
|
|
1084
1084
|
*/
|
|
1085
1085
|
getOAuthAccessAt(provider: string, position: number, options?: AuthApiKeyOptions): Promise<OAuthAccessResolution | undefined>;
|
|
1086
|
+
/**
|
|
1087
|
+
* Resolve one stored OAuth credential by its durable storage row id.
|
|
1088
|
+
*
|
|
1089
|
+
* Unlike the normal session resolver, this method never ranks, rotates, or
|
|
1090
|
+
* falls back to sibling credentials. A forced refresh re-mints only the
|
|
1091
|
+
* requested row, preserving exact-account affinity for operations whose
|
|
1092
|
+
* provenance and policy boundary are tied to one workspace.
|
|
1093
|
+
*
|
|
1094
|
+
* Returns `undefined` when the row does not exist for `provider` or an
|
|
1095
|
+
* explicit runtime/config API-key override suppresses OAuth.
|
|
1096
|
+
*/
|
|
1097
|
+
getOAuthAccessByCredentialId(provider: string, credentialId: number, options?: AuthApiKeyOptions): Promise<OAuthAccessResolution | undefined>;
|
|
1086
1098
|
/**
|
|
1087
1099
|
* List saved rate-limit resets for every stored OAuth account of `provider`
|
|
1088
1100
|
* (Codex), fetched LIVE from the dedicated `rate-limit-reset-credits` route.
|
|
@@ -10,7 +10,7 @@ export type AnthropicHeaderOptions = {
|
|
|
10
10
|
modelHeaders?: Record<string, string>;
|
|
11
11
|
isCloudflareAiGateway?: boolean;
|
|
12
12
|
claudeCodeSessionId?: string;
|
|
13
|
-
|
|
13
|
+
coworkBetas?: readonly string[];
|
|
14
14
|
/** Allow explicit fingerprint headers to replace OAuth defaults on non-official endpoints. */
|
|
15
15
|
allowAnthropicHeaderOverrides?: boolean;
|
|
16
16
|
};
|
|
@@ -33,19 +33,17 @@ export declare function clearAnthropicFastModeFallback(providerSessionState: Map
|
|
|
33
33
|
*/
|
|
34
34
|
export declare function isAnthropicFastModeFallbackDisabled(providerSessionState: Map<string, ProviderSessionState> | undefined, model: Model<Api>): boolean;
|
|
35
35
|
export * from "./claude-code-fingerprint.js";
|
|
36
|
-
export declare function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}`;
|
|
37
36
|
export declare function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}`;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"X-Stainless-
|
|
37
|
+
/** Static headers emitted by Cowork's Linux Claude runtime. */
|
|
38
|
+
export declare const coworkHeaders: {
|
|
39
|
+
"X-Stainless-Arch": "arm64" | "x64" | "x86" | `other::${string}`;
|
|
40
|
+
"X-Stainless-Lang": string;
|
|
41
|
+
"X-Stainless-OS": string;
|
|
41
42
|
"X-Stainless-Package-Version": string;
|
|
43
|
+
"X-Stainless-Retry-Count": string;
|
|
42
44
|
"X-Stainless-Runtime": string;
|
|
43
|
-
"X-Stainless-
|
|
44
|
-
"X-Stainless-Arch": "arm64" | "x64" | "x86" | `other::${string}`;
|
|
45
|
-
"X-Stainless-OS": "FreeBSD" | "Linux" | "MacOS" | "Windows" | `Other::${string}`;
|
|
45
|
+
"X-Stainless-Runtime-Version": string;
|
|
46
46
|
"X-Stainless-Timeout": string;
|
|
47
|
-
"anthropic-client-platform": string;
|
|
48
|
-
"anthropic-client-version": string;
|
|
49
47
|
};
|
|
50
48
|
/**
|
|
51
49
|
* Wraps a fetch implementation to patch the Claude Code billing-header `cch`
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Cowork inference-fingerprint constants, kept in a leaf module so consumers
|
|
3
|
+
* outside the provider (`registry/oauth/anthropic`, `usage/claude`) don't
|
|
4
|
+
* import the heavy `providers/anthropic` module.
|
|
5
|
+
*
|
|
5
6
|
* That import edge was a live init cycle: `providers/anthropic` → `stream` →
|
|
6
7
|
* `registry` → `registry/oauth/anthropic` → back into the still-initializing
|
|
7
|
-
* provider module
|
|
8
|
-
* `providers/anthropic` was the first module loaded.
|
|
8
|
+
* provider module.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
export declare const
|
|
12
|
-
|
|
10
|
+
/** Claude runtime version bundled by the current Cowork desktop release. */
|
|
11
|
+
export declare const claudeCodeVersion = "2.1.220";
|
|
12
|
+
/** User-Agent emitted by Cowork's `claude-desktop` inference entrypoint. */
|
|
13
|
+
export declare const coworkUserAgent = "claude-cli/2.1.220 (external, claude-desktop)";
|
|
14
|
+
/** Prefix used to isolate custom Anthropic OAuth tools from built-in tools. */
|
|
13
15
|
export declare const claudeToolPrefix: string;
|
|
16
|
+
/** Identity block prepended by Cowork's Claude runtime. */
|
|
14
17
|
export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
18
|
+
/** Cowork's per-request output-token ceiling. */
|
|
15
19
|
export declare const CLAUDE_CODE_MAX_OUTPUT_TOKENS = 64000;
|
|
@@ -29,6 +29,8 @@ export interface ConnectProxiedSocketOptions {
|
|
|
29
29
|
signal?: AbortSignal;
|
|
30
30
|
/** Maximum wall-clock time to establish the final TLS tunnel. Disabled when absent or non-positive. */
|
|
31
31
|
timeoutMs?: number;
|
|
32
|
+
/** Target TLS profile. Cursor defaults to HTTP/2 when this is absent. */
|
|
33
|
+
tls?: tls.ConnectionOptions;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Tunnel a socket connection through an HTTP CONNECT proxy.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.1",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "17.2.1",
|
|
42
|
+
"@oh-my-pi/pi-utils": "17.2.1",
|
|
43
|
+
"@oh-my-pi/pi-wire": "17.2.1",
|
|
44
44
|
"arktype": "2.2.3",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/auth-storage.ts
CHANGED
|
@@ -5476,6 +5476,33 @@ export class AuthStorage {
|
|
|
5476
5476
|
return this.#resolveStoredOAuthAccess(provider, selection, providerKey, options);
|
|
5477
5477
|
}
|
|
5478
5478
|
|
|
5479
|
+
/**
|
|
5480
|
+
* Resolve one stored OAuth credential by its durable storage row id.
|
|
5481
|
+
*
|
|
5482
|
+
* Unlike the normal session resolver, this method never ranks, rotates, or
|
|
5483
|
+
* falls back to sibling credentials. A forced refresh re-mints only the
|
|
5484
|
+
* requested row, preserving exact-account affinity for operations whose
|
|
5485
|
+
* provenance and policy boundary are tied to one workspace.
|
|
5486
|
+
*
|
|
5487
|
+
* Returns `undefined` when the row does not exist for `provider` or an
|
|
5488
|
+
* explicit runtime/config API-key override suppresses OAuth.
|
|
5489
|
+
*/
|
|
5490
|
+
async getOAuthAccessByCredentialId(
|
|
5491
|
+
provider: string,
|
|
5492
|
+
credentialId: number,
|
|
5493
|
+
options?: AuthApiKeyOptions,
|
|
5494
|
+
): Promise<OAuthAccessResolution | undefined> {
|
|
5495
|
+
if (this.#runtimeOverrides.has(provider) || this.#configOverrides.has(provider)) {
|
|
5496
|
+
return undefined;
|
|
5497
|
+
}
|
|
5498
|
+
const selection = this.#getStoredOAuthSelections(provider).find(
|
|
5499
|
+
candidate => candidate.credentialId === credentialId,
|
|
5500
|
+
);
|
|
5501
|
+
if (!selection) return undefined;
|
|
5502
|
+
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
5503
|
+
return this.#resolveStoredOAuthAccess(provider, selection, providerKey, options);
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5479
5506
|
/**
|
|
5480
5507
|
* List saved rate-limit resets for every stored OAuth account of `provider`
|
|
5481
5508
|
* (Codex), fetched LIVE from the dedicated `rate-limit-reset-credits` route.
|
|
@@ -87,11 +87,10 @@ import {
|
|
|
87
87
|
} from "./anthropic-wire";
|
|
88
88
|
import {
|
|
89
89
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS,
|
|
90
|
-
claudeAgentSdkVersion,
|
|
91
|
-
claudeClientVersion,
|
|
92
90
|
claudeCodeSystemInstruction,
|
|
93
91
|
claudeCodeVersion,
|
|
94
92
|
claudeToolPrefix,
|
|
93
|
+
coworkUserAgent,
|
|
95
94
|
} from "./claude-code-fingerprint";
|
|
96
95
|
import {
|
|
97
96
|
buildCopilotDynamicHeaders,
|
|
@@ -111,7 +110,7 @@ export type AnthropicHeaderOptions = {
|
|
|
111
110
|
modelHeaders?: Record<string, string>;
|
|
112
111
|
isCloudflareAiGateway?: boolean;
|
|
113
112
|
claudeCodeSessionId?: string;
|
|
114
|
-
|
|
113
|
+
coworkBetas?: readonly string[];
|
|
115
114
|
/** Allow explicit fingerprint headers to replace OAuth defaults on non-official endpoints. */
|
|
116
115
|
allowAnthropicHeaderOverrides?: boolean;
|
|
117
116
|
};
|
|
@@ -158,52 +157,49 @@ function mergeAnthropicBetaHeader(callerHeaders: Record<string, string>, beta: s
|
|
|
158
157
|
const midConversationSystemBeta = "mid-conversation-system-2026-04-07";
|
|
159
158
|
const contextManagementBeta = "context-management-2025-06-27";
|
|
160
159
|
const structuredOutputsBeta = "structured-outputs-2025-12-15";
|
|
161
|
-
const
|
|
162
|
-
|
|
160
|
+
const context1mBeta = "context-1m-2025-08-07";
|
|
161
|
+
const thinkingTokenCountBeta = "thinking-token-count-2026-05-13";
|
|
162
|
+
const fallbackCreditBeta = "fallback-credit-2026-06-01";
|
|
163
|
+
const coworkUtilityBetaDefaults = [
|
|
163
164
|
"interleaved-thinking-2025-05-14",
|
|
165
|
+
thinkingTokenCountBeta,
|
|
164
166
|
contextManagementBeta,
|
|
165
167
|
"prompt-caching-scope-2026-01-05",
|
|
166
168
|
structuredOutputsBeta,
|
|
167
169
|
] as const;
|
|
168
|
-
const
|
|
170
|
+
const coworkAgentBetaDefaults = [
|
|
169
171
|
"claude-code-20250219",
|
|
170
|
-
"oauth-2025-04-20",
|
|
171
172
|
"interleaved-thinking-2025-05-14",
|
|
173
|
+
thinkingTokenCountBeta,
|
|
172
174
|
contextManagementBeta,
|
|
173
175
|
"prompt-caching-scope-2026-01-05",
|
|
174
176
|
midConversationSystemBeta,
|
|
175
177
|
"advanced-tool-use-2025-11-20",
|
|
176
178
|
] as const;
|
|
177
179
|
const extendedCacheTtlBeta = "extended-cache-ttl-2025-04-11";
|
|
178
|
-
const claudeCodeAgentPostEffortBetas = [extendedCacheTtlBeta] as const;
|
|
179
180
|
const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
|
|
180
181
|
const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
|
|
181
|
-
// Asks the API to redact thinking blocks from responses. Only sent when the
|
|
182
|
-
// caller explicitly hides thinking (`thinkingDisplay: "omitted"`); sending it
|
|
183
|
-
// by default suppresses the thinking traces callers expect to stream.
|
|
184
|
-
const redactThinkingBeta = "redact-thinking-2026-02-12";
|
|
185
182
|
const fastModeBeta = "fast-mode-2026-02-01";
|
|
186
183
|
const taskBudgetBeta = "task-budgets-2026-03-13";
|
|
187
184
|
const effortBeta = "effort-2025-11-24";
|
|
188
185
|
const serverSideFallbackBeta = "server-side-fallback-2026-06-01";
|
|
189
186
|
|
|
190
|
-
function
|
|
187
|
+
function buildCoworkBetas(
|
|
191
188
|
agentRequest: boolean,
|
|
192
189
|
thinkingRequest: boolean,
|
|
193
|
-
|
|
190
|
+
longContext: boolean,
|
|
194
191
|
disableStrictTools = false,
|
|
195
192
|
): readonly string[] {
|
|
196
|
-
if (!agentRequest && !
|
|
193
|
+
if (!agentRequest && !disableStrictTools) return coworkUtilityBetaDefaults;
|
|
197
194
|
const betas: string[] = [];
|
|
198
|
-
for (const beta of agentRequest ?
|
|
195
|
+
for (const beta of agentRequest ? coworkAgentBetaDefaults : coworkUtilityBetaDefaults) {
|
|
199
196
|
if (disableStrictTools && beta === structuredOutputsBeta) continue;
|
|
200
197
|
betas.push(beta);
|
|
201
|
-
|
|
202
|
-
if (redactThinking && beta === interleavedThinkingBeta) betas.push(redactThinkingBeta);
|
|
198
|
+
if (agentRequest && longContext && beta === "claude-code-20250219") betas.push(context1mBeta);
|
|
203
199
|
}
|
|
204
200
|
if (!agentRequest) return betas;
|
|
205
201
|
if (thinkingRequest) betas.push(effortBeta);
|
|
206
|
-
betas.push(
|
|
202
|
+
betas.push(fallbackCreditBeta);
|
|
207
203
|
return betas;
|
|
208
204
|
}
|
|
209
205
|
|
|
@@ -246,11 +242,10 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
246
242
|
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
247
243
|
const incomingAuthorization = getHeaderCaseInsensitive(options.modelHeaders, "Authorization");
|
|
248
244
|
const incomingApiKey = getHeaderCaseInsensitive(options.modelHeaders, "X-Api-Key");
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// the streaming path (buildAnthropicClientOptions passes [] for non-OAuth).
|
|
245
|
+
// Cowork's beta profile is part of the OAuth fingerprint; API-key requests
|
|
246
|
+
// default to extras only, matching the streaming path.
|
|
252
247
|
const betaHeader = buildBetaHeader(
|
|
253
|
-
options.
|
|
248
|
+
options.coworkBetas ?? (oauthToken ? buildCoworkBetas(true, true, false) : []),
|
|
254
249
|
extraBetas,
|
|
255
250
|
);
|
|
256
251
|
const acceptHeader = oauthToken ? "application/json" : stream ? "text/event-stream" : "application/json";
|
|
@@ -308,19 +303,22 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
308
303
|
}
|
|
309
304
|
|
|
310
305
|
if (oauthToken) {
|
|
311
|
-
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
|
|
312
|
-
? incomingUserAgent
|
|
313
|
-
: `claude-cli/${claudeCodeVersion} (external, local-agent, agent-sdk/${claudeAgentSdkVersion})`;
|
|
306
|
+
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent) ? incomingUserAgent : coworkUserAgent;
|
|
314
307
|
const headers = {
|
|
315
308
|
...modelHeaders,
|
|
316
|
-
...claudeCodeHeaders,
|
|
317
309
|
Accept: acceptHeader,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
"User-Agent": userAgent,
|
|
321
312
|
...(options.claudeCodeSessionId ? { "X-Claude-Code-Session-Id": options.claudeCodeSessionId } : {}),
|
|
313
|
+
...coworkHeaders,
|
|
314
|
+
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
315
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
316
|
+
"anthropic-version": "2023-06-01",
|
|
317
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
318
|
+
"x-app": "cli",
|
|
322
319
|
"x-client-request-id": nodeCrypto.randomUUID(),
|
|
323
|
-
"
|
|
320
|
+
Connection: "keep-alive",
|
|
321
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
324
322
|
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
|
325
323
|
};
|
|
326
324
|
return allowAnthropicHeaderOverrides ? mergeHeaders(headers, anthropicHeaderOverrides) : headers;
|
|
@@ -503,26 +501,10 @@ function getCacheControl(
|
|
|
503
501
|
};
|
|
504
502
|
}
|
|
505
503
|
|
|
506
|
-
//
|
|
507
|
-
// leaf module so registry/usage consumers avoid an init cycle
|
|
504
|
+
// Cowork mode: mimic the desktop agent's direct inference transport. Constants
|
|
505
|
+
// live in the leaf module so registry/usage consumers avoid an init cycle.
|
|
508
506
|
export * from "./claude-code-fingerprint";
|
|
509
507
|
|
|
510
|
-
export function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
|
|
511
|
-
switch (platform.toLowerCase()) {
|
|
512
|
-
case "darwin":
|
|
513
|
-
return "MacOS";
|
|
514
|
-
case "windows":
|
|
515
|
-
case "win32":
|
|
516
|
-
return "Windows";
|
|
517
|
-
case "linux":
|
|
518
|
-
return "Linux";
|
|
519
|
-
case "freebsd":
|
|
520
|
-
return "FreeBSD";
|
|
521
|
-
default:
|
|
522
|
-
return `Other::${platform.toLowerCase()}`;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
508
|
export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
|
|
527
509
|
switch (arch.toLowerCase()) {
|
|
528
510
|
case "amd64":
|
|
@@ -540,22 +522,21 @@ export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other
|
|
|
540
522
|
}
|
|
541
523
|
}
|
|
542
524
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"X-Stainless-
|
|
525
|
+
/** Static headers emitted by Cowork's Linux Claude runtime. */
|
|
526
|
+
export const coworkHeaders = {
|
|
527
|
+
"X-Stainless-Arch": mapStainlessArch(process.arch),
|
|
528
|
+
"X-Stainless-Lang": "js",
|
|
529
|
+
"X-Stainless-OS": "Linux",
|
|
546
530
|
"X-Stainless-Package-Version": "0.94.0",
|
|
531
|
+
"X-Stainless-Retry-Count": "0",
|
|
547
532
|
"X-Stainless-Runtime": "node",
|
|
548
|
-
"X-Stainless-
|
|
549
|
-
"X-Stainless-
|
|
550
|
-
"X-Stainless-OS": mapStainlessOs(process.platform),
|
|
551
|
-
"X-Stainless-Timeout": "900",
|
|
552
|
-
"anthropic-client-platform": "desktop_app",
|
|
553
|
-
"anthropic-client-version": claudeClientVersion,
|
|
533
|
+
"X-Stainless-Runtime-Version": "v26.3.0",
|
|
534
|
+
"X-Stainless-Timeout": "600",
|
|
554
535
|
};
|
|
555
536
|
|
|
556
537
|
const enforcedHeaderKeys = new Set(
|
|
557
538
|
[
|
|
558
|
-
...Object.keys(
|
|
539
|
+
...Object.keys(coworkHeaders),
|
|
559
540
|
"Accept",
|
|
560
541
|
"Accept-Encoding",
|
|
561
542
|
"Connection",
|
|
@@ -574,7 +555,7 @@ const enforcedHeaderKeys = new Set(
|
|
|
574
555
|
);
|
|
575
556
|
|
|
576
557
|
const overridableAnthropicHeaderKeys = new Set(
|
|
577
|
-
[...Object.keys(
|
|
558
|
+
[...Object.keys(coworkHeaders), "anthropic-beta", "User-Agent", "x-app"].map(key => key.toLowerCase()),
|
|
578
559
|
);
|
|
579
560
|
|
|
580
561
|
const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
@@ -591,7 +572,7 @@ function createClaudeBillingHeader(firstUserMessageText: string): string {
|
|
|
591
572
|
.slice(0, 3);
|
|
592
573
|
// cch=00000: placeholder replaced with the real attestation hash by wrapFetchForCch
|
|
593
574
|
// before the request hits the wire (see below).
|
|
594
|
-
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${versionSuffix}; cc_entrypoint=
|
|
575
|
+
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${versionSuffix}; cc_entrypoint=claude-desktop; ${CCH_PLACEHOLDER_STR};`;
|
|
595
576
|
}
|
|
596
577
|
|
|
597
578
|
// cch attestation: XXHash64(body_with_placeholder, seed) low-20-bits, 5 hex chars.
|
|
@@ -1184,7 +1165,7 @@ export type AnthropicClientOptionsResult = {
|
|
|
1184
1165
|
fetchOptions?: AnthropicFetchOptions;
|
|
1185
1166
|
};
|
|
1186
1167
|
|
|
1187
|
-
const
|
|
1168
|
+
const COWORK_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
|
|
1188
1169
|
|
|
1189
1170
|
type FoundryTlsOptions = {
|
|
1190
1171
|
ca?: string | string[];
|
|
@@ -1347,7 +1328,7 @@ function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTl
|
|
|
1347
1328
|
return resolved;
|
|
1348
1329
|
}
|
|
1349
1330
|
|
|
1350
|
-
function
|
|
1331
|
+
function buildCoworkTlsFetchOptions(
|
|
1351
1332
|
model: Model<"anthropic-messages">,
|
|
1352
1333
|
baseUrl: string | undefined,
|
|
1353
1334
|
): AnthropicFetchOptions | undefined {
|
|
@@ -1369,7 +1350,7 @@ function buildClaudeCodeTlsFetchOptions(
|
|
|
1369
1350
|
tls: {
|
|
1370
1351
|
rejectUnauthorized: true,
|
|
1371
1352
|
serverName,
|
|
1372
|
-
...(
|
|
1353
|
+
...(COWORK_TLS_CIPHERS ? { ciphers: COWORK_TLS_CIPHERS } : {}),
|
|
1373
1354
|
...(foundryTlsOptions ?? {}),
|
|
1374
1355
|
},
|
|
1375
1356
|
};
|
|
@@ -2865,7 +2846,6 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2865
2846
|
dynamicHeaders,
|
|
2866
2847
|
hasTools = false,
|
|
2867
2848
|
thinkingEnabled = false,
|
|
2868
|
-
thinkingDisplay,
|
|
2869
2849
|
isOAuth,
|
|
2870
2850
|
maxRetryDelayMs,
|
|
2871
2851
|
claudeCodeSessionId,
|
|
@@ -2903,7 +2883,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2903
2883
|
const needsFineGrainedToolStreamingBeta =
|
|
2904
2884
|
hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
|
|
2905
2885
|
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
|
|
2906
|
-
const tlsFetchOptions =
|
|
2886
|
+
const tlsFetchOptions = buildCoworkTlsFetchOptions(model, baseUrl);
|
|
2907
2887
|
// Disable Bun's native ~300s pre-response fetch timeout (issue #2422).
|
|
2908
2888
|
// `AnthropicMessagesClient` already arms its own DEFAULT_TIMEOUT_MS timer
|
|
2909
2889
|
// per request, so the native ceiling can only short-circuit slow-prefill
|
|
@@ -2969,11 +2949,11 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2969
2949
|
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
|
|
2970
2950
|
allowAnthropicHeaderOverrides: model.compat.allowAnthropicHeaderOverrides,
|
|
2971
2951
|
claudeCodeSessionId,
|
|
2972
|
-
|
|
2973
|
-
?
|
|
2952
|
+
coworkBetas: oauthToken
|
|
2953
|
+
? buildCoworkBetas(
|
|
2974
2954
|
hasTools || thinkingEnabled,
|
|
2975
2955
|
thinkingEnabled,
|
|
2976
|
-
|
|
2956
|
+
(model.contextWindow ?? 0) >= 1_000_000,
|
|
2977
2957
|
disableStrictTools,
|
|
2978
2958
|
)
|
|
2979
2959
|
: [],
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Cowork inference-fingerprint constants, kept in a leaf module so consumers
|
|
3
|
+
* outside the provider (`registry/oauth/anthropic`, `usage/claude`) don't
|
|
4
|
+
* import the heavy `providers/anthropic` module.
|
|
5
|
+
*
|
|
5
6
|
* That import edge was a live init cycle: `providers/anthropic` → `stream` →
|
|
6
7
|
* `registry` → `registry/oauth/anthropic` → back into the still-initializing
|
|
7
|
-
* provider module
|
|
8
|
-
* `providers/anthropic` was the first module loaded.
|
|
8
|
+
* provider module.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export const
|
|
13
|
-
|
|
11
|
+
/** Claude runtime version bundled by the current Cowork desktop release. */
|
|
12
|
+
export const claudeCodeVersion = "2.1.220";
|
|
13
|
+
/** User-Agent emitted by Cowork's `claude-desktop` inference entrypoint. */
|
|
14
|
+
export const coworkUserAgent = `claude-cli/${claudeCodeVersion} (external, claude-desktop)`;
|
|
15
|
+
/** Prefix used to isolate custom Anthropic OAuth tools from built-in tools. */
|
|
14
16
|
export const claudeToolPrefix: string = "_";
|
|
17
|
+
/** Identity block prepended by Cowork's Claude runtime. */
|
|
15
18
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
16
|
-
|
|
17
|
-
// higher (e.g. Opus 4.8 supports 128k); OAuth requests clamp to match the wire
|
|
18
|
-
// fingerprint. API-key requests keep the full model ceiling.
|
|
19
|
+
/** Cowork's per-request output-token ceiling. */
|
|
19
20
|
export const CLAUDE_CODE_MAX_OUTPUT_TOKENS = 64000;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ClientRequest, IncomingMessage } from "node:http";
|
|
2
|
+
import * as https from "node:https";
|
|
3
|
+
import * as stream from "node:stream";
|
|
4
|
+
import * as tls from "node:tls";
|
|
5
|
+
import * as zlib from "node:zlib";
|
|
6
|
+
import type { FetchImpl } from "../types";
|
|
7
|
+
import { connectProxiedSocket } from "../utils/proxy";
|
|
8
|
+
|
|
9
|
+
type CoworkTlsOptions = {
|
|
10
|
+
ca?: string | string[];
|
|
11
|
+
cert?: string;
|
|
12
|
+
key?: string;
|
|
13
|
+
rejectUnauthorized?: boolean;
|
|
14
|
+
serverName?: string;
|
|
15
|
+
ciphers?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type CoworkRequestInit = RequestInit & {
|
|
19
|
+
proxy?: string;
|
|
20
|
+
tls?: CoworkTlsOptions;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type RequestBody = string | Uint8Array;
|
|
24
|
+
|
|
25
|
+
type AgentLease = {
|
|
26
|
+
agent: https.Agent;
|
|
27
|
+
release?: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const directAgent = new https.Agent({ keepAlive: true });
|
|
31
|
+
const fallbackFetch: FetchImpl = globalThis.fetch;
|
|
32
|
+
|
|
33
|
+
function isHeaderRecord(headers: RequestInit["headers"]): headers is Record<string, string> {
|
|
34
|
+
return headers !== undefined && !(headers instanceof Headers) && !Array.isArray(headers);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveBody(body: RequestInit["body"]): RequestBody | undefined {
|
|
38
|
+
if (typeof body === "string" || body instanceof Uint8Array) return body;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildOrderedHeaders(
|
|
43
|
+
url: URL,
|
|
44
|
+
source: Record<string, string>,
|
|
45
|
+
body: RequestBody | undefined,
|
|
46
|
+
): Record<string, string> {
|
|
47
|
+
const headers: Record<string, string> = {};
|
|
48
|
+
let hasHost = false;
|
|
49
|
+
let hasContentLength = false;
|
|
50
|
+
for (const name in source) {
|
|
51
|
+
const lowerName = name.toLowerCase();
|
|
52
|
+
if (lowerName === "host") hasHost = true;
|
|
53
|
+
if (lowerName === "content-length") hasContentLength = true;
|
|
54
|
+
if (lowerName === "accept-encoding" && !hasHost) {
|
|
55
|
+
headers.Host = url.host;
|
|
56
|
+
hasHost = true;
|
|
57
|
+
}
|
|
58
|
+
headers[name] = source[name];
|
|
59
|
+
}
|
|
60
|
+
if (!hasHost) headers.Host = url.host;
|
|
61
|
+
const length = typeof body === "string" ? Buffer.byteLength(body) : body?.byteLength;
|
|
62
|
+
if (!hasContentLength && length !== undefined) headers["Content-Length"] = String(length);
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveTlsOptions(url: URL, options: CoworkTlsOptions | undefined): tls.ConnectionOptions {
|
|
67
|
+
const resolved: tls.ConnectionOptions = {
|
|
68
|
+
ALPNProtocols: ["http/1.1"],
|
|
69
|
+
ciphers: options?.ciphers ?? tls.DEFAULT_CIPHERS,
|
|
70
|
+
rejectUnauthorized: options?.rejectUnauthorized ?? true,
|
|
71
|
+
servername: options?.serverName ?? url.hostname,
|
|
72
|
+
};
|
|
73
|
+
if (options?.ca !== undefined) resolved.ca = options.ca;
|
|
74
|
+
if (options?.cert !== undefined) resolved.cert = options.cert;
|
|
75
|
+
if (options?.key !== undefined) resolved.key = options.key;
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function acquireAgent(
|
|
80
|
+
url: URL,
|
|
81
|
+
proxy: string | undefined,
|
|
82
|
+
tlsOptions: tls.ConnectionOptions,
|
|
83
|
+
signal: AbortSignal | undefined,
|
|
84
|
+
): Promise<AgentLease> {
|
|
85
|
+
if (!proxy) return { agent: directAgent };
|
|
86
|
+
const socket = await connectProxiedSocket(proxy, url.origin, { signal, tls: tlsOptions });
|
|
87
|
+
const agent = new https.Agent({ keepAlive: false });
|
|
88
|
+
agent.createConnection = () => socket;
|
|
89
|
+
return { agent, release: () => agent.destroy() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function responseHeaders(message: IncomingMessage): Headers {
|
|
93
|
+
const headers = new Headers();
|
|
94
|
+
for (let index = 0; index < message.rawHeaders.length; index += 2) {
|
|
95
|
+
headers.append(message.rawHeaders[index], message.rawHeaders[index + 1]);
|
|
96
|
+
}
|
|
97
|
+
return headers;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function decodedResponseStream(message: IncomingMessage): stream.Readable {
|
|
101
|
+
const rawEncoding = message.headers["content-encoding"];
|
|
102
|
+
const encoding = (Array.isArray(rawEncoding) ? rawEncoding[0] : rawEncoding)?.trim().toLowerCase();
|
|
103
|
+
switch (encoding) {
|
|
104
|
+
case "gzip":
|
|
105
|
+
return message.pipe(zlib.createGunzip());
|
|
106
|
+
case "deflate":
|
|
107
|
+
return message.pipe(zlib.createInflate());
|
|
108
|
+
case "br":
|
|
109
|
+
return message.pipe(zlib.createBrotliDecompress());
|
|
110
|
+
case "zstd":
|
|
111
|
+
return message.pipe(zlib.createZstdDecompress());
|
|
112
|
+
default:
|
|
113
|
+
return message;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createResponse(message: IncomingMessage, method: string): Response {
|
|
118
|
+
const status = message.statusCode;
|
|
119
|
+
if (status === undefined) throw new Error("Cowork transport received a response without an HTTP status.");
|
|
120
|
+
const hasBody = method !== "HEAD" && status !== 204 && status !== 304;
|
|
121
|
+
const body = hasBody ? stream.Readable.toWeb(decodedResponseStream(message)) : null;
|
|
122
|
+
return new Response(body, {
|
|
123
|
+
status,
|
|
124
|
+
statusText: message.statusMessage,
|
|
125
|
+
headers: responseHeaders(message),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function sendCoworkRequest(
|
|
130
|
+
url: URL,
|
|
131
|
+
init: CoworkRequestInit,
|
|
132
|
+
sourceHeaders: Record<string, string>,
|
|
133
|
+
body: RequestBody | undefined,
|
|
134
|
+
): Promise<Response> {
|
|
135
|
+
const method = init.method ?? "GET";
|
|
136
|
+
const signal = init.signal ?? undefined;
|
|
137
|
+
const tlsOptions = resolveTlsOptions(url, init.tls);
|
|
138
|
+
const lease = await acquireAgent(url, init.proxy, tlsOptions, signal);
|
|
139
|
+
const headers = buildOrderedHeaders(url, sourceHeaders, body);
|
|
140
|
+
const result = Promise.withResolvers<Response>();
|
|
141
|
+
let request: ClientRequest | undefined;
|
|
142
|
+
const release = (): void => {
|
|
143
|
+
signal?.removeEventListener("abort", abort);
|
|
144
|
+
lease.release?.();
|
|
145
|
+
};
|
|
146
|
+
const abort = (): void => {
|
|
147
|
+
const reason = signal?.reason;
|
|
148
|
+
request?.destroy(reason instanceof Error ? reason : new DOMException("The operation was aborted.", "AbortError"));
|
|
149
|
+
};
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
release();
|
|
152
|
+
signal.throwIfAborted();
|
|
153
|
+
}
|
|
154
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
155
|
+
request = https.request(
|
|
156
|
+
{
|
|
157
|
+
protocol: url.protocol,
|
|
158
|
+
hostname: url.hostname,
|
|
159
|
+
port: url.port || 443,
|
|
160
|
+
path: `${url.pathname}${url.search}`,
|
|
161
|
+
method,
|
|
162
|
+
headers,
|
|
163
|
+
agent: lease.agent,
|
|
164
|
+
...tlsOptions,
|
|
165
|
+
},
|
|
166
|
+
message => {
|
|
167
|
+
message.once("close", release);
|
|
168
|
+
try {
|
|
169
|
+
result.resolve(createResponse(message, method));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
message.destroy();
|
|
172
|
+
release();
|
|
173
|
+
result.reject(error);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
request.once("error", error => {
|
|
178
|
+
release();
|
|
179
|
+
result.reject(error);
|
|
180
|
+
});
|
|
181
|
+
request.end(body);
|
|
182
|
+
return result.promise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Sends Cowork-profiled HTTPS requests with stable header order, HTTP/1.1, and streaming decompression. */
|
|
186
|
+
export const coworkFetch: FetchImpl = async (input, init) => {
|
|
187
|
+
if (input instanceof Request || init === undefined || !isHeaderRecord(init.headers)) {
|
|
188
|
+
return fallbackFetch(input, init);
|
|
189
|
+
}
|
|
190
|
+
let url: URL;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(input);
|
|
193
|
+
} catch {
|
|
194
|
+
return fallbackFetch(input, init);
|
|
195
|
+
}
|
|
196
|
+
if (url.protocol !== "https:") return fallbackFetch(input, init);
|
|
197
|
+
const body = resolveBody(init.body);
|
|
198
|
+
if (init.body != null && body === undefined) return fallbackFetch(input, init);
|
|
199
|
+
const coworkInit: CoworkRequestInit = init;
|
|
200
|
+
return sendCoworkRequest(url, coworkInit, init.headers, body);
|
|
201
|
+
};
|
package/src/stream.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { isInvalidatedOAuthTokenError } from "./error/auth-classify";
|
|
|
24
24
|
import { isUsageLimitOutcome } from "./error/rate-limit";
|
|
25
25
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
26
26
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
27
|
+
import { coworkFetch } from "./providers/cowork-fetch";
|
|
27
28
|
import type { CursorOptions } from "./providers/cursor";
|
|
28
29
|
import type { DevinOptions } from "./providers/devin";
|
|
29
30
|
import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
|
|
@@ -81,6 +82,11 @@ import { wrapFetchForProxy } from "./utils/proxy";
|
|
|
81
82
|
import { withRequestDebugFetch } from "./utils/request-debug";
|
|
82
83
|
import { withGeminiThinkingLoopGuard } from "./utils/thinking-loop";
|
|
83
84
|
|
|
85
|
+
function defaultFetchForModel(model: Model<Api>): FetchImpl {
|
|
86
|
+
if (model.provider === "anthropic" && model.api === "anthropic-messages") return coworkFetch;
|
|
87
|
+
return globalThis.fetch;
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
function isGoogleVertexAuthenticatedModel(model: Model<Api>): boolean {
|
|
85
91
|
return (
|
|
86
92
|
model.provider === "google-vertex" &&
|
|
@@ -769,11 +775,12 @@ function streamDispatch<TApi extends Api>(
|
|
|
769
775
|
context: Context,
|
|
770
776
|
options?: OptionsForApi<TApi>,
|
|
771
777
|
): AssistantMessageEventStream {
|
|
772
|
-
const
|
|
778
|
+
const inputOptions = (options || {}) as StreamOptions;
|
|
779
|
+
const baseOptions = { ...inputOptions, fetch: inputOptions.fetch ?? defaultFetchForModel(model) };
|
|
773
780
|
const debugOptions = withExtraCaFetch(withRequestDebugFetch(baseOptions));
|
|
774
781
|
const requestOptions = {
|
|
775
782
|
...debugOptions,
|
|
776
|
-
fetch: wrapFetchForProxy(debugOptions.fetch
|
|
783
|
+
fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
|
|
777
784
|
} as OptionsForApi<TApi>;
|
|
778
785
|
assertExplicitOpenAIResponsesPromptCacheSupport(model, requestOptions);
|
|
779
786
|
|
|
@@ -1009,11 +1016,12 @@ export function streamSimple<TApi extends Api>(
|
|
|
1009
1016
|
context: Context,
|
|
1010
1017
|
options?: SimpleStreamOptions,
|
|
1011
1018
|
): AssistantMessageEventStream {
|
|
1012
|
-
const
|
|
1019
|
+
const inputOptions = (options || {}) as SimpleStreamOptions;
|
|
1020
|
+
const baseOptions = { ...inputOptions, fetch: inputOptions.fetch ?? defaultFetchForModel(model) };
|
|
1013
1021
|
const debugOptions = withExtraCaFetch(withRequestDebugFetch(baseOptions));
|
|
1014
1022
|
const requestOptions = {
|
|
1015
1023
|
...debugOptions,
|
|
1016
|
-
fetch: wrapFetchForProxy(debugOptions.fetch
|
|
1024
|
+
fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
|
|
1017
1025
|
} as SimpleStreamOptions;
|
|
1018
1026
|
|
|
1019
1027
|
const apiKeyResolver = isApiKeyResolver(requestOptions?.apiKey) ? requestOptions.apiKey : undefined;
|
package/src/utils/proxy.ts
CHANGED
|
@@ -177,6 +177,8 @@ export interface ConnectProxiedSocketOptions {
|
|
|
177
177
|
signal?: AbortSignal;
|
|
178
178
|
/** Maximum wall-clock time to establish the final TLS tunnel. Disabled when absent or non-positive. */
|
|
179
179
|
timeoutMs?: number;
|
|
180
|
+
/** Target TLS profile. Cursor defaults to HTTP/2 when this is absent. */
|
|
181
|
+
tls?: tls.ConnectionOptions;
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
/**
|
|
@@ -261,10 +263,12 @@ export async function connectProxiedSocket(
|
|
|
261
263
|
return;
|
|
262
264
|
}
|
|
263
265
|
|
|
266
|
+
const tlsOptions = options?.tls;
|
|
264
267
|
tunnelSocket = tls.connect({
|
|
268
|
+
...tlsOptions,
|
|
265
269
|
socket: rawSocket,
|
|
266
|
-
servername: targetHost,
|
|
267
|
-
ALPNProtocols: ["h2"],
|
|
270
|
+
servername: tlsOptions?.servername ?? targetHost,
|
|
271
|
+
ALPNProtocols: tlsOptions?.ALPNProtocols ?? ["h2"],
|
|
268
272
|
});
|
|
269
273
|
tunnelSocket.once("secureConnect", onTunnelReady);
|
|
270
274
|
tunnelSocket.once("error", onTunnelError);
|