@oh-my-pi/pi-ai 18.3.0 → 18.3.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/THIRD-PARTY-NOTICES.txt +2 -2
  3. package/dist/types/auth/policy.d.ts +8 -1
  4. package/dist/types/auth/pool.d.ts +8 -0
  5. package/dist/types/auth/types.d.ts +17 -7
  6. package/dist/types/auth/usage.d.ts +2 -0
  7. package/dist/types/auth-broker/discover.d.ts +22 -1
  8. package/dist/types/auth-storage.d.ts +35 -11
  9. package/dist/types/error/rate-limit.d.ts +3 -2
  10. package/dist/types/index.d.ts +1 -0
  11. package/dist/types/providers/anthropic-slow-mode.d.ts +109 -0
  12. package/dist/types/providers/anthropic-wire.d.ts +4 -0
  13. package/dist/types/providers/mock.d.ts +3 -1
  14. package/dist/types/providers/openai-codex/live-steering.d.ts +77 -0
  15. package/dist/types/providers/transform-messages.d.ts +9 -1
  16. package/dist/types/types.d.ts +56 -0
  17. package/dist/types/usage/xai-oauth.d.ts +6 -1
  18. package/dist/types/utils/http-inspector.d.ts +6 -0
  19. package/package.json +6 -6
  20. package/src/auth/policy.ts +27 -6
  21. package/src/auth/pool.ts +35 -2
  22. package/src/auth/refresh.ts +2 -2
  23. package/src/auth/types.ts +17 -7
  24. package/src/auth/usage.ts +5 -0
  25. package/src/auth-broker/discover.ts +57 -27
  26. package/src/auth-storage.ts +121 -35
  27. package/src/error/flags.ts +2 -1
  28. package/src/error/rate-limit.ts +8 -4
  29. package/src/index.ts +1 -0
  30. package/src/providers/anthropic-slow-mode.ts +232 -0
  31. package/src/providers/anthropic-wire.ts +4 -0
  32. package/src/providers/anthropic.ts +322 -22
  33. package/src/providers/cowork-fetch.ts +11 -4
  34. package/src/providers/google-shared.ts +30 -7
  35. package/src/providers/inference-headers.ts +7 -1
  36. package/src/providers/mock.ts +4 -0
  37. package/src/providers/openai-codex/live-steering.ts +237 -0
  38. package/src/providers/openai-codex-responses.ts +372 -73
  39. package/src/providers/transform-messages.ts +27 -7
  40. package/src/stream.ts +32 -2
  41. package/src/types.ts +59 -0
  42. package/src/usage/registry.ts +2 -1
  43. package/src/usage/xai-oauth.ts +31 -1
  44. package/src/utils/http-inspector.ts +21 -2
  45. package/src/utils/openrouter-headers.ts +3 -3
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Mid-turn steering over the Codex Responses WebSocket (`response.steer`).
3
+ *
4
+ * While a response streams, {@link CodexSteerPump} pulls user input from the
5
+ * caller's {@link LiveSteering} source and submits it to that response. Accepted
6
+ * input belongs to the server from then on: it either continues automatically in
7
+ * a successor response (nothing client-owned is pending) or is prepended to the
8
+ * next explicit `response.create` that returns the pending tool output.
9
+ * {@link planSteeredRequest} maps the caller's next request onto whichever of
10
+ * the two the server will do.
11
+ *
12
+ * @example
13
+ * ```ts ignore
14
+ * const pump = new CodexSteerPump(options.liveSteering, connection, toSteerInput);
15
+ * pump.start(responseId); // on `response.created`
16
+ * const { accepted } = await pump.finish(); // after the terminal event
17
+ * ```
18
+ */
19
+ import { logger } from "@oh-my-pi/pi-utils";
20
+ import type { LiveSteering, UserMessage } from "../../types";
21
+ import type { InputItem } from "./request-transformer";
22
+
23
+ /** Server acknowledgement of one `response.steer` submission. */
24
+ export type CodexSteerAck = { accepted: true; id: string } | { accepted: false; code?: string; message?: string };
25
+
26
+ /** Socket surface the pump submits through. */
27
+ export interface CodexSteerSocket {
28
+ /** Sends `response.steer`; resolves on the matching acknowledgement, rejects when the socket closes first. */
29
+ steer(previousResponseId: string, input: InputItem[]): Promise<CodexSteerAck>;
30
+ }
31
+
32
+ /** Steering the server accepted, with the exact input items it queued. */
33
+ export interface CodexAcceptedSteer {
34
+ id: string;
35
+ items: InputItem[];
36
+ }
37
+
38
+ /** What a finished pump delivered into its response. */
39
+ export interface CodexSteerOutcome {
40
+ responseId: string | undefined;
41
+ accepted: CodexAcceptedSteer[];
42
+ /**
43
+ * A submission ended without an acknowledgement, so the server may or may
44
+ * not hold it. The caller must drop the socket rather than chain from it.
45
+ */
46
+ uncertain: boolean;
47
+ }
48
+
49
+ /** How long to wait for `response.steer.accepted`/`failed` before treating the outcome as unknown. */
50
+ const STEER_ACK_TIMEOUT_MS = 10_000;
51
+ /** Back-off when the source woke but had nothing deliverable (e.g. input still being prepared). */
52
+ const EMPTY_CLAIM_BACKOFF_MS = 25;
53
+
54
+ /**
55
+ * Submits caller steering to one in-flight response. Stops after the first
56
+ * rejection: the server only rejects when the response no longer accepts input,
57
+ * and later submissions would reorder the caller's input.
58
+ */
59
+ export class CodexSteerPump {
60
+ readonly #source: LiveSteering;
61
+ readonly #socket: CodexSteerSocket;
62
+ readonly #toInput: (messages: readonly UserMessage[]) => InputItem[] | undefined;
63
+ readonly #stop = new AbortController();
64
+ readonly #accepted: CodexAcceptedSteer[] = [];
65
+ #responseId: string | undefined;
66
+ #run: Promise<void> | undefined;
67
+ #uncertain = false;
68
+
69
+ constructor(
70
+ source: LiveSteering,
71
+ socket: CodexSteerSocket,
72
+ toInput: (messages: readonly UserMessage[]) => InputItem[] | undefined,
73
+ ) {
74
+ this.#source = source;
75
+ this.#socket = socket;
76
+ this.#toInput = toInput;
77
+ }
78
+
79
+ /** The response this pump steers, once started. */
80
+ get responseId(): string | undefined {
81
+ return this.#responseId;
82
+ }
83
+
84
+ /** Starts submitting steering to `responseId`; later calls are ignored. */
85
+ start(responseId: string): void {
86
+ if (this.#run || this.#stop.signal.aborted) return;
87
+ this.#responseId = responseId;
88
+ this.#run = this.#loop(responseId);
89
+ }
90
+
91
+ /** Stops claiming input, settles the submission in flight, and reports what the server accepted. */
92
+ async finish(): Promise<CodexSteerOutcome> {
93
+ this.#stop.abort();
94
+ await this.#run;
95
+ return { responseId: this.#responseId, accepted: this.#accepted, uncertain: this.#uncertain };
96
+ }
97
+
98
+ async #loop(responseId: string): Promise<void> {
99
+ const signal = this.#stop.signal;
100
+ try {
101
+ while (!signal.aborted) {
102
+ await this.#source.wait(signal);
103
+ if (signal.aborted) return;
104
+ const claimedAt = performance.now();
105
+ const claim = await this.#source.claim(signal);
106
+ if (!claim) {
107
+ await Bun.sleep(EMPTY_CLAIM_BACKOFF_MS);
108
+ continue;
109
+ }
110
+ // The response ended while the input was being prepared: the caller
111
+ // delivers it with its next request instead.
112
+ const input = signal.aborted ? undefined : this.#toInput(claim.messages);
113
+ if (!input) {
114
+ claim.reject();
115
+ return;
116
+ }
117
+ const sentAt = performance.now();
118
+ let ack: CodexSteerAck | undefined;
119
+ try {
120
+ ack = await withTimeout(this.#socket.steer(responseId, input), STEER_ACK_TIMEOUT_MS);
121
+ } catch (error) {
122
+ logger.debug("Codex steering acknowledgement missing", {
123
+ responseId,
124
+ error: error instanceof Error ? error.message : String(error),
125
+ });
126
+ }
127
+ if (!ack) {
128
+ this.#uncertain = true;
129
+ claim.reject();
130
+ return;
131
+ }
132
+ if (!ack.accepted) {
133
+ logger.debug("Codex steering rejected", { responseId, code: ack.code, message: ack.message });
134
+ claim.reject();
135
+ return;
136
+ }
137
+ this.#accepted.push({ id: ack.id, items: input });
138
+ claim.accept();
139
+ logger.debug("Codex steering accepted", {
140
+ responseId,
141
+ steerId: ack.id,
142
+ prepareMs: Math.round(sentAt - claimedAt),
143
+ ackMs: Math.round(performance.now() - sentAt),
144
+ });
145
+ }
146
+ } catch (error) {
147
+ logger.warn("Codex steering pump failed", {
148
+ responseId,
149
+ error: error instanceof Error ? error.message : String(error),
150
+ });
151
+ }
152
+ }
153
+ }
154
+
155
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | undefined> {
156
+ const { promise: timeout, resolve } = Promise.withResolvers<undefined>();
157
+ const timer = setTimeout(() => resolve(undefined), timeoutMs);
158
+ try {
159
+ return await Promise.race([promise, timeout]);
160
+ } finally {
161
+ clearTimeout(timer);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Converted steering input, or `undefined` when an item is not a plain user
167
+ * message (the only shape `response.steer` accepts).
168
+ */
169
+ export function toSteerInputItems(items: readonly InputItem[]): InputItem[] | undefined {
170
+ if (items.length === 0) return undefined;
171
+ for (const item of items) {
172
+ if (item.role !== "user" || (item.type != null && item.type !== "message")) return undefined;
173
+ }
174
+ return [...items];
175
+ }
176
+
177
+ /** How the request after a steered response continues on the server. */
178
+ export type CodexSteerPlan =
179
+ /** The server continues on its own: read its successor, send nothing. */
180
+ | { kind: "attach" }
181
+ /** The server awaits tool output: send only `input`; it prepends the accepted steering itself. */
182
+ | { kind: "create"; input: InputItem[] }
183
+ /** The request cannot line up with the server's queue: drop the socket and replay in full. */
184
+ | { kind: "discard" };
185
+
186
+ /**
187
+ * Line the next request up with steering the server accepted for the previous
188
+ * response.
189
+ *
190
+ * `delta` is the chained input (new items after the previous response), or
191
+ * `undefined` when the chain broke. The accepted steering must appear in it, in
192
+ * order; what remains decides the plan: nothing means the server's automatic
193
+ * successor is exactly this request, tool output means the server is waiting
194
+ * for it, anything else runs concurrently with a successor and cannot be sent.
195
+ */
196
+ export function planSteeredRequest(
197
+ delta: readonly InputItem[] | undefined,
198
+ steering: readonly InputItem[],
199
+ ): CodexSteerPlan {
200
+ if (!delta) return { kind: "discard" };
201
+ const rest: InputItem[] = [];
202
+ let next = 0;
203
+ for (const item of delta) {
204
+ if (next < steering.length && steerItemKey(item) === steerItemKey(steering[next]!)) {
205
+ next++;
206
+ continue;
207
+ }
208
+ rest.push(item);
209
+ }
210
+ if (next < steering.length) return { kind: "discard" };
211
+ if (rest.length === 0) return { kind: "attach" };
212
+ // Client-owned results (`*_output`) mean the server is waiting for them.
213
+ if (rest.some(item => typeof item.type === "string" && item.type.endsWith("_output"))) {
214
+ return { kind: "create", input: rest };
215
+ }
216
+ return { kind: "discard" };
217
+ }
218
+
219
+ /**
220
+ * Identity of a user message item across the steer submission and the replayed
221
+ * request: role plus content, ignoring the transport-only image `detail` hint
222
+ * that Responses Lite strips from request bodies.
223
+ */
224
+ function steerItemKey(item: InputItem): string {
225
+ if (item.role !== "user") return `\0${item.type ?? ""}:${item.call_id ?? item.id ?? ""}`;
226
+ const content =
227
+ typeof item.content === "string"
228
+ ? [{ type: "input_text", text: item.content }]
229
+ : Array.isArray(item.content)
230
+ ? item.content.map(part => {
231
+ if (!part || typeof part !== "object") return part;
232
+ const { detail: _detail, ...rest } = part as Record<string, unknown>;
233
+ return rest;
234
+ })
235
+ : item.content;
236
+ return JSON.stringify(content);
237
+ }