@jameslovespancakes/pi-plus 1.0.0 → 1.0.2

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 (41) 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/images/usage_demo.png +0 -0
  7. package/package.json +67 -67
  8. package/server/board-server.mjs +641 -641
  9. package/server/package.json +17 -17
  10. package/src/core/accounts/registry.ts +93 -93
  11. package/src/core/anthropic/client-identity.ts +241 -241
  12. package/src/core/catalog/quality.ts +314 -314
  13. package/src/core/config.ts +169 -169
  14. package/src/core/env.ts +58 -58
  15. package/src/core/exec/process.ts +146 -146
  16. package/src/core/exec/ssh-config.ts +157 -157
  17. package/src/core/policy/policy.ts +183 -183
  18. package/src/core/quota/pool.ts +64 -64
  19. package/src/core/quota/usage-source.ts +289 -289
  20. package/src/core/store.ts +43 -43
  21. package/src/domains/agents/board-setup.ts +409 -409
  22. package/src/domains/agents/index.ts +462 -462
  23. package/src/domains/models/catalog-tool.ts +361 -361
  24. package/src/domains/models/index.ts +14 -14
  25. package/src/domains/models/policy-gate.ts +169 -169
  26. package/src/domains/models/provider-picker.ts +207 -207
  27. package/src/domains/remote/config-path.ts +41 -41
  28. package/src/domains/remote/index.ts +866 -866
  29. package/src/domains/remote/setup.ts +425 -425
  30. package/src/domains/setup/index.ts +220 -220
  31. package/src/domains/subscriptions/accounts.ts +242 -242
  32. package/src/domains/subscriptions/footer.ts +182 -182
  33. package/src/domains/subscriptions/index.ts +42 -42
  34. package/src/domains/subscriptions/provider.ts +219 -219
  35. package/src/domains/subscriptions/providers/anthropic.ts +149 -149
  36. package/src/domains/subscriptions/providers/codex.ts +148 -148
  37. package/src/domains/subscriptions/routing.ts +72 -72
  38. package/src/services/usage-service.ts +186 -186
  39. package/src/ui/format.ts +73 -73
  40. package/src/ui/usage-bars.ts +154 -154
  41. 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
+ }