@oh-my-pi/pi-ai 3.20.0 → 3.34.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 {
@@ -28,6 +28,16 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode";
28
28
 
29
29
  import { transformMessages } from "./transorm-messages";
30
30
 
31
+ // Stealth mode: Mimic Claude Code's tool naming exactly
32
+ const claudeCodeVersion = "2.1.2";
33
+
34
+ // Prefix all tool names to avoid collisions with Claude Code's built-in tools
35
+ const toolNamePrefix = "cli_";
36
+
37
+ const toClaudeCodeName = (name: string) => toolNamePrefix + name;
38
+ const fromClaudeCodeName = (name: string) =>
39
+ name.startsWith(toolNamePrefix) ? name.slice(toolNamePrefix.length) : name;
40
+
31
41
  /**
32
42
  * Convert content blocks to Anthropic API format
33
43
  */
@@ -157,7 +167,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
157
167
  const block: Block = {
158
168
  type: "toolCall",
159
169
  id: event.content_block.id,
160
- name: event.content_block.name,
170
+ name: isOAuthToken ? fromClaudeCodeName(event.content_block.name) : event.content_block.name,
161
171
  arguments: event.content_block.input as Record<string, any>,
162
172
  partialJson: "",
163
173
  index: event.index,
@@ -278,23 +288,68 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
278
288
  return stream;
279
289
  };
280
290
 
291
+ function isOAuthToken(apiKey: string): boolean {
292
+ return apiKey.includes("sk-ant-oat");
293
+ }
294
+
295
+ // Build deduplicated beta header string
296
+ function buildBetaHeader(baseBetas: string[], extraBetas: string[]): string {
297
+ const seen = new Set<string>();
298
+ const result: string[] = [];
299
+ for (const beta of [...baseBetas, ...extraBetas]) {
300
+ const trimmed = beta.trim();
301
+ if (trimmed && !seen.has(trimmed)) {
302
+ seen.add(trimmed);
303
+ result.push(trimmed);
304
+ }
305
+ }
306
+ return result.join(",");
307
+ }
308
+
281
309
  function createClient(
282
310
  model: Model<"anthropic-messages">,
283
311
  apiKey: string,
284
312
  interleavedThinking: boolean,
285
313
  ): { 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");
314
+ const oauthToken = isOAuthToken(apiKey);
315
+
316
+ // Base betas required for Claude Code compatibility
317
+ const baseBetas = oauthToken
318
+ ? [
319
+ "claude-code-20250219",
320
+ "oauth-2025-04-20",
321
+ "interleaved-thinking-2025-05-14",
322
+ "fine-grained-tool-streaming-2025-05-14",
323
+ ]
324
+ : ["fine-grained-tool-streaming-2025-05-14"];
325
+
326
+ // Add interleaved thinking if requested (and not already in base)
327
+ const extraBetas: string[] = [];
328
+ if (interleavedThinking && !oauthToken) {
329
+ extraBetas.push("interleaved-thinking-2025-05-14");
330
+ }
331
+
332
+ // Include any betas from model headers
333
+ const modelBeta = model.headers?.["anthropic-beta"];
334
+ if (modelBeta) {
335
+ extraBetas.push(...modelBeta.split(","));
289
336
  }
290
337
 
291
- if (apiKey.includes("sk-ant-oat")) {
338
+ const betaHeader = buildBetaHeader(baseBetas, extraBetas);
339
+
340
+ if (oauthToken) {
341
+ // Stealth mode: Mimic Claude Code's headers exactly
292
342
  const defaultHeaders = {
293
343
  accept: "application/json",
294
344
  "anthropic-dangerous-direct-browser-access": "true",
295
- "anthropic-beta": `oauth-2025-04-20,${betaFeatures.join(",")}`,
345
+ "anthropic-beta": betaHeader,
346
+ "user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
347
+ "x-app": "cli",
296
348
  ...(model.headers || {}),
297
349
  };
350
+ // Don't duplicate anthropic-beta from model.headers
351
+ delete (defaultHeaders as Record<string, string>)["anthropic-beta"];
352
+ (defaultHeaders as Record<string, string>)["anthropic-beta"] = betaHeader;
298
353
 
299
354
  const client = new Anthropic({
300
355
  apiKey: null,
@@ -305,23 +360,25 @@ function createClient(
305
360
  });
306
361
 
307
362
  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
- };
363
+ }
315
364
 
316
- const client = new Anthropic({
317
- apiKey,
318
- baseURL: model.baseUrl,
319
- dangerouslyAllowBrowser: true,
320
- defaultHeaders,
321
- });
365
+ const defaultHeaders = {
366
+ accept: "application/json",
367
+ "anthropic-dangerous-direct-browser-access": "true",
368
+ "anthropic-beta": betaHeader,
369
+ ...(model.headers || {}),
370
+ };
371
+ // Ensure our beta header takes precedence
372
+ (defaultHeaders as Record<string, string>)["anthropic-beta"] = betaHeader;
373
+
374
+ const client = new Anthropic({
375
+ apiKey,
376
+ baseURL: model.baseUrl,
377
+ dangerouslyAllowBrowser: true,
378
+ defaultHeaders,
379
+ });
322
380
 
323
- return { client, isOAuthToken: false };
324
- }
381
+ return { client, isOAuthToken: false };
325
382
  }
326
383
 
327
384
  function buildParams(
@@ -332,7 +389,7 @@ function buildParams(
332
389
  ): MessageCreateParamsStreaming {
333
390
  const params: MessageCreateParamsStreaming = {
334
391
  model: model.id,
335
- messages: convertMessages(context.messages, model),
392
+ messages: convertMessages(context.messages, model, isOAuthToken),
336
393
  max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
337
394
  stream: true,
338
395
  };
@@ -375,7 +432,7 @@ function buildParams(
375
432
  }
376
433
 
377
434
  if (context.tools) {
378
- params.tools = convertTools(context.tools);
435
+ params.tools = convertTools(context.tools, isOAuthToken);
379
436
  }
380
437
 
381
438
  if (options?.thinkingEnabled && model.reasoning) {
@@ -388,6 +445,9 @@ function buildParams(
388
445
  if (options?.toolChoice) {
389
446
  if (typeof options.toolChoice === "string") {
390
447
  params.tool_choice = { type: options.toolChoice };
448
+ } else if (isOAuthToken && options.toolChoice.name) {
449
+ // Prefix tool name in tool_choice for OAuth mode
450
+ params.tool_choice = { ...options.toolChoice, name: toClaudeCodeName(options.toolChoice.name) };
391
451
  } else {
392
452
  params.tool_choice = options.toolChoice;
393
453
  }
@@ -402,7 +462,11 @@ function sanitizeToolCallId(id: string): string {
402
462
  return id.replace(/[^a-zA-Z0-9_-]/g, "_");
403
463
  }
404
464
 
405
- function convertMessages(messages: Message[], model: Model<"anthropic-messages">): MessageParam[] {
465
+ function convertMessages(
466
+ messages: Message[],
467
+ model: Model<"anthropic-messages">,
468
+ isOAuthToken: boolean,
469
+ ): MessageParam[] {
406
470
  const params: MessageParam[] = [];
407
471
 
408
472
  // Transform messages for cross-provider compatibility
@@ -481,7 +545,7 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages">
481
545
  blocks.push({
482
546
  type: "tool_use",
483
547
  id: sanitizeToolCallId(block.id),
484
- name: block.name,
548
+ name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
485
549
  input: block.arguments,
486
550
  });
487
551
  }
@@ -547,14 +611,14 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages">
547
611
  return params;
548
612
  }
549
613
 
550
- function convertTools(tools: Tool[]): Anthropic.Messages.Tool[] {
614
+ function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
551
615
  if (!tools) return [];
552
616
 
553
617
  return tools.map((tool) => {
554
618
  const jsonSchema = tool.parameters as any; // TypeBox already generates JSON Schema
555
619
 
556
620
  return {
557
- name: tool.name,
621
+ name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,
558
622
  description: tool.description,
559
623
  input_schema: {
560
624
  type: "object" as const,