@oh-my-pi/pi-ai 15.3.1 → 15.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/dist/types/auth-broker/client.d.ts +21 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -1
- package/dist/types/auth-broker/server.d.ts +6 -0
- package/dist/types/auth-broker/types.d.ts +36 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +148 -0
- package/dist/types/auth-storage.d.ts +65 -0
- package/dist/types/providers/openai-responses-shared.d.ts +24 -0
- package/package.json +2 -2
- package/src/auth-broker/client.ts +97 -0
- package/src/auth-broker/remote-store.ts +145 -20
- package/src/auth-broker/server.ts +191 -1
- package/src/auth-broker/types.ts +43 -0
- package/src/auth-broker/wire-schemas.ts +38 -0
- package/src/auth-gateway/server.ts +24 -0
- package/src/auth-storage.ts +239 -8
- package/src/providers/openai-responses-shared.ts +70 -0
- package/src/providers/openai-responses.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.3.2] - 2026-05-25
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `GET /v1/snapshot/stream` for live auth-broker snapshot updates via SSE with `snapshot`, `entry`, and `removed` event frames
|
|
9
|
+
- Added `AuthBrokerClient.openSnapshotStream()` for consuming SSE snapshot streams from `/v1/snapshot/stream`
|
|
10
|
+
- Added `streamSnapshots` option to `RemoteAuthCredentialStore` (default `true`) to enable or disable SSE-based snapshot synchronization
|
|
11
|
+
- Added `streamKeepaliveMs` to `startAuthBroker()` to tune heartbeat frequency for the SSE stream
|
|
12
|
+
- Added `AuthStorage.checkCredentials({ signal?, timeoutMs?, baseUrlResolver? })` that returns a per-credential `CredentialHealthResult` with tri-state `ok` (`true` / `false` / `null`-unverifiable), the credential's identity (provider, type, email/accountId, broker-refresh flag), and the upstream error string when the probe fails. Iterates sequentially over `listAuthCredentials()`, exercises OAuth refresh on expiry, then calls the per-provider `UsageProvider.fetchUsage` without swallowing errors — so callers can identify which row in a multi-account broker is producing 401s instead of getting a silently-deduplicated `fetchUsageReports` list.
|
|
13
|
+
- Added `GET /v1/credentials/check` to `startAuthGateway()` that forwards to `AuthStorage.checkCredentials` and returns `{ generatedAt, credentials }`. Gated by the same bearer as the rest of the gateway.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Changed `RemoteAuthCredentialStore` to prefer SSE snapshot streaming and automatically fall back to long-polling when a broker returns 404 for `/v1/snapshot/stream`
|
|
18
|
+
- Changed snapshot write-refresh flow so `RemoteAuthCredentialStore` skips immediate `/v1/snapshot` refreshes when SSE streaming is active
|
|
19
|
+
- Changed broker SSE stream behavior to keep connections open with periodic keepalives and an increased server idle timeout
|
|
20
|
+
|
|
5
21
|
## [15.3.0] - 2026-05-25
|
|
6
22
|
|
|
7
23
|
### Added
|
|
8
24
|
|
|
9
25
|
- Added DeepSeek to the built-in API-key login provider catalog so `omp login deepseek` stores a reusable `DEEPSEEK_API_KEY` credential for the bundled DeepSeek models.
|
|
10
26
|
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Fixed `openai-responses` requests intermittently 400ing with `No tool call found for function call output with call_id …` after an aborted turn or a locally-rejected tool call (e.g. argument-validation failure). `convertConversationMessages` now folds orphan `function_call_output` / `custom_tool_call_output` items — those whose matching `function_call` was wiped by an earlier `dt: false` snapshot splice or never landed in any persisted provider payload — into assistant text notes, preserving the payload while keeping the request grammatically valid ([#1351](https://github.com/can1357/oh-my-pi/issues/1351)).
|
|
30
|
+
|
|
11
31
|
## [15.2.4] - 2026-05-22
|
|
12
32
|
|
|
13
33
|
### Fixed
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthCredential } from "../auth-storage";
|
|
2
|
-
import type { CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, UsageResponse } from "./types";
|
|
2
|
+
import type { CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types";
|
|
3
3
|
export interface AuthBrokerClientOptions {
|
|
4
4
|
/** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
|
|
5
5
|
url: string;
|
|
@@ -21,6 +21,14 @@ export declare class AuthBrokerError extends Error {
|
|
|
21
21
|
cause?: unknown;
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Thrown when a broker responds 404 to `GET /v1/snapshot/stream` — old
|
|
26
|
+
* brokers that predate the SSE endpoint. Callers (`RemoteAuthCredentialStore`)
|
|
27
|
+
* detect this sentinel to fall back to long-polling permanently.
|
|
28
|
+
*/
|
|
29
|
+
export declare class AuthBrokerStreamUnsupportedError extends AuthBrokerError {
|
|
30
|
+
constructor(message?: string);
|
|
31
|
+
}
|
|
24
32
|
export interface FetchSnapshotOptions {
|
|
25
33
|
ifGenerationGt?: number;
|
|
26
34
|
waitMs?: number;
|
|
@@ -39,6 +47,18 @@ export declare class AuthBrokerClient {
|
|
|
39
47
|
constructor(opts: AuthBrokerClientOptions);
|
|
40
48
|
healthz(signal?: AbortSignal): Promise<HealthzResponse>;
|
|
41
49
|
fetchSnapshot(opts?: FetchSnapshotOptions): Promise<FetchSnapshotResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Subscribe to the broker's SSE snapshot stream. The first frame is always
|
|
52
|
+
* a full `snapshot`; subsequent frames are `entry` upserts / refreshes or
|
|
53
|
+
* `removed` deletes. Caller controls lifecycle via `opts.signal`.
|
|
54
|
+
*
|
|
55
|
+
* Throws {@link AuthBrokerStreamUnsupportedError} when the broker responds
|
|
56
|
+
* 404 — older brokers predate this endpoint and the caller should fall back
|
|
57
|
+
* to long-polling for the remainder of its lifetime.
|
|
58
|
+
*/
|
|
59
|
+
openSnapshotStream(opts?: {
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
}): AsyncGenerator<SnapshotStreamEvent>;
|
|
42
62
|
fetchUsage(signal?: AbortSignal): Promise<UsageResponse>;
|
|
43
63
|
refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse>;
|
|
44
64
|
disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
|
|
@@ -2,7 +2,7 @@ import { type AuthCredential, type AuthCredentialStore, type OAuthCredential, ty
|
|
|
2
2
|
import type { Provider } from "../types";
|
|
3
3
|
import type { UsageReport } from "../usage";
|
|
4
4
|
import type { OAuthCredentials } from "../utils/oauth/types";
|
|
5
|
-
import type
|
|
5
|
+
import { type AuthBrokerClient } from "./client";
|
|
6
6
|
import type { SnapshotResponse } from "./types";
|
|
7
7
|
export interface RemoteAuthCredentialStoreOptions {
|
|
8
8
|
client: AuthBrokerClient;
|
|
@@ -11,6 +11,11 @@ export interface RemoteAuthCredentialStoreOptions {
|
|
|
11
11
|
* {@link RemoteAuthCredentialStore.refreshSnapshot} before the first read.
|
|
12
12
|
*/
|
|
13
13
|
initialSnapshot?: SnapshotResponse;
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe to the broker's SSE snapshot stream when available. Falls back
|
|
16
|
+
* to long-poll permanently when the broker returns 404. Default `true`.
|
|
17
|
+
*/
|
|
18
|
+
streamSnapshots?: boolean;
|
|
14
19
|
}
|
|
15
20
|
export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
16
21
|
#private;
|
|
@@ -14,6 +14,12 @@ export interface AuthBrokerServerOptions {
|
|
|
14
14
|
refreshIntervalMs?: number;
|
|
15
15
|
/** Disable the background refresher (e.g. for tests). */
|
|
16
16
|
disableRefresher?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Override SSE keepalive cadence in milliseconds for `/v1/snapshot/stream`.
|
|
19
|
+
* Internal-only — tests use a short interval so they can assert heartbeats
|
|
20
|
+
* without long sleeps. Default {@link DEFAULT_STREAM_KEEPALIVE_MS}.
|
|
21
|
+
*/
|
|
22
|
+
streamKeepaliveMs?: number;
|
|
17
23
|
}
|
|
18
24
|
export interface AuthBrokerServerHandle {
|
|
19
25
|
/** Bound URL (`http://host:port`). */
|
|
@@ -56,6 +56,35 @@ export interface CredentialUploadRequest {
|
|
|
56
56
|
export interface CredentialUploadResponse {
|
|
57
57
|
entries: AuthCredentialSnapshotEntry[];
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* SSE event kinds emitted on `GET /v1/snapshot/stream`. The same value is set
|
|
61
|
+
* as the SSE `event:` name (load-bearing for clients) **and** embedded as a
|
|
62
|
+
* `kind` field inside the JSON body so a Zod discriminated union can validate
|
|
63
|
+
* the payload without consulting the line metadata.
|
|
64
|
+
*/
|
|
65
|
+
export type SnapshotStreamEventKind = "snapshot" | "entry" | "removed";
|
|
66
|
+
/** Initial frame emitted on connect — the full {@link SnapshotResponse}. */
|
|
67
|
+
export interface SnapshotStreamSnapshotEvent extends SnapshotResponse {
|
|
68
|
+
kind: "snapshot";
|
|
69
|
+
}
|
|
70
|
+
/** Single credential added/changed (upsert or refresh). */
|
|
71
|
+
export interface SnapshotStreamEntryEvent {
|
|
72
|
+
kind: "entry";
|
|
73
|
+
generation: number;
|
|
74
|
+
serverNowMs: number;
|
|
75
|
+
refresher: RefresherSchedule;
|
|
76
|
+
entry: SnapshotEntry;
|
|
77
|
+
}
|
|
78
|
+
/** Single credential disabled/deleted. */
|
|
79
|
+
export interface SnapshotStreamRemovedEvent {
|
|
80
|
+
kind: "removed";
|
|
81
|
+
generation: number;
|
|
82
|
+
serverNowMs: number;
|
|
83
|
+
refresher: RefresherSchedule;
|
|
84
|
+
id: number;
|
|
85
|
+
}
|
|
86
|
+
/** Discriminated union of every event the snapshot stream emits. */
|
|
87
|
+
export type SnapshotStreamEvent = SnapshotStreamSnapshotEvent | SnapshotStreamEntryEvent | SnapshotStreamRemovedEvent;
|
|
59
88
|
/**
|
|
60
89
|
* Default bearer-protected route prefix. The broker exposes `/v1/healthz`
|
|
61
90
|
* unauthenticated for liveness probes; everything else requires a bearer.
|
|
@@ -67,3 +96,10 @@ export declare const DEFAULT_AUTH_BROKER_BIND = "127.0.0.1:8765";
|
|
|
67
96
|
export declare const DEFAULT_REFRESH_SKEW_MS: number;
|
|
68
97
|
/** Default broker refresh-loop cadence. */
|
|
69
98
|
export declare const DEFAULT_REFRESH_INTERVAL_MS = 60000;
|
|
99
|
+
/** Keepalive cadence for `GET /v1/snapshot/stream` SSE comments. */
|
|
100
|
+
export declare const DEFAULT_STREAM_KEEPALIVE_MS = 20000;
|
|
101
|
+
/**
|
|
102
|
+
* Bun.serve `idleTimeout` (seconds) used by the broker. Default Bun idle
|
|
103
|
+
* timeout (10s) would close long-lived SSE connections between keepalives.
|
|
104
|
+
*/
|
|
105
|
+
export declare const DEFAULT_SERVER_IDLE_TIMEOUT_S = 255;
|
|
@@ -138,6 +138,154 @@ export declare const snapshotResponseSchema: z.ZodObject<{
|
|
|
138
138
|
rotatesInMs: z.ZodNullable<z.ZodNumber>;
|
|
139
139
|
}, z.core.$strict>>;
|
|
140
140
|
}, z.core.$strict>;
|
|
141
|
+
/** First frame on connect — full snapshot embedded inline with a `kind` tag. */
|
|
142
|
+
export declare const snapshotStreamSnapshotEventSchema: z.ZodObject<{
|
|
143
|
+
generation: z.ZodNumber;
|
|
144
|
+
generatedAt: z.ZodNumber;
|
|
145
|
+
serverNowMs: z.ZodNumber;
|
|
146
|
+
refresher: z.ZodObject<{
|
|
147
|
+
enabled: z.ZodBoolean;
|
|
148
|
+
intervalMs: z.ZodNumber;
|
|
149
|
+
skewMs: z.ZodNumber;
|
|
150
|
+
nextSweepInMs: z.ZodNumber;
|
|
151
|
+
}, z.core.$strict>;
|
|
152
|
+
credentials: z.ZodArray<z.ZodObject<{
|
|
153
|
+
id: z.ZodNumber;
|
|
154
|
+
provider: z.ZodString;
|
|
155
|
+
credential: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
156
|
+
type: z.ZodLiteral<"oauth">;
|
|
157
|
+
access: z.ZodString;
|
|
158
|
+
expires: z.ZodNumber;
|
|
159
|
+
enterpriseUrl: z.ZodOptional<z.ZodString>;
|
|
160
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
161
|
+
email: z.ZodOptional<z.ZodString>;
|
|
162
|
+
accountId: z.ZodOptional<z.ZodString>;
|
|
163
|
+
refresh: z.ZodLiteral<"__remote__">;
|
|
164
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
165
|
+
type: z.ZodLiteral<"api_key">;
|
|
166
|
+
key: z.ZodString;
|
|
167
|
+
}, z.core.$strict>], "type">;
|
|
168
|
+
identityKey: z.ZodNullable<z.ZodString>;
|
|
169
|
+
rotatesInMs: z.ZodNullable<z.ZodNumber>;
|
|
170
|
+
}, z.core.$strict>>;
|
|
171
|
+
kind: z.ZodLiteral<"snapshot">;
|
|
172
|
+
}, z.core.$strict>;
|
|
173
|
+
/** Per-credential upsert/refresh delta. */
|
|
174
|
+
export declare const snapshotStreamEntryEventSchema: z.ZodObject<{
|
|
175
|
+
kind: z.ZodLiteral<"entry">;
|
|
176
|
+
generation: z.ZodNumber;
|
|
177
|
+
serverNowMs: z.ZodNumber;
|
|
178
|
+
refresher: z.ZodObject<{
|
|
179
|
+
enabled: z.ZodBoolean;
|
|
180
|
+
intervalMs: z.ZodNumber;
|
|
181
|
+
skewMs: z.ZodNumber;
|
|
182
|
+
nextSweepInMs: z.ZodNumber;
|
|
183
|
+
}, z.core.$strict>;
|
|
184
|
+
entry: z.ZodObject<{
|
|
185
|
+
id: z.ZodNumber;
|
|
186
|
+
provider: z.ZodString;
|
|
187
|
+
credential: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
188
|
+
type: z.ZodLiteral<"oauth">;
|
|
189
|
+
access: z.ZodString;
|
|
190
|
+
expires: z.ZodNumber;
|
|
191
|
+
enterpriseUrl: z.ZodOptional<z.ZodString>;
|
|
192
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
193
|
+
email: z.ZodOptional<z.ZodString>;
|
|
194
|
+
accountId: z.ZodOptional<z.ZodString>;
|
|
195
|
+
refresh: z.ZodLiteral<"__remote__">;
|
|
196
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
197
|
+
type: z.ZodLiteral<"api_key">;
|
|
198
|
+
key: z.ZodString;
|
|
199
|
+
}, z.core.$strict>], "type">;
|
|
200
|
+
identityKey: z.ZodNullable<z.ZodString>;
|
|
201
|
+
rotatesInMs: z.ZodNullable<z.ZodNumber>;
|
|
202
|
+
}, z.core.$strict>;
|
|
203
|
+
}, z.core.$strict>;
|
|
204
|
+
/** Per-credential delete delta. */
|
|
205
|
+
export declare const snapshotStreamRemovedEventSchema: z.ZodObject<{
|
|
206
|
+
kind: z.ZodLiteral<"removed">;
|
|
207
|
+
generation: z.ZodNumber;
|
|
208
|
+
serverNowMs: z.ZodNumber;
|
|
209
|
+
refresher: z.ZodObject<{
|
|
210
|
+
enabled: z.ZodBoolean;
|
|
211
|
+
intervalMs: z.ZodNumber;
|
|
212
|
+
skewMs: z.ZodNumber;
|
|
213
|
+
nextSweepInMs: z.ZodNumber;
|
|
214
|
+
}, z.core.$strict>;
|
|
215
|
+
id: z.ZodNumber;
|
|
216
|
+
}, z.core.$strict>;
|
|
217
|
+
/** Discriminated union over every event frame the snapshot stream emits. */
|
|
218
|
+
export declare const snapshotStreamEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
219
|
+
generation: z.ZodNumber;
|
|
220
|
+
generatedAt: z.ZodNumber;
|
|
221
|
+
serverNowMs: z.ZodNumber;
|
|
222
|
+
refresher: z.ZodObject<{
|
|
223
|
+
enabled: z.ZodBoolean;
|
|
224
|
+
intervalMs: z.ZodNumber;
|
|
225
|
+
skewMs: z.ZodNumber;
|
|
226
|
+
nextSweepInMs: z.ZodNumber;
|
|
227
|
+
}, z.core.$strict>;
|
|
228
|
+
credentials: z.ZodArray<z.ZodObject<{
|
|
229
|
+
id: z.ZodNumber;
|
|
230
|
+
provider: z.ZodString;
|
|
231
|
+
credential: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
232
|
+
type: z.ZodLiteral<"oauth">;
|
|
233
|
+
access: z.ZodString;
|
|
234
|
+
expires: z.ZodNumber;
|
|
235
|
+
enterpriseUrl: z.ZodOptional<z.ZodString>;
|
|
236
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
237
|
+
email: z.ZodOptional<z.ZodString>;
|
|
238
|
+
accountId: z.ZodOptional<z.ZodString>;
|
|
239
|
+
refresh: z.ZodLiteral<"__remote__">;
|
|
240
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
241
|
+
type: z.ZodLiteral<"api_key">;
|
|
242
|
+
key: z.ZodString;
|
|
243
|
+
}, z.core.$strict>], "type">;
|
|
244
|
+
identityKey: z.ZodNullable<z.ZodString>;
|
|
245
|
+
rotatesInMs: z.ZodNullable<z.ZodNumber>;
|
|
246
|
+
}, z.core.$strict>>;
|
|
247
|
+
kind: z.ZodLiteral<"snapshot">;
|
|
248
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
249
|
+
kind: z.ZodLiteral<"entry">;
|
|
250
|
+
generation: z.ZodNumber;
|
|
251
|
+
serverNowMs: z.ZodNumber;
|
|
252
|
+
refresher: z.ZodObject<{
|
|
253
|
+
enabled: z.ZodBoolean;
|
|
254
|
+
intervalMs: z.ZodNumber;
|
|
255
|
+
skewMs: z.ZodNumber;
|
|
256
|
+
nextSweepInMs: z.ZodNumber;
|
|
257
|
+
}, z.core.$strict>;
|
|
258
|
+
entry: z.ZodObject<{
|
|
259
|
+
id: z.ZodNumber;
|
|
260
|
+
provider: z.ZodString;
|
|
261
|
+
credential: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
262
|
+
type: z.ZodLiteral<"oauth">;
|
|
263
|
+
access: z.ZodString;
|
|
264
|
+
expires: z.ZodNumber;
|
|
265
|
+
enterpriseUrl: z.ZodOptional<z.ZodString>;
|
|
266
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
267
|
+
email: z.ZodOptional<z.ZodString>;
|
|
268
|
+
accountId: z.ZodOptional<z.ZodString>;
|
|
269
|
+
refresh: z.ZodLiteral<"__remote__">;
|
|
270
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
271
|
+
type: z.ZodLiteral<"api_key">;
|
|
272
|
+
key: z.ZodString;
|
|
273
|
+
}, z.core.$strict>], "type">;
|
|
274
|
+
identityKey: z.ZodNullable<z.ZodString>;
|
|
275
|
+
rotatesInMs: z.ZodNullable<z.ZodNumber>;
|
|
276
|
+
}, z.core.$strict>;
|
|
277
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
278
|
+
kind: z.ZodLiteral<"removed">;
|
|
279
|
+
generation: z.ZodNumber;
|
|
280
|
+
serverNowMs: z.ZodNumber;
|
|
281
|
+
refresher: z.ZodObject<{
|
|
282
|
+
enabled: z.ZodBoolean;
|
|
283
|
+
intervalMs: z.ZodNumber;
|
|
284
|
+
skewMs: z.ZodNumber;
|
|
285
|
+
nextSweepInMs: z.ZodNumber;
|
|
286
|
+
}, z.core.$strict>;
|
|
287
|
+
id: z.ZodNumber;
|
|
288
|
+
}, z.core.$strict>], "kind">;
|
|
141
289
|
export declare const healthzResponseSchema: z.ZodObject<{
|
|
142
290
|
ok: z.ZodBoolean;
|
|
143
291
|
version: z.ZodOptional<z.ZodString>;
|
|
@@ -44,6 +44,45 @@ export interface StoredAuthCredential {
|
|
|
44
44
|
credential: AuthCredential;
|
|
45
45
|
disabledCause: string | null;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Per-credential health record returned by {@link AuthStorage.checkCredentials}.
|
|
49
|
+
*
|
|
50
|
+
* Use this to identify which credential in a multi-account pool is causing
|
|
51
|
+
* auth errors. `ok` is tri-state:
|
|
52
|
+
*
|
|
53
|
+
* - `true` — credential authenticated against the provider's auth-verifying
|
|
54
|
+
* probe (today: the usage endpoint). For OAuth this also exercises refresh
|
|
55
|
+
* when the access token was expired.
|
|
56
|
+
* - `false` — the probe rejected the credential (401/403/refresh failure/etc).
|
|
57
|
+
* `reason` carries the upstream error string.
|
|
58
|
+
* - `null` — no probe is configured for this provider (or the configured
|
|
59
|
+
* probe doesn't support this credential type). The credential's auth
|
|
60
|
+
* status is unverifiable from here.
|
|
61
|
+
*/
|
|
62
|
+
export interface CredentialHealthResult {
|
|
63
|
+
/** Database row id (matches {@link StoredAuthCredential.id}). */
|
|
64
|
+
id: number;
|
|
65
|
+
provider: string;
|
|
66
|
+
type: AuthCredential["type"];
|
|
67
|
+
/** OAuth email if known on the stored credential or surfaced by the probe. */
|
|
68
|
+
email?: string;
|
|
69
|
+
/** OAuth account id / org id if known. */
|
|
70
|
+
accountId?: string;
|
|
71
|
+
/** `true` when the refresh token lives on a remote broker (sentinel was present). */
|
|
72
|
+
remoteRefresh?: true;
|
|
73
|
+
ok: boolean | null;
|
|
74
|
+
/** Failure / unverifiable reason; absent when `ok === true`. */
|
|
75
|
+
reason?: string;
|
|
76
|
+
/** Probe usage report (raw payload stripped) when `ok === true`. */
|
|
77
|
+
report?: Omit<UsageReport, "raw">;
|
|
78
|
+
}
|
|
79
|
+
export interface CheckCredentialsOptions {
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
/** Per-credential probe timeout (ms). Defaults to the configured usage request timeout. */
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
/** Provider → base URL override, same shape as {@link AuthStorage.fetchUsageReports}. */
|
|
84
|
+
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
85
|
+
}
|
|
47
86
|
/**
|
|
48
87
|
* Sentinel value placed in OAuth `refresh` fields when a credential is shared
|
|
49
88
|
* via {@link AuthStorage.exportSnapshot}. Refresh tokens never leave the broker;
|
|
@@ -252,6 +291,10 @@ type AuthApiKeyOptions = {
|
|
|
252
291
|
*/
|
|
253
292
|
signal?: AbortSignal;
|
|
254
293
|
};
|
|
294
|
+
export interface InvalidateCredentialMatchingOptions {
|
|
295
|
+
signal?: AbortSignal;
|
|
296
|
+
sessionId?: string;
|
|
297
|
+
}
|
|
255
298
|
/**
|
|
256
299
|
* Credential storage backed by an AuthCredentialStore.
|
|
257
300
|
* Reads from storage on reload(), manages round-robin credential selection,
|
|
@@ -401,6 +444,27 @@ export declare class AuthStorage {
|
|
|
401
444
|
/** Caller's cancel signal; only rejects this caller, never the shared upstream fetch. */
|
|
402
445
|
signal?: AbortSignal;
|
|
403
446
|
}): Promise<UsageReport[] | null>;
|
|
447
|
+
/**
|
|
448
|
+
* Probe each stored credential against its provider's auth-verifying usage
|
|
449
|
+
* endpoint and report per-credential auth health.
|
|
450
|
+
*
|
|
451
|
+
* Surfaces the identity of failing credentials so callers running a
|
|
452
|
+
* multi-account pool (e.g. a broker-backed auth-gateway) can tell which
|
|
453
|
+
* row is producing 401s. The probe mirrors the per-credential fan-out
|
|
454
|
+
* inside {@link AuthStorage.fetchUsageReports} (OAuth refresh-on-expiry,
|
|
455
|
+
* then `UsageProvider.fetchUsage`) but does NOT swallow errors — every
|
|
456
|
+
* credential gets either `ok: true`, `ok: false` with `reason`, or
|
|
457
|
+
* `ok: null` when no probe is configured for the provider.
|
|
458
|
+
*
|
|
459
|
+
* Iterates sequentially to avoid synchronized N-account fan-out that
|
|
460
|
+
* upstream `/usage` rate limiters (per source IP) treat as a burst.
|
|
461
|
+
*
|
|
462
|
+
* Only inspects active rows from {@link AuthCredentialStore.listAuthCredentials};
|
|
463
|
+
* soft-disabled rows are already known-bad and don't need a network probe.
|
|
464
|
+
* Environment-variable API keys are not enumerated — the caller's intent
|
|
465
|
+
* here is "which of my stored credentials is broken".
|
|
466
|
+
*/
|
|
467
|
+
checkCredentials(options?: CheckCredentialsOptions): Promise<CredentialHealthResult[]>;
|
|
404
468
|
/**
|
|
405
469
|
* Marks the current session's credential as temporarily blocked due to usage limits.
|
|
406
470
|
* Uses usage reports to determine accurate reset time when available.
|
|
@@ -429,6 +493,7 @@ export declare class AuthStorage {
|
|
|
429
493
|
* 6. Fallback resolver (models.yml custom providers, last-resort)
|
|
430
494
|
*/
|
|
431
495
|
getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
496
|
+
invalidateCredentialMatching(provider: string, apiKey: string, options?: InvalidateCredentialMatchingOptions): Promise<boolean>;
|
|
432
497
|
invalidateCredentialMatching(provider: string, apiKey: string, signal?: AbortSignal): Promise<boolean>;
|
|
433
498
|
/**
|
|
434
499
|
* Build a redacted snapshot of all loaded credentials for the auth-broker
|
|
@@ -12,6 +12,30 @@ export declare function normalizeResponsesToolCallIdForTransform(id: string, mod
|
|
|
12
12
|
export declare function collectKnownCallIds(messages: ResponseInput): Set<string>;
|
|
13
13
|
/** Scan replay items for call_ids that were originally custom tool calls. */
|
|
14
14
|
export declare function collectCustomCallIds(messages: ResponseInput): Set<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Convert orphan `function_call_output` / `custom_tool_call_output` items —
|
|
17
|
+
* those whose `call_id` has no matching preceding `function_call` /
|
|
18
|
+
* `custom_tool_call` in the same input — into assistant text notes.
|
|
19
|
+
*
|
|
20
|
+
* The Responses API rejects unpaired outputs with
|
|
21
|
+
* `400 No tool call found for function call output with call_id …`. Orphans
|
|
22
|
+
* sneak in through two paths today:
|
|
23
|
+
*
|
|
24
|
+
* - A previous turn's `providerPayload` snapshot replaces the input array via
|
|
25
|
+
* the `dt: false` splice (see {@link convertConversationMessages}), wiping
|
|
26
|
+
* the matching `function_call` while leaving the matching
|
|
27
|
+
* `function_call_output` queued in a later `toolResult`.
|
|
28
|
+
* - A locally-rejected tool call (argument-validation failure, hook reject,
|
|
29
|
+
* aborted turn before the call streamed) produces a tool result without a
|
|
30
|
+
* `function_call` ever landing in any persisted provider payload.
|
|
31
|
+
*
|
|
32
|
+
* Dropping the result loses information the model needs to recover; sending
|
|
33
|
+
* it as-is 400s the request. Folding it into an assistant `message` preserves
|
|
34
|
+
* the payload (call_id + truncated output) while staying within the Responses
|
|
35
|
+
* input grammar. Matches the behavior of {@link transformRequestBody} in the
|
|
36
|
+
* codex provider — issue #1351 / regression of #472.
|
|
37
|
+
*/
|
|
38
|
+
export declare function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput;
|
|
15
39
|
export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean): ResponseInputContent[] | undefined;
|
|
16
40
|
export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>): ResponseInput;
|
|
17
41
|
export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.3.
|
|
4
|
+
"version": "15.3.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@oh-my-pi/pi-utils": "15.3.
|
|
46
|
+
"@oh-my-pi/pi-utils": "15.3.2",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `omp auth-broker status` (liveness checks). All endpoints except
|
|
6
6
|
* `/v1/healthz` require a bearer token.
|
|
7
7
|
*/
|
|
8
|
+
import { readSseEvents } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { ZodType, infer as zInfer } from "zod/v4";
|
|
9
10
|
import type { AuthCredential } from "../auth-storage";
|
|
10
11
|
import type {
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
CredentialUploadResponse,
|
|
16
17
|
HealthzResponse,
|
|
17
18
|
SnapshotResponse,
|
|
19
|
+
SnapshotStreamEvent,
|
|
18
20
|
UsageResponse,
|
|
19
21
|
} from "./types";
|
|
20
22
|
import {
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
credentialUploadResponseSchema,
|
|
24
26
|
healthzResponseSchema,
|
|
25
27
|
snapshotResponseSchema,
|
|
28
|
+
snapshotStreamEventSchema,
|
|
26
29
|
usageResponseSchema,
|
|
27
30
|
} from "./wire-schemas";
|
|
28
31
|
|
|
@@ -50,6 +53,18 @@ export class AuthBrokerError extends Error {
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Thrown when a broker responds 404 to `GET /v1/snapshot/stream` — old
|
|
58
|
+
* brokers that predate the SSE endpoint. Callers (`RemoteAuthCredentialStore`)
|
|
59
|
+
* detect this sentinel to fall back to long-polling permanently.
|
|
60
|
+
*/
|
|
61
|
+
export class AuthBrokerStreamUnsupportedError extends AuthBrokerError {
|
|
62
|
+
constructor(message = "Auth broker does not support /v1/snapshot/stream") {
|
|
63
|
+
super(message, { status: 404 });
|
|
64
|
+
this.name = "AuthBrokerStreamUnsupportedError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
export interface FetchSnapshotOptions {
|
|
54
69
|
ifGenerationGt?: number;
|
|
55
70
|
waitMs?: number;
|
|
@@ -128,6 +143,88 @@ export class AuthBrokerClient {
|
|
|
128
143
|
return { status: 200, snapshot, generation: etagGeneration ?? snapshot.generation };
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Subscribe to the broker's SSE snapshot stream. The first frame is always
|
|
148
|
+
* a full `snapshot`; subsequent frames are `entry` upserts / refreshes or
|
|
149
|
+
* `removed` deletes. Caller controls lifecycle via `opts.signal`.
|
|
150
|
+
*
|
|
151
|
+
* Throws {@link AuthBrokerStreamUnsupportedError} when the broker responds
|
|
152
|
+
* 404 — older brokers predate this endpoint and the caller should fall back
|
|
153
|
+
* to long-polling for the remainder of its lifetime.
|
|
154
|
+
*/
|
|
155
|
+
async *openSnapshotStream(opts: { signal?: AbortSignal } = {}): AsyncGenerator<SnapshotStreamEvent> {
|
|
156
|
+
const url = `${this.#baseUrl}/v1/snapshot/stream`;
|
|
157
|
+
const headers: Record<string, string> = {
|
|
158
|
+
Accept: "text/event-stream",
|
|
159
|
+
Authorization: `Bearer ${this.#token}`,
|
|
160
|
+
};
|
|
161
|
+
if (opts.signal?.aborted) {
|
|
162
|
+
throw new AuthBrokerError("Auth broker request aborted", { cause: opts.signal.reason });
|
|
163
|
+
}
|
|
164
|
+
// No timeout: this connection is intentionally long-lived. Caller's signal
|
|
165
|
+
// is the only cancel path.
|
|
166
|
+
const response = await this.#fetch(url, { method: "GET", headers, signal: opts.signal });
|
|
167
|
+
if (response.status === 404) {
|
|
168
|
+
// Drain the body so the socket can be reused; tiny payload.
|
|
169
|
+
await response.text().catch(() => {});
|
|
170
|
+
throw new AuthBrokerStreamUnsupportedError();
|
|
171
|
+
}
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const text = await response.text().catch(() => "");
|
|
174
|
+
throw new AuthBrokerError(`Auth broker stream failed: ${response.status} ${response.statusText}`, {
|
|
175
|
+
status: response.status,
|
|
176
|
+
body: text,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (!response.body) {
|
|
180
|
+
throw new AuthBrokerError("Auth broker stream response had no body", { status: response.status });
|
|
181
|
+
}
|
|
182
|
+
const contentType = response.headers.get("content-type")?.toLowerCase();
|
|
183
|
+
if (contentType?.split(";", 1)[0].trim() !== "text/event-stream") {
|
|
184
|
+
await response.body.cancel().catch(() => {});
|
|
185
|
+
throw new AuthBrokerError("Auth broker stream returned non-SSE response", {
|
|
186
|
+
status: response.status,
|
|
187
|
+
body: contentType ?? "",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let sawFirstEvent = false;
|
|
192
|
+
for await (const sse of readSseEvents(response.body, opts.signal)) {
|
|
193
|
+
if (sse.event === null && sse.data === "") continue; // keepalive comment frames
|
|
194
|
+
let parsed: unknown;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(sse.data);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
throw new AuthBrokerError("Auth broker stream returned malformed JSON", {
|
|
199
|
+
body: sse.data,
|
|
200
|
+
cause: err,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const validated = snapshotStreamEventSchema.safeParse(parsed);
|
|
204
|
+
if (!validated.success) {
|
|
205
|
+
throw new AuthBrokerError("Auth broker stream event failed schema validation", {
|
|
206
|
+
body: validated.error.message,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const event = validated.data;
|
|
210
|
+
if (!sawFirstEvent) {
|
|
211
|
+
sawFirstEvent = true;
|
|
212
|
+
if (event.kind !== "snapshot") {
|
|
213
|
+
throw new AuthBrokerError("Auth broker stream did not start with snapshot", { body: sse.data });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
yield event;
|
|
217
|
+
}
|
|
218
|
+
if (!opts.signal?.aborted) {
|
|
219
|
+
throw new AuthBrokerError(
|
|
220
|
+
sawFirstEvent
|
|
221
|
+
? "Auth broker stream ended unexpectedly"
|
|
222
|
+
: "Auth broker stream ended before initial snapshot",
|
|
223
|
+
{ status: response.status },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
131
228
|
fetchUsage(signal?: AbortSignal): Promise<UsageResponse> {
|
|
132
229
|
// Validates the envelope (`generatedAt`, `reports[].provider`, `limits`,
|
|
133
230
|
// `metadata`) but leaves provider-specific extension fields permissive so
|