@jameslovespancakes/pi-plus 1.0.0 → 1.0.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.
Files changed (40) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +190 -190
  3. package/config/pi-plus.example.json +60 -60
  4. package/config/skills/model-routing/SKILL.md +86 -86
  5. package/images/pi-plus.svg +10 -10
  6. package/package.json +67 -67
  7. package/server/board-server.mjs +641 -641
  8. package/server/package.json +17 -17
  9. package/src/core/accounts/registry.ts +93 -93
  10. package/src/core/anthropic/client-identity.ts +241 -241
  11. package/src/core/catalog/quality.ts +314 -314
  12. package/src/core/config.ts +169 -169
  13. package/src/core/env.ts +58 -58
  14. package/src/core/exec/process.ts +146 -146
  15. package/src/core/exec/ssh-config.ts +157 -157
  16. package/src/core/policy/policy.ts +183 -183
  17. package/src/core/quota/pool.ts +64 -64
  18. package/src/core/quota/usage-source.ts +289 -289
  19. package/src/core/store.ts +43 -43
  20. package/src/domains/agents/board-setup.ts +409 -409
  21. package/src/domains/agents/index.ts +462 -462
  22. package/src/domains/models/catalog-tool.ts +361 -361
  23. package/src/domains/models/index.ts +14 -14
  24. package/src/domains/models/policy-gate.ts +169 -169
  25. package/src/domains/models/provider-picker.ts +207 -207
  26. package/src/domains/remote/config-path.ts +41 -41
  27. package/src/domains/remote/index.ts +866 -866
  28. package/src/domains/remote/setup.ts +425 -425
  29. package/src/domains/setup/index.ts +220 -220
  30. package/src/domains/subscriptions/accounts.ts +242 -242
  31. package/src/domains/subscriptions/footer.ts +182 -182
  32. package/src/domains/subscriptions/index.ts +42 -42
  33. package/src/domains/subscriptions/provider.ts +219 -219
  34. package/src/domains/subscriptions/providers/anthropic.ts +149 -149
  35. package/src/domains/subscriptions/providers/codex.ts +148 -148
  36. package/src/domains/subscriptions/routing.ts +72 -72
  37. package/src/services/usage-service.ts +186 -186
  38. package/src/ui/format.ts +73 -73
  39. package/src/ui/usage-bars.ts +154 -154
  40. package/src/vendor/anthropic.ts +109 -109
@@ -1,241 +1,241 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { xxhash64 } from "./xxhash64.ts";
3
-
4
- /**
5
- * Claude Code client identity headers.
6
- *
7
- * ---------------------------------------------------------------------------
8
- * READ THIS BEFORE CHANGING ANYTHING HERE
9
- *
10
- * These headers make a request indistinguishable from Anthropic's official
11
- * Claude Code CLI. Without them Anthropic classifies the caller as a
12
- * third-party app and answers:
13
- *
14
- * "Third-party apps now draw from your extra usage, not your plan limits."
15
- *
16
- * So the purpose of this file is to bill against plan limits rather than extra
17
- * usage. That is plausibly contrary to Anthropic's intent as stated in that
18
- * message, and the exposure falls on the account owner.
19
- *
20
- * It is isolated in one file, and referenced from exactly one place, so it can
21
- * be deleted or replaced without touching the rest of the provider. Removing it
22
- * does not break anything: requests keep working and bill to extra usage.
23
- *
24
- * Ported from @cortexkit/anthropic-auth-core during extraction.
25
- * ---------------------------------------------------------------------------
26
- */
27
-
28
- /** Pinned to the Claude Code release being imitated. */
29
- export const CLAUDE_CODE_VERSION = "2.1.258";
30
-
31
- /**
32
- * Anti-tamper checksum, replicated from the official client.
33
- *
34
- * The real CLI signs the serialised request body with xxHash64 under a fixed
35
- * seed and writes the low 20 bits into a `cch=` placeholder. Anthropic added
36
- * this so a client cannot simply assert it is Claude Code in a header: it has
37
- * to prove it by producing a value derived from the request itself.
38
- *
39
- * The salt and sample positions are constants in the official bundle, not
40
- * derivable from anything, and they will change when the CLI is updated.
41
- */
42
- const CCH_SEED = 0x4d659218e32a3268n;
43
- const CCH_SALT = "59cf53e54c78";
44
- const CCH_POSITIONS = [4, 7, 20];
45
- const CCH_PLACEHOLDER = "cch=00000;";
46
-
47
- const CCH_SIGNED = /("system":\[\{"type":"text","text":"x-anthropic-billing-header: cc_version=[^;"]+; cc_entrypoint=[^;"]+; )cch=[0-9a-f]{5};/;
48
- const CCH_UNSIGNED = /("system":\[\{"type":"text","text":"x-anthropic-billing-header: cc_version=[^;"]+; cc_entrypoint=[^;"]+; )cch=00000;/;
49
-
50
- /** Final text block of the first user message; a few characters are sampled. */
51
- export function firstUserText(messages: any[] | undefined): string {
52
- const message = (messages ?? []).find((m) => m?.role === "user");
53
- if (!message) return "";
54
- if (typeof message.content === "string") return message.content;
55
- if (!Array.isArray(message.content)) return "";
56
-
57
- const texts = message.content.filter((b: any) => b?.type === "text" && typeof b.text === "string");
58
- const command = texts.find((b: any) => b.text.includes("<command-name>"));
59
- if (command) return command.text.slice(command.text.indexOf("<command-name>"));
60
- return texts.at(-1)?.text ?? "";
61
- }
62
-
63
- /** Three hex characters appended to cc_version. */
64
- export function ccVersionSuffix(sample: string, version = CLAUDE_CODE_VERSION): string {
65
- const sampled = CCH_POSITIONS.map((i) => sample[i] ?? "0").join("");
66
- return createHash("sha256").update(`${CCH_SALT}${sampled}${version}`).digest("hex").slice(0, 3);
67
- }
68
-
69
- /** Billing header with a placeholder, ready for `signRequestBody`. */
70
- export function billingHeader(sample: string, entrypoint = "cli", version = CLAUDE_CODE_VERSION): string {
71
- return `x-anthropic-billing-header: cc_version=${version}.${ccVersionSuffix(sample, version)}; cc_entrypoint=${entrypoint}; ${CCH_PLACEHOLDER}`;
72
- }
73
-
74
- /**
75
- * Fills in the `cch` placeholder of an already-serialised request body.
76
- *
77
- * `model` and `max_tokens` are blanked before hashing, matching the official
78
- * canonicalisation. Returns the input unchanged when no billing header is
79
- * present, so this is a no-op for non-Anthropic traffic.
80
- */
81
- export async function signRequestBody(bodyString: string): Promise<string> {
82
- if (!CCH_SIGNED.test(bodyString)) return bodyString;
83
-
84
- const unsigned = bodyString.replace(CCH_SIGNED, `$1${CCH_PLACEHOLDER}`);
85
- const canonical = JSON.parse(unsigned);
86
- canonical.model = "";
87
- delete canonical.max_tokens;
88
-
89
- const bytes = new TextEncoder().encode(JSON.stringify(canonical));
90
- const token = ((await xxhash64(bytes, CCH_SEED)) & 0xfffffn).toString(16).padStart(5, "0");
91
- return unsigned.replace(CCH_UNSIGNED, `$1cch=${token};`);
92
- }
93
- const USER_AGENT = `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`;
94
- const STAINLESS_PACKAGE_VERSION = "0.112.1";
95
- const STAINLESS_RUNTIME_VERSION = "v26.3.0";
96
-
97
- /** Sent when the request carries no tools. */
98
- const BASE_BETAS = [
99
- "oauth-2025-04-20",
100
- "interleaved-thinking-2025-05-14",
101
- "thinking-token-count-2026-05-13",
102
- "context-management-2025-06-27",
103
- "prompt-caching-scope-2026-01-05",
104
- "advisor-tool-2026-03-01",
105
- "advanced-tool-use-2025-11-20",
106
- "extended-cache-ttl-2025-04-11",
107
- "cache-diagnosis-2026-04-07",
108
- ];
109
-
110
- /** Sent for a full agent turn: tools, system blocks and thinking together. */
111
- const FULL_AGENT_BETAS = [
112
- ...BASE_BETAS,
113
- "claude-code-20250219",
114
- "mid-conversation-system-2026-04-07",
115
- "effort-2025-11-24",
116
- "fallback-credit-2026-06-01",
117
- ];
118
-
119
- function stainlessOS(): string {
120
- switch (process.platform) {
121
- case "darwin": return "MacOS";
122
- case "win32": return "Windows";
123
- case "linux": return "Linux";
124
- case "freebsd": return "FreeBSD";
125
- default: return "Unknown";
126
- }
127
- }
128
-
129
- function stainlessArch(): string {
130
- switch (process.arch) {
131
- case "arm64": return "arm64";
132
- case "x64": return "x64";
133
- case "ia32": return "x32";
134
- default: return process.arch;
135
- }
136
- }
137
-
138
- /** One session id per process, matching the real CLI's lifetime. */
139
- let sessionId: string | undefined;
140
- function currentSessionId(): string {
141
- return (sessionId ??= randomUUID());
142
- }
143
-
144
- function hasFullAgentShape(body: any): boolean {
145
- return Array.isArray(body?.tools) && body.tools.length > 0
146
- && Array.isArray(body?.system)
147
- && !!body?.thinking && typeof body.thinking === "object";
148
- }
149
-
150
- /**
151
- * Paragraphs containing this anchor cannot sit in the top-level `system` array.
152
- *
153
- * Two lines of pi's documentation paragraph are each independently sufficient to
154
- * make Anthropic answer 400 "Third-party apps now draw from your extra usage".
155
- * The same text is accepted inside `messages`, so it is moved there. Confirmed
156
- * by bisection: the identical payload returns 200 with the paragraph removed and
157
- * 400 with it present, and entry count and payload size are not the factors.
158
- */
159
- const DOCS_ANCHOR = "Pi documentation";
160
-
161
- /** Lone surrogates are invalid UTF-8 and are rejected outright. */
162
- function sanitizePrompt(text: string): string {
163
- return text.replace(/[\uD800-\uDFFF]/gu, "\uFFFD");
164
- }
165
-
166
- /**
167
- * Splits pi's system prompt into what may stay in `system` and what must move
168
- * into the first user message.
169
- *
170
- * An unrecognised prompt shape (no docs paragraph found) is moved whole, the
171
- * same conservative fallback the vendor uses: better to shift the entire prompt
172
- * into messages than to guess and trigger the 400.
173
- */
174
- export function splitSystemPrompt(prompt: string): { systemText?: string; messageText: string } {
175
- const paragraphs = sanitizePrompt(prompt).split(/\n\n+/);
176
- const docs = paragraphs.filter((p) => p.includes(DOCS_ANCHOR));
177
- if (docs.length === 0) return { messageText: sanitizePrompt(prompt) };
178
-
179
- const keep = paragraphs.filter((p) => !p.includes(DOCS_ANCHOR));
180
- return {
181
- ...(keep.length ? { systemText: keep.join("\n\n") } : {}),
182
- messageText: docs.join("\n\n"),
183
- };
184
- }
185
-
186
- /**
187
- * Inserts text as its own cache-controlled block ahead of the first user
188
- * message.
189
- *
190
- * A separate block rather than merged text, so the cache prefix ends before the
191
- * user's own words and a new conversation with a different first message still
192
- * reads this from cache. `cache_control` is explicit because the message-level
193
- * breakpoint elsewhere only covers the last user message.
194
- */
195
- export function prependPromptBlock(messages: any[], text: string): void {
196
- const firstUser = (messages ?? []).find((m) => m?.role === "user");
197
- if (!firstUser || !text) return;
198
-
199
- const block = { type: "text", text, cache_control: { type: "ephemeral" } };
200
- if (typeof firstUser.content === "string") {
201
- firstUser.content = [block, { type: "text", text: firstUser.content }];
202
- return;
203
- }
204
- if (Array.isArray(firstUser.content)) firstUser.content.unshift(block);
205
- }
206
-
207
- export function selectBetas(body: unknown, extra: string[] = []): string {
208
- const selected = hasFullAgentShape(body) ? [...FULL_AGENT_BETAS] : [...BASE_BETAS];
209
- for (const beta of extra) {
210
- const trimmed = beta.trim();
211
- if (trimmed) selected.push(trimmed);
212
- }
213
- return [...new Set(selected)].join(",");
214
- }
215
-
216
- /**
217
- * Headers presenting this client as Claude Code.
218
- * Merged over whatever pi already set.
219
- */
220
- export function clientIdentityHeaders(body?: unknown, existingBetas?: string): Record<string, string> {
221
- const incoming = (existingBetas ?? "").split(",").map((b) => b.trim()).filter(Boolean);
222
- return {
223
- "user-agent": USER_AGENT,
224
- // Required: pi's Anthropic client reads this header to decide which betas
225
- // to send, then puts them in the body's `betas` field.
226
- "anthropic-beta": selectBetas(body, incoming),
227
- "anthropic-version": "2023-06-01",
228
- "anthropic-dangerous-direct-browser-access": "true",
229
- "x-app": "cli",
230
- "x-client-request-id": randomUUID(),
231
- "x-claude-code-session-id": currentSessionId(),
232
- "x-stainless-arch": stainlessArch(),
233
- "x-stainless-lang": "js",
234
- "x-stainless-os": stainlessOS(),
235
- "x-stainless-package-version": STAINLESS_PACKAGE_VERSION,
236
- "x-stainless-retry-count": "0",
237
- "x-stainless-runtime": "node",
238
- "x-stainless-runtime-version": STAINLESS_RUNTIME_VERSION,
239
- "x-stainless-timeout": "600",
240
- };
241
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { xxhash64 } from "./xxhash64.ts";
3
+
4
+ /**
5
+ * Claude Code client identity headers.
6
+ *
7
+ * ---------------------------------------------------------------------------
8
+ * READ THIS BEFORE CHANGING ANYTHING HERE
9
+ *
10
+ * These headers make a request indistinguishable from Anthropic's official
11
+ * Claude Code CLI. Without them Anthropic classifies the caller as a
12
+ * third-party app and answers:
13
+ *
14
+ * "Third-party apps now draw from your extra usage, not your plan limits."
15
+ *
16
+ * So the purpose of this file is to bill against plan limits rather than extra
17
+ * usage. That is plausibly contrary to Anthropic's intent as stated in that
18
+ * message, and the exposure falls on the account owner.
19
+ *
20
+ * It is isolated in one file, and referenced from exactly one place, so it can
21
+ * be deleted or replaced without touching the rest of the provider. Removing it
22
+ * does not break anything: requests keep working and bill to extra usage.
23
+ *
24
+ * Ported from @cortexkit/anthropic-auth-core during extraction.
25
+ * ---------------------------------------------------------------------------
26
+ */
27
+
28
+ /** Pinned to the Claude Code release being imitated. */
29
+ export const CLAUDE_CODE_VERSION = "2.1.258";
30
+
31
+ /**
32
+ * Anti-tamper checksum, replicated from the official client.
33
+ *
34
+ * The real CLI signs the serialised request body with xxHash64 under a fixed
35
+ * seed and writes the low 20 bits into a `cch=` placeholder. Anthropic added
36
+ * this so a client cannot simply assert it is Claude Code in a header: it has
37
+ * to prove it by producing a value derived from the request itself.
38
+ *
39
+ * The salt and sample positions are constants in the official bundle, not
40
+ * derivable from anything, and they will change when the CLI is updated.
41
+ */
42
+ const CCH_SEED = 0x4d659218e32a3268n;
43
+ const CCH_SALT = "59cf53e54c78";
44
+ const CCH_POSITIONS = [4, 7, 20];
45
+ const CCH_PLACEHOLDER = "cch=00000;";
46
+
47
+ const CCH_SIGNED = /("system":\[\{"type":"text","text":"x-anthropic-billing-header: cc_version=[^;"]+; cc_entrypoint=[^;"]+; )cch=[0-9a-f]{5};/;
48
+ const CCH_UNSIGNED = /("system":\[\{"type":"text","text":"x-anthropic-billing-header: cc_version=[^;"]+; cc_entrypoint=[^;"]+; )cch=00000;/;
49
+
50
+ /** Final text block of the first user message; a few characters are sampled. */
51
+ export function firstUserText(messages: any[] | undefined): string {
52
+ const message = (messages ?? []).find((m) => m?.role === "user");
53
+ if (!message) return "";
54
+ if (typeof message.content === "string") return message.content;
55
+ if (!Array.isArray(message.content)) return "";
56
+
57
+ const texts = message.content.filter((b: any) => b?.type === "text" && typeof b.text === "string");
58
+ const command = texts.find((b: any) => b.text.includes("<command-name>"));
59
+ if (command) return command.text.slice(command.text.indexOf("<command-name>"));
60
+ return texts.at(-1)?.text ?? "";
61
+ }
62
+
63
+ /** Three hex characters appended to cc_version. */
64
+ export function ccVersionSuffix(sample: string, version = CLAUDE_CODE_VERSION): string {
65
+ const sampled = CCH_POSITIONS.map((i) => sample[i] ?? "0").join("");
66
+ return createHash("sha256").update(`${CCH_SALT}${sampled}${version}`).digest("hex").slice(0, 3);
67
+ }
68
+
69
+ /** Billing header with a placeholder, ready for `signRequestBody`. */
70
+ export function billingHeader(sample: string, entrypoint = "cli", version = CLAUDE_CODE_VERSION): string {
71
+ return `x-anthropic-billing-header: cc_version=${version}.${ccVersionSuffix(sample, version)}; cc_entrypoint=${entrypoint}; ${CCH_PLACEHOLDER}`;
72
+ }
73
+
74
+ /**
75
+ * Fills in the `cch` placeholder of an already-serialised request body.
76
+ *
77
+ * `model` and `max_tokens` are blanked before hashing, matching the official
78
+ * canonicalisation. Returns the input unchanged when no billing header is
79
+ * present, so this is a no-op for non-Anthropic traffic.
80
+ */
81
+ export async function signRequestBody(bodyString: string): Promise<string> {
82
+ if (!CCH_SIGNED.test(bodyString)) return bodyString;
83
+
84
+ const unsigned = bodyString.replace(CCH_SIGNED, `$1${CCH_PLACEHOLDER}`);
85
+ const canonical = JSON.parse(unsigned);
86
+ canonical.model = "";
87
+ delete canonical.max_tokens;
88
+
89
+ const bytes = new TextEncoder().encode(JSON.stringify(canonical));
90
+ const token = ((await xxhash64(bytes, CCH_SEED)) & 0xfffffn).toString(16).padStart(5, "0");
91
+ return unsigned.replace(CCH_UNSIGNED, `$1cch=${token};`);
92
+ }
93
+ const USER_AGENT = `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`;
94
+ const STAINLESS_PACKAGE_VERSION = "0.112.1";
95
+ const STAINLESS_RUNTIME_VERSION = "v26.3.0";
96
+
97
+ /** Sent when the request carries no tools. */
98
+ const BASE_BETAS = [
99
+ "oauth-2025-04-20",
100
+ "interleaved-thinking-2025-05-14",
101
+ "thinking-token-count-2026-05-13",
102
+ "context-management-2025-06-27",
103
+ "prompt-caching-scope-2026-01-05",
104
+ "advisor-tool-2026-03-01",
105
+ "advanced-tool-use-2025-11-20",
106
+ "extended-cache-ttl-2025-04-11",
107
+ "cache-diagnosis-2026-04-07",
108
+ ];
109
+
110
+ /** Sent for a full agent turn: tools, system blocks and thinking together. */
111
+ const FULL_AGENT_BETAS = [
112
+ ...BASE_BETAS,
113
+ "claude-code-20250219",
114
+ "mid-conversation-system-2026-04-07",
115
+ "effort-2025-11-24",
116
+ "fallback-credit-2026-06-01",
117
+ ];
118
+
119
+ function stainlessOS(): string {
120
+ switch (process.platform) {
121
+ case "darwin": return "MacOS";
122
+ case "win32": return "Windows";
123
+ case "linux": return "Linux";
124
+ case "freebsd": return "FreeBSD";
125
+ default: return "Unknown";
126
+ }
127
+ }
128
+
129
+ function stainlessArch(): string {
130
+ switch (process.arch) {
131
+ case "arm64": return "arm64";
132
+ case "x64": return "x64";
133
+ case "ia32": return "x32";
134
+ default: return process.arch;
135
+ }
136
+ }
137
+
138
+ /** One session id per process, matching the real CLI's lifetime. */
139
+ let sessionId: string | undefined;
140
+ function currentSessionId(): string {
141
+ return (sessionId ??= randomUUID());
142
+ }
143
+
144
+ function hasFullAgentShape(body: any): boolean {
145
+ return Array.isArray(body?.tools) && body.tools.length > 0
146
+ && Array.isArray(body?.system)
147
+ && !!body?.thinking && typeof body.thinking === "object";
148
+ }
149
+
150
+ /**
151
+ * Paragraphs containing this anchor cannot sit in the top-level `system` array.
152
+ *
153
+ * Two lines of pi's documentation paragraph are each independently sufficient to
154
+ * make Anthropic answer 400 "Third-party apps now draw from your extra usage".
155
+ * The same text is accepted inside `messages`, so it is moved there. Confirmed
156
+ * by bisection: the identical payload returns 200 with the paragraph removed and
157
+ * 400 with it present, and entry count and payload size are not the factors.
158
+ */
159
+ const DOCS_ANCHOR = "Pi documentation";
160
+
161
+ /** Lone surrogates are invalid UTF-8 and are rejected outright. */
162
+ function sanitizePrompt(text: string): string {
163
+ return text.replace(/[\uD800-\uDFFF]/gu, "\uFFFD");
164
+ }
165
+
166
+ /**
167
+ * Splits pi's system prompt into what may stay in `system` and what must move
168
+ * into the first user message.
169
+ *
170
+ * An unrecognised prompt shape (no docs paragraph found) is moved whole, the
171
+ * same conservative fallback the vendor uses: better to shift the entire prompt
172
+ * into messages than to guess and trigger the 400.
173
+ */
174
+ export function splitSystemPrompt(prompt: string): { systemText?: string; messageText: string } {
175
+ const paragraphs = sanitizePrompt(prompt).split(/\n\n+/);
176
+ const docs = paragraphs.filter((p) => p.includes(DOCS_ANCHOR));
177
+ if (docs.length === 0) return { messageText: sanitizePrompt(prompt) };
178
+
179
+ const keep = paragraphs.filter((p) => !p.includes(DOCS_ANCHOR));
180
+ return {
181
+ ...(keep.length ? { systemText: keep.join("\n\n") } : {}),
182
+ messageText: docs.join("\n\n"),
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Inserts text as its own cache-controlled block ahead of the first user
188
+ * message.
189
+ *
190
+ * A separate block rather than merged text, so the cache prefix ends before the
191
+ * user's own words and a new conversation with a different first message still
192
+ * reads this from cache. `cache_control` is explicit because the message-level
193
+ * breakpoint elsewhere only covers the last user message.
194
+ */
195
+ export function prependPromptBlock(messages: any[], text: string): void {
196
+ const firstUser = (messages ?? []).find((m) => m?.role === "user");
197
+ if (!firstUser || !text) return;
198
+
199
+ const block = { type: "text", text, cache_control: { type: "ephemeral" } };
200
+ if (typeof firstUser.content === "string") {
201
+ firstUser.content = [block, { type: "text", text: firstUser.content }];
202
+ return;
203
+ }
204
+ if (Array.isArray(firstUser.content)) firstUser.content.unshift(block);
205
+ }
206
+
207
+ export function selectBetas(body: unknown, extra: string[] = []): string {
208
+ const selected = hasFullAgentShape(body) ? [...FULL_AGENT_BETAS] : [...BASE_BETAS];
209
+ for (const beta of extra) {
210
+ const trimmed = beta.trim();
211
+ if (trimmed) selected.push(trimmed);
212
+ }
213
+ return [...new Set(selected)].join(",");
214
+ }
215
+
216
+ /**
217
+ * Headers presenting this client as Claude Code.
218
+ * Merged over whatever pi already set.
219
+ */
220
+ export function clientIdentityHeaders(body?: unknown, existingBetas?: string): Record<string, string> {
221
+ const incoming = (existingBetas ?? "").split(",").map((b) => b.trim()).filter(Boolean);
222
+ return {
223
+ "user-agent": USER_AGENT,
224
+ // Required: pi's Anthropic client reads this header to decide which betas
225
+ // to send, then puts them in the body's `betas` field.
226
+ "anthropic-beta": selectBetas(body, incoming),
227
+ "anthropic-version": "2023-06-01",
228
+ "anthropic-dangerous-direct-browser-access": "true",
229
+ "x-app": "cli",
230
+ "x-client-request-id": randomUUID(),
231
+ "x-claude-code-session-id": currentSessionId(),
232
+ "x-stainless-arch": stainlessArch(),
233
+ "x-stainless-lang": "js",
234
+ "x-stainless-os": stainlessOS(),
235
+ "x-stainless-package-version": STAINLESS_PACKAGE_VERSION,
236
+ "x-stainless-retry-count": "0",
237
+ "x-stainless-runtime": "node",
238
+ "x-stainless-runtime-version": STAINLESS_RUNTIME_VERSION,
239
+ "x-stainless-timeout": "600",
240
+ };
241
+ }