@oh-my-pi/pi-ai 18.12.2 → 18.12.4

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 (46) hide show
  1. package/CHANGELOG.md +44 -78
  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/google-shared.d.ts +17 -0
  12. package/dist/types/types.d.ts +2 -0
  13. package/dist/types/utils/http-inspector.d.ts +2 -0
  14. package/dist/types/utils/tool-call-loop-guard.d.ts +2 -0
  15. package/dist/types/utils/validation.d.ts +11 -1
  16. package/package.json +6 -6
  17. package/src/auth/policy.ts +26 -10
  18. package/src/auth/rank.ts +6 -0
  19. package/src/auth/select.ts +25 -6
  20. package/src/auth/types.ts +2 -0
  21. package/src/auth-broker/discover.ts +67 -17
  22. package/src/auth-broker/remote-store.ts +59 -16
  23. package/src/error/flags.ts +2 -2
  24. package/src/images/google-antigravity.ts +0 -1
  25. package/src/providers/amazon-bedrock.ts +421 -142
  26. package/src/providers/anthropic-compaction.ts +13 -0
  27. package/src/providers/anthropic-state.ts +16 -2
  28. package/src/providers/anthropic-wire.ts +9 -1
  29. package/src/providers/anthropic.ts +271 -74
  30. package/src/providers/cursor-pi-args.ts +3 -1
  31. package/src/providers/devin.ts +13 -43
  32. package/src/providers/factory-droid/gemini.ts +9 -30
  33. package/src/providers/factory-droid/tool-names.ts +44 -0
  34. package/src/providers/factory-droid.ts +91 -20
  35. package/src/providers/google-gemini-cli.ts +0 -1
  36. package/src/providers/google-shared.ts +61 -6
  37. package/src/types.ts +2 -0
  38. package/src/usage/devin.ts +3 -3
  39. package/src/utils/http-inspector.ts +3 -2
  40. package/src/utils/leaked-thinking-stream.ts +28 -4
  41. package/src/utils/request-debug.ts +3 -2
  42. package/src/utils/schema/CONSTRAINTS.md +12 -5
  43. package/src/utils/schema/allowlist.ts +144 -18
  44. package/src/utils/thinking-loop.ts +75 -14
  45. package/src/utils/tool-call-loop-guard.ts +14 -2
  46. package/src/utils/validation.ts +47 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.12.4] - 2026-10-20
6
+
7
+ ### Added
8
+
9
+ - 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))
10
+ - 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))
11
+ - 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))
12
+ - 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)).
13
+ - `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))
14
+ - 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))
15
+
16
+ ### Changed
17
+
18
+ - `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))
19
+
20
+ ### Fixed
21
+
22
+ - 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))
23
+ - 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))
24
+ - 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))
25
+ - 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))
26
+ - 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))
27
+ - 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))
28
+ - 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))
29
+ - 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))
30
+ - 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))
31
+ - 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))
32
+
33
+ ## [18.12.3] - 2026-10-19
34
+
35
+ ### Added
36
+
37
+ - Added `validateToolArguments(tool, call, { keepUnknownRootKeys: true })` to keep unknown root keys that a closed schema would otherwise drop during argument repair ([#16317](https://github.com/can1357/oh-my-pi/issues/16317))
38
+
39
+ ### Fixed
40
+
41
+ - Fixed Claude streams that a gateway cut off before `message_delta` failing the turn instead of being retried ([#16341](https://github.com/can1357/oh-my-pi/pull/16341) by [@lz-lunzi](https://github.com/lz-lunzi))
42
+ - Fixed Gemini pairing parallel same-name tool results with the wrong call when history stores them out of call order (sessions from before 18.4.2, SDK-built contexts) ([#16259](https://github.com/can1357/oh-my-pi/pull/16259) by [@will-bogusz](https://github.com/will-bogusz))
43
+ - Fixed Factory Droid Gemini dropping calls to tools with names over 64 characters (most MCP tools) and their results from later requests, so the model called the tool again; such names now shorten to 64 characters ([#16283](https://github.com/can1357/oh-my-pi/pull/16283) by [@will-bogusz](https://github.com/will-bogusz))
44
+ - Fixed Factory Droid Gemini requests failing with 400 "For schema with items, schema type should be ARRAY" when a tool parameter accepts a string or a list, as the default `task` tool's `model` field does ([#16249](https://github.com/can1357/oh-my-pi/pull/16249) by [@will-bogusz](https://github.com/will-bogusz))
45
+ - Fixed `PI_REQ_DEBUG=1` request dumps writing your API key, bearer token and response cookies to disk in plain text: credential headers now read `[redacted]`, as in rejected-request dumps ([#16293](https://github.com/can1357/oh-my-pi/pull/16293) by [@will-bogusz](https://github.com/will-bogusz))
46
+ - Fixed finalized signed reasoning disappearing from displayed thinking when no deltas streamed ([#16227](https://github.com/can1357/oh-my-pi/issues/16227))
47
+
5
48
  ## [18.12.2] - 2026-10-18
6
49
 
7
50
  ### Added
@@ -2277,81 +2320,4 @@
2277
2320
  - 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))
2278
2321
  - 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))
2279
2322
 
2280
- ## [16.1.16] - 2026-06-23
2281
-
2282
- ### Fixed
2283
-
2284
- - 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))
2285
- - 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.
2286
-
2287
- ## [16.1.15] - 2026-06-22
2288
-
2289
- ### Fixed
2290
-
2291
- - 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))
2292
- - 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))
2293
-
2294
- ## [16.1.14] - 2026-06-22
2295
-
2296
- ### Added
2297
-
2298
- - Added proxy support for model providers via `PI_PROXY` and `PI_PROXY_<PROVIDER>` variables
2299
- - Added `NO_PROXY` environment variable support for bypassing proxy configuration
2300
- - Added support for Sakana AI provider
2301
- - Added Sakana AI login and request base URL support for `SAKANA_*` / `FUGU_*` environment variables
2302
-
2303
- ### Changed
2304
-
2305
- - Consolidated API key authentication logic across registry providers
2306
- - Disabled parallel tool calls for Devin provider requests
2307
-
2308
- ### Fixed
2309
-
2310
- - Improved proxy bypass logic to correctly handle private IP ranges and local metadata services
2311
- - Enhanced memoization for proxy environment variable lookups to improve performance
2312
-
2313
- ## [16.1.13] - 2026-06-22
2314
-
2315
- ### Added
2316
-
2317
- - Added support for Devin as a provider
2318
-
2319
- ### Changed
2320
-
2321
- - Updated tool call arguments to use `Record<string, unknown>` and `unknown` for tool results
2322
-
2323
- ### Fixed
2324
-
2325
- - 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))
2326
- - 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))
2327
-
2328
- ## [16.1.11] - 2026-06-21
2329
-
2330
- ### Fixed
2331
-
2332
- - 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))
2333
- - 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.
2334
-
2335
- ## [16.1.10] - 2026-06-21
2336
-
2337
- ### Changed
2338
-
2339
- - Improved JSON robustness by replacing external dependency with a custom, high-performance parser
2340
- - Strengthened streaming JSON parsing to prevent non-finite numbers from surfacing as `undefined/NaN`
2341
- - Configured JSON parser to reject JS-specific `NaN` and `Infinity` values for tool arguments
2342
- - Replaced the JSON repair/parse helpers (`parseJsonWithRepair`, `parseStreamingJson`) with a single from-scratch tolerant parser (`RelaxedJson`) that accepts single-quoted strings, unquoted object keys, trailing/stray commas, `//` and `/* */` comments, Python `True`/`False`/`None`, raw control characters, invalid escapes, and unescaped apostrophes (`'it's'`). Final parsing still throws on truncated/garbage input (so a malformed tool call is skipped rather than executed with half-formed args) and rejects JS-only `NaN`/`Infinity`; streaming parsing stays non-throwing and rolls back incomplete trailing tokens instead of surfacing `undefined`/`NaN`. The Cursor provider's ad-hoc regex + JSON5 tool-argument parser now routes through the shared parser.
2343
-
2344
- ### Fixed
2345
-
2346
- - Fixed tool call ID normalization for Anthropic-compatible models
2347
- - Fixed Anthropic Messages replay sanitizing malformed tool-call IDs, including aborted native tool calls with empty IDs, so retries no longer send invalid `tool_use.id` / `tool_result.tool_use_id` pairs.
2348
- - Fixed the Codex Responses WebSocket transport attributing a prior turn's output to the current one on a reused connection: a trailing/duplicate frame from a cleanly-completed previous response that slipped past the queue drain could be consumed as this request's terminal (ending the turn with empty output) or as a stale tool call. Frames are now keyed by `response.id` — a frame carrying the previous response's id is dropped, and one carrying a third id (or a regressed `sequence_number`) fails closed so the turn retries instead of mixing two responses' streams. Idless frames (deltas, the rate-limit/metadata preamble, `response.created`-less streams) still pass through, matching upstream codex-rs.
2349
- - Fixed `transformMessages` pulling an earlier, orphaned tool result onto a later tool call that reused the same id (left behind when compaction folded the originating `tool_use` into a summary). The pending-call flush now pairs each call with a result positioned _after_ its assistant turn, so a reused id surfaces its own output rather than a prior turn's.
2350
- - Fixed DashScope 429 rate-limit messages that mention authorization being classified as credential failures, preventing valid API keys from being invalidated after throttling. ([#3172](https://github.com/can1357/oh-my-pi/issues/3172))
2351
- - Fixed OpenCode Go `401 Insufficient balance` quota errors being treated as unknown failures instead of usage-limit errors, restoring credential rotation and fallback chains. ([#3169](https://github.com/can1357/oh-my-pi/issues/3169))
2352
-
2353
- ### Removed
2354
-
2355
- - Removed the `partial-json` dependency; streaming JSON parsing now uses the in-house `RelaxedJson` parser.
2356
-
2357
- Older entries are archived in [packages/ai/CHANGELOG.md@bb602d67c9b6](https://github.com/can1357/oh-my-pi/blob/bb602d67c9b685535bbf8ef1a1cc9de3c944ede4/packages/ai/CHANGELOG.md).
2323
+ Older entries are archived in [packages/ai/CHANGELOG.md@47e6af055a17](https://github.com/can1357/oh-my-pi/blob/47e6af055a17862bc1cab6b5ebedd3db75ac2573/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>;
@@ -76,6 +76,23 @@ export declare function isThinkingPart(part: Pick<Part, "thought" | "thoughtSign
76
76
  */
77
77
  export declare function retainThoughtSignature(existing: string | undefined, incoming: string | undefined): string | undefined;
78
78
  export declare const SKIP_THOUGHT_SIGNATURE = "skip_thought_signature_validator";
79
+ /**
80
+ * Gemini pairs id-less function responses with calls by position. The agent loop stores
81
+ * parallel results in call order, but histories can still arrive out of call order (sessions
82
+ * saved by older versions, contexts built through the SDK), so the converters guard the wire:
83
+ * record each call as it is emitted and each response with its call, then {@link apply} sorts
84
+ * every turn's matched responses back into call order within the slots they already occupy.
85
+ */
86
+ export declare class FunctionCallOrder {
87
+ #private;
88
+ call(id: string): void;
89
+ /** The call ordinal of `id`, if that call was recorded. */
90
+ ordinal(id: string): number | undefined;
91
+ response(part: Part, callId: string): void;
92
+ apply(contents: readonly {
93
+ parts?: Part[];
94
+ }[]): void;
95
+ }
79
96
  /**
80
97
  * Convert internal messages to Gemini Content[] format.
81
98
  */
@@ -885,6 +885,8 @@ export interface AnthropicCompactionPayload {
885
885
  content: string;
886
886
  /** Signature of an on-demand block; replayed verbatim. */
887
887
  signature?: string;
888
+ /** Net tool changes across the summarized range; covered by the signature, replayed verbatim. */
889
+ toolChanges?: Record<string, unknown>[];
888
890
  /** Legacy threshold block state (`compact-2026-01-12`); replay-only. */
889
891
  encryptedContent?: string;
890
892
  /**
@@ -85,3 +85,5 @@ export declare function rewriteCopilotError(errorMessage: string, error: unknown
85
85
  * credential rotation (sibling keys fail identically).
86
86
  */
87
87
  export declare function rewriteClinePassError(errorMessage: string, provider: string): string;
88
+ /** Copy of `headers` with every credential value replaced by `[redacted]`; shared by every persisted dump. */
89
+ export declare function redactHeaders<T>(headers: Record<string, T> | undefined): Record<string, T | string> | undefined;
@@ -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 {
@@ -19,10 +19,20 @@ import type { Tool, ToolCall } from "../types.js";
19
19
  * @throws Error if tool is not found or validation fails
20
20
  */
21
21
  export declare function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"];
22
+ /** Options for {@link validateToolArguments}. */
23
+ export interface ValidateToolArgumentsOptions {
24
+ /**
25
+ * Keep unknown root keys on the validated value instead of letting the
26
+ * unrecognized-key repair delete them from a closed schema. Set for tools
27
+ * that own their refusal (`lenientArgValidation`), so execute() can reject
28
+ * fields the model sent rather than run without them.
29
+ */
30
+ keepUnknownRootKeys?: boolean;
31
+ }
22
32
  /**
23
33
  * Validates tool call arguments against an ArkType or plain JSON Schema schema.
24
34
  * Applies conservative LLM-quirk normalization before declaring failure.
25
35
  *
26
36
  * @throws Error with a formatted message when validation cannot be reconciled.
27
37
  */
28
- export declare function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall["arguments"];
38
+ export declare function validateToolArguments(tool: Tool, toolCall: ToolCall, options?: ValidateToolArgumentsOptions): ToolCall["arguments"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.12.2",
3
+ "version": "18.12.4",
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.2",
159
- "@oh-my-pi/pi-catalog": "18.12.2",
160
- "@oh-my-pi/pi-natives": "18.12.2",
161
- "@oh-my-pi/pi-utils": "18.12.2",
162
- "@oh-my-pi/pi-wire": "18.12.2"
158
+ "@oh-my-pi/omptype": "18.12.4",
159
+ "@oh-my-pi/pi-catalog": "18.12.4",
160
+ "@oh-my-pi/pi-natives": "18.12.4",
161
+ "@oh-my-pi/pi-utils": "18.12.4",
162
+ "@oh-my-pi/pi-wire": "18.12.4"
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. */