@oh-my-pi/pi-ai 18.10.4 → 18.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/types/auth/usage-cache.d.ts +3 -1
  3. package/dist/types/error/flags.d.ts +12 -0
  4. package/dist/types/images/shared.d.ts +26 -2
  5. package/dist/types/judgment/native.d.ts +16 -1
  6. package/dist/types/judgment/text.d.ts +11 -5
  7. package/dist/types/judgment/types.d.ts +5 -1
  8. package/dist/types/judgment/wire.d.ts +18 -0
  9. package/dist/types/provider-session-state.d.ts +2 -2
  10. package/dist/types/providers/anthropic.d.ts +9 -5
  11. package/dist/types/providers/cursor.d.ts +7 -0
  12. package/dist/types/providers/openai-codex-responses.d.ts +6 -0
  13. package/dist/types/providers/systemone-server.d.ts +4 -3
  14. package/dist/types/providers/transform-messages.d.ts +8 -0
  15. package/dist/types/types.d.ts +15 -7
  16. package/dist/types/utils/dsml-leak.d.ts +4 -2
  17. package/dist/types/utils/tool-call-arguments.d.ts +2 -1
  18. package/package.json +6 -6
  19. package/src/auth/usage-cache.ts +23 -7
  20. package/src/auth/usage.ts +2 -4
  21. package/src/auth-broker/remote-store.ts +31 -6
  22. package/src/auth-gateway/routes/images.ts +2 -0
  23. package/src/auth-gateway/routes/systemone.ts +17 -2
  24. package/src/error/flags.ts +20 -2
  25. package/src/images/codex-format-directive.md +3 -0
  26. package/src/images/google-antigravity.ts +2 -1
  27. package/src/images/google-generative-ai.ts +4 -2
  28. package/src/images/openai-hosted.ts +25 -2
  29. package/src/images/openai-images.ts +2 -0
  30. package/src/images/shared.ts +77 -3
  31. package/src/judgment/chat.ts +17 -1
  32. package/src/judgment/native.ts +79 -6
  33. package/src/judgment/openai-decisions.ts +27 -6
  34. package/src/judgment/systemone.ts +17 -5
  35. package/src/judgment/text.ts +20 -7
  36. package/src/judgment/types.ts +5 -1
  37. package/src/judgment/wire.ts +19 -0
  38. package/src/provider-session-state.ts +2 -2
  39. package/src/providers/anthropic-messages-server.ts +84 -1
  40. package/src/providers/anthropic.ts +266 -110
  41. package/src/providers/cursor.ts +49 -9
  42. package/src/providers/images-server.ts +6 -1
  43. package/src/providers/openai-codex-responses.ts +27 -2
  44. package/src/providers/openai-completions.ts +35 -9
  45. package/src/providers/systemone-server.ts +34 -10
  46. package/src/providers/transform-messages.ts +23 -12
  47. package/src/stream.ts +1 -0
  48. package/src/types.ts +15 -7
  49. package/src/usage/openai-codex.ts +11 -0
  50. package/src/usage/opencode-go.ts +7 -8
  51. package/src/utils/dsml-leak.ts +30 -14
  52. package/src/utils/tool-call-arguments.ts +2 -1
  53. package/src/utils.ts +37 -18
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.11.1] - 2026-10-15
6
+
7
+ ### Added
8
+
9
+ - 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)).
10
+ - `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))
11
+
12
+ ### Fixed
13
+
14
+ - Fixed Codex-hosted image generation ignoring the requested `aspect_ratio`/`image_size`, and `3:2`/`2:3` requests sending no size to OpenAI image models ([#15601](https://github.com/can1357/oh-my-pi/pull/15601) by [@chulmin-dev](https://github.com/chulmin-dev))
15
+ - Fixed Claude subagents failing to return structured results when `yield` arguments arrived as invalid JSON ([#15605](https://github.com/can1357/oh-my-pi/pull/15605) by [@farnoy](https://github.com/farnoy))
16
+ - Fixed turns stopping instead of retrying when a gateway reports its upstream WebSocket closed before `response.completed` ([#15628](https://github.com/can1357/oh-my-pi/pull/15628) by [@nqrwhal](https://github.com/nqrwhal)).
17
+ - Fixed Claude on Google Vertex ignoring `cacheRetention: "long"`: prompt caches now get the 1h TTL instead of expiring after 5 minutes, and the `extended-cache-ttl` beta (which Vertex rejects) is never sent there ([#15505](https://github.com/can1357/oh-my-pi/pull/15505) by [@hpost](https://github.com/hpost))
18
+ - Fixed Gemini tool continuations failing on OpenAI-compatible gateways that return `reasoning_signature`, including restored sessions ([#15678](https://github.com/can1357/oh-my-pi/pull/15678) by [@Zzz141592653](https://github.com/Zzz141592653))
19
+ - Fixed Cursor sessions failing every rebuilt request with `PROVIDER_ERROR` after the server re-sent a tool call: the call is recorded once, and histories that already repeat it replay it once with its latest result ([#15749](https://github.com/can1357/oh-my-pi/issues/15749))
20
+ - Fixed Cursor turns cut off mid-stream failing every retry with `PROVIDER_ERROR` until you sent a new message; the retry now starts a fresh Cursor conversation ([#15830](https://github.com/can1357/oh-my-pi/issues/15830))
21
+ - Fixed malformed image-generation inputs being forwarded to providers or reported as credential/upstream errors ([#15765](https://github.com/can1357/oh-my-pi/pull/15765) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)).
22
+ - Fixed broker-backed auth-gateway processes retaining a cache entry for every distinct client session indefinitely under sustained traffic ([#15841](https://github.com/can1357/oh-my-pi/issues/15841)).
23
+ - Fixed turns ending on a pinned error when a provider reports "temporarily unavailable" or finishes with an `unknown`, `other` or `unspecified` finish reason: these now retry automatically ([#15424](https://github.com/can1357/oh-my-pi/pull/15424) by [@lockwo](https://github.com/lockwo))
24
+
25
+ ## [18.11.0] - 2026-10-14
26
+
27
+ ### Fixed
28
+
29
+ - Fixed parallel tool calls failing with "Tool call arguments are not valid JSON" on some OpenAI-compatible gateways, such as Gemini ([#15682](https://github.com/can1357/oh-my-pi/pull/15682) by [@Zzz141592653](https://github.com/Zzz141592653))
30
+ - Fixed Anthropic fallback-credit redemption losing its credit on errors that only mention `400` in their text, such as an overload message ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC))
31
+ - Fixed Anthropic fallback-credit redemption sending an extra `fallback-credit-2026-06-01` beta when the refused request used a newer one ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC))
32
+ - Fixed Anthropic fallback-credit redemptions failing when the refused turn included unsigned thinking ([#15702](https://github.com/can1357/oh-my-pi/pull/15702) by [@H4vC](https://github.com/H4vC))
33
+ - Fixed turns ending without automatic retry when a gateway forwards Go's `use of closed network connection` transport error; it now retries like other dropped connections ([#15385](https://github.com/can1357/oh-my-pi/issues/15385))
34
+ - Fixed Codex SSE streams failing with an abort instead of retrying when the first event stalls ([#15542](https://github.com/can1357/oh-my-pi/issues/15542))
35
+ - Fixed Codex (ChatGPT subscription) requests with reasoning disabled, such as judges, titles, commit messages, and thinking `off`, leaving effort to the server default; they now send the lowest supported effort, or `none` where the model accepts it ([#15514](https://github.com/can1357/oh-my-pi/issues/15514))
36
+ - Fixed a Codex account whose token OpenAI rejects (401/403) continuing to show its last quota as healthy; `omp usage` now drops the stale report and `omp auth-gateway check` reports the credential as failing ([#15439](https://github.com/can1357/oh-my-pi/issues/15439))
37
+ - Fixed usage reports being served indefinitely while their provider keeps failing; the last successful report is now shown for at most 24 hours after it was fetched ([#15439](https://github.com/can1357/oh-my-pi/issues/15439))
38
+ - Fixed Responses requests 400ing (DeepSeek: "The `reasoning_text` in the thinking mode must be passed back to the API") after a tool call whose arguments were invalid JSON: the call is now replayed with its parse diagnostic instead of being dropped and leaving a trailing `[Orphan tool result]` note ([#15568](https://github.com/can1357/oh-my-pi/issues/15568))
39
+ - Fixed DeepSeek on OpenAI-compatible gateways leaving doubled-pipe DSML markup (`</||DSML||parameter>`) in assistant text and reasoning ([#15485](https://github.com/can1357/oh-my-pi/issues/15485))
40
+ - Fixed sessions failing every request with `400 … thinking or redacted_thinking blocks in the latest assistant message cannot be modified` after a response fell back to another model mid-turn (e.g. through a gateway that opts into Anthropic server-side fallback); the turn now replays one model's reasoning, and a session already stuck on such a turn recovers by dropping that turn's reasoning and retrying ([#15654](https://github.com/can1357/oh-my-pi/pull/15654) by [@abilliontokens](https://github.com/abilliontokens))
41
+ - Fixed `omp auth-gateway`'s Anthropic `/v1/messages` stream misnumbering content blocks after a redacted reasoning block, which misrouted every later delta in index-addressed clients ([#15654](https://github.com/can1357/oh-my-pi/pull/15654) by [@abilliontokens](https://github.com/abilliontokens))
42
+
5
43
  ## [18.10.4] - 2026-10-13
6
44
 
7
45
  ### Fixed
@@ -2,7 +2,7 @@ import type { AuthCredentialStore } from "./store.js";
2
2
  import type { AuthCredential, OAuthCredential } from "./types.js";
3
3
  import type { CredentialPool } from "./pool.js";
4
4
  import type { Provider } from "../types.js";
5
- import type { UsageCredential, UsageProvider } from "../usage.js";
5
+ import type { UsageCredential, UsageProvider, UsageReport } from "../usage.js";
6
6
  /** Minimum interval between non-exhausted header snapshots; used by UsageService. */
7
7
  export declare const USAGE_HEADER_INGEST_INTERVAL_MS = 60000;
8
8
  /**
@@ -56,6 +56,8 @@ export declare class UsageCache {
56
56
  failureKey(reportKey: string): string;
57
57
  get<T>(key: string): UsageCacheEntry<T> | undefined;
58
58
  getStale<T>(key: string): UsageCacheEntry<T> | undefined;
59
+ /** Last successful report under `key` while inside its retention window; the failure-path fallback. */
60
+ lastGood(key: string): UsageReport | null;
59
61
  set<T>(key: string, entry: UsageCacheEntry<T>): void;
60
62
  deletePrefix(prefix: string): boolean;
61
63
  markForceRefresh(provider?: Provider): void;
@@ -68,10 +68,22 @@ export declare function isCodexSteerRejection(message: {
68
68
  api?: Api;
69
69
  errorMessage?: string;
70
70
  }): boolean;
71
+ /**
72
+ * Transport-failure wording that marks an error transient. Covers
73
+ * diagnostics forwarded verbatim by gateways/proxies in other runtimes, e.g.
74
+ * undici's "other side closed" and Go's `net.ErrClosed` ("use of closed
75
+ * network connection", #15385).
76
+ */
71
77
  export declare const TRANSIENT_TRANSPORT_PATTERN: RegExp;
72
78
  export declare const ANTHROPIC_ACCOUNT_POLICY_PATTERN: RegExp;
73
79
  /** Whether an error message represents an Anthropic account-scoped permission/policy denial. */
74
80
  export declare function isAnthropicAccountPolicyText(text: string, provider?: string, statusArg?: number): boolean;
81
+ /**
82
+ * Anthropic's rejection of a replayed latest assistant turn whose `thinking` or
83
+ * `redacted_thinking` blocks no longer verify. Resending the same turn fails the
84
+ * same way; only a request that changes that turn's reasoning can succeed.
85
+ */
86
+ export declare function isImmutableLatestThinkingText(text: string): boolean;
75
87
  /**
76
88
  * Local llama.cpp / Ollama deterministic tool-call argument JSON parse failure.
77
89
  * The model emitted invalid JSON in a tool call and the server returned HTTP 500
@@ -1,7 +1,8 @@
1
1
  import type { FetchImpl, Model, Usage } from "@oh-my-pi/pi-catalog/types";
2
2
  import type { ApiKey } from "../auth-retry.js";
3
3
  import * as AIError from "../error/index.js";
4
- import type { GeneratedImage } from "./types.js";
4
+ import type { ImageContent } from "../types.js";
5
+ import type { GeneratedImage, ImageInput } from "./types.js";
5
6
  export declare class ImageApiError extends AIError.ProviderHttpError {
6
7
  readonly name = "ImageApiError";
7
8
  }
@@ -38,6 +39,29 @@ export declare function decodeImageResponse(value: unknown, fetch: FetchImpl, si
38
39
  * multi-MB payloads are never fully decoded just to be labelled.
39
40
  */
40
41
  export declare function sniffBase64ImageMimeType(base64: string): string | undefined;
41
- export declare function toDataUrl(image: GeneratedImage): string;
42
+ /**
43
+ * Validate provider-bound image input without changing it. Accepts non-empty standard base64
44
+ * with canonical trailing bits and optional padding, and case-insensitive image MIME syntax
45
+ * (RFC 6838 restricted names). Does not sniff the MIME type, decode image pixels, or enforce
46
+ * provider-specific format support; data-URL transports additionally go through `toDataUrl`.
47
+ *
48
+ * @throws {AIError.ValidationError} If the base64 data or image MIME type is invalid.
49
+ */
50
+ export declare function validateImageInput(image: ImageInput): void;
51
+ /**
52
+ * Encode validated image input as a base64 data URL.
53
+ *
54
+ * @throws {AIError.ValidationError} If the input is invalid or its MIME type cannot survive a data URL.
55
+ */
56
+ export declare function toDataUrl(image: ImageInput): string;
57
+ /**
58
+ * Whether a MIME type survives a data URL intact: a raw `#` starts a URL fragment and
59
+ * data-URL consumers leave a `%23` escape undecoded, so no data-URL transport preserves it.
60
+ */
61
+ export declare function isDataUrlMimeType(mimeType: string): boolean;
62
+ /** Whether a value is an embedded image with valid base64 data, MIME type, and detail. */
63
+ export declare function isEmbeddedImageContent(image: unknown): image is ImageContent;
64
+ /** Convert one validated embedded image to a lowercase-MIME inline data URL. */
65
+ export declare function embeddedImageDataUrl(image: unknown): string;
42
66
  export declare function resolveOpenAIImageSize(aspectRatio?: string, imageSize?: string): string | undefined;
43
67
  export {};
@@ -1,7 +1,7 @@
1
1
  import type { Api, FetchImpl } from "@oh-my-pi/pi-catalog/types";
2
2
  import { type ApiKey } from "../auth-retry.js";
3
3
  import * as AIError from "../error/index.js";
4
- import type { Model, Usage } from "../types.js";
4
+ import type { ImageContent, Model, Usage } from "../types.js";
5
5
  import { type Judge, type JudgeOptions, type JudgmentRequest, type JudgmentResult, type Questions } from "./types.js";
6
6
  import type { JudgmentWire } from "./wire.js";
7
7
  export declare const TYPESAFE_PROVIDER = "typesafe";
@@ -16,6 +16,14 @@ declare const JUDGMENT_WIRES: {
16
16
  export type JudgmentApi = keyof typeof JUDGMENT_WIRES;
17
17
  /** Whether a catalog API answers judgments natively. */
18
18
  export declare function isJudgmentApi(api: Api): api is JudgmentApi;
19
+ /** Whether both the native protocol and catalog model support image input. */
20
+ export declare function nativeJudgmentSupportsImages(model: Model): boolean;
21
+ /**
22
+ * Reject `images` that `model`'s native judgment API cannot take — no image support, or a
23
+ * documented per-dialect cap exceeded — before any credential or upstream work.
24
+ * @throws {AIError.ValidationError} naming the reason.
25
+ */
26
+ export declare function checkNativeJudgmentImages(model: Model, images: readonly ImageContent[]): void;
19
27
  /** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
20
28
  export declare function typesafeBaseUrl(): string;
21
29
  /** `TYPESAFE_DEFAULT_MODEL` when set, else {@link TYPESAFE_DEFAULT_MODEL}. */
@@ -43,6 +51,11 @@ export interface NativeJudgeOptions extends NativeJudgeConnection {
43
51
  * requests are split. Unset: no cap.
44
52
  */
45
53
  maxQuestions?: number;
54
+ /**
55
+ * Whether the catalogued model accepts images. `false` disables image input;
56
+ * a `true` value cannot enable a wire dialect that does not support images.
57
+ */
58
+ supportsImages?: boolean;
46
59
  }
47
60
  /** Per-call options for {@link NativeJudge.judge}. */
48
61
  export interface NativeJudgeCallOptions extends JudgeOptions {
@@ -60,6 +73,8 @@ export declare class JudgmentApiError extends AIError.ProviderHttpError {
60
73
  export declare class NativeJudge implements Judge {
61
74
  #private;
62
75
  readonly label: string;
76
+ /** True when the selected native wire and model both accept image inputs. */
77
+ readonly supportsImages?: boolean;
63
78
  readonly api: JudgmentApi;
64
79
  readonly provider: string;
65
80
  /** Wire model id sent with every request. */
@@ -1,8 +1,10 @@
1
- import type { Usage } from "../types.js";
1
+ import type { ImageContent, Usage } from "../types.js";
2
2
  import { type Answer, type Judge, type JudgeOptions, type JudgmentRequest, type JudgmentResult, type JudgmentState, type Question, type Questions } from "./types.js";
3
3
  export interface TextPrompt {
4
4
  system: string;
5
5
  user: string;
6
+ /** Images are kept as distinct content blocks, never rendered into JSON text. */
7
+ images?: readonly ImageContent[];
6
8
  /** Format-correction attempt; chat backends may enforce tool suppression on the wire. */
7
9
  retry?: boolean;
8
10
  }
@@ -16,6 +18,8 @@ export interface TextBackend {
16
18
  readonly api: string;
17
19
  readonly provider: string;
18
20
  readonly model: string;
21
+ /** Whether this backend can send image content on the wire. */
22
+ readonly supportsImages?: boolean;
19
23
  /** State guard for agent-tuned chat models; on-device classifier workers may disable it. */
20
24
  readonly guardState?: boolean;
21
25
  /** Format-correction retries after a completion cannot be parsed. */
@@ -26,10 +30,11 @@ export interface TextBackend {
26
30
  export declare function renderJudgmentState(state: JudgmentState): string;
27
31
  /**
28
32
  * Render a request: the system prompt carries the preamble and question
29
- * definitions (constant across states, so prompt caches hit); the user message
30
- * carries XML-field state followed by the answer-format cue. Small models act
31
- * on a bare request (answer it, emit tool calls) instead of classifying it —
32
- * explicit tags and the trailing cue keep them on task.
33
+ * definitions (constant across states, so prompt caches hit); the user text
34
+ * carries XML-field state followed by the answer-format cue. Images remain
35
+ * separate content blocks. Small models act on a bare request (answer it,
36
+ * emit tool calls) instead of classifying it — explicit tags and the trailing
37
+ * cue keep them on task.
33
38
  */
34
39
  export declare function renderJudgmentPrompt(request: JudgmentRequest, options?: {
35
40
  guardState?: boolean;
@@ -53,6 +58,7 @@ export declare function splitAnswerLines(text: string, ids: readonly string[]):
53
58
  export declare function parseAnswer(id: string, question: Question, reply: string): Answer;
54
59
  export declare class TextJudge implements Judge {
55
60
  #private;
61
+ readonly supportsImages?: boolean;
56
62
  readonly label: string;
57
63
  constructor(backend: TextBackend);
58
64
  judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: JudgeOptions): Promise<JudgmentResult<Q>>;
@@ -11,7 +11,7 @@
11
11
  * bridge ({@link TextJudge}) renders the same questions into keyword prompts
12
12
  * for an ordinary chat model.
13
13
  */
14
- import type { Usage } from "../types.js";
14
+ import type { ImageContent, Usage } from "../types.js";
15
15
  /** JSON-compatible value; readonly containers are accepted so `as const` state passes through. */
16
16
  export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
17
17
  readonly [key: string]: JsonValue;
@@ -73,6 +73,8 @@ export type AnswerFor<Q extends Question> = Q extends ChoiceQuestion<infer L> ?
73
73
  export interface JudgmentRequest<Q extends Questions = Questions> {
74
74
  state: JudgmentState;
75
75
  questions: Q;
76
+ /** Images evaluated alongside the state; never part of the JSON state itself. */
77
+ images?: readonly ImageContent[];
76
78
  }
77
79
  export interface JudgmentResult<Q extends Questions = Questions> {
78
80
  /** Transport that produced the answers (a judgment api such as `typesafe`, or the chat model's api). */
@@ -89,6 +91,8 @@ export interface JudgeOptions {
89
91
  }
90
92
  /** Answers typed questions about a state. Implementations are stateless and safe to share. */
91
93
  export interface Judge {
94
+ /** Whether this backend can consume separate image content blocks. */
95
+ readonly supportsImages?: boolean;
92
96
  /** Backend description for logs, e.g. `typesafe/jev-latest`. */
93
97
  readonly label: string;
94
98
  judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: JudgeOptions): Promise<JudgmentResult<Q>>;
@@ -20,6 +20,22 @@ export interface DecodedJudgment {
20
20
  refused: readonly string[];
21
21
  usage: Usage;
22
22
  }
23
+ /**
24
+ * Image input one judgment dialect accepts. Every cap is optional; {@link NativeJudge}
25
+ * rejects a request exceeding one locally instead of spending an upstream call on it.
26
+ */
27
+ export interface JudgmentImageInput {
28
+ /** Accepted lowercase MIME types; unset accepts any image type. */
29
+ mimeTypes?: readonly string[];
30
+ /** Images per request. */
31
+ maxCount?: number;
32
+ /** Decoded bytes per image. */
33
+ maxBytes?: number;
34
+ /** Decoded bytes across all images. */
35
+ maxTotalBytes?: number;
36
+ /** Pixels per image, when the image header reports dimensions. */
37
+ maxPixels?: number;
38
+ }
23
39
  /** How one judgment API addresses, encodes, and decodes a request. */
24
40
  export interface JudgmentWire {
25
41
  /**
@@ -29,6 +45,8 @@ export interface JudgmentWire {
29
45
  target(baseUrl: string, model: string, key: string): JudgmentTarget;
30
46
  /** Request body for `model`. */
31
47
  encode(request: JudgmentRequest, model: string): unknown;
48
+ /** Image input this dialect accepts; absent means text-only. */
49
+ images?: JudgmentImageInput;
32
50
  /** The response's answers and usage; `undefined` when the payload is not this API's response shape. */
33
51
  decode(payload: unknown): DecodedJudgment | undefined;
34
52
  }
@@ -18,8 +18,8 @@
18
18
  * siblings are endpoint-scoped and stay: `strictToolsDisabled`
19
19
  * (grammar-too-large 400 for the model's tool schema),
20
20
  * `replayUnsignedThinkingDisabled` / `thinkingReplayDisabled` (the endpoint
21
- * is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a prefix-binding
22
- * 400 made the client strip).
21
+ * is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a rejected
22
+ * replay made the client strip).
23
23
  * - **OpenAI Responses** — the `previous_response_id` chain baselines are
24
24
  * account-scoped: a stored response belongs to the account that created it.
25
25
  * Strict-tools / reasoning-effort fallbacks, replay warmup and the chaining
@@ -118,7 +118,8 @@ export interface AnthropicOptions extends StreamOptions {
118
118
  * header is auto-attached; the response parser then honors mid-stream
119
119
  * `fallback` content blocks and `usage.iterations` for served-model
120
120
  * promotion and per-attempt pricing. Opt-in ONLY — leaving this
121
- * undefined preserves the pre-fallback behavior on every code path.
121
+ * undefined sends neither and keeps both off; a `fallback` block in the
122
+ * response (a gateway opted in) is still persisted as a replay boundary.
122
123
  */
123
124
  fallbacks?: FallbackParam[];
124
125
  }
@@ -132,6 +133,8 @@ export type AnthropicClientOptionsArgs = {
132
133
  dynamicHeaders?: Record<string, string>;
133
134
  isOAuth?: boolean;
134
135
  hasTools?: boolean;
136
+ /** Whether a tool in ANTHROPIC_BUFFERED_INPUT_TOOLS is present; suppresses the request-wide fine-grained streaming beta. */
137
+ hasBufferedInputTools?: boolean;
135
138
  thinkingEnabled?: boolean;
136
139
  thinkingDisplay?: AnthropicThinkingDisplay;
137
140
  disableStrictTools?: boolean;
@@ -283,10 +286,11 @@ export type AnthropicMessageParam = MessageParam;
283
286
  * Serialize omp {@link Message}s to Anthropic wire messages.
284
287
  *
285
288
  * `opts.serverSideFallbackEnabled` — when the CURRENT request itself
286
- * opts into the server-side-fallback beta chain. Only then may a persisted
287
- * `fallback` content block from a prior turn be replayed on the wire;
288
- * otherwise the block is dropped to avoid a 400 on non-fallback requests
289
- * that don't send the beta.
289
+ * opts into the server-side-fallback beta chain. Only then, and only on the
290
+ * official endpoint, may a persisted `fallback` content block from a prior
291
+ * turn be replayed on the wire; otherwise the block is dropped to avoid a 400
292
+ * on requests that don't send the beta, and its turn keeps one model's
293
+ * reasoning (see {@link collapseFallbackSegments}).
290
294
  *
291
295
  * `opts.replayCompaction` — replay a user-role compaction summary that
292
296
  * carries an {@link AnthropicCompactionPayload} from this provider as a
@@ -218,6 +218,13 @@ export declare function mergeCursorMcpToolCallArgs(streamed: Record<string, unkn
218
218
  * call handed to an external executor is only a handoff while the marker is
219
219
  * absent, and the consumer that runs it needs a runnable block.
220
220
  *
221
+ * A frame whose `toolCallId` already has a block in `output` is the server
222
+ * re-dispatching that call (a new `execId`, e.g. a `read` re-sent once a
223
+ * parallel shell created the file). It adds no block: a second block with the
224
+ * same id makes every rebuilt history replay the call twice, which Cursor
225
+ * rejects (issue #15749). The re-run's result supersedes the first one in the
226
+ * host's result buffer.
227
+ *
221
228
  * Exported for tests to exercise ordering with adjacent text/thinking blocks.
222
229
  */
223
230
  export declare function synthesizeCursorExecToolCall(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, toolCallId: string, toolName: string, args: Record<string, unknown>, { executed }?: {
@@ -6,6 +6,12 @@ export type { CodexAttestationProvider } from "./openai-codex-attestation.js";
6
6
  import type { ResponseInput } from "./openai-responses-wire.js";
7
7
  export interface OpenAICodexResponsesOptions extends StreamOptions {
8
8
  reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
9
+ /**
10
+ * Run with the least reasoning the model allows; takes precedence over
11
+ * `reasoning`. Sends the `none` tier where the catalog's disable mode is
12
+ * `none-effort`, else the model's lowest supported effort.
13
+ */
14
+ disableReasoning?: boolean;
9
15
  reasoningSummary?: "auto" | "concise" | "detailed" | null;
10
16
  /** Explicit `reasoning.context` replay scope. Omitted by default so Codex applies its native request policy. */
11
17
  reasoningContext?: CodexReasoningContext;
@@ -14,10 +14,11 @@ export interface SystemOneResponseBody {
14
14
  };
15
15
  }
16
16
  /**
17
- * Validate the routing-relevant parts of a System One request body.
17
+ * Validate the routing-relevant parts of a System One request body, including
18
+ * optional embedded base64 images.
18
19
  *
19
- * @throws {AIError.ValidationError} naming the offending field when `model`,
20
- * `state`, or `questions` is missing or malformed.
20
+ * @throws {AIError.ValidationError} when required routing fields are missing
21
+ * or malformed, or a supplied image is malformed.
21
22
  */
22
23
  export declare function parseRequest(body: unknown): SystemOneParsedRequest;
23
24
  /** Re-encode a judgment result in TypeSafe's response shape. */
@@ -36,6 +36,14 @@ export declare function isMalformedToolCallName(name: unknown): boolean;
36
36
  * off by `length` / `error` / `aborted` that carries no text and no tool call.
37
37
  */
38
38
  export declare function shouldDropTruncatedThinkingOnlyAssistant(msg: AssistantMessage): boolean;
39
+ /**
40
+ * Whether `msg` reaches a replay as an assistant turn, so it can be the latest
41
+ * surviving one: {@link sanitizeMalformedToolCalls} drops its malformed tool
42
+ * calls, then {@link shouldDropTruncatedThinkingOnlyAssistant} a truncated
43
+ * turn left with no text and no tool call. On an already sanitized list this
44
+ * is `!shouldDropTruncatedThinkingOnlyAssistant(msg)`.
45
+ */
46
+ export declare function isSurvivingAssistant(msg: AssistantMessage): boolean;
39
47
  /**
40
48
  * Normalize tool call ID for cross-provider compatibility.
41
49
  * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
@@ -9,7 +9,7 @@ import type { ApiKey } from "./auth-retry.js";
9
9
  import type { OAuthRequestIdentity } from "./auth/types.js";
10
10
  import type { BedrockOptions } from "./providers/amazon-bedrock.js";
11
11
  import type { AnthropicOptions } from "./providers/anthropic.js";
12
- import type { FallbackParam, StopDetails } from "./providers/anthropic-wire.js";
12
+ import type { FallbackParam, MessageCreateParamsStreaming, StopDetails } from "./providers/anthropic-wire.js";
13
13
  import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js";
14
14
  import type { CursorOptions } from "./providers/cursor.js";
15
15
  import type { AppleFoundationModelsOptions } from "./providers/apple-foundation-models.js";
@@ -650,11 +650,12 @@ export interface RedactedThinkingContent {
650
650
  }
651
651
  /**
652
652
  * Anthropic server-side-fallback boundary marker persisted on assistant
653
- * turns whose provider request opted into
654
- * `AnthropicOptions.fallbacks`. Consumers other than the Anthropic
655
- * provider MUST ignore it — `transformMessages` strips the block on any
656
- * cross-provider hop and on non-official Anthropic replays, so downstream
657
- * converters never see it.
653
+ * turns whose response switched models mid-stream, whether the request
654
+ * opted into `AnthropicOptions.fallbacks` or a gateway opted in on its
655
+ * behalf. Consumers other than the Anthropic provider MUST ignore it —
656
+ * `transformMessages` strips the block on any non-Anthropic hop, and
657
+ * `convertAnthropicMessages` only puts it on the wire for an opted-in
658
+ * request to the official endpoint.
658
659
  */
659
660
  export interface AnthropicFallbackContent {
660
661
  type: "fallback";
@@ -981,6 +982,12 @@ export type AssistantRetryRecovery = {
981
982
  export interface ContextSnapshot {
982
983
  promptTokens: number;
983
984
  nonMessageTokens: number;
985
+ /**
986
+ * Estimated tool-schema tokens included in `nonMessageTokens` (0 when the
987
+ * model defers schemas server-side). Lets a later model whose schema policy
988
+ * differs rebase the provider anchor instead of inheriting its schemas.
989
+ */
990
+ toolSchemaTokens?: number;
984
991
  /** Estimated prompt tokens removed by local history rewrites after this provider snapshot was recorded. */
985
992
  historyRewriteTokensRemoved?: number;
986
993
  /**
@@ -1422,7 +1429,8 @@ export type AssistantMessageEvent = {
1422
1429
  export interface AnthropicFallbackCreditHandle {
1423
1430
  token: string;
1424
1431
  prefillClaim?: boolean | null;
1425
- params: unknown;
1432
+ /** The refused request's body, replayed with the credit token on the fallback model. */
1433
+ params: MessageCreateParamsStreaming;
1426
1434
  betas?: readonly string[];
1427
1435
  betaHeader?: string;
1428
1436
  expiresAt: number;
@@ -23,7 +23,9 @@ export declare function isDsmlLeakRecoveryTarget(model: Model): boolean;
23
23
  */
24
24
  export declare function stripDsmlToolMarkup(text: string): string | undefined;
25
25
  /**
26
- * Remove leaked DSML tool-call markup from `message`'s text blocks in place,
27
- * dropping blocks left empty. Returns true when anything was removed.
26
+ * Remove leaked DSML tool-call markup from an assistant message before commit,
27
+ * including reasoning blocks and turns with valid structured calls. Rewritten
28
+ * thinking with an opaque signature is discarded; OpenAI-compatible reasoning
29
+ * field names are replay markers, not signatures, and survive cleanup.
28
30
  */
29
31
  export declare function removeDsmlToolMarkupLeak(message: AssistantMessage): boolean;
@@ -5,7 +5,8 @@ export declare function parseToolCallArguments(json: string | undefined): ToolCa
5
5
  * Native Responses `function_call.arguments` to persist for replay.
6
6
  *
7
7
  * {@link parseToolCallArguments} repairs some invalid JSON (e.g. `"name": ,`) and the tool runs with the
8
- * repaired value; replay drops a call whose stored arguments do not parse, orphaning its output (#14155).
8
+ * repaired value; replay must carry what was executed (#14155), not the {@link invalidToolCallArguments}
9
+ * diagnostic it substitutes for stored arguments that do not parse (#15568).
9
10
  * Returns `raw` when it already parses or the arguments were unrepairable, else the executed arguments.
10
11
  */
11
12
  export declare function replayableToolCallArguments(raw: string, executed: ToolCall["arguments"]): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.10.4",
3
+ "version": "18.11.1",
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.10.4",
159
- "@oh-my-pi/pi-catalog": "18.10.4",
160
- "@oh-my-pi/pi-natives": "18.10.4",
161
- "@oh-my-pi/pi-utils": "18.10.4",
162
- "@oh-my-pi/pi-wire": "18.10.4"
158
+ "@oh-my-pi/omptype": "18.11.1",
159
+ "@oh-my-pi/pi-catalog": "18.11.1",
160
+ "@oh-my-pi/pi-natives": "18.11.1",
161
+ "@oh-my-pi/pi-utils": "18.11.1",
162
+ "@oh-my-pi/pi-wire": "18.11.1"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -3,6 +3,7 @@ import type { AuthCredential, OAuthCredential } from "./types";
3
3
  import type { CredentialPool } from "./pool";
4
4
  import type { Provider } from "../types";
5
5
  import type { UsageCredential, UsageProvider, UsageReport } from "../usage";
6
+ import { isRecord } from "../utils";
6
7
 
7
8
  const USAGE_CACHE_PREFIX = "usage_cache:";
8
9
  const USAGE_FORCE_REFRESH_CACHE_PREFIX = "force-refresh:";
@@ -53,6 +54,17 @@ function withoutRawPayload(value: unknown): unknown {
53
54
  return rest;
54
55
  }
55
56
 
57
+ /**
58
+ * End of a cached value's durable retention. A report is retained for 24h past
59
+ * its last successful fetch, so re-storing it as a failure fallback never
60
+ * extends that window; other values are retained for 24h from now.
61
+ */
62
+ function retentionDeadline(value: unknown): number {
63
+ const fetchedAt = isRecord(value) ? value.fetchedAt : undefined;
64
+ const start = typeof fetchedAt === "number" && Number.isFinite(fetchedAt) && fetchedAt > 0 ? fetchedAt : Date.now();
65
+ return start + USAGE_LAST_GOOD_RETENTION_MS;
66
+ }
67
+
56
68
  /** Convert a stored credential for usage providers; used by usage and health services. */
57
69
  export function buildUsageCredential(credential: AuthCredential): UsageCredential {
58
70
  if (credential.type === "api_key") {
@@ -203,15 +215,19 @@ export class UsageCache {
203
215
  return parseUsageCacheEntry<T>(raw);
204
216
  }
205
217
 
218
+ /** Last successful report under `key` while inside its retention window; the failure-path fallback. */
219
+ lastGood(key: string): UsageReport | null {
220
+ const report = this.getStale<UsageReport | null>(key)?.value;
221
+ return report && Date.now() < retentionDeadline(report) ? report : null;
222
+ }
223
+
206
224
  set<T>(key: string, entry: UsageCacheEntry<T>): void {
207
225
  const value = withoutRawPayload(entry.value);
208
- const payload = JSON.stringify({
209
- value,
210
- expiresAt: entry.expiresAt,
211
- });
212
- const durableExpiresAt =
213
- value === null ? entry.expiresAt : Math.max(entry.expiresAt, Date.now() + USAGE_LAST_GOOD_RETENTION_MS);
214
- this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor(durableExpiresAt / 1000));
226
+ const deadline = value === null ? undefined : retentionDeadline(value);
227
+ // Failure cooldowns must not make a last-good report fresh beyond retention.
228
+ const expiresAt = deadline === undefined ? entry.expiresAt : Math.min(entry.expiresAt, deadline);
229
+ const payload = JSON.stringify({ value, expiresAt });
230
+ this.#store.setCache(`${USAGE_CACHE_PREFIX}${key}`, payload, Math.floor((deadline ?? expiresAt) / 1000));
215
231
  }
216
232
 
217
233
  deletePrefix(prefix: string): boolean {
package/src/auth/usage.ts CHANGED
@@ -334,7 +334,7 @@ export class UsageService implements UsageApi {
334
334
 
335
335
  const failure = this.#deps.cache.get<null>(failureKey);
336
336
  if (failure && failure.expiresAt > now) {
337
- return this.#deps.cache.getStale<UsageReport | null>(cacheKey)?.value ?? null;
337
+ return this.#deps.cache.lastGood(cacheKey);
338
338
  }
339
339
 
340
340
  // Invalidation must not fork an already-running credential probe. Share it;
@@ -378,9 +378,7 @@ export class UsageService implements UsageApi {
378
378
  // so an expired login does not display stale quota indefinitely.
379
379
  const providerImpl = this.providerFor(request.provider);
380
380
  const retainLastGood = !invalidated && providerImpl?.retainLastGoodOnFailure !== false;
381
- const lastGood = retainLastGood
382
- ? (this.#deps.cache.getStale<UsageReport | null>(cacheKey)?.value ?? null)
383
- : null;
381
+ const lastGood = retainLastGood ? this.#deps.cache.lastGood(cacheKey) : null;
384
382
  const failureBackoffMs = providerImpl?.failureBackoffMs ?? USAGE_FAILURE_BACKOFF_MS;
385
383
  const backoffJitter = failureBackoffMs * (Math.random() * 0.5 - 0.25);
386
384
  const coolDown = Date.now() + failureBackoffMs + backoffJitter;