@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.
- package/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- 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
|
+
}
|