@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 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
- claudeCodeBetas?: readonly string[];
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
- export declare const claudeCodeHeaders: {
39
- "X-Stainless-Retry-Count": string;
40
- "X-Stainless-Runtime-Version": string;
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-Lang": string;
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
- * Claude Code stealth-fingerprint constants, kept in a leaf module so
3
- * fingerprint consumers outside the provider (`registry/oauth/anthropic`,
4
- * `usage/claude`) don't import the heavy `providers/anthropic` module.
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, which threw a TDZ ReferenceError whenever
8
- * `providers/anthropic` was the first module loaded.
8
+ * provider module.
9
9
  */
10
- export declare const claudeCodeVersion = "2.1.165";
11
- export declare const claudeAgentSdkVersion = "0.3.165";
12
- export declare const claudeClientVersion = "1.11187.4";
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;
@@ -0,0 +1,3 @@
1
+ import type { FetchImpl } from "../types.js";
2
+ /** Sends Cowork-profiled HTTPS requests with stable header order, HTTP/1.1, and streaming decompression. */
3
+ export declare const coworkFetch: FetchImpl;
@@ -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.0",
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.0",
42
- "@oh-my-pi/pi-utils": "17.2.0",
43
- "@oh-my-pi/pi-wire": "17.2.0",
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
  },
@@ -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
- claudeCodeBetas?: readonly string[];
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 claudeCodeUtilityBetaDefaults = [
162
- "oauth-2025-04-20",
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 claudeCodeAgentBetaDefaults = [
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 buildClaudeCodeBetas(
187
+ function buildCoworkBetas(
191
188
  agentRequest: boolean,
192
189
  thinkingRequest: boolean,
193
- redactThinking: boolean,
190
+ longContext: boolean,
194
191
  disableStrictTools = false,
195
192
  ): readonly string[] {
196
- if (!agentRequest && !redactThinking && !disableStrictTools) return claudeCodeUtilityBetaDefaults;
193
+ if (!agentRequest && !disableStrictTools) return coworkUtilityBetaDefaults;
197
194
  const betas: string[] = [];
198
- for (const beta of agentRequest ? claudeCodeAgentBetaDefaults : claudeCodeUtilityBetaDefaults) {
195
+ for (const beta of agentRequest ? coworkAgentBetaDefaults : coworkUtilityBetaDefaults) {
199
196
  if (disableStrictTools && beta === structuredOutputsBeta) continue;
200
197
  betas.push(beta);
201
- // Match CC's header order: redact-thinking immediately follows interleaved-thinking.
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(...claudeCodeAgentPostEffortBetas);
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
- // Claude Code betas (oauth-2025-04-20, claude-code-20250219, …) are part of
250
- // the OAuth fingerprint; API-key requests default to extras only, matching
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.claudeCodeBetas ?? (oauthToken ? buildClaudeCodeBetas(true, true, false) : []),
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
- Authorization: `Bearer ${options.apiKey}`,
319
- ...sharedHeaders,
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
- "User-Agent": userAgent,
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
- // Stealth mode: mimic Claude Code's request fingerprint. Constants live in the
507
- // leaf module so registry/usage consumers avoid an init cycle through this file.
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
- export const claudeCodeHeaders = {
544
- "X-Stainless-Retry-Count": "0",
545
- "X-Stainless-Runtime-Version": "v24.3.0",
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-Lang": "js",
549
- "X-Stainless-Arch": mapStainlessArch(process.arch),
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(claudeCodeHeaders),
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(claudeCodeHeaders), "anthropic-beta", "User-Agent", "x-app"].map(key => key.toLowerCase()),
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=local-agent; ${CCH_PLACEHOLDER_STR};`;
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 CLAUDE_CODE_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
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 buildClaudeCodeTlsFetchOptions(
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
- ...(CLAUDE_CODE_TLS_CIPHERS ? { ciphers: CLAUDE_CODE_TLS_CIPHERS } : {}),
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 = buildClaudeCodeTlsFetchOptions(model, baseUrl);
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
- claudeCodeBetas: oauthToken
2973
- ? buildClaudeCodeBetas(
2952
+ coworkBetas: oauthToken
2953
+ ? buildCoworkBetas(
2974
2954
  hasTools || thinkingEnabled,
2975
2955
  thinkingEnabled,
2976
- thinkingDisplay === "omitted",
2956
+ (model.contextWindow ?? 0) >= 1_000_000,
2977
2957
  disableStrictTools,
2978
2958
  )
2979
2959
  : [],
@@ -1,19 +1,20 @@
1
1
  /**
2
- * Claude Code stealth-fingerprint constants, kept in a leaf module so
3
- * fingerprint consumers outside the provider (`registry/oauth/anthropic`,
4
- * `usage/claude`) don't import the heavy `providers/anthropic` module.
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, which threw a TDZ ReferenceError whenever
8
- * `providers/anthropic` was the first module loaded.
8
+ * provider module.
9
9
  */
10
10
 
11
- export const claudeCodeVersion = "2.1.165";
12
- export const claudeAgentSdkVersion = "0.3.165";
13
- export const claudeClientVersion = "1.11187.4";
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
- // Claude Code caps requested output at 64k tokens even when the model ceiling is
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 baseOptions = (options || {}) as StreamOptions;
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 ?? (globalThis.fetch as FetchImpl), model.provider),
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 baseOptions = (options || {}) as SimpleStreamOptions;
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 ?? (globalThis.fetch as FetchImpl), model.provider),
1024
+ fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
1017
1025
  } as SimpleStreamOptions;
1018
1026
 
1019
1027
  const apiKeyResolver = isApiKeyResolver(requestOptions?.apiKey) ? requestOptions.apiKey : undefined;
@@ -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);