@oh-my-pi/pi-ai 18.2.0 → 18.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +17 -0
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/session-state.d.ts +118 -0
- package/dist/types/auth-storage.d.ts +17 -0
- package/dist/types/error/body-error.d.ts +15 -0
- package/dist/types/error/flags.d.ts +16 -0
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/oneshot-retry.d.ts +6 -0
- package/dist/types/provider-session-state.d.ts +46 -0
- package/dist/types/providers/amazon-bedrock.d.ts +3 -0
- package/dist/types/providers/aws-sigv4.d.ts +12 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +27 -0
- package/dist/types/providers/openai-responses.d.ts +15 -0
- package/dist/types/providers/openai-shared.d.ts +20 -3
- package/dist/types/registry/oauth/perplexity.d.ts +1 -7
- package/dist/types/registry/oauth/types.d.ts +8 -0
- package/dist/types/stream.d.ts +2 -0
- package/dist/types/types.d.ts +3 -1
- package/dist/types/usage/openai-codex.d.ts +3 -1
- package/dist/types/usage.d.ts +11 -1
- package/dist/types/utils/block-symbols.d.ts +36 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/dist/types/utils/retry-after.d.ts +2 -0
- package/dist/types/utils/schema/wire.d.ts +4 -5
- package/dist/types/utils.d.ts +9 -0
- package/package.json +6 -6
- package/src/auth/sqlite-credential-store.ts +8 -33
- package/src/auth-broker/remote-store.ts +73 -8
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/server.ts +186 -74
- package/src/auth-gateway/session-state.ts +312 -0
- package/src/auth-storage.ts +146 -15
- package/src/error/body-error.ts +310 -0
- package/src/error/flags.ts +63 -13
- package/src/error/index.ts +1 -0
- package/src/error/retryable.ts +2 -0
- package/src/index.ts +1 -0
- package/src/oneshot-retry.ts +13 -3
- package/src/provider-session-state.ts +56 -0
- package/src/providers/amazon-bedrock.ts +20 -3
- package/src/providers/anthropic-messages-server.ts +104 -23
- package/src/providers/anthropic-signature.ts +5 -2
- package/src/providers/anthropic.ts +101 -15
- package/src/providers/aws-sigv4.ts +16 -5
- package/src/providers/cursor.ts +60 -10
- package/src/providers/devin.ts +82 -28
- package/src/providers/openai-chat-server.ts +4 -0
- package/src/providers/openai-codex/request-transformer.ts +36 -0
- package/src/providers/openai-codex-responses.ts +35 -12
- package/src/providers/openai-completions.ts +49 -12
- package/src/providers/openai-reasoning-fallback.ts +6 -6
- package/src/providers/openai-responses-server.ts +2 -1
- package/src/providers/openai-responses.ts +52 -4
- package/src/providers/openai-shared.ts +199 -51
- package/src/registry/oauth/perplexity.ts +94 -28
- package/src/registry/oauth/types.ts +9 -0
- package/src/stream.ts +23 -2
- package/src/types.ts +3 -0
- package/src/usage/claude.ts +33 -0
- package/src/usage/google-antigravity.ts +8 -2
- package/src/usage/openai-codex.ts +94 -11
- package/src/usage.ts +8 -1
- package/src/utils/block-symbols.ts +57 -0
- package/src/utils/http-inspector.ts +20 -0
- package/src/utils/openai-http.ts +39 -3
- package/src/utils/retry-after.ts +12 -0
- package/src/utils/schema/normalize.ts +3 -3
- package/src/utils/schema/stamps.ts +33 -45
- package/src/utils/schema/wire.ts +9 -7
- package/src/utils.ts +67 -22
package/src/utils/openai-http.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* captured response body for the strict-tools fallback and the responses
|
|
15
15
|
* chain-state detectors, which regex over `error.message`.
|
|
16
16
|
*/
|
|
17
|
-
import { fetchWithRetry,
|
|
17
|
+
import { fetchWithRetry, readSseJsonOrText, type SseEventObserver } from "@oh-my-pi/pi-utils";
|
|
18
18
|
import * as AIError from "../error";
|
|
19
19
|
import { OpenAIHttpError } from "../error";
|
|
20
20
|
|
|
@@ -68,6 +68,8 @@ export interface OpenAIStreamRequestInit {
|
|
|
68
68
|
body: unknown;
|
|
69
69
|
signal: AbortSignal;
|
|
70
70
|
fetch?: FetchImpl;
|
|
71
|
+
/** Optional caller-specific gate composed with shared transport retry exclusions. */
|
|
72
|
+
shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
|
|
71
73
|
/** Raw wire-frame observer (`onSseEvent` debug pipeline). */
|
|
72
74
|
onSseEvent?: SseEventObserver;
|
|
73
75
|
}
|
|
@@ -98,7 +100,9 @@ export async function postOpenAIStream<TEvent>(init: OpenAIStreamRequestInit): P
|
|
|
98
100
|
// A proxy concurrency-admission 429 (`rate_limit_type: max_parallel_requests`)
|
|
99
101
|
// surfaces immediately instead of being slept-and-retried here; session
|
|
100
102
|
// recovery owns its backoff/fallback (issue #8854).
|
|
101
|
-
shouldRetryResponse: (response, bodyText) =>
|
|
103
|
+
shouldRetryResponse: async (response, bodyText) =>
|
|
104
|
+
!isConcurrencyAdmissionRejection(response, bodyText) &&
|
|
105
|
+
(init.shouldRetryResponse === undefined || (await init.shouldRetryResponse(response, bodyText))),
|
|
102
106
|
// Bun's native fetch enforces a hard ~300s pre-response timeout (issue #2422).
|
|
103
107
|
// Cold large-context streams legitimately exceed it; the caller's
|
|
104
108
|
// `firstEventTimeoutMs`/`AbortSignal` already govern stuck requests.
|
|
@@ -113,12 +117,44 @@ export async function postOpenAIStream<TEvent>(init: OpenAIStreamRequestInit): P
|
|
|
113
117
|
});
|
|
114
118
|
}
|
|
115
119
|
return {
|
|
116
|
-
events:
|
|
120
|
+
events: decodeStream<TEvent>(response.body, init.signal, init.onSseEvent),
|
|
117
121
|
response,
|
|
118
122
|
requestId: response.headers.get("x-request-id"),
|
|
119
123
|
};
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Consume `readSseJsonOrText` and turn a non-JSON `data:` frame into a
|
|
128
|
+
* classified in-band error. A reverse proxy that already committed to an HTTP
|
|
129
|
+
* 200 stream (so the status line can no longer carry the failure) answers with
|
|
130
|
+
* plain text — `data: 429 Too Many Requests`, an nginx throttle page — and
|
|
131
|
+
* that has to advance the fallback chain like a real 429 (body-error.ts).
|
|
132
|
+
* Frames that are not recognisable throttles rethrow the original parse error,
|
|
133
|
+
* preserving the pre-existing loud failure for genuinely malformed payloads.
|
|
134
|
+
* `readSseJsonOrText` also yields a frame that was a JSON-encoded *string* on the
|
|
135
|
+
* wire (a double-encoded proxy error page); it is not a usable event either, so
|
|
136
|
+
* it is classified the same way and then dropped — every consumer here already
|
|
137
|
+
* ignored a string chunk, the completions loop by its `typeof !== "object"` test.
|
|
138
|
+
*/
|
|
139
|
+
async function* decodeStream<TEvent>(
|
|
140
|
+
body: ReadableStream<Uint8Array>,
|
|
141
|
+
signal: AbortSignal | undefined,
|
|
142
|
+
onSseEvent: SseEventObserver | undefined,
|
|
143
|
+
): AsyncGenerator<TEvent> {
|
|
144
|
+
for await (const frame of readSseJsonOrText<TEvent>(body, signal, onSseEvent)) {
|
|
145
|
+
if (typeof frame === "string") {
|
|
146
|
+
const inBand = AIError.createInBandProviderErrorFromText(frame);
|
|
147
|
+
if (inBand) throw inBand;
|
|
148
|
+
// Not a recognisable throttle: reproduce the exact strict-parse failure the
|
|
149
|
+
// previous reader raised, so genuinely malformed payloads stay equally
|
|
150
|
+
// loud. A frame that parses again was a JSON string, not a malformed one.
|
|
151
|
+
JSON.parse(frame);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
yield frame;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
122
158
|
/** Decode a non-2xx response into an {@link OpenAIHttpError} without consuming it twice. */
|
|
123
159
|
export async function captureOpenAIHttpError(response: Response): Promise<AIError.OpenAIHttpError> {
|
|
124
160
|
let bodyText: string | undefined;
|
package/src/utils/retry-after.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
import { retryResetTimezoneOffsetFor } from "@oh-my-pi/pi-catalog/compat/behavior";
|
|
2
|
+
import { extractRetryHint } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
1
4
|
export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
|
|
2
5
|
|
|
6
|
+
/** Extracts retry timing using the provider's catalog-declared timestamp timezone. */
|
|
7
|
+
export function extractProviderRetryHint(
|
|
8
|
+
provider: string | undefined,
|
|
9
|
+
message: string | undefined,
|
|
10
|
+
): number | undefined {
|
|
11
|
+
const naiveResetTimezoneOffset = provider === undefined ? undefined : retryResetTimezoneOffsetFor(provider);
|
|
12
|
+
return extractRetryHint(undefined, message, { naiveResetTimezoneOffset });
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
const RETRY_AFTER_HINT = "retry-after-ms=";
|
|
4
16
|
|
|
5
17
|
export function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string {
|
|
@@ -1877,9 +1877,9 @@ function inferStrictPrimitiveTypeFromEnumOrConst(node: Record<string, unknown>):
|
|
|
1877
1877
|
}
|
|
1878
1878
|
|
|
1879
1879
|
/**
|
|
1880
|
-
* Per-schema-object memoization
|
|
1881
|
-
* is
|
|
1882
|
-
*
|
|
1880
|
+
* Per-schema-object memoization key. The result of `tryEnforceStrictSchema`
|
|
1881
|
+
* is memoized against the input via `stamp(target, kStrictSchema, …)` so
|
|
1882
|
+
* repeated calls (different providers, retries, batching) reuse the same
|
|
1883
1883
|
* computed pair without re-walking the tree.
|
|
1884
1884
|
*/
|
|
1885
1885
|
const kStrictSchema = Symbol("pi.schema.strict");
|
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lazy memoization keyed by host object identity, held in a module-level
|
|
3
|
+
* weak side table.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The bookkeeping deliberately lives outside the host rather than in a
|
|
6
|
+
* non-enumerable symbol property: schema objects are caller-owned and may be
|
|
7
|
+
* sealed, frozen, or deep-frozen after their first traversal. A recursive
|
|
8
|
+
* freeze that enumerates with `Reflect.ownKeys` reaches symbol slots, so an
|
|
9
|
+
* on-host slot would be frozen along with the schema and every later write
|
|
10
|
+
* would throw. A side table is also invisible to `{...spread}`,
|
|
11
|
+
* `Object.keys`, `JSON.stringify`, and `toEqual`-style deep equality.
|
|
9
12
|
*
|
|
10
|
-
* Caveats:
|
|
13
|
+
* Caveats: an entry lives as long as the host object, even after callers
|
|
11
14
|
* release their references to the cached value — only use this for caches
|
|
12
|
-
* whose lifetime should match the host.
|
|
13
|
-
* `define` silently skips them, so memoization/visit-tracking degrades to
|
|
14
|
-
* best-effort (recompute on every call, no cycle protection) instead of
|
|
15
|
-
* throwing.
|
|
15
|
+
* whose lifetime should match the host.
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
if (Object.isFrozen(target)) return;
|
|
19
|
-
Object.defineProperty(target, key, { value, writable: true, configurable: true });
|
|
20
|
-
}
|
|
17
|
+
const memos = new WeakMap<object, Map<symbol, unknown>>();
|
|
21
18
|
|
|
22
19
|
export function stamp<T extends object, V>(target: T, key: symbol, compute: (target: T) => V): V {
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
let slots = memos.get(target);
|
|
21
|
+
if (!slots) {
|
|
22
|
+
slots = new Map();
|
|
23
|
+
memos.set(target, slots);
|
|
24
|
+
}
|
|
25
|
+
const existing = slots.get(key) as V | undefined;
|
|
25
26
|
if (existing !== undefined) return existing;
|
|
26
27
|
const value = compute(target);
|
|
27
|
-
|
|
28
|
+
slots.set(key, value);
|
|
28
29
|
return value;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* Epoch-keyed cycle guard. Cheaper than `WeakSet` for recursive
|
|
33
|
-
* because the marker is a single
|
|
34
|
-
* once and overwritten in place on every subsequent traversal —
|
|
35
|
-
*
|
|
33
|
+
* Epoch-keyed cycle guard. Cheaper than a per-call `WeakSet` for recursive
|
|
34
|
+
* traversal because the marker is a single side-table entry per host object,
|
|
35
|
+
* written once and overwritten in place on every subsequent traversal — no
|
|
36
|
+
* per-walk allocation.
|
|
36
37
|
*
|
|
37
38
|
* Usage:
|
|
38
39
|
* function walk(node, epoch = epochNext()) {
|
|
@@ -40,7 +41,7 @@ export function stamp<T extends object, V>(target: T, key: symbol, compute: (tar
|
|
|
40
41
|
* for (const child of node.children) walk(child, epoch);
|
|
41
42
|
* }
|
|
42
43
|
*/
|
|
43
|
-
const
|
|
44
|
+
const epochs = new WeakMap<object, number>();
|
|
44
45
|
let __epoch = 0;
|
|
45
46
|
|
|
46
47
|
export function epochNext(): number {
|
|
@@ -53,11 +54,9 @@ export function epochNext(): number {
|
|
|
53
54
|
* subsequent call within the same epoch.
|
|
54
55
|
*/
|
|
55
56
|
export function once<T extends object>(target: T, epoch: number): boolean {
|
|
56
|
-
const
|
|
57
|
-
const cur = slot[kEpoch];
|
|
57
|
+
const cur = epochs.get(target);
|
|
58
58
|
if (cur !== undefined && cur >= epoch) return false;
|
|
59
|
-
|
|
60
|
-
else slot[kEpoch] = epoch;
|
|
59
|
+
epochs.set(target, epoch);
|
|
61
60
|
return true;
|
|
62
61
|
}
|
|
63
62
|
|
|
@@ -65,13 +64,9 @@ export function once<T extends object>(target: T, epoch: number): boolean {
|
|
|
65
64
|
* Counter-based path tracker. Use when a traversal needs to distinguish
|
|
66
65
|
* "currently on the recursion path" from "previously visited" — i.e. cycle
|
|
67
66
|
* detection that throws while still allowing DAG sharing. Increment on
|
|
68
|
-
* entry, decrement on exit; the
|
|
67
|
+
* entry, decrement on exit; the counter returns to 0 after a balanced walk so
|
|
69
68
|
* subsequent top-level calls see a fresh state without any reset.
|
|
70
69
|
*
|
|
71
|
-
* Unlike a `WeakSet` with `seen.delete(...)`, the property is never deleted
|
|
72
|
-
* — only incremented and decremented — so the host object's hidden class
|
|
73
|
-
* is never invalidated.
|
|
74
|
-
*
|
|
75
70
|
* Usage:
|
|
76
71
|
* function walk(node) {
|
|
77
72
|
* if (!enter(node)) throw new Error("cycle");
|
|
@@ -79,7 +74,7 @@ export function once<T extends object>(target: T, epoch: number): boolean {
|
|
|
79
74
|
* finally { exit(node); }
|
|
80
75
|
* }
|
|
81
76
|
*/
|
|
82
|
-
const
|
|
77
|
+
const depths = new WeakMap<object, number>();
|
|
83
78
|
|
|
84
79
|
/**
|
|
85
80
|
* Returns `true` on first entry, `false` if `target` is already on the
|
|
@@ -89,21 +84,14 @@ const kDepth = Symbol("pi.schema.depth");
|
|
|
89
84
|
* make every later top-level walk of the same object misreport a cycle.
|
|
90
85
|
*/
|
|
91
86
|
export function enter<T extends object>(target: T): boolean {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
define(target, kDepth, 1);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
if (cur !== 0) return false;
|
|
99
|
-
slot[kDepth] = 1;
|
|
87
|
+
const cur = depths.get(target);
|
|
88
|
+
if (cur !== undefined && cur !== 0) return false;
|
|
89
|
+
depths.set(target, 1);
|
|
100
90
|
return true;
|
|
101
91
|
}
|
|
102
92
|
|
|
103
93
|
export function exit<T extends object>(target: T): void {
|
|
104
|
-
const
|
|
105
|
-
const cur = slot[kDepth];
|
|
106
|
-
// Frozen targets never received the kDepth stamp in `enter` — nothing to unwind.
|
|
94
|
+
const cur = depths.get(target);
|
|
107
95
|
if (cur === undefined) return;
|
|
108
|
-
|
|
96
|
+
depths.set(target, cur - 1);
|
|
109
97
|
}
|
package/src/utils/schema/wire.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Type } from "@oh-my-pi/omptype";
|
|
10
|
+
import { structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
10
11
|
import type { Tool, TSchema } from "../../types";
|
|
11
12
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
12
13
|
import { stamp } from "./stamps";
|
|
@@ -107,7 +108,7 @@ function arkJsonAstToWire(value: unknown): unknown {
|
|
|
107
108
|
return {};
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
+
/** `stamp` cache keys; the entries themselves live in a weak side table keyed by schema identity. */
|
|
111
112
|
const kJsonWireSchema = Symbol("pi.schema.json.wire");
|
|
112
113
|
const kArkWireSchema = Symbol("pi.schema.ark.wire");
|
|
113
114
|
const kStrippedSchema = Symbol("pi.schema.descriptions.stripped");
|
|
@@ -602,7 +603,9 @@ export function toolWireSchema(tool: Tool): Record<string, unknown> {
|
|
|
602
603
|
const params: TSchema = tool.parameters;
|
|
603
604
|
if (isArkSchema(params)) return arkToWireSchema(params);
|
|
604
605
|
return stamp(params as Record<string, unknown>, kJsonWireSchema, p => {
|
|
605
|
-
|
|
606
|
+
// Legacy schemas may carry non-cloneable metadata (helper functions); the JSON
|
|
607
|
+
// fallback drops it the same way the wire serializer always has.
|
|
608
|
+
const raw = isArkJsonAst(p) ? arkJsonAstToWire(p) : structuredCloneJSON(p);
|
|
606
609
|
const upgraded = upgradeJsonSchemaTo202012(raw) as Record<string, unknown>;
|
|
607
610
|
return postProcessJsonSchema(upgraded);
|
|
608
611
|
});
|
|
@@ -663,10 +666,9 @@ function stripSchemaDescriptionsInPlace(node: unknown): void {
|
|
|
663
666
|
|
|
664
667
|
/**
|
|
665
668
|
* Return a deep clone of `schema` with every `description` annotation removed.
|
|
666
|
-
* The result is memoized
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* system-prompt/UI rendering.
|
|
669
|
+
* The result is memoized against the input (`stamp`) so repeated provider
|
|
670
|
+
* requests reuse the same stripped object; the input is never mutated, so the
|
|
671
|
+
* memoized `toolWireSchema` result stays intact for system-prompt/UI rendering.
|
|
670
672
|
*/
|
|
671
673
|
export function stripSchemaDescriptions(schema: Record<string, unknown>): Record<string, unknown> {
|
|
672
674
|
return stamp(schema, kStrippedSchema, source => {
|
|
@@ -682,7 +684,7 @@ export function stripSchemaDescriptions(schema: Record<string, unknown>): Record
|
|
|
682
684
|
* Used when the full tool catalog is rendered into the system prompt instead, so
|
|
683
685
|
* the descriptions ride the wire once (in the prompt) rather than duplicated on
|
|
684
686
|
* every tool definition. Parameters are resolved to wire JSON Schema and cloned,
|
|
685
|
-
* leaving the original tool objects and the
|
|
687
|
+
* leaving the original tool objects and the memoized schema cache untouched.
|
|
686
688
|
*/
|
|
687
689
|
export function stripToolDescriptions(tools: readonly Tool[]): Tool[] {
|
|
688
690
|
return tools.map(tool => ({
|
package/src/utils.ts
CHANGED
|
@@ -45,8 +45,10 @@ export function normalizeResponsesToolCallId(
|
|
|
45
45
|
id: string,
|
|
46
46
|
itemPrefix: ResponsesToolItemIdPrefix = "fc",
|
|
47
47
|
): { callId: string; itemId: string } {
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
48
|
+
const sep = id.search(/[\n|]/);
|
|
49
|
+
if (sep > 0) {
|
|
50
|
+
const callId = id.slice(0, sep);
|
|
51
|
+
const itemId = id.slice(sep + 1);
|
|
50
52
|
return { callId, itemId: normalizeResponsesItemId(itemId, itemPrefix) };
|
|
51
53
|
}
|
|
52
54
|
const hash = Bun.hash(id).toString(36);
|
|
@@ -324,6 +326,15 @@ export function stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay(items:
|
|
|
324
326
|
* Returns `undefined` for hidden-empty turns that only contain reasoning and an
|
|
325
327
|
* empty assistant message, allowing callers to rebuild visible transcript
|
|
326
328
|
* history instead of replaying stale native state.
|
|
329
|
+
*
|
|
330
|
+
* When the turn does carry replayable output, whitespace-only assistant
|
|
331
|
+
* messages are dropped rather than replayed. gpt-5.6 Codex ends a turn whose
|
|
332
|
+
* text all landed in the `commentary` phase with an empty `final_answer`
|
|
333
|
+
* message; replaying that item verbatim seeds the next turn with an empty slot
|
|
334
|
+
* the model then fills - `""` → `"\n\n"` → stray words → non-Latin residue -
|
|
335
|
+
* and each contaminated item is replayed in turn, so the drift compounds until
|
|
336
|
+
* the visible answer collapses. Codex CLI replays the empty item too
|
|
337
|
+
* (openai/codex#32389); this is the layer where omp can refuse to.
|
|
327
338
|
*/
|
|
328
339
|
export function sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
329
340
|
items: Array<Record<string, unknown>>,
|
|
@@ -331,34 +342,68 @@ export function sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
|
331
342
|
): ResponseInput | undefined {
|
|
332
343
|
const sanitized = sanitizeOpenAIResponsesHistoryItemsForReplay(items, options);
|
|
333
344
|
let hasReplayableAssistantOutput = false;
|
|
345
|
+
let hasEmptyAssistantMessage = false;
|
|
334
346
|
|
|
335
347
|
for (const item of sanitized) {
|
|
336
348
|
if (item.type === "reasoning") continue;
|
|
337
|
-
if (item
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
if (isEmptyAssistantMessage(item)) {
|
|
350
|
+
hasEmptyAssistantMessage = true;
|
|
351
|
+
continue;
|
|
340
352
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
353
|
+
hasReplayableAssistantOutput = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!hasReplayableAssistantOutput) return undefined;
|
|
357
|
+
if (!hasEmptyAssistantMessage) return sanitized;
|
|
358
|
+
return dropEmptyAssistantMessagesForReplay(sanitized);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isEmptyAssistantMessage(item: OpenAIResponsesReplayItem): boolean {
|
|
362
|
+
if (item.type !== "message" || item.role !== "assistant") return false;
|
|
363
|
+
if (typeof item.content === "string") return !NON_WHITESPACE_RE.test(item.content);
|
|
364
|
+
for (const part of item.content) {
|
|
365
|
+
if (part.type === "output_text" && NON_WHITESPACE_RE.test(part.text)) return false;
|
|
366
|
+
if (part.type === "refusal" && NON_WHITESPACE_RE.test(part.refusal)) return false;
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Remove whitespace-only assistant messages plus any reasoning item left with
|
|
373
|
+
* no output to introduce: a reasoning item is only meaningful ahead of the
|
|
374
|
+
* output it produced, and a dangling one is rejected by the Responses API when
|
|
375
|
+
* it carries an id.
|
|
376
|
+
*/
|
|
377
|
+
function dropEmptyAssistantMessagesForReplay(items: ResponseInput): ResponseInput {
|
|
378
|
+
const kept: ResponseInput = [];
|
|
379
|
+
let pendingReasoning: ResponseInput = [];
|
|
380
|
+
let removedAssistantMessage = false;
|
|
381
|
+
const flushReasoning = (): void => {
|
|
382
|
+
kept.push(...pendingReasoning);
|
|
383
|
+
pendingReasoning = [];
|
|
384
|
+
};
|
|
385
|
+
for (const item of items) {
|
|
386
|
+
if (isOpenAIResponsesClientInputBoundary(item as unknown as Record<string, unknown>)) {
|
|
387
|
+
pendingReasoning = [];
|
|
388
|
+
removedAssistantMessage = false;
|
|
389
|
+
kept.push(item);
|
|
346
390
|
continue;
|
|
347
391
|
}
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (part.type === "refusal" && NON_WHITESPACE_RE.test(part.refusal)) {
|
|
354
|
-
hasReplayableAssistantOutput = true;
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
392
|
+
if (item.type === "reasoning") {
|
|
393
|
+
if (removedAssistantMessage) pendingReasoning = [];
|
|
394
|
+
removedAssistantMessage = false;
|
|
395
|
+
pendingReasoning.push(item);
|
|
396
|
+
continue;
|
|
357
397
|
}
|
|
358
|
-
if (
|
|
398
|
+
if (isEmptyAssistantMessage(item)) {
|
|
399
|
+
removedAssistantMessage = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
flushReasoning();
|
|
403
|
+
removedAssistantMessage = false;
|
|
404
|
+
kept.push(item);
|
|
359
405
|
}
|
|
360
|
-
|
|
361
|
-
return hasReplayableAssistantOutput ? sanitized : undefined;
|
|
406
|
+
return kept;
|
|
362
407
|
}
|
|
363
408
|
|
|
364
409
|
/**
|