@f5-sales-demo/pi-ai 21.29.4 → 21.30.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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Added
8
+
9
+ - Added native Anthropic Claude Fable 5 and 5.1 catalog and transport support, including always-on
10
+ adaptive thinking, accurate limits and pricing, strict-tool compatibility, and permanent
11
+ entitlement/client-version error handling.
12
+
7
13
  ### Changed
8
14
 
9
15
  - Replaced the bundled Vertex Gemini 3.7 Flash model and provider default with GA Gemini 3.8 Flash,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "21.29.4",
4
+ "version": "21.30.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "0.123.0",
47
47
  "@aws-sdk/client-bedrock-runtime": "3.1126.0",
48
48
  "@bufbuild/protobuf": "2.14.1",
49
- "@f5-sales-demo/pi-utils": "21.29.4",
49
+ "@f5-sales-demo/pi-utils": "21.30.1",
50
50
  "@google/genai": "2.21.0",
51
51
  "@sinclair/typebox": "0.34.52",
52
52
  "@smithy/node-http-handler": "4.12.1",
@@ -120,7 +120,12 @@ const ANTHROPIC_ADAPTIVE_EFFORTS: readonly Effort[] = [
120
120
  * Adding a version here without a probe is the defect this set exists to
121
121
  * prevent: an unprobed model must fail closed to DEFAULT_REASONING_EFFORTS.
122
122
  */
123
- const ANTHROPIC_EXTENDED_EFFORT_VERSIONS: ReadonlySet<string> = new Set(["5.0"]);
123
+ const ANTHROPIC_EXTENDED_EFFORT_MODELS: ReadonlySet<string> = new Set([
124
+ "fable-5.0",
125
+ "fable-5.1",
126
+ "opus-5.0",
127
+ "sonnet-5.0",
128
+ ]);
124
129
 
125
130
  const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
126
131
  const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
@@ -135,7 +140,7 @@ type SemVer = {
135
140
  };
136
141
 
137
142
  type GeminiKind = "pro" | "flash";
138
- type AnthropicKind = "opus" | "sonnet";
143
+ type AnthropicKind = "fable" | "opus" | "sonnet";
139
144
  type OpenAIVariant = "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "mini" | "max" | "nano";
140
145
 
141
146
  const CODEX_GPT_5_4_PRIORITY_BY_VARIANT: Partial<Record<OpenAIVariant, number>> = {
@@ -453,6 +458,9 @@ function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingCo
453
458
  supportedLevels: efforts.map(effort => ({ effort, description: EFFORT_DESCRIPTIONS[effort] })),
454
459
  defaultLevel: efforts.includes(ReasoningEffort.Medium) ? ReasoningEffort.Medium : efforts[0]!,
455
460
  };
461
+ if (parsedModel.family === "anthropic" && parsedModel.kind === "fable") {
462
+ thinking.defaultLevel = Effort.High;
463
+ }
456
464
  if (
457
465
  model.provider === "google-vertex" &&
458
466
  parsedModel.family === "gemini" &&
@@ -572,7 +580,9 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
572
580
  // every future Claude the moment it lands in the catalog — the same
573
581
  // open-ended grant that caused this bug. Unprobed versions fail closed to
574
582
  // the conservative range; add one here only with a probe to back it.
575
- return ANTHROPIC_EXTENDED_EFFORT_VERSIONS.has(`${parsedModel.version.major}.${parsedModel.version.minor}`)
583
+ return ANTHROPIC_EXTENDED_EFFORT_MODELS.has(
584
+ `${parsedModel.kind}-${parsedModel.version.major}.${parsedModel.version.minor}`,
585
+ )
576
586
  ? ANTHROPIC_ADAPTIVE_EFFORTS
577
587
  : DEFAULT_REASONING_EFFORTS;
578
588
  }
@@ -667,7 +677,7 @@ function parseGeminiModel(modelId: string): GeminiModel | null {
667
677
  }
668
678
 
669
679
  function parseAnthropicModel(modelId: string): AnthropicModel | null {
670
- const match = /claude-(opus|sonnet)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
680
+ const match = /claude-(fable|opus|sonnet)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
671
681
  if (!match) {
672
682
  return null;
673
683
  }
@@ -678,6 +688,12 @@ function parseAnthropicModel(modelId: string): AnthropicModel | null {
678
688
  return { family: "anthropic", kind: match[1] as AnthropicKind, version };
679
689
  }
680
690
 
691
+ /** Whether the model requires adaptive thinking on every request. */
692
+ export function isAnthropicAlwaysThinkingModel<TApi extends Api>(model: ApiModel<TApi>): boolean {
693
+ const parsed = parseKnownModel(model.id);
694
+ return model.api === "anthropic-messages" && parsed.family === "anthropic" && parsed.kind === "fable";
695
+ }
696
+
681
697
  function parseOpenAIModel(modelId: string): OpenAIModel | null {
682
698
  const match = /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|mini|max|nano))?\b/.exec(modelId);
683
699
  if (!match) {
package/src/models.json CHANGED
@@ -3158,6 +3158,106 @@
3158
3158
  }
3159
3159
  },
3160
3160
  "anthropic": {
3161
+ "claude-fable-5": {
3162
+ "id": "claude-fable-5",
3163
+ "name": "Claude Fable 5",
3164
+ "api": "anthropic-messages",
3165
+ "provider": "anthropic",
3166
+ "baseUrl": "https://api.anthropic.com",
3167
+ "reasoning": true,
3168
+ "input": [
3169
+ "text",
3170
+ "image"
3171
+ ],
3172
+ "cost": {
3173
+ "input": 10,
3174
+ "output": 50,
3175
+ "cacheRead": 1,
3176
+ "cacheWrite": 12.5
3177
+ },
3178
+ "contextWindow": 1000000,
3179
+ "maxTokens": 128000,
3180
+ "thinking": {
3181
+ "mode": "anthropic-adaptive",
3182
+ "supportedLevels": [
3183
+ {
3184
+ "effort": "minimal",
3185
+ "description": "Very brief reasoning"
3186
+ },
3187
+ {
3188
+ "effort": "low",
3189
+ "description": "Light reasoning"
3190
+ },
3191
+ {
3192
+ "effort": "medium",
3193
+ "description": "Balanced reasoning"
3194
+ },
3195
+ {
3196
+ "effort": "high",
3197
+ "description": "Deep reasoning"
3198
+ },
3199
+ {
3200
+ "effort": "xhigh",
3201
+ "description": "Very deep reasoning"
3202
+ },
3203
+ {
3204
+ "effort": "max",
3205
+ "description": "Maximum reasoning"
3206
+ }
3207
+ ],
3208
+ "defaultLevel": "high"
3209
+ }
3210
+ },
3211
+ "claude-fable-5-1": {
3212
+ "id": "claude-fable-5-1",
3213
+ "name": "Claude Fable 5.1",
3214
+ "api": "anthropic-messages",
3215
+ "provider": "anthropic",
3216
+ "baseUrl": "https://api.anthropic.com",
3217
+ "reasoning": true,
3218
+ "input": [
3219
+ "text",
3220
+ "image"
3221
+ ],
3222
+ "cost": {
3223
+ "input": 10,
3224
+ "output": 50,
3225
+ "cacheRead": 0.25,
3226
+ "cacheWrite": 12.5
3227
+ },
3228
+ "contextWindow": 1000000,
3229
+ "maxTokens": 128000,
3230
+ "thinking": {
3231
+ "mode": "anthropic-adaptive",
3232
+ "supportedLevels": [
3233
+ {
3234
+ "effort": "minimal",
3235
+ "description": "Very brief reasoning"
3236
+ },
3237
+ {
3238
+ "effort": "low",
3239
+ "description": "Light reasoning"
3240
+ },
3241
+ {
3242
+ "effort": "medium",
3243
+ "description": "Balanced reasoning"
3244
+ },
3245
+ {
3246
+ "effort": "high",
3247
+ "description": "Deep reasoning"
3248
+ },
3249
+ {
3250
+ "effort": "xhigh",
3251
+ "description": "Very deep reasoning"
3252
+ },
3253
+ {
3254
+ "effort": "max",
3255
+ "description": "Maximum reasoning"
3256
+ }
3257
+ ],
3258
+ "defaultLevel": "high"
3259
+ }
3260
+ },
3161
3261
  "claude-3-5-sonnet-20240620": {
3162
3262
  "id": "claude-3-5-sonnet-20240620",
3163
3263
  "name": "Claude Sonnet 3.5",
@@ -9,7 +9,12 @@ import type {
9
9
  MessageParam,
10
10
  } from "@anthropic-ai/sdk/resources/messages";
11
11
  import { $env, abortableSleep, isEnoent } from "@f5-sales-demo/pi-utils";
12
- import { type AnthropicAdaptiveEffort, mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
12
+ import {
13
+ type AnthropicAdaptiveEffort,
14
+ Effort,
15
+ isAnthropicAlwaysThinkingModel,
16
+ mapEffortToAnthropicAdaptiveEffort,
17
+ } from "../model-thinking";
13
18
  import { calculateCost } from "../models";
14
19
  import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
15
20
  import type {
@@ -186,7 +191,7 @@ function getCacheControl(
186
191
  }
187
192
 
188
193
  // Stealth mode: Mimic Claude Code headers and tool prefixing.
189
- export const claudeCodeVersion = "2.1.63";
194
+ export const claudeCodeVersion = "2.1.274";
190
195
  export const claudeToolPrefix: string = "proxy_";
191
196
  export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
192
197
 
@@ -624,6 +629,7 @@ function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
624
629
  export function isProviderRetryableError(error: unknown): boolean {
625
630
  if (!(error instanceof Error)) return false;
626
631
  const msg = error.message.toLowerCase();
632
+ if (isAnthropicPermanentErrorMessage(msg)) return false;
627
633
  return (
628
634
  /rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
629
635
  msg,
@@ -633,6 +639,26 @@ export function isProviderRetryableError(error: unknown): boolean {
633
639
  );
634
640
  }
635
641
 
642
+ export function isAnthropicPermanentErrorMessage(message: string): boolean {
643
+ return /credits_required|obsolete[_ -]?client|client[_ -]?(?:is[_ -]?)?too[_ -]?old|version of (?:claude code|the client) is no longer supported/i.test(
644
+ message,
645
+ );
646
+ }
647
+
648
+ export function rewriteAnthropicPermanentError(errorMessage: string): string {
649
+ if (/credits_required/i.test(errorMessage)) {
650
+ return "Anthropic access requires credits for this model. Check the account's Claude subscription or API billing entitlement, then retry.";
651
+ }
652
+ if (
653
+ /obsolete[_ -]?client|client[_ -]?(?:is[_ -]?)?too[_ -]?old|version of (?:claude code|the client) is no longer supported/i.test(
654
+ errorMessage,
655
+ )
656
+ ) {
657
+ return "Anthropic rejected this xcsh client version. Upgrade xcsh to the latest release, then retry.";
658
+ }
659
+ return errorMessage;
660
+ }
661
+
636
662
  /**
637
663
  * Map an Anthropic `citations_delta` citation to a `WebCitation`.
638
664
  *
@@ -1114,6 +1140,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1114
1140
  output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
1115
1141
  output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
1116
1142
  output.errorMessage = rewriteCopilotAuthError(output.errorMessage, error, model.provider);
1143
+ if (model.provider === "anthropic") {
1144
+ output.errorMessage = rewriteAnthropicPermanentError(output.errorMessage);
1145
+ }
1117
1146
  output.duration = Date.now() - startTime;
1118
1147
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1119
1148
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -1250,7 +1279,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
1250
1279
  apiKey: oauthToken ? null : apiKey,
1251
1280
  authToken: oauthToken ? apiKey : undefined,
1252
1281
  baseURL: baseUrl,
1253
- maxRetries: 5,
1282
+ maxRetries: 0,
1254
1283
  dangerouslyAllowBrowser: true,
1255
1284
  defaultHeaders,
1256
1285
  logLevel: ANTHROPIC_SDK_LOG_LEVEL,
@@ -1529,6 +1558,12 @@ function buildParams(
1529
1558
  options?: AnthropicOptions,
1530
1559
  ): MessageCreateParamsStreaming {
1531
1560
  const { cacheControl } = getCacheControl(baseUrl, options?.cacheRetention);
1561
+ const alwaysThinking = isAnthropicAlwaysThinkingModel(model);
1562
+ const toolChoice =
1563
+ alwaysThinking &&
1564
+ (options?.toolChoice === "any" || (typeof options?.toolChoice === "object" && options.toolChoice.type === "tool"))
1565
+ ? "auto"
1566
+ : options?.toolChoice;
1532
1567
  const params: AnthropicSamplingParams = {
1533
1568
  model: model.id,
1534
1569
  messages: convertAnthropicMessages(context.messages, model, isOAuthToken),
@@ -1536,7 +1571,7 @@ function buildParams(
1536
1571
  stream: true,
1537
1572
  };
1538
1573
 
1539
- if (options?.temperature !== undefined) {
1574
+ if (!alwaysThinking && options?.temperature !== undefined) {
1540
1575
  params.temperature = options.temperature;
1541
1576
  }
1542
1577
  if (options?.topP !== undefined) {
@@ -1547,22 +1582,26 @@ function buildParams(
1547
1582
  }
1548
1583
 
1549
1584
  if (context.tools) {
1550
- const toolChoice = options?.toolChoice;
1551
1585
  const forcedToolName =
1552
1586
  model.provider === "anthropic" &&
1553
1587
  supportsAnthropicStrictToolUse(model.id) &&
1554
- typeof toolChoice === "object" &&
1555
- toolChoice.type === "tool"
1556
- ? toolChoice.name
1588
+ typeof options?.toolChoice === "object" &&
1589
+ options.toolChoice.type === "tool"
1590
+ ? options.toolChoice.name
1557
1591
  : undefined;
1558
1592
  params.tools = convertTools(context.tools, isOAuthToken, forcedToolName);
1559
1593
  }
1560
1594
 
1561
- if (options?.thinkingEnabled && model.reasoning) {
1595
+ if ((options?.thinkingEnabled || alwaysThinking) && model.reasoning) {
1562
1596
  const mode = model.thinking?.mode;
1563
- const requestedEffort = options.reasoning;
1597
+ const requestedEffort = options?.reasoning;
1564
1598
  const effort =
1565
- options.effort ?? (requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
1599
+ options?.effort ??
1600
+ (requestedEffort
1601
+ ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort)
1602
+ : alwaysThinking
1603
+ ? mapEffortToAnthropicAdaptiveEffort(model, Effort.High)
1604
+ : undefined);
1566
1605
 
1567
1606
  if (mode === "anthropic-adaptive") {
1568
1607
  params.thinking = { type: "adaptive" };
@@ -1572,7 +1611,7 @@ function buildParams(
1572
1611
  } else {
1573
1612
  params.thinking = {
1574
1613
  type: "enabled",
1575
- budget_tokens: options.thinkingBudgetTokens || 1024,
1614
+ budget_tokens: options?.thinkingBudgetTokens || 1024,
1576
1615
  };
1577
1616
  if (mode === "anthropic-budget-effort" && effort) {
1578
1617
  params.output_config = { effort };
@@ -1580,7 +1619,7 @@ function buildParams(
1580
1619
  }
1581
1620
  // Anthropic requires temperature=1 when thinking is enabled; override any
1582
1621
  // caller-supplied value (e.g. temperature=0 set for deterministic non-interactive mode).
1583
- params.temperature = 1;
1622
+ if (!alwaysThinking) params.temperature = 1;
1584
1623
  }
1585
1624
 
1586
1625
  const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
@@ -1588,16 +1627,16 @@ function buildParams(
1588
1627
  params.metadata = { user_id: metadataUserId };
1589
1628
  }
1590
1629
 
1591
- if (options?.toolChoice) {
1592
- if (typeof options.toolChoice === "string") {
1593
- params.tool_choice = { type: options.toolChoice };
1594
- } else if (isOAuthToken && options.toolChoice.name) {
1630
+ if (toolChoice) {
1631
+ if (typeof toolChoice === "string") {
1632
+ params.tool_choice = { type: toolChoice };
1633
+ } else if (isOAuthToken && toolChoice.name) {
1595
1634
  params.tool_choice = {
1596
- ...options.toolChoice,
1597
- name: applyClaudeToolPrefix(options.toolChoice.name),
1635
+ ...toolChoice,
1636
+ name: applyClaudeToolPrefix(toolChoice.name),
1598
1637
  };
1599
1638
  } else {
1600
- params.tool_choice = options.toolChoice;
1639
+ params.tool_choice = toolChoice;
1601
1640
  }
1602
1641
  }
1603
1642
 
@@ -79,7 +79,7 @@ export function resolveAntigravityServingModelId(requestedModelId: string): stri
79
79
  * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
80
80
  */
81
81
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
82
- const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.35.3";
82
+ const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.60.0";
83
83
  const platform = process.platform === "win32" ? "win32" : process.platform;
84
84
  const arch = process.arch === "x64" ? "x64" : process.arch;
85
85
  return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
package/src/stream.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { $env, $pickenv } from "@f5-sales-demo/pi-utils";
2
2
  import { getCustomApi } from "./api-registry";
3
- import type { Effort, ReasoningEffort } from "./model-thinking";
4
3
  import {
5
4
  clampEffortThroughXHigh,
5
+ Effort,
6
6
  type EffortThroughXHigh,
7
+ isAnthropicAlwaysThinkingModel,
7
8
  mapEffortToAnthropicAdaptiveEffort,
8
9
  mapEffortToGoogleThinkingLevel,
10
+ type ReasoningEffort,
9
11
  requireSupportedEffort,
10
12
  requireSupportedReasoningEffort,
11
13
  } from "./model-thinking";
@@ -428,8 +430,9 @@ export function mapOptionsForApi<TApi extends Api>(
428
430
 
429
431
  switch (model.api) {
430
432
  case "anthropic-messages": {
431
- // Explicitly disable thinking when reasoning is not specified or model doesn't support it
432
- const reasoning = options?.reasoning;
433
+ // Fable requires adaptive thinking on every request. Other Anthropic models
434
+ // retain the caller-controlled behavior.
435
+ const reasoning = options?.reasoning ?? (isAnthropicAlwaysThinkingModel(model) ? Effort.High : undefined);
433
436
  if (!reasoning || !model.reasoning) {
434
437
  return castApi<"anthropic-messages">({
435
438
  ...base,
@@ -438,7 +441,7 @@ export function mapOptionsForApi<TApi extends Api>(
438
441
  });
439
442
  }
440
443
 
441
- let thinkingBudget = options.thinkingBudgets?.[reasoning] ?? ANTHROPIC_THINKING[reasoning];
444
+ let thinkingBudget = options?.thinkingBudgets?.[reasoning] ?? ANTHROPIC_THINKING[reasoning];
442
445
  if (thinkingBudget <= 0) {
443
446
  return castApi<"anthropic-messages">({
444
447
  ...base,