@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.
- package/CHANGELOG.md +20 -0
- package/THIRD-PARTY-NOTICES.txt +2 -2
- package/dist/types/auth/policy.d.ts +8 -1
- package/dist/types/auth/pool.d.ts +8 -0
- package/dist/types/auth/types.d.ts +17 -7
- package/dist/types/auth/usage.d.ts +2 -0
- package/dist/types/auth-broker/discover.d.ts +22 -1
- package/dist/types/auth-storage.d.ts +35 -11
- package/dist/types/error/rate-limit.d.ts +3 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/anthropic-slow-mode.d.ts +109 -0
- package/dist/types/providers/anthropic-wire.d.ts +4 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/live-steering.d.ts +77 -0
- package/dist/types/providers/transform-messages.d.ts +9 -1
- package/dist/types/types.d.ts +56 -0
- package/dist/types/usage/xai-oauth.d.ts +6 -1
- package/dist/types/utils/http-inspector.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/policy.ts +27 -6
- package/src/auth/pool.ts +35 -2
- package/src/auth/refresh.ts +2 -2
- package/src/auth/types.ts +17 -7
- package/src/auth/usage.ts +5 -0
- package/src/auth-broker/discover.ts +57 -27
- package/src/auth-storage.ts +121 -35
- package/src/error/flags.ts +2 -1
- package/src/error/rate-limit.ts +8 -4
- package/src/index.ts +1 -0
- package/src/providers/anthropic-slow-mode.ts +232 -0
- package/src/providers/anthropic-wire.ts +4 -0
- package/src/providers/anthropic.ts +322 -22
- package/src/providers/cowork-fetch.ts +11 -4
- package/src/providers/google-shared.ts +30 -7
- package/src/providers/inference-headers.ts +7 -1
- package/src/providers/mock.ts +4 -0
- package/src/providers/openai-codex/live-steering.ts +237 -0
- package/src/providers/openai-codex-responses.ts +372 -73
- package/src/providers/transform-messages.ts +27 -7
- package/src/stream.ts +32 -2
- package/src/types.ts +59 -0
- package/src/usage/registry.ts +2 -1
- package/src/usage/xai-oauth.ts +31 -1
- package/src/utils/http-inspector.ts +21 -2
- 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
|
+
}
|