@centerforagenticai/pi-multi-account 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,531 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2026 Leo Henon
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in
14
+ * all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ *
24
+ * Provenance: pi-anthropic-oauth@0.2.4-intel.2, private pin
25
+ * 99ac00f290efbcb93e7f23e6b7482d8727367aa7, upstream baseline
26
+ * 53266ecb51b6d1890ef3f7251a64cb1d71d96099, source src/stream.ts.
27
+ * Local delta: adaptive thinking emits type=adaptive and mapped output effort,
28
+ * without budget_tokens; all other request and response behavior is unchanged.
29
+ */
30
+
31
+ import { Anthropic } from "@anthropic-ai/sdk";
32
+ import type { MessageCreateParamsStreaming } from "@anthropic-ai/sdk/resources/messages.js";
33
+ import {
34
+ type Api,
35
+ type AssistantMessage,
36
+ type AssistantMessageEventStream,
37
+ calculateCost,
38
+ type Context,
39
+ createAssistantMessageEventStream,
40
+ type Model,
41
+ type SimpleStreamOptions,
42
+ type StopReason,
43
+ } from "@earendil-works/pi-ai";
44
+ type IndexedBlock =
45
+ | ({ type: "text"; text: string } & { index: number })
46
+ | ({ type: "thinking"; thinking: string; thinkingSignature?: string } & {
47
+ index: number;
48
+ })
49
+ | ({
50
+ type: "toolCall";
51
+ id: string;
52
+ name: string;
53
+ arguments: Record<string, unknown>;
54
+ partialJson: string;
55
+ } & { index: number });
56
+ type UpstreamHelpers = {
57
+ isClaudeOAuthAccessToken: (apiKey: string) => boolean;
58
+ USER_AGENT: string;
59
+ convertPiMessagesToAnthropic: (
60
+ messages: Context["messages"],
61
+ isOAuth: boolean,
62
+ model: Pick<Model<Api>, "provider" | "api" | "id">,
63
+ ) => unknown;
64
+ convertPiToolsToAnthropic: (tools: NonNullable<Context["tools"]>, isOAuth: boolean) => unknown;
65
+ fromClaudeCodeToolName: (name: string, tools?: NonNullable<Context["tools"]>) => string;
66
+ buildAnthropicSystemPrompt: (systemPrompt: string | undefined, isOAuth: boolean) => unknown;
67
+ };
68
+
69
+ const upstreamAuthPath = "../packages/pi-anthropic-oauth/src/auth.ts";
70
+ const upstreamConvertPath = "../packages/pi-anthropic-oauth/src/convert.ts";
71
+ const upstreamPromptPath = "../packages/pi-anthropic-oauth/src/prompt.ts";
72
+ const upstreamHelpers = {
73
+ ...(await import(upstreamAuthPath)),
74
+ ...(await import(upstreamConvertPath)),
75
+ ...(await import(upstreamPromptPath)),
76
+ } as unknown as UpstreamHelpers;
77
+ const {
78
+ isClaudeOAuthAccessToken,
79
+ USER_AGENT,
80
+ convertPiMessagesToAnthropic,
81
+ convertPiToolsToAnthropic,
82
+ fromClaudeCodeToolName,
83
+ buildAnthropicSystemPrompt,
84
+ } = upstreamHelpers;
85
+
86
+ const REQUIRED_BETAS = [
87
+ "claude-code-20250219",
88
+ "oauth-2025-04-20",
89
+ // fine-grained-tool-streaming removed: it ships the model's raw, unvalidated
90
+ // tool-input JSON. For large edits full of quotes/newlines the streamed
91
+ // string escaping breaks, so a field (e.g. edit.oldText) swallows the rest of
92
+ // the structure — surfacing as either a hard JSON.parse crash or a wrong-shape
93
+ // schema-validation failure. Default streaming has the server validate/buffer
94
+ // tool JSON, guaranteeing well-formed, correctly-structured input.
95
+ "interleaved-thinking-2025-05-14",
96
+ ] as const;
97
+
98
+ function mapStopReason(reason: string | null | undefined): StopReason {
99
+ switch (reason) {
100
+ case "end_turn":
101
+ case "pause_turn":
102
+ case "stop_sequence":
103
+ return "stop";
104
+ case "max_tokens":
105
+ return "length";
106
+ case "tool_use":
107
+ return "toolUse";
108
+ default:
109
+ return "error";
110
+ }
111
+ }
112
+
113
+ function headersToRecord(headers: Headers): Record<string, string> {
114
+ const record: Record<string, string> = {};
115
+ headers.forEach((value, key) => {
116
+ record[key] = value;
117
+ });
118
+ return record;
119
+ }
120
+
121
+ function makeDefaultHeaders(
122
+ isOAuth: boolean,
123
+ options?: SimpleStreamOptions,
124
+ ): Record<string, string> {
125
+ const headers: Record<string, string> = {
126
+ accept: "application/json",
127
+ "anthropic-dangerous-direct-browser-access": "true",
128
+ };
129
+
130
+ if (isOAuth) {
131
+ headers["anthropic-beta"] = REQUIRED_BETAS.join(",");
132
+ headers["user-agent"] = USER_AGENT;
133
+ headers["x-app"] = "cli";
134
+ } else {
135
+ headers["anthropic-beta"] = ["interleaved-thinking-2025-05-14"].join(",");
136
+ }
137
+
138
+ if (options?.headers) {
139
+ for (const [key, value] of Object.entries(options.headers)) {
140
+ const normalizedKey = key.toLowerCase();
141
+ if (
142
+ isOAuth &&
143
+ (normalizedKey === "x-api-key" || normalizedKey === "authorization")
144
+ ) {
145
+ continue;
146
+ }
147
+ const existingKey = Object.keys(headers).find(
148
+ (header) => header.toLowerCase() === normalizedKey,
149
+ );
150
+ if (existingKey) delete headers[existingKey];
151
+ if (value !== null) headers[key] = value;
152
+ }
153
+ }
154
+
155
+ return headers;
156
+ }
157
+
158
+ export function streamAnthropicAdaptive(
159
+ model: Model<Api>,
160
+ context: Context,
161
+ options?: SimpleStreamOptions,
162
+ ): AssistantMessageEventStream {
163
+ const stream = createAssistantMessageEventStream();
164
+
165
+ void (async () => {
166
+ const output: AssistantMessage = {
167
+ role: "assistant",
168
+ content: [],
169
+ api: model.api,
170
+ provider: model.provider,
171
+ model: model.id,
172
+ usage: {
173
+ input: 0,
174
+ output: 0,
175
+ cacheRead: 0,
176
+ cacheWrite: 0,
177
+ totalTokens: 0,
178
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
179
+ },
180
+ stopReason: "stop",
181
+ timestamp: Date.now(),
182
+ };
183
+
184
+ try {
185
+ const apiKey = options?.apiKey;
186
+ if (!apiKey) {
187
+ throw new Error(
188
+ "No Anthropic auth available. Run /login and choose Claude Pro/Max.",
189
+ );
190
+ }
191
+
192
+ const isOAuth = isClaudeOAuthAccessToken(apiKey);
193
+ const defaultHeaders = makeDefaultHeaders(isOAuth, options);
194
+
195
+ if (isOAuth) defaultHeaders.authorization = `Bearer ${apiKey}`;
196
+
197
+ const client = new Anthropic({
198
+ baseURL: model.baseUrl,
199
+ apiKey: isOAuth ? null : apiKey,
200
+ authToken: isOAuth ? apiKey : null,
201
+ defaultHeaders,
202
+ dangerouslyAllowBrowser: true,
203
+ });
204
+
205
+ const maxTokens =
206
+ options?.maxTokens || Math.floor(model.maxTokens / 3);
207
+
208
+ const params: MessageCreateParamsStreaming = {
209
+ model: model.id,
210
+ messages: convertPiMessagesToAnthropic(
211
+ context.messages,
212
+ isOAuth,
213
+ model,
214
+ ) as MessageCreateParamsStreaming["messages"],
215
+ max_tokens: maxTokens,
216
+ stream: true,
217
+ };
218
+
219
+ const system = buildAnthropicSystemPrompt(context.systemPrompt, isOAuth);
220
+ if (system) params.system = system as never;
221
+ if (context.tools?.length)
222
+ params.tools = convertPiToolsToAnthropic(
223
+ context.tools,
224
+ isOAuth,
225
+ ) as NonNullable<MessageCreateParamsStreaming["tools"]>;
226
+
227
+ // The pinned stream gates both thinking modes on maxTokens > 1 because a
228
+ // budget must stay below the output cap. Adaptive thinking carries no
229
+ // budget, so that condition applies to the legacy branch only.
230
+ if (options?.reasoning && model.reasoning) {
231
+ const forceAdaptiveThinking =
232
+ (model.compat as { forceAdaptiveThinking?: boolean } | undefined)
233
+ ?.forceAdaptiveThinking === true;
234
+ if (forceAdaptiveThinking) {
235
+ const defaultEfforts: Record<string, string> = {
236
+ minimal: "low",
237
+ low: "low",
238
+ medium: "medium",
239
+ };
240
+ const mappedEffort = (model as Model<Api> & {
241
+ thinkingLevelMap?: Record<string, string | null>;
242
+ }).thinkingLevelMap?.[options.reasoning];
243
+ params.thinking = { type: "adaptive" } as never;
244
+ (params as MessageCreateParamsStreaming & {
245
+ output_config?: { effort: string };
246
+ }).output_config = {
247
+ effort:
248
+ typeof mappedEffort === "string"
249
+ ? mappedEffort
250
+ : defaultEfforts[options.reasoning] ?? "high",
251
+ };
252
+ } else if (maxTokens > 1) {
253
+ const defaultBudgets: Record<string, number> = {
254
+ minimal: 1024,
255
+ low: 4096,
256
+ medium: 10240,
257
+ high: 20480,
258
+ xhigh: 32000,
259
+ };
260
+ const customBudget =
261
+ options.thinkingBudgets?.[
262
+ options.reasoning as keyof typeof options.thinkingBudgets
263
+ ];
264
+ const requestedBudget =
265
+ customBudget ?? defaultBudgets[options.reasoning] ?? 10240;
266
+
267
+ params.thinking = {
268
+ type: "enabled",
269
+ budget_tokens: Math.min(requestedBudget, maxTokens - 1),
270
+ };
271
+ }
272
+ }
273
+
274
+ // Raw stream instead of the MessageStream helper: MessageStream
275
+ // accumulates tool_use input and JSON.parses it on content_block_stop,
276
+ // which throws under fine-grained-tool-streaming (input may be invalid
277
+ // mid-flight) and aborts the turn. The raw stream yields the same
278
+ // RawMessageStreamEvents; tool args are already parsed leniently below.
279
+ const { data: anthropicStream, response: httpResponse } =
280
+ await client.messages
281
+ .create(params, {
282
+ signal: options?.signal,
283
+ })
284
+ .withResponse();
285
+
286
+ if (options?.onResponse) {
287
+ try {
288
+ await options.onResponse(
289
+ {
290
+ status: httpResponse.status,
291
+ headers: headersToRecord(httpResponse.headers),
292
+ },
293
+ model,
294
+ );
295
+ } catch {
296
+ // Response hooks are best-effort and should not break streaming.
297
+ }
298
+ }
299
+
300
+ stream.push({ type: "start", partial: output });
301
+
302
+ const blocks = output.content as IndexedBlock[];
303
+
304
+ for await (const event of anthropicStream) {
305
+ if (event.type === "message_start") {
306
+ output.usage.input = event.message.usage.input_tokens || 0;
307
+ output.usage.output = event.message.usage.output_tokens || 0;
308
+ output.usage.cacheRead =
309
+ (event.message.usage as { cache_read_input_tokens?: number })
310
+ .cache_read_input_tokens || 0;
311
+ output.usage.cacheWrite =
312
+ (event.message.usage as { cache_creation_input_tokens?: number })
313
+ .cache_creation_input_tokens || 0;
314
+ output.usage.totalTokens =
315
+ output.usage.input +
316
+ output.usage.output +
317
+ output.usage.cacheRead +
318
+ output.usage.cacheWrite;
319
+ calculateCost(model, output.usage);
320
+ continue;
321
+ }
322
+
323
+ if (event.type === "content_block_start") {
324
+ if (event.content_block.type === "text") {
325
+ output.content.push({
326
+ type: "text",
327
+ text: "",
328
+ index: event.index,
329
+ } as IndexedBlock);
330
+ stream.push({
331
+ type: "text_start",
332
+ contentIndex: output.content.length - 1,
333
+ partial: output,
334
+ });
335
+ } else if (event.content_block.type === "thinking") {
336
+ output.content.push({
337
+ type: "thinking",
338
+ thinking: "",
339
+ thinkingSignature: "",
340
+ index: event.index,
341
+ } as IndexedBlock);
342
+ stream.push({
343
+ type: "thinking_start",
344
+ contentIndex: output.content.length - 1,
345
+ partial: output,
346
+ });
347
+ } else if (event.content_block.type === "redacted_thinking") {
348
+ output.content.push({
349
+ type: "thinking",
350
+ thinking: "[Reasoning redacted]",
351
+ thinkingSignature: event.content_block.data,
352
+ redacted: true,
353
+ index: event.index,
354
+ } as IndexedBlock);
355
+ stream.push({
356
+ type: "thinking_start",
357
+ contentIndex: output.content.length - 1,
358
+ partial: output,
359
+ });
360
+ } else if (event.content_block.type === "tool_use") {
361
+ output.content.push({
362
+ type: "toolCall",
363
+ id: event.content_block.id,
364
+ name: isOAuth
365
+ ? fromClaudeCodeToolName(
366
+ event.content_block.name,
367
+ context.tools,
368
+ )
369
+ : event.content_block.name,
370
+ arguments: {},
371
+ partialJson: "",
372
+ index: event.index,
373
+ } as IndexedBlock);
374
+ stream.push({
375
+ type: "toolcall_start",
376
+ contentIndex: output.content.length - 1,
377
+ partial: output,
378
+ });
379
+ }
380
+ continue;
381
+ }
382
+
383
+ if (event.type === "content_block_delta") {
384
+ const contentIndex = blocks.findIndex(
385
+ (block) => block.index === event.index,
386
+ );
387
+ const block = blocks[contentIndex];
388
+ if (!block) continue;
389
+
390
+ if (event.delta.type === "text_delta" && block.type === "text") {
391
+ block.text += event.delta.text;
392
+ stream.push({
393
+ type: "text_delta",
394
+ contentIndex,
395
+ delta: event.delta.text,
396
+ partial: output,
397
+ });
398
+ } else if (
399
+ event.delta.type === "thinking_delta" &&
400
+ block.type === "thinking"
401
+ ) {
402
+ block.thinking += event.delta.thinking;
403
+ stream.push({
404
+ type: "thinking_delta",
405
+ contentIndex,
406
+ delta: event.delta.thinking,
407
+ partial: output,
408
+ });
409
+ } else if (
410
+ event.delta.type === "signature_delta" &&
411
+ block.type === "thinking"
412
+ ) {
413
+ block.thinkingSignature =
414
+ (block.thinkingSignature || "") + event.delta.signature;
415
+ } else if (
416
+ event.delta.type === "input_json_delta" &&
417
+ block.type === "toolCall"
418
+ ) {
419
+ block.partialJson += event.delta.partial_json;
420
+ try {
421
+ block.arguments = JSON.parse(block.partialJson) as Record<
422
+ string,
423
+ unknown
424
+ >;
425
+ } catch {}
426
+ stream.push({
427
+ type: "toolcall_delta",
428
+ contentIndex,
429
+ delta: event.delta.partial_json,
430
+ partial: output,
431
+ });
432
+ }
433
+ continue;
434
+ }
435
+
436
+ if (event.type === "content_block_stop") {
437
+ const contentIndex = blocks.findIndex(
438
+ (block) => block.index === event.index,
439
+ );
440
+ const block = blocks[contentIndex];
441
+ if (!block) continue;
442
+
443
+ delete (block as { index?: number }).index;
444
+ if (block.type === "text") {
445
+ stream.push({
446
+ type: "text_end",
447
+ contentIndex,
448
+ content: block.text,
449
+ partial: output,
450
+ });
451
+ } else if (block.type === "thinking") {
452
+ stream.push({
453
+ type: "thinking_end",
454
+ contentIndex,
455
+ content: block.thinking,
456
+ partial: output,
457
+ });
458
+ } else if (block.type === "toolCall") {
459
+ try {
460
+ block.arguments = JSON.parse(block.partialJson) as Record<
461
+ string,
462
+ unknown
463
+ >;
464
+ } catch {}
465
+ delete (block as { partialJson?: string }).partialJson;
466
+ stream.push({
467
+ type: "toolcall_end",
468
+ contentIndex,
469
+ toolCall: block,
470
+ partial: output,
471
+ });
472
+ }
473
+ continue;
474
+ }
475
+
476
+ if (event.type === "message_delta") {
477
+ output.stopReason = mapStopReason(event.delta.stop_reason);
478
+ output.usage.input =
479
+ (event.usage as { input_tokens?: number }).input_tokens ||
480
+ output.usage.input;
481
+ output.usage.output =
482
+ (event.usage as { output_tokens?: number }).output_tokens ||
483
+ output.usage.output;
484
+ output.usage.cacheRead =
485
+ (event.usage as { cache_read_input_tokens?: number })
486
+ .cache_read_input_tokens || 0;
487
+ output.usage.cacheWrite =
488
+ (event.usage as { cache_creation_input_tokens?: number })
489
+ .cache_creation_input_tokens || 0;
490
+ const thinkingTokens = (
491
+ event.usage as {
492
+ output_tokens_details?: { thinking_tokens?: number };
493
+ }
494
+ ).output_tokens_details?.thinking_tokens;
495
+ if (thinkingTokens != null) {
496
+ output.usage.reasoning = thinkingTokens;
497
+ }
498
+ output.usage.totalTokens =
499
+ output.usage.input +
500
+ output.usage.output +
501
+ output.usage.cacheRead +
502
+ output.usage.cacheWrite;
503
+ calculateCost(model, output.usage);
504
+ }
505
+ }
506
+
507
+ if (options?.signal?.aborted) throw new Error("Request aborted");
508
+ stream.push({
509
+ type: "done",
510
+ reason: output.stopReason as "stop" | "length" | "toolUse",
511
+ message: output,
512
+ });
513
+ stream.end();
514
+ } catch (error) {
515
+ for (const block of output.content as Array<{
516
+ index?: number;
517
+ partialJson?: string;
518
+ }>) {
519
+ delete block.index;
520
+ delete block.partialJson;
521
+ }
522
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
523
+ output.errorMessage =
524
+ error instanceof Error ? error.message : String(error);
525
+ stream.push({ type: "error", reason: output.stopReason, error: output });
526
+ stream.end();
527
+ }
528
+ })();
529
+
530
+ return stream;
531
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ createAssistantMessageEventStream,
3
+ type Api,
4
+ type AssistantMessage,
5
+ type AssistantMessageEvent,
6
+ type AssistantMessageEventStream,
7
+ type Context,
8
+ type Model,
9
+ type ProviderResponse,
10
+ type SimpleStreamOptions,
11
+ } from "@earendil-works/pi-ai";
12
+ import {
13
+ sanitizeDiagnosticText,
14
+ sanitizeHeaderValue,
15
+ } from "./diagnostics.js";
16
+
17
+ export const ANTHROPIC_ALIAS_API = "hypha-anthropic-oauth" as const;
18
+
19
+ export type AnthropicUpstreamStream = (
20
+ model: Model<Api>,
21
+ context: Context,
22
+ options?: SimpleStreamOptions,
23
+ ) => AssistantMessageEventStream;
24
+
25
+ /**
26
+ * Copies and sanitizes response metadata before it crosses into an
27
+ * extension-owned callback. The upstream response and headers are never
28
+ * retained or serialized by this adapter.
29
+ */
30
+ export function sanitizeAnthropicProviderResponse(
31
+ response: ProviderResponse,
32
+ ): ProviderResponse {
33
+ return {
34
+ status: response.status,
35
+ headers: Object.fromEntries(
36
+ Object.entries(response.headers).map(([name, value]) => [
37
+ sanitizeDiagnosticText(name),
38
+ sanitizeHeaderValue(name, value),
39
+ ]),
40
+ ),
41
+ };
42
+ }
43
+
44
+ function withAliasAttribution(
45
+ message: AssistantMessage,
46
+ aliasModel: Model<Api>,
47
+ ): AssistantMessage {
48
+ return {
49
+ ...message,
50
+ api: aliasModel.api,
51
+ provider: aliasModel.provider,
52
+ model: aliasModel.id,
53
+ };
54
+ }
55
+
56
+ function sanitizeUpstreamError(message: AssistantMessage): AssistantMessage {
57
+ if (message.errorMessage === undefined) return message;
58
+ return {
59
+ ...message,
60
+ errorMessage: sanitizeDiagnosticText(message.errorMessage),
61
+ };
62
+ }
63
+
64
+ function withAliasEvent(
65
+ event: AssistantMessageEvent,
66
+ aliasModel: Model<Api>,
67
+ ): AssistantMessageEvent {
68
+ switch (event.type) {
69
+ case "done":
70
+ return {
71
+ ...event,
72
+ message: withAliasAttribution(event.message, aliasModel),
73
+ };
74
+ case "error":
75
+ return {
76
+ ...event,
77
+ error: withAliasAttribution(
78
+ sanitizeUpstreamError(event.error),
79
+ aliasModel,
80
+ ),
81
+ };
82
+ default:
83
+ return {
84
+ ...event,
85
+ partial: withAliasAttribution(event.partial, aliasModel),
86
+ };
87
+ }
88
+ }
89
+
90
+ function reattributeStream(
91
+ upstream: AssistantMessageEventStream,
92
+ aliasModel: Model<Api>,
93
+ ): AssistantMessageEventStream {
94
+ const attributed = createAssistantMessageEventStream();
95
+
96
+ void (async () => {
97
+ for await (const event of upstream) {
98
+ attributed.push(withAliasEvent(event, aliasModel));
99
+ }
100
+ })();
101
+
102
+ return attributed;
103
+ }
104
+
105
+ /**
106
+ * Creates the sole extension-owned Anthropic alias handler. Pi resolves the
107
+ * alias credential before calling this function, so the adapter forwards the
108
+ * complete options object (including apiKey) to upstream. Its only request-side
109
+ * model change is a shallow clone with provider rebound to `anthropic`, which
110
+ * keeps the exact upstream OAuth/Claude Code transport path active.
111
+ */
112
+ export function createAnthropicAliasStream(
113
+ upstreamStream: AnthropicUpstreamStream,
114
+ ): AnthropicUpstreamStream {
115
+ return (aliasModel, context, options) => {
116
+ const upstreamModel: Model<Api> = {
117
+ ...aliasModel,
118
+ provider: "anthropic",
119
+ };
120
+
121
+ let upstreamOptions = options;
122
+ if (options?.onResponse) {
123
+ const aliasOnResponse = options.onResponse;
124
+ upstreamOptions = {
125
+ ...options,
126
+ onResponse: async (response) => {
127
+ await aliasOnResponse(
128
+ sanitizeAnthropicProviderResponse(response),
129
+ aliasModel,
130
+ );
131
+ },
132
+ };
133
+ }
134
+
135
+ return reattributeStream(
136
+ upstreamStream(upstreamModel, context, upstreamOptions),
137
+ aliasModel,
138
+ );
139
+ };
140
+ }