@oh-my-pi/pi-ai 3.20.1 → 3.35.0

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/src/models.ts CHANGED
@@ -12,34 +12,28 @@ for (const [provider, models] of Object.entries(MODELS)) {
12
12
  modelRegistry.set(provider, providerModels);
13
13
  }
14
14
 
15
- type ProviderModels = typeof MODELS;
16
- type ProviderWithModels = keyof ProviderModels;
17
-
18
15
  type ModelApi<
19
- TProvider extends ProviderWithModels,
20
- TModelId extends keyof ProviderModels[TProvider],
21
- > = ProviderModels[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;
16
+ TProvider extends KnownProvider,
17
+ TModelId extends keyof (typeof MODELS)[TProvider],
18
+ > = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;
22
19
 
23
- export function getModel<TProvider extends ProviderWithModels, TModelId extends keyof ProviderModels[TProvider]>(
20
+ export function getModel<TProvider extends KnownProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(
24
21
  provider: TProvider,
25
22
  modelId: TModelId,
26
- ): Model<ModelApi<TProvider, TModelId>>;
27
- export function getModel(provider: KnownProvider, modelId: string): Model<Api> | undefined;
28
- export function getModel(provider: KnownProvider, modelId: string): Model<Api> | undefined {
29
- return modelRegistry.get(provider)?.get(modelId as string) as Model<Api> | undefined;
23
+ ): Model<ModelApi<TProvider, TModelId>> {
24
+ const providerModels = modelRegistry.get(provider);
25
+ return providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;
30
26
  }
31
27
 
32
28
  export function getProviders(): KnownProvider[] {
33
29
  return Array.from(modelRegistry.keys()) as KnownProvider[];
34
30
  }
35
31
 
36
- export function getModels<TProvider extends ProviderWithModels>(
32
+ export function getModels<TProvider extends KnownProvider>(
37
33
  provider: TProvider,
38
- ): Model<ModelApi<TProvider, keyof ProviderModels[TProvider]>>[];
39
- export function getModels(provider: KnownProvider): Model<Api>[];
40
- export function getModels(provider: KnownProvider): Model<Api>[] {
34
+ ): Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {
41
35
  const models = modelRegistry.get(provider);
42
- return models ? (Array.from(models.values()) as Model<Api>[]) : [];
36
+ return models ? (Array.from(models.values()) as Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[]) : [];
43
37
  }
44
38
 
45
39
  export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"] {
@@ -56,7 +50,7 @@ const XHIGH_MODELS = new Set(["gpt-5.1-codex-max", "gpt-5.2", "gpt-5.2-codex"]);
56
50
 
57
51
  /**
58
52
  * Check if a model supports xhigh thinking level.
59
- * Currently only certain OpenAI models support this.
53
+ * Currently only certain OpenAI Codex models support this.
60
54
  */
61
55
  export function supportsXhigh<TApi extends Api>(model: Model<TApi>): boolean {
62
56
  return XHIGH_MODELS.has(model.id);
@@ -3,7 +3,7 @@ import type {
3
3
  ContentBlockParam,
4
4
  MessageCreateParamsStreaming,
5
5
  MessageParam,
6
- } from "@anthropic-ai/sdk/resources/messages.js";
6
+ } from "@anthropic-ai/sdk/resources/messages";
7
7
  import { calculateCost } from "../models";
8
8
  import { getEnvApiKey } from "../stream";
9
9
  import type {
@@ -24,10 +24,21 @@ import type {
24
24
  } from "../types";
25
25
  import { AssistantMessageEventStream } from "../utils/event-stream";
26
26
  import { parseStreamingJson } from "../utils/json-parse";
27
+ import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
27
28
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
28
29
 
29
30
  import { transformMessages } from "./transorm-messages";
30
31
 
32
+ // Stealth mode: Mimic Claude Code's tool naming exactly
33
+ const claudeCodeVersion = "2.1.2";
34
+
35
+ // Prefix all tool names to avoid collisions with Claude Code's built-in tools
36
+ const toolNamePrefix = "cli_";
37
+
38
+ const toClaudeCodeName = (name: string) => toolNamePrefix + name;
39
+ const fromClaudeCodeName = (name: string) =>
40
+ name.startsWith(toolNamePrefix) ? name.slice(toolNamePrefix.length) : name;
41
+
31
42
  /**
32
43
  * Convert content blocks to Anthropic API format
33
44
  */
@@ -157,7 +168,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
157
168
  const block: Block = {
158
169
  type: "toolCall",
159
170
  id: event.content_block.id,
160
- name: event.content_block.name,
171
+ name: isOAuthToken ? fromClaudeCodeName(event.content_block.name) : event.content_block.name,
161
172
  arguments: event.content_block.input as Record<string, any>,
162
173
  partialJson: "",
163
174
  index: event.index,
@@ -269,7 +280,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
269
280
  } catch (error) {
270
281
  for (const block of output.content) delete (block as any).index;
271
282
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
272
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
283
+ output.errorMessage = formatErrorMessageWithRetryAfter(error);
273
284
  stream.push({ type: "error", reason: output.stopReason, error: output });
274
285
  stream.end();
275
286
  }
@@ -278,23 +289,68 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
278
289
  return stream;
279
290
  };
280
291
 
292
+ function isOAuthToken(apiKey: string): boolean {
293
+ return apiKey.includes("sk-ant-oat");
294
+ }
295
+
296
+ // Build deduplicated beta header string
297
+ function buildBetaHeader(baseBetas: string[], extraBetas: string[]): string {
298
+ const seen = new Set<string>();
299
+ const result: string[] = [];
300
+ for (const beta of [...baseBetas, ...extraBetas]) {
301
+ const trimmed = beta.trim();
302
+ if (trimmed && !seen.has(trimmed)) {
303
+ seen.add(trimmed);
304
+ result.push(trimmed);
305
+ }
306
+ }
307
+ return result.join(",");
308
+ }
309
+
281
310
  function createClient(
282
311
  model: Model<"anthropic-messages">,
283
312
  apiKey: string,
284
313
  interleavedThinking: boolean,
285
314
  ): { client: Anthropic; isOAuthToken: boolean } {
286
- const betaFeatures = ["fine-grained-tool-streaming-2025-05-14"];
287
- if (interleavedThinking) {
288
- betaFeatures.push("interleaved-thinking-2025-05-14");
315
+ const oauthToken = isOAuthToken(apiKey);
316
+
317
+ // Base betas required for Claude Code compatibility
318
+ const baseBetas = oauthToken
319
+ ? [
320
+ "claude-code-20250219",
321
+ "oauth-2025-04-20",
322
+ "interleaved-thinking-2025-05-14",
323
+ "fine-grained-tool-streaming-2025-05-14",
324
+ ]
325
+ : ["fine-grained-tool-streaming-2025-05-14"];
326
+
327
+ // Add interleaved thinking if requested (and not already in base)
328
+ const extraBetas: string[] = [];
329
+ if (interleavedThinking && !oauthToken) {
330
+ extraBetas.push("interleaved-thinking-2025-05-14");
331
+ }
332
+
333
+ // Include any betas from model headers
334
+ const modelBeta = model.headers?.["anthropic-beta"];
335
+ if (modelBeta) {
336
+ extraBetas.push(...modelBeta.split(","));
289
337
  }
290
338
 
291
- if (apiKey.includes("sk-ant-oat")) {
339
+ const betaHeader = buildBetaHeader(baseBetas, extraBetas);
340
+
341
+ if (oauthToken) {
342
+ // Stealth mode: Mimic Claude Code's headers exactly
292
343
  const defaultHeaders = {
293
344
  accept: "application/json",
294
345
  "anthropic-dangerous-direct-browser-access": "true",
295
- "anthropic-beta": `oauth-2025-04-20,${betaFeatures.join(",")}`,
346
+ "anthropic-beta": betaHeader,
347
+ "user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
348
+ "x-app": "cli",
296
349
  ...(model.headers || {}),
297
350
  };
351
+ // Don't duplicate anthropic-beta from model.headers
352
+ delete (defaultHeaders as Record<string, string>)["anthropic-beta"];
353
+ (defaultHeaders as Record<string, string>)["anthropic-beta"] = betaHeader;
298
354
 
299
355
  const client = new Anthropic({
300
356
  apiKey: null,
@@ -305,23 +361,25 @@ function createClient(
305
361
  });
306
362
 
307
363
  return { client, isOAuthToken: true };
308
- } else {
309
- const defaultHeaders = {
310
- accept: "application/json",
311
- "anthropic-dangerous-direct-browser-access": "true",
312
- "anthropic-beta": betaFeatures.join(","),
313
- ...(model.headers || {}),
314
- };
364
+ }
315
365
 
316
- const client = new Anthropic({
317
- apiKey,
318
- baseURL: model.baseUrl,
319
- dangerouslyAllowBrowser: true,
320
- defaultHeaders,
321
- });
366
+ const defaultHeaders = {
367
+ accept: "application/json",
368
+ "anthropic-dangerous-direct-browser-access": "true",
369
+ "anthropic-beta": betaHeader,
370
+ ...(model.headers || {}),
371
+ };
372
+ // Ensure our beta header takes precedence
373
+ (defaultHeaders as Record<string, string>)["anthropic-beta"] = betaHeader;
374
+
375
+ const client = new Anthropic({
376
+ apiKey,
377
+ baseURL: model.baseUrl,
378
+ dangerouslyAllowBrowser: true,
379
+ defaultHeaders,
380
+ });
322
381
 
323
- return { client, isOAuthToken: false };
324
- }
382
+ return { client, isOAuthToken: false };
325
383
  }
326
384
 
327
385
  function buildParams(
@@ -332,7 +390,7 @@ function buildParams(
332
390
  ): MessageCreateParamsStreaming {
333
391
  const params: MessageCreateParamsStreaming = {
334
392
  model: model.id,
335
- messages: convertMessages(context.messages, model),
393
+ messages: convertMessages(context.messages, model, isOAuthToken),
336
394
  max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
337
395
  stream: true,
338
396
  };
@@ -375,7 +433,7 @@ function buildParams(
375
433
  }
376
434
 
377
435
  if (context.tools) {
378
- params.tools = convertTools(context.tools);
436
+ params.tools = convertTools(context.tools, isOAuthToken);
379
437
  }
380
438
 
381
439
  if (options?.thinkingEnabled && model.reasoning) {
@@ -388,6 +446,9 @@ function buildParams(
388
446
  if (options?.toolChoice) {
389
447
  if (typeof options.toolChoice === "string") {
390
448
  params.tool_choice = { type: options.toolChoice };
449
+ } else if (isOAuthToken && options.toolChoice.name) {
450
+ // Prefix tool name in tool_choice for OAuth mode
451
+ params.tool_choice = { ...options.toolChoice, name: toClaudeCodeName(options.toolChoice.name) };
391
452
  } else {
392
453
  params.tool_choice = options.toolChoice;
393
454
  }
@@ -402,7 +463,11 @@ function sanitizeToolCallId(id: string): string {
402
463
  return id.replace(/[^a-zA-Z0-9_-]/g, "_");
403
464
  }
404
465
 
405
- function convertMessages(messages: Message[], model: Model<"anthropic-messages">): MessageParam[] {
466
+ function convertMessages(
467
+ messages: Message[],
468
+ model: Model<"anthropic-messages">,
469
+ isOAuthToken: boolean,
470
+ ): MessageParam[] {
406
471
  const params: MessageParam[] = [];
407
472
 
408
473
  // Transform messages for cross-provider compatibility
@@ -481,7 +546,7 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages">
481
546
  blocks.push({
482
547
  type: "tool_use",
483
548
  id: sanitizeToolCallId(block.id),
484
- name: block.name,
549
+ name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
485
550
  input: block.arguments,
486
551
  });
487
552
  }
@@ -547,14 +612,14 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages">
547
612
  return params;
548
613
  }
549
614
 
550
- function convertTools(tools: Tool[]): Anthropic.Messages.Tool[] {
615
+ function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
551
616
  if (!tools) return [];
552
617
 
553
618
  return tools.map((tool) => {
554
619
  const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
555
620
 
556
621
  return {
557
- name: tool.name,
622
+ name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,
558
623
  description: tool.description,
559
624
  input_schema: {
560
625
  type: "object" as const,