@oh-my-pi/pi-ai 18.12.3 → 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 (36) hide show
  1. package/CHANGELOG.md +29 -56
  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/types.d.ts +2 -0
  12. package/dist/types/utils/tool-call-loop-guard.d.ts +2 -0
  13. package/package.json +6 -6
  14. package/src/auth/policy.ts +26 -10
  15. package/src/auth/rank.ts +6 -0
  16. package/src/auth/select.ts +25 -6
  17. package/src/auth/types.ts +2 -0
  18. package/src/auth-broker/discover.ts +67 -17
  19. package/src/auth-broker/remote-store.ts +59 -16
  20. package/src/images/google-antigravity.ts +0 -1
  21. package/src/providers/amazon-bedrock.ts +421 -142
  22. package/src/providers/anthropic-compaction.ts +13 -0
  23. package/src/providers/anthropic-state.ts +16 -2
  24. package/src/providers/anthropic-wire.ts +9 -1
  25. package/src/providers/anthropic.ts +271 -74
  26. package/src/providers/cursor-pi-args.ts +3 -1
  27. package/src/providers/devin.ts +13 -43
  28. package/src/providers/factory-droid/gemini.ts +1 -30
  29. package/src/providers/factory-droid/tool-names.ts +44 -0
  30. package/src/providers/factory-droid.ts +91 -20
  31. package/src/providers/google-gemini-cli.ts +0 -1
  32. package/src/types.ts +2 -0
  33. package/src/usage/devin.ts +3 -3
  34. package/src/utils/leaked-thinking-stream.ts +12 -0
  35. package/src/utils/thinking-loop.ts +75 -14
  36. package/src/utils/tool-call-loop-guard.ts +14 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
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
+
5
33
  ## [18.12.3] - 2026-10-19
6
34
 
7
35
  ### Added
@@ -2292,59 +2320,4 @@
2292
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))
2293
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))
2294
2322
 
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).
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>;
@@ -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
  /**
@@ -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.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.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.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. */
@@ -27,7 +27,7 @@ import {
27
27
  } from "../auth-storage";
28
28
  import * as AIError from "../error";
29
29
  import { AuthBrokerClient, AuthBrokerError } from "./client";
30
- import { type AuthBrokerAccountPool, RemoteAuthCredentialStore } from "./remote-store";
30
+ import { type AuthBrokerAccountPool, type AuthBrokerProviderPool, RemoteAuthCredentialStore } from "./remote-store";
31
31
  import { readAuthBrokerSnapshotCache, scheduleAuthBrokerSnapshotCacheWrite } from "./snapshot-cache";
32
32
  import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types";
33
33
 
@@ -122,7 +122,12 @@ function parseAuthAccountPolicies(value: unknown): AuthAccountPolicies {
122
122
  }
123
123
  const policy = entry as Record<string, unknown>;
124
124
  const unknownPolicyFields = Object.keys(policy).filter(
125
- key => key !== "provider" && key !== "account" && key !== "priority" && key !== "reservePct",
125
+ key =>
126
+ key !== "provider" &&
127
+ key !== "account" &&
128
+ key !== "priority" &&
129
+ key !== "reservePct" &&
130
+ key !== "reclaimAbovePct",
126
131
  );
127
132
  if (unknownPolicyFields.length > 0) {
128
133
  throw new AIError.ConfigurationError(`${path} has unknown fields: ${unknownPolicyFields.join(", ")}`);
@@ -171,6 +176,12 @@ function parseAuthAccountPolicies(value: unknown): AuthAccountPolicies {
171
176
  ) {
172
177
  throw new AIError.ConfigurationError(`${path}.reservePct must be between 0 and 100`);
173
178
  }
179
+ if (
180
+ policy.reclaimAbovePct !== undefined &&
181
+ (typeof policy.reclaimAbovePct !== "number" || !Number.isFinite(policy.reclaimAbovePct))
182
+ ) {
183
+ throw new AIError.ConfigurationError(`${path}.reclaimAbovePct must be a finite number`);
184
+ }
174
185
 
175
186
  return {
176
187
  provider,
@@ -182,6 +193,7 @@ function parseAuthAccountPolicies(value: unknown): AuthAccountPolicies {
182
193
  },
183
194
  ...(typeof policy.priority === "number" ? { priority: policy.priority } : {}),
184
195
  ...(typeof policy.reservePct === "number" ? { reservePct: policy.reservePct } : {}),
196
+ ...(typeof policy.reclaimAbovePct === "number" ? { reclaimAbovePct: policy.reclaimAbovePct } : {}),
185
197
  };
186
198
  });
187
199
  }
@@ -272,7 +284,7 @@ export async function loadAuthBrokerAccountPool(): Promise<AuthBrokerAccountPool
272
284
  throw new AIError.ConfigurationError("OMP_AUTH_BROKER_ACCOUNT_POOL_FILE must contain a JSON object");
273
285
  }
274
286
 
275
- const accountPool = new Map<string, ReadonlySet<string>>();
287
+ const accountPool = new Map<string, ReadonlySet<string> | AuthBrokerProviderPool>();
276
288
  for (const [provider, value] of Object.entries(parsed)) {
277
289
  const normalizedProvider = provider.trim();
278
290
  if (normalizedProvider.length === 0) {
@@ -283,28 +295,66 @@ export async function loadAuthBrokerAccountPool(): Promise<AuthBrokerAccountPool
283
295
  "OMP_AUTH_BROKER_ACCOUNT_POOL_FILE contains a provider id with surrounding whitespace",
284
296
  );
285
297
  }
286
- if (!Array.isArray(value)) {
287
- throw new AIError.ConfigurationError(
288
- `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} must be an array of identity keys`,
289
- );
290
- }
291
- const identities = new Set<string>();
292
- for (const identity of value) {
293
- if (typeof identity !== "string" || identity.length === 0) {
298
+ accountPool.set(provider, parseAccountPoolEntry(provider, value));
299
+ }
300
+ return accountPool;
301
+ }
302
+
303
+ /**
304
+ * One pool-file entry: an identity array (visible OAuth accounts, API keys
305
+ * kept), `true` (everything visible), `false` (nothing visible), or a rule
306
+ * object `{ "accounts"?: [...], "apiKeys"?: boolean }`.
307
+ */
308
+ function parseAccountPoolEntry(provider: string, value: unknown): ReadonlySet<string> | AuthBrokerProviderPool {
309
+ if (Array.isArray(value)) return parseAccountPoolIdentities(provider, value);
310
+ if (value === true) return {};
311
+ if (value === false) return { accounts: new Set(), apiKeys: false };
312
+ if (value === null || typeof value !== "object") {
313
+ throw new AIError.ConfigurationError(
314
+ `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} must be an array of identity keys, a boolean, or an object with "accounts"/"apiKeys"`,
315
+ );
316
+ }
317
+ const rule: { accounts?: ReadonlySet<string>; apiKeys?: boolean } = {};
318
+ for (const [key, field] of Object.entries(value)) {
319
+ if (key === "accounts") {
320
+ if (!Array.isArray(field)) {
294
321
  throw new AIError.ConfigurationError(
295
- `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} contains an invalid identity key`,
322
+ `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} has "accounts" that is not an array of identity keys`,
296
323
  );
297
324
  }
298
- if (identity !== identity.trim()) {
325
+ rule.accounts = parseAccountPoolIdentities(provider, field);
326
+ } else if (key === "apiKeys") {
327
+ if (typeof field !== "boolean") {
299
328
  throw new AIError.ConfigurationError(
300
- `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} contains an identity key with surrounding whitespace`,
329
+ `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} has "apiKeys" that is not a boolean`,
301
330
  );
302
331
  }
303
- identities.add(identity);
332
+ rule.apiKeys = field;
333
+ } else {
334
+ throw new AIError.ConfigurationError(
335
+ `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} has unknown field ${JSON.stringify(key)}`,
336
+ );
304
337
  }
305
- accountPool.set(provider, identities);
306
338
  }
307
- return accountPool;
339
+ return rule;
340
+ }
341
+
342
+ function parseAccountPoolIdentities(provider: string, value: unknown[]): Set<string> {
343
+ const identities = new Set<string>();
344
+ for (const identity of value) {
345
+ if (typeof identity !== "string" || identity.length === 0) {
346
+ throw new AIError.ConfigurationError(
347
+ `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} contains an invalid identity key`,
348
+ );
349
+ }
350
+ if (identity !== identity.trim()) {
351
+ throw new AIError.ConfigurationError(
352
+ `OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} contains an identity key with surrounding whitespace`,
353
+ );
354
+ }
355
+ identities.add(identity);
356
+ }
357
+ return identities;
308
358
  }
309
359
 
310
360
  function resolveSnapshotTtlMs(): number {