@oh-my-pi/pi-ai 18.12.3 → 18.12.5

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +39 -73
  2. package/dist/types/auth/rank.d.ts +4 -0
  3. package/dist/types/auth/types.d.ts +2 -0
  4. package/dist/types/auth-broker/remote-store.d.ts +16 -6
  5. package/dist/types/providers/amazon-bedrock.d.ts +7 -5
  6. package/dist/types/providers/anthropic-compaction.d.ts +8 -0
  7. package/dist/types/providers/anthropic-state.d.ts +8 -2
  8. package/dist/types/providers/anthropic-wire.d.ts +3 -0
  9. package/dist/types/providers/anthropic.d.ts +19 -2
  10. package/dist/types/providers/factory-droid/tool-names.d.ts +21 -0
  11. package/dist/types/providers/openai-responses-wire.d.ts +2 -2
  12. package/dist/types/types.d.ts +8 -2
  13. package/dist/types/utils/tool-call-loop-guard.d.ts +2 -0
  14. package/package.json +6 -6
  15. package/src/auth/policy.ts +26 -10
  16. package/src/auth/rank.ts +6 -0
  17. package/src/auth/select.ts +25 -6
  18. package/src/auth/types.ts +2 -0
  19. package/src/auth-broker/discover.ts +67 -17
  20. package/src/auth-broker/remote-store.ts +59 -16
  21. package/src/images/google-antigravity.ts +0 -1
  22. package/src/providers/amazon-bedrock.ts +421 -142
  23. package/src/providers/anthropic-compaction.ts +13 -0
  24. package/src/providers/anthropic-state.ts +16 -2
  25. package/src/providers/anthropic-wire.ts +9 -1
  26. package/src/providers/anthropic.ts +271 -74
  27. package/src/providers/cursor-pi-args.ts +3 -1
  28. package/src/providers/devin.ts +13 -43
  29. package/src/providers/factory-droid/gemini.ts +1 -30
  30. package/src/providers/factory-droid/tool-names.ts +44 -0
  31. package/src/providers/factory-droid.ts +91 -20
  32. package/src/providers/google-gemini-cli.ts +0 -1
  33. package/src/providers/openai-completions.ts +6 -0
  34. package/src/providers/openai-responses-wire.ts +2 -2
  35. package/src/providers/openai-responses.ts +17 -9
  36. package/src/types.ts +8 -2
  37. package/src/usage/devin.ts +3 -3
  38. package/src/utils/leaked-thinking-stream.ts +12 -0
  39. package/src/utils/thinking-loop.ts +75 -14
  40. package/src/utils/tool-call-loop-guard.ts +14 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.12.5] - 2026-10-21
6
+
7
+ ### Added
8
+
9
+ - Added OpenAI 12-hour prompt caching for GPT-5.6+ on the Responses API: `cacheRetention: "long"` now requests `prompt_cache_options.ttl: "12h"` (instead of the ignored legacy `prompt_cache_retention: "24h"`), and `promptCache.ttl` accepts `"12h"`
10
+
11
+ ### Fixed
12
+
13
+ - Fixed every OpenAI Codex request failing with HTTP 404 `{"detail":"Not Found"}` on personal ChatGPT accounts whose token is not region-pinned ([#16493](https://github.com/can1357/oh-my-pi/issues/16493))
14
+
15
+ ## [18.12.4] - 2026-10-20
16
+
17
+ ### Added
18
+
19
+ - Added opt-in `reclaimAbovePct` to `auth.accountPolicies`: sessions automatically moved to a lower-priority account return to the preferred one once it has that much quota left ([#14237](https://github.com/can1357/oh-my-pi/pull/14237) by [@will-bogusz](https://github.com/will-bogusz))
20
+ - Added `reachesBudgetedHost()`: whether the catalog's host request budgets (inline image bytes, official image count) apply to a request, true on a model's official endpoint and on Factory Droid's own hosts ([#16288](https://github.com/can1357/oh-my-pi/pull/16288) by [@will-bogusz](https://github.com/will-bogusz))
21
+ - Added Inception API-key login (`/login inception`) and `INCEPTION_API_KEY` environment authentication ([#16439](https://github.com/can1357/oh-my-pi/pull/16439) by [@eggpeat](https://github.com/eggpeat))
22
+ - Added image support for native OpenAI Decisions and Workers AI judgments, prompted vision chat, and auth-gateway System One requests; images exceeding a judgment API's documented caps (e.g. Clef's 4 images, 4 MiB each) are rejected locally ([#15471](https://github.com/can1357/oh-my-pi/pull/15471) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)).
23
+ - `isSurvivingAssistant` in `providers/transform-messages`: whether an assistant turn reaches a replay, malformed tool calls dropped first, so callers outside `transformMessages` pick the same latest turn ([#15791](https://github.com/can1357/oh-my-pi/pull/15791) by [@will-bogusz](https://github.com/will-bogusz))
24
+ - Broker account-pool files (`OMP_AUTH_BROKER_ACCOUNT_POOL_FILE`) can hide whole providers and API keys: `"*": false` makes the file an allow-list, and an entry can be `true`, `false`, or `{ "accounts": [...], "apiKeys": false }` ([#14235](https://github.com/can1357/oh-my-pi/pull/14235) by [@will-bogusz](https://github.com/will-bogusz))
25
+
26
+ ### Changed
27
+
28
+ - `cursorRawReadPath()` writes a bare start line as `raw:N-` (`file:10` → `file:raw:10-`), so Cursor exec reads of such a path still read from line `N` onward now that `read` takes `N:raw` as one line ([#16438](https://github.com/can1357/oh-my-pi/pull/16438) by [@will-bogusz](https://github.com/will-bogusz))
29
+
30
+ ### Fixed
31
+
32
+ - Fixed Anthropic and Bedrock logging one warning per dropped thinking block (hundreds for one request) after a conversation change; the drop now logs one warning with the count ([#16392](https://github.com/can1357/oh-my-pi/pull/16392) by [@will-bogusz](https://github.com/will-bogusz))
33
+ - Fixed Factory Droid Core models (Kimi, GLM, DeepSeek and others) dropping calls to tools with names over 64 characters and their results from later requests; such names now shorten to 64 characters, and shorter names are sent unchanged ([#16384](https://github.com/can1357/oh-my-pi/pull/16384) by [@will-bogusz](https://github.com/will-bogusz))
34
+ - Fixed Devin `/usage` showing no data for accounts signed in with `/login devin`, and Devin chat sending a failing extra auth request for legacy Windsurf Enterprise keys ([#13677](https://github.com/can1357/oh-my-pi/pull/13677) by [@will-bogusz](https://github.com/will-bogusz))
35
+ - Turns stopped for a thinking loop now keep the tokens and cost the provider had reported, the response id, the request's start time, and its time to first token and duration, instead of recording zero usage and no timing ([#16437](https://github.com/can1357/oh-my-pi/pull/16437) by [@will-bogusz](https://github.com/will-bogusz))
36
+ - Fixed Claude requests answering a turn whose attachment is a video, PDF, HEIC or other format sent outside a JPEG/PNG/GIF/WebP image as if nothing were attached; the turn now fails with an error naming the format ([#16407](https://github.com/can1357/oh-my-pi/pull/16407) by [@will-bogusz](https://github.com/will-bogusz))
37
+ - Fixed the repeated-tool-call guard missing a loop the model interleaves with a `wait` that elapses with nothing new; a wait that returns finished work still ends the repetition ([#11874](https://github.com/can1357/oh-my-pi/pull/11874) by [@dickyudhandika](https://github.com/dickyudhandika))
38
+ - Fixed Google Antigravity chat and image requests failing with false 429 quota-exhausted errors ([#11849](https://github.com/can1357/oh-my-pi/pull/11849) by [@jotafurtado](https://github.com/jotafurtado))
39
+ - Fixed the stream loop guard never stopping a model stuck repeating punctuation or symbols (`?!?!`, `✓✓✓`) until the output limit; such runs now trip once they repeat for 1 KB, while digit and whitespace runs such as padding and hexdumps are left alone ([#14436](https://github.com/can1357/oh-my-pi/pull/14436) by [@F0Rextasy](https://github.com/F0Rextasy))
40
+ - Fixed native remote compaction for supported Bedrock Claude models without changing ordinary Converse turns; a custom non-AWS `baseUrl` needs `remoteCompaction.enabled: true` ([#16343](https://github.com/can1357/oh-my-pi/pull/16343) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy))
41
+ - Fixed Anthropic compaction blocks dropping their signed `tool_changes`, which made the replayed block fail its signature ([#16343](https://github.com/can1357/oh-my-pi/pull/16343) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy))
42
+
5
43
  ## [18.12.3] - 2026-10-19
6
44
 
7
45
  ### Added
@@ -2275,76 +2313,4 @@
2275
2313
  - Fixed OAuth refresh persisting the rotated token by a positional index captured before the refresh `await`. A concurrent disable could reorder or shrink a provider's credential array while the refresh was in flight, landing the new token on the wrong row (or silently dropping it) and leaving accounts with a stale refresh token that failed — and was then disabled — on the next cycle. Refresh persistence, selection-index resync, and CAS-disable now address the row by id across `forceRefreshCredentialById`, candidate preflight, and in-stream selection (`#replaceCredentialById` / `#disableCredentialByIdIfMatches`).
2276
2314
  - Fixed `isDefinitiveOAuthFailure` treating a bare HTTP 403 (and generic `unauthorized` / access-token-expired wording) as a definitive credential failure, which permanently disabled healthy OAuth accounts on WAF, egress rate-limit, permission, and account-verification responses. Bare 403, rate limits (429), gateway/5xx, and more network errors (`ECONNRESET`, `ETIMEDOUT`, `EAI_AGAIN`, …) are now classified transient; only explicit dead-grant errors (`invalid_grant`, `invalid_token`, `unauthorized_client`, revoked, `refresh token … expired`) or a bare 401 tear the credential down.
2277
2315
 
2278
- ## [16.1.17] - 2026-06-24
2279
-
2280
- ### Added
2281
-
2282
- - Added provider-level `notes?: string[]` field to `UsageReport` for disclaimers that apply to every limit (e.g. "OMP-observed spend only"). The field is declared in both the `usage.ts` schema and the auth-broker wire schema copy so it survives the `"+": "reject"` deserialization gate. ([#3268](https://github.com/can1357/oh-my-pi/issues/3268))
2283
-
2284
- ### Fixed
2285
-
2286
- - Moved the OpenCode Go "OMP-observed spend only" disclaimer from per-limit `notes` to provider-level `notes`, so it renders once per provider instead of duplicating across every account × window. ([#3268](https://github.com/can1357/oh-my-pi/issues/3268))
2287
- - Fixed Anthropic rate-limit header usage cache entries retaining legacy missing account metadata after refresh.
2288
- - Fixed Anthropic-compatible budget-effort models dropping the selected effort before request serialization, so `output_config.effort` is emitted alongside `thinking.budget_tokens` when model metadata declares `mode: "anthropic-budget-effort"`.
2289
- - Fixed `anthropic-messages` silently dropping caller-supplied `Authorization` / `X-Api-Key` from `model.headers` and `ANTHROPIC_CUSTOM_HEADERS`, blocking custom proxy auth schemes. Non-OAuth requests now honor the caller's value (matching `openai-responses`); the lower-level client also suppresses its `X-Api-Key` add when a custom `Authorization` is supplied for a non-official endpoint so the proxy receives a single credential. OAuth bearer + Cloudflare AI Gateway keep their pre-existing enforced auth headers. ([#3391](https://github.com/can1357/oh-my-pi/issues/3391))
2290
- - Fixed Ollama Cloud `num_predict` ignoring the provider's 65536 output-token cap so stale `models.db` rows (or custom `modelOverrides` re-enabling output caps) that carried `maxTokens: 1048576` from a pre-omitMaxOutputTokens catalog 400'd every request with `max_tokens (1048576) exceeds model's maximum output tokens (65536) for model deepseek-v4-pro`. The Ollama provider now clamps `num_predict` for any `ollama-cloud` request at the documented 65536 cap before sending, independent of the cached spec's `maxTokens` and on top of the existing `omitMaxOutputTokens` policy — so the request stays valid even when the load-time policy never normalized the spec. Self-hosted `ollama` traffic is unaffected. ([#3392](https://github.com/can1357/oh-my-pi/issues/3392))
2291
- - Fixed OpenRouter Anthropic models on the Responses path omitting `cache_control`, so prompt caching engages without forcing Chat Completions. ([#3397](https://github.com/can1357/oh-my-pi/issues/3397))
2292
- - Fixed OpenRouter Anthropic Responses follow-up requests replaying prior reasoning items with stale signatures, which caused HTTP 400 `Invalid signature in thinking block` errors after a thinking turn. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399))
2293
- - Fixed OpenRouter Anthropic models on the Responses path omitting `cache_control`, so prompt caching engages without forcing Chat Completions. `cacheRetention: "long"` now upgrades the breakpoint to `ttl: "1h"`. ([#3397](https://github.com/can1357/oh-my-pi/issues/3397))
2294
-
2295
- ## [16.1.16] - 2026-06-23
2296
-
2297
- ### Fixed
2298
-
2299
- - Fixed Anthropic-compatible thinking requests sending replayed thinking blocks without `context_management.keep: "all"`, preserving multi-turn reasoning context for API-key providers. API-key requests now also advertise the required `context-management-2025-06-27` beta header so the field is honored instead of rejected. Injected SDK clients, GitHub Copilot's Anthropic proxy, and Vertex rawPredict are excluded because this code path cannot add the beta to caller-owned clients, Copilot strips Anthropic betas and demotes thinking blocks to text upstream, and Vertex expects betas in the JSON body rather than the Anthropic HTTP beta header. ([#3288](https://github.com/can1357/oh-my-pi/issues/3288))
2300
- - Fixed OpenRouter Responses native history replay leaking Gemini reasoning item `format` metadata back into follow-up requests, which caused HTTP 400 rejections while preserving encrypted reasoning replay.
2301
-
2302
- ## [16.1.15] - 2026-06-22
2303
-
2304
- ### Fixed
2305
-
2306
- - Fixed API-key `/login` providers replacing sibling credentials instead of appending new keys for the same provider. ([#3265](https://github.com/can1357/oh-my-pi/issues/3265))
2307
- - Fixed OpenAI Codex OAuth account rotation for quota failures that surface as bare HTTP 429 or `insufficient_quota`, so pre-content failures temporarily block only the exhausted credential and retry a healthy sibling. The 429 status-only fallback applies only to absent/opaque bodies; informative transient bodies (`Too many requests`, `Service overloaded 529`, `Please retry in 5s`, …) defer to `parseRateLimitReason` and stay in the provider's own backoff layer instead of burning sibling credentials. ([#3231](https://github.com/can1357/oh-my-pi/issues/3231))
2308
-
2309
- ## [16.1.14] - 2026-06-22
2310
-
2311
- ### Added
2312
-
2313
- - Added proxy support for model providers via `PI_PROXY` and `PI_PROXY_<PROVIDER>` variables
2314
- - Added `NO_PROXY` environment variable support for bypassing proxy configuration
2315
- - Added support for Sakana AI provider
2316
- - Added Sakana AI login and request base URL support for `SAKANA_*` / `FUGU_*` environment variables
2317
-
2318
- ### Changed
2319
-
2320
- - Consolidated API key authentication logic across registry providers
2321
- - Disabled parallel tool calls for Devin provider requests
2322
-
2323
- ### Fixed
2324
-
2325
- - Improved proxy bypass logic to correctly handle private IP ranges and local metadata services
2326
- - Enhanced memoization for proxy environment variable lookups to improve performance
2327
-
2328
- ## [16.1.13] - 2026-06-22
2329
-
2330
- ### Added
2331
-
2332
- - Added support for Devin as a provider
2333
-
2334
- ### Changed
2335
-
2336
- - Updated tool call arguments to use `Record<string, unknown>` and `unknown` for tool results
2337
-
2338
- ### Fixed
2339
-
2340
- - Fixed OpenAI Responses native history replay dropping failed/incomplete image generation calls instead of resending their transient `ig_...` item IDs, preventing follow-up requests from failing with `404 Item with id ... not found`. ([#3225](https://github.com/can1357/oh-my-pi/issues/3225))
2341
- - Fixed `/login fireworks` rejecting valid `fw_…` keys with `Fireworks API key validation failed (500): Error listing deployed models`. The validator pinged `/inference/v1/models`, which Fireworks serves from the per-account deployment registry and 500s for accounts without active deployments. Login now hits the static control-plane `List Models` catalog (`GET /v1/accounts/fireworks/models?filter=supports_serverless=true&pageSize=1`) — the same endpoint discovery already uses — so authentication no longer depends on the caller's deployment state. ([#3219](https://github.com/can1357/oh-my-pi/issues/3219))
2342
-
2343
- ## [16.1.11] - 2026-06-21
2344
-
2345
- ### Fixed
2346
-
2347
- - Fixed OpenAI Responses native history replay leaking image generation provider-only fields into the next request, which made OpenAI-compatible proxies reject `pi` tool-calling sessions with `Unknown parameter: input[1].action`. ([#3201](https://github.com/can1357/oh-my-pi/issues/3201))
2348
- - Fixed a stream thought-leakage issue for `gemini-3.5-flash` where the model's internal reasoning JSON could leak into the visible text stream. The stream parser now uses a brace-balanced counting algorithm to accurately slice and discard the leading thought JSON block, with a robust fallback for unescaped double quotes, dynamic tool-name derivation, and preservation of subsequent text deltas without triggering empty-response retries.
2349
-
2350
- Older entries are archived in [packages/ai/CHANGELOG.md@5eb78e90fa4e](https://github.com/can1357/oh-my-pi/blob/5eb78e90fa4ec4318b9520302f1977fa58c48745/packages/ai/CHANGELOG.md).
2316
+ Older entries are archived in [packages/ai/CHANGELOG.md@7a5f1425b9bd](https://github.com/can1357/oh-my-pi/blob/7a5f1425b9bd79c3c74ae1d8946782b0494c8aa9/packages/ai/CHANGELOG.md).
@@ -23,6 +23,10 @@ export type UsageCandidate<T extends AuthCredential> = {
23
23
  * spent, so if it still serves it draws on paid overage such as Codex credits.
24
24
  */
25
25
  allowanceSpent?: boolean;
26
+ /** Present after policy-aware ranking: the account's configured `priority` (0 when unset). */
27
+ accountPriority?: number;
28
+ /** Present after policy-aware ranking: unblocked and measured at or above its `reclaimAbovePct`. */
29
+ reclaimReady?: boolean;
26
30
  };
27
31
  /** OAuth credential eligible for usage ranking. */
28
32
  export type OAuthCandidate = UsageCandidate<OAuthCredential>;
@@ -36,6 +36,8 @@ export interface AuthAccountPolicy {
36
36
  readonly priority?: number;
37
37
  /** Protected remaining quota percentage for this account. */
38
38
  readonly reservePct?: number;
39
+ /** Remaining quota percentage, above the reserve, at which this account takes back lower-priority warm pins. */
40
+ readonly reclaimAbovePct?: number;
39
41
  }
40
42
  /** Read-only set of per-account routing policies. */
41
43
  export type AuthAccountPolicies = readonly AuthAccountPolicy[];
@@ -5,12 +5,21 @@ import type { Provider } from "../types.js";
5
5
  import type { ClientUsageIdentity, ObservedUsageEntry, UsageReport } from "../usage.js";
6
6
  import { type AuthBrokerClient } from "./client.js";
7
7
  import type { SnapshotResponse } from "./types.js";
8
+ /** Visibility rule for one provider's broker credentials. */
9
+ export interface AuthBrokerProviderPool {
10
+ /** OAuth `identityKey`s visible to this client; omitted = every OAuth account. */
11
+ readonly accounts?: ReadonlySet<string>;
12
+ /** Whether API-key credentials are visible. Default `true`. */
13
+ readonly apiKeys?: boolean;
14
+ }
8
15
  /**
9
- * Per-provider OAuth identities visible to this trusted broker client.
10
- * Missing providers are unrestricted; an empty set excludes that provider's
11
- * OAuth credentials. API keys are never filtered.
16
+ * Per-provider credential visibility for this trusted broker client. A set
17
+ * value lists the provider's visible OAuth identities and keeps its API keys
18
+ * (an empty set excludes every OAuth credential); a rule object can also hide
19
+ * API keys. Providers without an entry follow the `"*"` rule, which defaults
20
+ * to unrestricted.
12
21
  */
13
- export type AuthBrokerAccountPool = ReadonlyMap<string, ReadonlySet<string>>;
22
+ export type AuthBrokerAccountPool = ReadonlyMap<string, ReadonlySet<string> | AuthBrokerProviderPool>;
14
23
  export interface RemoteAuthCredentialStoreOptions {
15
24
  client: AuthBrokerClient;
16
25
  /**
@@ -30,8 +39,9 @@ export interface RemoteAuthCredentialStoreOptions {
30
39
  */
31
40
  onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
32
41
  /**
33
- * OAuth identities visible through this store. This is a trusted-client
34
- * routing policy, not broker authorization.
42
+ * Broker credentials visible through this store (OAuth identities and API
43
+ * keys per provider). This is a trusted-client routing policy, not broker
44
+ * authorization.
35
45
  */
36
46
  accountPool?: AuthBrokerAccountPool;
37
47
  /** Flush cadence for batched observed-usage reports. Default 10s. */
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Amazon Bedrock Converse Stream provider.
2
+ * Amazon Bedrock chat transport.
3
3
  *
4
- * Talks directly to `bedrock-runtime.{region}.amazonaws.com` over HTTPS with
5
- * SigV4 signing and decodes the `application/vnd.amazon.eventstream` response.
6
- * No `@aws-sdk/*`, no `@smithy/*`, no `proxy-agent`. Proxies are honored via
7
- * Bun's native `HTTPS_PROXY` support.
4
+ * Ordinary requests use Converse Stream; server compaction uses the
5
+ * Anthropic Messages format over Bedrock's InvokeModelWithResponseStream API.
6
+ * Both call `bedrock-runtime.{region}.amazonaws.com` directly over HTTPS with
7
+ * shared credential resolution and SigV4 signing. The normal path decodes
8
+ * `application/vnd.amazon.eventstream`; no AWS SDK, Smithy, or proxy-agent
9
+ * dependency is required. Bun's native `HTTPS_PROXY` support is honored.
8
10
  *
9
11
  * A `models.yml` `baseUrl` is the request origin verbatim (VPC endpoint, gateway, …);
10
12
  * only AWS's own regional host is re-pointed at the resolved region. SigV4 unaffected.
@@ -4,6 +4,14 @@ import type { AnthropicMessagesClientLike } from "./anthropic-client.js";
4
4
  export declare function resolvesToOfficialAnthropicEndpoint(model: Model<"anthropic-messages">): boolean;
5
5
  /** Whether model policy and the effective deployment support on-demand compaction. */
6
6
  export declare function supportsAnthropicCompaction(model: Model<"anthropic-messages">, effectiveBaseUrl?: string): boolean;
7
+ /**
8
+ * Whether a Bedrock Converse model compacts through a Bedrock Runtime
9
+ * `InvokeModelWithResponseStream` side request. AWS bedrock-runtime origins
10
+ * (public, FIPS, PrivateLink) qualify by default; any other `baseUrl` is a
11
+ * gateway that may implement only Converse, so it needs
12
+ * `remoteCompaction.enabled: true`. `enabled: false` always opts out.
13
+ */
14
+ export declare function supportsBedrockConverseCompaction(model: Model<"bedrock-converse-stream">): boolean;
7
15
  /** Read a caller-owned client's endpoint for request and compaction routing. */
8
16
  export declare function injectedClientBaseUrl(client: AnthropicMessagesClientLike): string | undefined;
9
17
  /** Whether a caller-owned Anthropic client targets a compaction-capable endpoint. */
@@ -10,10 +10,16 @@ export declare function resolveDirectAnthropicBaseUrl(model: Model<Api>): string
10
10
  /**
11
11
  * Whether a request to `model` reaches the official endpoint its compat was built for. `compat.officialEndpoint`
12
12
  * classifies the configured `baseUrl`, but a first-party Anthropic model is rerouted at request time by
13
- * `ANTHROPIC_BASE_URL` or Foundry's `FOUNDRY_BASE_URL` without rebuilding compat. Gates official-only host
14
- * limits such as the catalog's inline image byte and image count budgets.
13
+ * `ANTHROPIC_BASE_URL` or Foundry's `FOUNDRY_BASE_URL` without rebuilding compat.
15
14
  */
16
15
  export declare function reachesOfficialEndpoint(model: Model<Api>): boolean;
16
+ /**
17
+ * Whether the catalog's host request budgets (inline image bytes, official image count) apply to a request to
18
+ * `model`: it reaches the official endpoint ({@link reachesOfficialEndpoint}), or its rules set
19
+ * `catalog-endpoint-budgets` (Factory Droid's hosted API) and it reaches the provider's own catalog endpoint
20
+ * (unset `baseUrl`, or an `endpoint-hosts` host). A custom gateway keeps its own limits.
21
+ */
22
+ export declare function reachesBudgetedHost(model: Model<Api>): boolean;
17
23
  /** Build the endpoint-and-model-scoped Anthropic session-state key. */
18
24
  export declare function anthropicProviderSessionStateKey(baseUrl: string, modelId: string): string;
19
25
  /** Re-arm fast mode across materialized Anthropic session-state entries. */
@@ -146,6 +146,8 @@ export declare const LEGACY_COMPACTION_BETA = "compact-2026-01-12";
146
146
  export type CompactionBlockParam = {
147
147
  type: "compaction";
148
148
  content: string;
149
+ /** Opaque signed tool changes from a compaction response. */
150
+ tool_changes?: Record<string, unknown>[];
149
151
  signature?: string;
150
152
  encrypted_content?: string | null;
151
153
  cache_control?: CacheControlEphemeral | null;
@@ -396,6 +398,7 @@ export type ResponseContentBlock = {
396
398
  content: string;
397
399
  signature?: string;
398
400
  encrypted_content?: string | null;
401
+ tool_changes?: Record<string, unknown>[];
399
402
  };
400
403
  export type ContentBlockDelta = {
401
404
  type: "text_delta";
@@ -1,7 +1,9 @@
1
1
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
2
- import type { AnthropicCompactionRequest, AnthropicOutputEffort, Api, FetchImpl, Message, Model, ServiceTier, SimpleStreamOptions, StreamFunction, StreamOptions, Usage } from "../types.js";
2
+ import type { AnthropicCompactionRequest, AnthropicOutputEffort, Api, Context, FetchImpl, Message, Model, ServiceTier, SimpleStreamOptions, StreamFunction, StreamOptions, Usage } from "../types.js";
3
+ import { AssistantMessageEventStream } from "../utils/event-stream.js";
4
+ import { type RawHttpRequestDump } from "../utils/http-inspector.js";
3
5
  import { type AnthropicFetchOptions, type AnthropicMessagesClientLike } from "./anthropic-client.js";
4
- import { type FallbackParam, type MessageParam, type TextBlockParam } from "./anthropic-wire.js";
6
+ import { type FallbackParam, type MessageCreateParamsStreaming, type MessageParam, type TextBlockParam } from "./anthropic-wire.js";
5
7
  import { resolvesToOfficialAnthropicEndpoint, supportsAnthropicCompaction, supportsAnthropicCompactionOnClient } from "./anthropic-compaction.js";
6
8
  import { applyClaudeToolPrefix, deriveClaudeDeviceId, generateClaudeCloakingUserId, isClaudeCloakingUserId, resolveAnthropicMetadataUserId, stripClaudeToolPrefix } from "./anthropic-identity.js";
7
9
  import { clearAnthropicFastModeFallback, isAnthropicFastModeFallbackDisabled, normalizeAnthropicBaseUrl } from "./anthropic-state.js";
@@ -204,6 +206,13 @@ export declare function applyAnthropicUsageExtras(usage: Usage, source: Anthropi
204
206
  /** Detects the preserved-thinking error caused by rewriting a signed block's conversation prefix. */
205
207
  export declare function isThinkingPrefixBindingError(message: string): boolean;
206
208
  export declare function isInvalidThinkingSignatureError(message: string): boolean;
209
+ /**
210
+ * Warn once for the signed thinking blocks the API dropped from one request
211
+ * after the conversation prefix changed: the count and first path at warn,
212
+ * every path at debug. One prefix change can drop hundreds of blocks in a
213
+ * single request, which would otherwise flood the log one line per block.
214
+ */
215
+ export declare function logDroppedThinking(provider: string, model: string, paths: Array<string | undefined>): void;
207
216
  /**
208
217
  * Prepend a pointed remediation to a thinking-signature rejection 400 when the
209
218
  * model looks like an unmarked custom signing proxy
@@ -288,6 +297,14 @@ export declare function resolveAnthropicCompactionEffort(model: Model<"anthropic
288
297
  * gate its header-level interleaved beta on the same decision.
289
298
  */
290
299
  export declare function shouldStripThinkingHistory(messages: readonly Message[]): boolean;
300
+ /**
301
+ * Route Bedrock's on-demand Anthropic compaction through the shared Messages
302
+ * stream consumer while keeping source-provider identity and pricing.
303
+ */
304
+ export declare function streamBedrockAnthropicCompaction(sourceModel: Model<"bedrock-converse-stream">, context: Context, options: AnthropicOptions, prepareRequest: (params: MessageCreateParamsStreaming, betas: readonly string[]) => Promise<{
305
+ payload: unknown;
306
+ rawRequestDump: RawHttpRequestDump;
307
+ }>, createClient: (getPayload: () => unknown) => AnthropicMessagesClientLike, prepareOptions: (options: AnthropicOptions) => AnthropicOptions, forceThinkingOffFields?: Record<string, unknown>): AssistantMessageEventStream;
291
308
  /**
292
309
  * A single Anthropic conversation turn, including the mid-conversation
293
310
  * `system` role (Opus 4.8+ and Fable/Mythos 5).
@@ -0,0 +1,21 @@
1
+ import type { Tool } from "../../types.js";
2
+ /**
3
+ * The CLI sanitizes tool names to `[a-zA-Z0-9_-]`; a name longer than
4
+ * `maxLength` (64 by default) keeps its first `maxLength - 9` chars and ends
5
+ * `_` + an 8-char sha256, `maxLength` in all, which stays inside the route's
6
+ * tool-name bound. Applied to declarations and replayed call/result names on
7
+ * every wire droid sanitizes.
8
+ */
9
+ export declare function sanitizeFactoryDroidToolName(name: string, maxLength?: number): string;
10
+ /**
11
+ * Chat-completions names go out verbatim (the route accepts dots) unless they
12
+ * exceed `maxLength`, the target's catalog `tool-call-name-max-length`; only
13
+ * those take the capped, sanitized form.
14
+ */
15
+ export declare function encodeFactoryDroidCompletionsToolName(name: string, maxLength: number): string;
16
+ /**
17
+ * Wire name -> caller tool name for the declared tools, so returned calls
18
+ * dispatch to the tool that was advertised. Two tools sharing one wire name
19
+ * could not be told apart, so that request is rejected.
20
+ */
21
+ export declare function factoryDroidToolNamesByWire(tools: Tool[] | undefined, encode?: (name: string) => string): Map<string, string>;
@@ -5680,10 +5680,10 @@ export interface ResponseCreateParamsBase {
5680
5680
  * `prompt_cache_retention` is not specified.
5681
5681
  */
5682
5682
  prompt_cache_retention?: "in_memory" | "24h" | null;
5683
- /** Explicit prompt-cache mode and minimum lifetime for GPT-5.6+ models. */
5683
+ /** Explicit prompt-cache mode and minimum lifetime (`12h` is Responses-only) for GPT-5.6+ models. */
5684
5684
  prompt_cache_options?: {
5685
5685
  mode: "implicit" | "explicit";
5686
- ttl?: "30m";
5686
+ ttl?: "30m" | "12h";
5687
5687
  } | null;
5688
5688
  /**
5689
5689
  * **gpt-5 and o-series models only**
@@ -240,8 +240,12 @@ export interface AnthropicCompactionRequest {
240
240
  export interface OpenAIPromptCacheOptions {
241
241
  /** `explicit` disables OpenAI's automatic latest-message breakpoint. */
242
242
  mode: "implicit" | "explicit";
243
- /** The only currently supported minimum breakpoint lifetime. */
244
- ttl?: "30m";
243
+ /**
244
+ * Minimum cache lifetime. `30m` is OpenAI's default; `12h` (Responses API only)
245
+ * bills writes at 2.5× input instead of 1.25×. Defaults to `12h` when
246
+ * `cacheRetention` resolves to `long`, otherwise `30m`.
247
+ */
248
+ ttl?: "30m" | "12h";
245
249
  /** By default, mark one existing block from stable history; `none` suppresses that marker. */
246
250
  breakpoint?: "latest-stable-message" | "none";
247
251
  }
@@ -885,6 +889,8 @@ export interface AnthropicCompactionPayload {
885
889
  content: string;
886
890
  /** Signature of an on-demand block; replayed verbatim. */
887
891
  signature?: string;
892
+ /** Net tool changes across the summarized range; covered by the signature, replayed verbatim. */
893
+ toolChanges?: Record<string, unknown>[];
888
894
  /** Legacy threshold block state (`compact-2026-01-12`); replay-only. */
889
895
  encryptedContent?: string;
890
896
  /**
@@ -8,6 +8,8 @@ export interface ToolCallLoopGuardOptions {
8
8
  export interface ToolCallLoopTurn {
9
9
  readonly message: AssistantMessage;
10
10
  readonly toolResults: readonly ToolResultMessage[];
11
+ /** The turn also delivered context outside its tool results (extension `additionalContext`). */
12
+ readonly injectedContext?: boolean;
11
13
  }
12
14
  /** Details needed to steer the model away from a repeated tool call. */
13
15
  export interface RepeatedToolCallDetection {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.12.3",
3
+ "version": "18.12.5",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.12.3",
159
- "@oh-my-pi/pi-catalog": "18.12.3",
160
- "@oh-my-pi/pi-natives": "18.12.3",
161
- "@oh-my-pi/pi-utils": "18.12.3",
162
- "@oh-my-pi/pi-wire": "18.12.3"
158
+ "@oh-my-pi/omptype": "18.12.5",
159
+ "@oh-my-pi/pi-catalog": "18.12.5",
160
+ "@oh-my-pi/pi-natives": "18.12.5",
161
+ "@oh-my-pi/pi-utils": "18.12.5",
162
+ "@oh-my-pi/pi-wire": "18.12.5"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -28,12 +28,13 @@ export class AccountPolicies {
28
28
  #warnedUnmatched = new Set<number>();
29
29
 
30
30
  constructor(policies: AuthAccountPolicies, defaultReservePct: number | undefined) {
31
- AccountPolicies.#validateAccountPolicyConfiguration(policies);
32
- this.#accountPolicies = policies;
33
- this.#defaultReservePct =
31
+ const effectiveDefaultReservePct =
34
32
  typeof defaultReservePct === "number" && Number.isFinite(defaultReservePct)
35
33
  ? Math.max(0, Math.min(100, defaultReservePct))
36
34
  : DEFAULT_USAGE_RESERVE_PCT;
35
+ AccountPolicies.#validateAccountPolicyConfiguration(policies, effectiveDefaultReservePct);
36
+ this.#accountPolicies = policies;
37
+ this.#defaultReservePct = effectiveDefaultReservePct;
37
38
  }
38
39
 
39
40
  /** Global usage reserve (0–100) for accounts without a per-account `reservePct`. */
@@ -58,7 +59,7 @@ export class AccountPolicies {
58
59
  this.#warnedUnmatched = next.#warnedUnmatched;
59
60
  }
60
61
 
61
- static #validateAccountPolicyConfiguration(accountPolicies: AuthAccountPolicies): void {
62
+ static #validateAccountPolicyConfiguration(accountPolicies: AuthAccountPolicies, defaultReservePct: number): void {
62
63
  for (let index = 0; index < accountPolicies.length; index += 1) {
63
64
  const policy = accountPolicies[index]!;
64
65
  const path = `auth.accountPolicies[${index}]`;
@@ -95,17 +96,32 @@ export class AccountPolicies {
95
96
  ) {
96
97
  throw new AIError.ConfigurationError(`${path}.reservePct must be a finite number between 0 and 100`);
97
98
  }
99
+ if (policy.reclaimAbovePct !== undefined) {
100
+ const reservePct = policy.reservePct ?? defaultReservePct;
101
+ if (
102
+ !Number.isFinite(policy.reclaimAbovePct) ||
103
+ policy.reclaimAbovePct <= reservePct ||
104
+ policy.reclaimAbovePct > 100
105
+ ) {
106
+ throw new AIError.ConfigurationError(
107
+ `${path}.reclaimAbovePct must be a finite number above the account's reserve (${reservePct}%) and at most 100`,
108
+ );
109
+ }
110
+ }
98
111
  }
99
112
  }
100
113
 
101
114
  validateUsageCapability(provider: string, canFetchUsage: boolean): void {
102
- const policyIndex = this.#accountPolicies.findIndex(
103
- policy => policy.provider === provider && policy.reservePct !== undefined,
104
- );
105
- if (policyIndex !== -1 && !canFetchUsage) {
106
- throw new AIError.ConfigurationError(
107
- `auth.accountPolicies[${policyIndex}].reservePct requires a usage provider for ${provider}`,
115
+ if (canFetchUsage) return;
116
+ for (const field of ["reservePct", "reclaimAbovePct"] as const) {
117
+ const policyIndex = this.#accountPolicies.findIndex(
118
+ policy => policy.provider === provider && policy[field] !== undefined,
108
119
  );
120
+ if (policyIndex !== -1) {
121
+ throw new AIError.ConfigurationError(
122
+ `auth.accountPolicies[${policyIndex}].${field} requires a usage provider for ${provider}`,
123
+ );
124
+ }
109
125
  }
110
126
  }
111
127
 
package/src/auth/rank.ts CHANGED
@@ -25,6 +25,10 @@ export type UsageCandidate<T extends AuthCredential> = {
25
25
  * spent, so if it still serves it draws on paid overage such as Codex credits.
26
26
  */
27
27
  allowanceSpent?: boolean;
28
+ /** Present after policy-aware ranking: the account's configured `priority` (0 when unset). */
29
+ accountPriority?: number;
30
+ /** Present after policy-aware ranking: unblocked and measured at or above its `reclaimAbovePct`. */
31
+ reclaimReady?: boolean;
28
32
  };
29
33
 
30
34
  /** OAuth credential eligible for usage ranking. */
@@ -152,5 +156,7 @@ export function orderUsageRankedCandidates<T extends AuthCredential>(
152
156
  inReserve: candidate.inReserve,
153
157
  reserveMeasured: candidate.reserveMeasured,
154
158
  allowanceSpent: candidate.allowanceSpent,
159
+ accountPriority: candidate.accountPriority,
160
+ reclaimReady: candidate.reclaimReady,
155
161
  }));
156
162
  }
@@ -489,6 +489,11 @@ export class CredentialSelector {
489
489
  reserveMeasured: reserveFraction !== undefined && remainingFraction !== undefined,
490
490
  accountPriority: policy?.priority === undefined || !Number.isFinite(policy.priority) ? 0 : policy.priority,
491
491
  allowanceSpent: remainingFraction === 0,
492
+ reclaimReady:
493
+ !blocked &&
494
+ policy?.reclaimAbovePct !== undefined &&
495
+ remainingFraction !== undefined &&
496
+ remainingFraction * 100 >= policy.reclaimAbovePct,
492
497
  usageMeasured,
493
498
  hasPriorityBoost: strategy?.hasPriorityBoost?.(primary, primaryUncapped, args.rankingContext) ?? false,
494
499
  planPriority: planPriority(args.planGate, usage),
@@ -689,13 +694,26 @@ export class CredentialSelector {
689
694
  candidate.selection.index === sessionPreferredIndex,
690
695
  );
691
696
  const preferredCandidate = sessionPreferredCandidate === -1 ? undefined : candidates[sessionPreferredCandidate];
692
- // A warm automatic pin normally wins. Two policies may evict it, each only
697
+ // A warm automatic pin normally wins. Three policies may evict it, each only
693
698
  // while a sibling is confirmed better: reserve (sibling measured outside
694
- // reserve) and spent allowance (unblocked sibling with allowance left).
695
- const automaticPinWouldBeEvicted = (excludePreflightFailures: boolean): boolean =>
696
- !sessionPinIsExplicit &&
697
- preferredCandidate !== undefined &&
698
- candidates.some(candidate => {
699
+ // reserve), spent allowance (unblocked sibling with allowance left), and
700
+ // opt-in reclaim (the sibling ranking picks next has higher priority and is
701
+ // measured at or above its `reclaimAbovePct`).
702
+ const automaticPinWouldBeEvicted = (excludePreflightFailures: boolean): boolean => {
703
+ if (sessionPinIsExplicit || preferredCandidate === undefined) return false;
704
+ const rankedWinner = candidates.find(
705
+ candidate =>
706
+ candidate !== preferredCandidate &&
707
+ !(excludePreflightFailures && preflightFailures.has(candidate)) &&
708
+ !this.#deps.blocks.isBlocked(provider, providerKey, candidate.selection.index, blockScopes),
709
+ );
710
+ if (
711
+ rankedWinner?.reclaimReady === true &&
712
+ (rankedWinner.accountPriority ?? 0) > (preferredCandidate.accountPriority ?? 0)
713
+ ) {
714
+ return true;
715
+ }
716
+ return candidates.some(candidate => {
699
717
  if (candidate === preferredCandidate) return false;
700
718
  if (excludePreflightFailures && preflightFailures.has(candidate)) return false;
701
719
  if (
@@ -712,6 +730,7 @@ export class CredentialSelector {
712
730
  !this.#deps.blocks.isBlocked(provider, providerKey, candidate.selection.index, blockScopes)
713
731
  );
714
732
  });
733
+ };
715
734
  const pinEvictedBeforePreflight = automaticPinWouldBeEvicted(false);
716
735
  if (
717
736
  !hasPlanRequirement &&
package/src/auth/types.ts CHANGED
@@ -62,6 +62,8 @@ export interface AuthAccountPolicy {
62
62
  readonly priority?: number;
63
63
  /** Protected remaining quota percentage for this account. */
64
64
  readonly reservePct?: number;
65
+ /** Remaining quota percentage, above the reserve, at which this account takes back lower-priority warm pins. */
66
+ readonly reclaimAbovePct?: number;
65
67
  }
66
68
 
67
69
  /** Read-only set of per-account routing policies. */