@oh-my-pi/pi-ai 15.10.8 → 15.10.9

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
@@ -2,7 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.10.9] - 2026-06-09
6
+
7
+ ### Added
8
+
9
+ - Added `antigravityRankingStrategy` and registered it as the default `CredentialRankingStrategy` for `google-antigravity`, so multi-account selection consumes the per-counter Antigravity usage reports (sorted ascending by `remainingFraction` in `fetchAntigravityUsage`) before falling back to round-robin — preventing the exhausted-counter credential from being chosen first when an unblocked sibling has headroom ([#2187](https://github.com/can1357/oh-my-pi/issues/2187)).
10
+ - Added Claude Fable 5 to the first-party Anthropic catalog, seeded directly via `ANTHROPIC_CURATED_FALLBACK_MODELS` rather than waiting on models.dev (1M context / 128k output, adaptive thinking, $10/$50 per MTok). The model parser recognizes the `fable` kind so effort tiers (low→max), adaptive thinking, and Opus-4.7-style sampling restrictions apply; token limits and pricing are pinned in `applyAnthropicCatalogPolicy`.
11
+
12
+ ### Fixed
13
+
14
+ - Fixed `google-antigravity` not rotating to another stored OAuth account when Cloud Code Assist returns `429 You have exhausted your capacity on this model. Your quota will reset after …`. `parseRateLimitReason` matched the literal `capacity` before the `quota will reset` suffix and downgraded the failure to `MODEL_CAPACITY_EXHAUSTED` (45–75 s backoff), and `isUsageLimitError` returned false for the same message — so `markUsageLimitReached` was never invoked and the agent kept hammering the exhausted credential while the retry layer bailed on the multi-hour `retry-after`. Both paths now treat the Antigravity phrasing as `QUOTA_EXHAUSTED` / usage-limit, blocking the current credential until reset and letting the session pick an unblocked sibling ([#2187](https://github.com/can1357/oh-my-pi/issues/2187)).
15
+ - Fixed OpenRouter Anthropic chat-completions requests placing `cache_control` on empty assistant tool-call content. The cache marker now skips empty text and attaches to the most recent non-empty text part, avoiding HTTP 400 payloads with `{type:"text", text:"", cache_control:...}`.
16
+ - Fixed Fable-only Anthropic request shaping to cover Claude Mythos 5, and added Mythos 5 to the first-party Anthropic catalog seed. Adaptive display, sampling suppression, mid-conversation system messages, forced-tool-choice downgrade, and Bedrock adaptive metadata now handle both model families.
17
+ - Fixed adaptive-only Claude models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5) returning HTTP 400 `"thinking.type.disabled" is not supported for this model` whenever thinking was turned off (utility calls and forced-tool turns route through the disable path). These models accept only `thinking.type: "adaptive"`; the request builder now omits the thinking field and pins the lowest adaptive effort instead of emitting `type: "disabled"`.
18
+ - Widened the OpenAI-completions first-event watchdog floor from 120s to 300s for DeepSeek V4 reasoning models hosted on the official DeepSeek API. The reasoner emits no SSE bytes until its private chain-of-thought finishes, which routinely takes longer than the generic 100s first-event budget under load — every chat then aborted with `OpenAI completions stream timed out while waiting for the first event` and silently retried. Mirrors the existing GLM coding-plan widening ([#2177](https://github.com/can1357/oh-my-pi/issues/2177)).
19
+
5
20
  ## [15.10.8] - 2026-06-09
21
+
6
22
  ### Added
7
23
 
8
24
  - Added optional `fetch` transport override (`fetch?: FetchImpl`) to Google, Ollama, and OpenAI-compatible model-manager options so dynamic model discovery and metadata lookups can use a caller-supplied HTTP client instead of only global `fetch`
@@ -3102,4 +3118,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
3102
3118
 
3103
3119
  ## [0.9.4] - 2025-11-26
3104
3120
 
3105
- Initial release with multi-provider LLM support.
3121
+ Initial release with multi-provider LLM support.
@@ -71,7 +71,7 @@ export declare function mapEffortToGoogleThinkingLevel<TApi extends Api>(model:
71
71
  /** Maps a normalized thinking effort to Anthropic adaptive effort values. */
72
72
  export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): "low" | "medium" | "high" | "xhigh" | "max";
73
73
  /**
74
- * Returns true for Anthropic models with Opus 4.7 API restrictions:
74
+ * Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
75
75
  * - Sampling parameters (temperature/top_p/top_k) return 400 error
76
76
  * - Thinking content is omitted by default (needs display: "summarized")
77
77
  */
@@ -79,7 +79,9 @@ export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
79
79
  /**
80
80
  * Mid-conversation `role: "system"` messages (system instructions appended at
81
81
  * non-first positions in the `messages` array) are supported starting with
82
- * Claude Opus 4.8. Earlier Claude models reject the role.
82
+ * Claude Opus 4.8 and the Claude Fable/Mythos 5 generation. Earlier Claude
83
+ * models reject the role.
83
84
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
84
85
  */
85
86
  export declare function supportsMidConversationSystemMessages(modelId: string): boolean;
87
+ export declare function isAnthropicFableOrMythosModel(modelId: string): boolean;
@@ -24,6 +24,17 @@ export interface ModelsDevModel {
24
24
  npm?: string;
25
25
  };
26
26
  }
27
+ /**
28
+ * Curated Anthropic models that are live or limited-availability on the
29
+ * first-party `/v1/models` endpoint but that models.dev has not catalogued yet.
30
+ * Seeded into model generation so the bundled catalog is never gated on
31
+ * models.dev's update cadence; deduped behind upstream catalog / models.dev
32
+ * entries once those appear. Token limits and pricing are pinned
33
+ * authoritatively in
34
+ * `applyAnthropicCatalogPolicy`, and `thinking` is derived by
35
+ * `refreshModelThinking` during generation.
36
+ */
37
+ export declare const ANTHROPIC_CURATED_FALLBACK_MODELS: readonly Model<"anthropic-messages">[];
27
38
  type SimpleProviderConfig = {
28
39
  apiKey?: string;
29
40
  baseUrl?: string;
@@ -27,11 +27,12 @@ export interface BedrockOptions extends StreamOptions {
27
27
  * - `"omitted"`: thinking content is suppressed; the encrypted signature still
28
28
  * travels back for multi-turn continuity.
29
29
  *
30
- * Starting with Claude Opus 4.7 the Anthropic API default is `"omitted"`, which
31
- * leaves callers waiting on a silent stream during long reasoning runs (issue
32
- * #1373). We default to `"summarized"` so adaptive-thinking models that accept
33
- * the field keep producing visible thinking deltas. Older adaptive-thinking
34
- * models (Opus 4.6, Sonnet 4.6+) reject the field, so we omit it for them.
30
+ * Starting with Claude Opus 4.7 and Claude Fable/Mythos 5 the Anthropic API
31
+ * default is `"omitted"`, which leaves callers waiting on a silent stream during
32
+ * long reasoning runs (issue #1373). We default to `"summarized"` so adaptive-
33
+ * thinking models that accept the field keep producing visible thinking deltas.
34
+ * Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field, so
35
+ * we omit it for them.
35
36
  */
36
37
  thinkingDisplay?: BedrockThinkingDisplay;
37
38
  }
@@ -56,17 +56,18 @@ export type AnthropicThinkingDisplay = "summarized" | "omitted";
56
56
  export interface AnthropicOptions extends StreamOptions {
57
57
  /**
58
58
  * Enable extended thinking.
59
- * For Opus 4.6+: uses adaptive thinking (Claude decides when/how much to think).
60
- * For older models: uses budget-based thinking with thinkingBudgetTokens.
59
+ * For adaptive-capable models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5):
60
+ * uses adaptive thinking (Claude decides when/how much to think). For older
61
+ * models: uses budget-based thinking with thinkingBudgetTokens.
61
62
  */
62
63
  thinkingEnabled?: boolean;
63
64
  /**
64
65
  * Token budget for extended thinking (older models only).
65
- * Ignored for Opus 4.6+ which uses adaptive thinking.
66
+ * Ignored for adaptive-capable models.
66
67
  */
67
68
  thinkingBudgetTokens?: number;
68
69
  /**
69
- * Effort level for adaptive thinking (Opus 4.6+ only).
70
+ * Effort level for adaptive thinking.
70
71
  * Controls how much thinking Claude allocates:
71
72
  * - "max": Always thinks with no constraints
72
73
  * - "high": Always thinks, deep reasoning (default)
@@ -182,7 +183,7 @@ export declare function normalizeExtraBetas(betas?: string[] | string): string[]
182
183
  export declare function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): AnthropicClientOptionsResult;
183
184
  /**
184
185
  * A single Anthropic conversation turn, including the mid-conversation
185
- * `system` role (Opus 4.8+).
186
+ * `system` role (Opus 4.8+ and Fable/Mythos 5).
186
187
  */
187
188
  export type AnthropicMessageParam = MessageParam;
188
189
  export declare function convertAnthropicMessages(messages: Message[], model: Model<"anthropic-messages">, isOAuthToken: boolean, baseUrl?: string | undefined): AnthropicMessageParam[];
@@ -41,7 +41,7 @@ export interface OpenAICompletionsOptions extends StreamOptions {
41
41
  * Exported for unit testing.
42
42
  */
43
43
  export declare function applyOpenRouterRoutingVariant(modelId: string, variant: string | undefined): string;
44
- /** Returns the widened OpenAI stream watchdog floor for slow GLM coding-plan reasoning models. */
44
+ /** Returns the widened OpenAI stream watchdog floor for slow reasoning models hosted on OpenAI-compatible endpoints. */
45
45
  export declare function getOpenAICompletionsStreamIdleTimeoutFallbackMs(model: Model<"openai-completions">): number | undefined;
46
46
  export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
47
47
  export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined): AssistantMessage["usage"];
@@ -5,10 +5,11 @@
5
5
  export type RateLimitReason = "QUOTA_EXHAUSTED" | "RATE_LIMIT_EXCEEDED" | "MODEL_CAPACITY_EXHAUSTED" | "SERVER_ERROR" | "UNKNOWN";
6
6
  /**
7
7
  * Classify a rate-limit error message into a reason category.
8
- * Priority order: MODEL_CAPACITY > RATE_LIMIT > QUOTA > SERVER_ERROR > UNKNOWN.
8
+ * Priority order: QUOTA (Antigravity "quota will reset") > MODEL_CAPACITY > QUOTA (account) >
9
+ * RATE_LIMIT > QUOTA (generic) > SERVER_ERROR > UNKNOWN.
9
10
  *
10
11
  * "resource exhausted" maps to MODEL_CAPACITY (transient, short wait)
11
- * "quota exceeded" maps to QUOTA_EXHAUSTED (long wait, switch account)
12
+ * "quota exceeded" / "quota will reset" maps to QUOTA_EXHAUSTED (long wait, switch account)
12
13
  */
13
14
  export declare function parseRateLimitReason(errorMessage: string): RateLimitReason;
14
15
  /**
@@ -741,11 +741,20 @@ export interface AnthropicCompat {
741
741
  supportsLongCacheRetention?: boolean;
742
742
  /**
743
743
  * Whether mid-conversation `role: "system"` messages are accepted in the
744
- * `messages` array (Claude Opus 4.8+ on the first-party Claude API and
745
- * Claude Platform on AWS). When unset, auto-detected from the model id and
746
- * base URL. Not available on Bedrock, Vertex AI, or Microsoft Foundry.
744
+ * `messages` array (Claude Opus 4.8+ and Claude Fable/Mythos 5 on the
745
+ * first-party Claude API and Claude Platform on AWS). When unset,
746
+ * auto-detected from the model id and base URL. Not available on Bedrock,
747
+ * Vertex AI, or Microsoft Foundry.
747
748
  */
748
749
  supportsMidConversationSystem?: boolean;
750
+ /**
751
+ * Whether the model accepts a forced `tool_choice` (`{ type: "any" }` or
752
+ * `{ type: "tool", name }`). Claude Fable/Mythos 5 reject forced tool use
753
+ * outright ("tool_choice forces tool use is not compatible with this model");
754
+ * the request builder downgrades forced choices to `auto` when this is false.
755
+ * When unset, auto-detected from the model id. Default: true.
756
+ */
757
+ supportsForcedToolChoice?: boolean;
749
758
  }
750
759
  /**
751
760
  * OpenRouter provider routing preferences.
@@ -1,2 +1,27 @@
1
- import type { UsageProvider } from "../usage";
1
+ import type { CredentialRankingStrategy, UsageProvider } from "../usage";
2
2
  export declare const antigravityUsageProvider: UsageProvider;
3
+ /**
4
+ * Credential ranking strategy for `google-antigravity`. Drives proactive
5
+ * multi-account selection in {@link AuthStorage} by reading the per-counter
6
+ * Antigravity usage reports.
7
+ *
8
+ * Antigravity reports one {@link UsageLimit} per backend counter (Google /
9
+ * Anthropic / OpenAI) per tier per window, and {@link fetchAntigravityUsage}
10
+ * sorts them ascending by `remainingFraction` — so `limits[0]` is always the
11
+ * most-pressured counter for the credential, and `limits[1]` (when present)
12
+ * is the next-most-pressured counter.
13
+ *
14
+ * `AuthStorage` compares the `secondary*` ranking metrics before `primary*`
15
+ * because other providers model a long-window budget as secondary. Antigravity
16
+ * does not expose a short/long split; every counter is a sibling bottleneck.
17
+ * Therefore the most-pressured counter goes in `secondary`, with the runner-up
18
+ * in `primary`, so proactive account selection always ranks the bottleneck
19
+ * before any healthier sibling counter.
20
+ *
21
+ * The Antigravity API exposes `resetTime` but not window duration, so the
22
+ * drain-rate calculation depends on `windowDefaults`. Antigravity quotas are
23
+ * effectively daily; 24h is the right fallback for both axes — any 5h tier
24
+ * still ranks correctly because both credentials are normalised against the
25
+ * same fallback.
26
+ */
27
+ export declare const antigravityRankingStrategy: CredentialRankingStrategy;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.10.8",
4
+ "version": "15.10.9",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@bufbuild/protobuf": "^2.12.0",
42
- "@oh-my-pi/pi-utils": "15.10.8",
42
+ "@oh-my-pi/pi-utils": "15.10.9",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
@@ -31,7 +31,7 @@ import type {
31
31
  import { claudeRankingStrategy, claudeUsageProvider } from "./usage/claude";
32
32
  import { googleGeminiCliUsageProvider } from "./usage/gemini";
33
33
  import { githubCopilotUsageProvider } from "./usage/github-copilot";
34
- import { antigravityUsageProvider } from "./usage/google-antigravity";
34
+ import { antigravityRankingStrategy, antigravityUsageProvider } from "./usage/google-antigravity";
35
35
  import { kimiUsageProvider } from "./usage/kimi";
36
36
  import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
37
37
  import { zaiUsageProvider } from "./usage/zai";
@@ -650,6 +650,7 @@ function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefi
650
650
  const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>([
651
651
  ["openai-codex", codexRankingStrategy],
652
652
  ["anthropic", claudeRankingStrategy],
653
+ ["google-antigravity", antigravityRankingStrategy],
653
654
  ]);
654
655
 
655
656
  function resolveDefaultRankingStrategy(provider: Provider): CredentialRankingStrategy | undefined {
@@ -23,7 +23,7 @@ type SemVer = {
23
23
  };
24
24
 
25
25
  type GeminiKind = "pro" | "flash";
26
- type AnthropicKind = "opus" | "sonnet";
26
+ type AnthropicKind = "opus" | "sonnet" | "fable" | "mythos";
27
27
  type OpenAIVariant = "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "mini" | "max" | "nano";
28
28
 
29
29
  const CODEX_GPT_5_4_PRIORITY_BY_VARIANT: Partial<Record<OpenAIVariant, number>> = {
@@ -308,7 +308,8 @@ export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
308
308
  ): "low" | "medium" | "high" | "xhigh" | "max" {
309
309
  const supported = requireSupportedEffort(model, effort);
310
310
  if (anthropicModelHasRealXHighEffort(model)) {
311
- // Opus 4.7+ on the Messages API exposes the full five-tier adaptive scale
311
+ // Opus 4.7+ and Fable/Mythos 5 on the Messages API expose the full
312
+ // five-tier adaptive scale
312
313
  // (low/medium/high/xhigh/max). Shift our user-facing efforts up one notch so
313
314
  // the top tier reaches the genuine "max" and "high" lands on Anthropic's
314
315
  // recommended "xhigh" coding/agentic default.
@@ -341,33 +342,44 @@ export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
341
342
  }
342
343
 
343
344
  /**
344
- * Returns true for Anthropic models with Opus 4.7 API restrictions:
345
+ * Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
345
346
  * - Sampling parameters (temperature/top_p/top_k) return 400 error
346
347
  * - Thinking content is omitted by default (needs display: "summarized")
347
348
  */
348
349
  export function hasOpus47ApiRestrictions(modelId: string): boolean {
349
350
  const parsed = parseAnthropicModel(getCanonicalModelId(modelId));
350
351
  if (!parsed) return false;
351
- return semverGte(parsed.version, "4.7") && parsed.kind === "opus";
352
+ return (parsed.kind === "opus" && semverGte(parsed.version, "4.7")) || isFableOrMythos(parsed.kind);
352
353
  }
353
354
 
354
355
  /**
355
356
  * Mid-conversation `role: "system"` messages (system instructions appended at
356
357
  * non-first positions in the `messages` array) are supported starting with
357
- * Claude Opus 4.8. Earlier Claude models reject the role.
358
+ * Claude Opus 4.8 and the Claude Fable/Mythos 5 generation. Earlier Claude
359
+ * models reject the role.
358
360
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
359
361
  */
360
362
  export function supportsMidConversationSystemMessages(modelId: string): boolean {
361
363
  const parsed = parseAnthropicModel(getCanonicalModelId(modelId));
362
364
  if (!parsed) return false;
363
- return parsed.kind === "opus" && semverGte(parsed.version, "4.8");
365
+ return (parsed.kind === "opus" && semverGte(parsed.version, "4.8")) || isFableOrMythos(parsed.kind);
366
+ }
367
+
368
+ export function isAnthropicFableOrMythosModel(modelId: string): boolean {
369
+ const parsed = parseAnthropicModel(getCanonicalModelId(modelId));
370
+ return parsed !== null && isFableOrMythos(parsed.kind);
371
+ }
372
+
373
+ function isFableOrMythos(kind: AnthropicKind): boolean {
374
+ return kind === "fable" || kind === "mythos";
364
375
  }
365
376
 
366
377
  function anthropicModelHasRealXHighEffort<TApi extends Api>(model: ApiModel<TApi>): boolean {
367
378
  if (model.api !== "anthropic-messages") return false;
368
379
  const parsedModel = parseKnownModel(model.id);
369
- if (parsedModel.family !== "anthropic" || parsedModel.kind !== "opus") return false;
370
- return semverGte(parsedModel.version, "4.7");
380
+ if (parsedModel.family !== "anthropic") return false;
381
+ if (isFableOrMythos(parsedModel.kind)) return true;
382
+ return parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.7");
371
383
  }
372
384
 
373
385
  function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
@@ -431,6 +443,19 @@ function applyAnthropicCatalogPolicy(model: ApiModel<Api>, parsedModel: Anthropi
431
443
  model.contextWindow = 1000000;
432
444
  model.maxTokens = 128000;
433
445
  }
446
+
447
+ // Claude Fable/Mythos 5: Anthropic's /v1/models omits token limits and
448
+ // pricing, and models.dev lags new releases. Pin authoritative values from
449
+ // the model card (1M context / 128k output) and pricing docs ($10 in / $50
450
+ // out per MTok).
451
+ if (model.provider === "anthropic" && isFableOrMythos(parsedModel.kind)) {
452
+ model.contextWindow = 1_000_000;
453
+ model.maxTokens = 128_000;
454
+ model.cost.input = 10;
455
+ model.cost.output = 50;
456
+ model.cost.cacheRead = 1;
457
+ model.cost.cacheWrite = 12.5;
458
+ }
434
459
  }
435
460
 
436
461
  function inferGeneratedApplyPatchToolType(
@@ -562,7 +587,9 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
562
587
  (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") &&
563
588
  semverGte(parsedModel.version, "4.6")
564
589
  ) {
565
- return parsedModel.kind === "opus" ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
590
+ return parsedModel.kind === "opus" || isFableOrMythos(parsedModel.kind)
591
+ ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH
592
+ : DEFAULT_REASONING_EFFORTS;
566
593
  }
567
594
  return inferFallbackEfforts(model);
568
595
  }
@@ -618,7 +645,10 @@ function inferThinkingControlMode<TApi extends Api>(
618
645
 
619
646
  case "bedrock-converse-stream":
620
647
  if (parsedModel.family === "anthropic") {
621
- if (semverGte(parsedModel.version, "4.6") && parsedModel.kind === "opus") {
648
+ if (
649
+ semverGte(parsedModel.version, "4.6") &&
650
+ (parsedModel.kind === "opus" || isFableOrMythos(parsedModel.kind))
651
+ ) {
622
652
  return "anthropic-adaptive";
623
653
  }
624
654
  if (semverGte(parsedModel.version, "4.5")) {
@@ -658,7 +688,7 @@ function parseGeminiModel(modelId: string): GeminiModel | null {
658
688
  }
659
689
 
660
690
  function parseAnthropicModel(modelId: string): AnthropicModel | null {
661
- const match = /claude-(opus|sonnet)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
691
+ const match = /claude-(opus|sonnet|fable|mythos)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
662
692
  if (!match) {
663
693
  return null;
664
694
  }