@oh-my-pi/pi-ai 18.11.1 → 18.11.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 (53) hide show
  1. package/CHANGELOG.md +23 -46
  2. package/README.md +2 -0
  3. package/dist/types/auth-gateway/clients.d.ts +67 -0
  4. package/dist/types/auth-gateway/dispatch.d.ts +28 -10
  5. package/dist/types/auth-gateway/http.d.ts +2 -0
  6. package/dist/types/auth-gateway/index.d.ts +1 -0
  7. package/dist/types/auth-gateway/server.d.ts +9 -4
  8. package/dist/types/auth-gateway/session-state.d.ts +6 -0
  9. package/dist/types/auth-gateway/types.d.ts +15 -1
  10. package/dist/types/judgment/native.d.ts +15 -8
  11. package/dist/types/judgment/systemone.d.ts +9 -2
  12. package/dist/types/judgment/wire.d.ts +25 -3
  13. package/dist/types/providers/openai-shared.d.ts +16 -0
  14. package/dist/types/providers/systemone-server.d.ts +5 -2
  15. package/dist/types/providers/transform-messages.d.ts +4 -0
  16. package/dist/types/utils.d.ts +1 -1
  17. package/package.json +6 -6
  18. package/src/auth-gateway/clients.ts +318 -0
  19. package/src/auth-gateway/dispatch.ts +66 -35
  20. package/src/auth-gateway/http.ts +11 -5
  21. package/src/auth-gateway/index.ts +1 -0
  22. package/src/auth-gateway/routes/embeddings.ts +4 -10
  23. package/src/auth-gateway/routes/images.ts +4 -10
  24. package/src/auth-gateway/routes/rerank.ts +4 -10
  25. package/src/auth-gateway/routes/speech.ts +10 -10
  26. package/src/auth-gateway/routes/systemone.ts +6 -11
  27. package/src/auth-gateway/routes/transcriptions.ts +4 -2
  28. package/src/auth-gateway/routes/video.ts +12 -15
  29. package/src/auth-gateway/server.ts +114 -34
  30. package/src/auth-gateway/session-state.ts +14 -0
  31. package/src/auth-gateway/types.ts +15 -1
  32. package/src/images/openai-images.ts +9 -6
  33. package/src/judgment/native.ts +109 -46
  34. package/src/judgment/systemone.ts +122 -8
  35. package/src/judgment/wire.ts +27 -3
  36. package/src/providers/amazon-bedrock.ts +1 -1
  37. package/src/providers/anthropic.ts +1 -1
  38. package/src/providers/devin.ts +10 -4
  39. package/src/providers/google-shared.ts +2 -6
  40. package/src/providers/ollama.ts +7 -5
  41. package/src/providers/openai-codex-responses.ts +8 -9
  42. package/src/providers/openai-completions.ts +78 -8
  43. package/src/providers/openai-responses.ts +2 -1
  44. package/src/providers/openai-shared.ts +35 -4
  45. package/src/providers/systemone-server.ts +45 -9
  46. package/src/providers/transform-messages.ts +16 -11
  47. package/src/usage/claude.ts +2 -9
  48. package/src/usage/github-copilot.ts +26 -4
  49. package/src/usage/openai-codex.ts +2 -7
  50. package/src/usage/xai-oauth.ts +2 -9
  51. package/src/utils/leaked-thinking-stream.ts +10 -0
  52. package/src/utils/validation.ts +22 -8
  53. package/src/utils.ts +1 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.11.2] - 2026-10-16
6
+
7
+ ### Breaking Changes
8
+
9
+ - `resolveGatewayApiKey` and `buildGatewayApiKeyResolver` (`@oh-my-pi/pi-ai/auth-gateway`) take the request's route options (`{ storage, client? }`) instead of an `AuthStorage`, their resolver callback receives the resolved credential, and `resolveGatewayAccount` takes that credential ([#15900](https://github.com/can1357/oh-my-pi/pull/15900) by [@alphastorm](https://github.com/alphastorm))
10
+
11
+ ### Added
12
+
13
+ - Added scoped clients to `startAuthGateway` (`clients`, `setClients`): each client bearer is limited to its own models and OAuth accounts, with no fallback to other accounts or API keys, and request log lines name the client and the credential a request started on ([#5098](https://github.com/can1357/oh-my-pi/issues/5098), [#15900](https://github.com/can1357/oh-my-pi/pull/15900) by [@alphastorm](https://github.com/alphastorm))
14
+ - Added experimental native image judgments for TypeSafe Jev's `jev-latest`, `jev-preview`, `jev-1.13.0`, and `jev-1.14.0`, plus OpenRouter's `typesafe/jev-1.13`, `typesafe/jev-1.14`, and `~typesafe/jev-latest` models; images outside Jev's documented size bounds fall through to a vision chat judge instead of failing ([#15887](https://github.com/can1357/oh-my-pi/pull/15887) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy))
15
+
16
+ ### Fixed
17
+
18
+ - Fixed error results from non-first-party providers regaining partial output the provider had discarded; an error with empty content now stays empty ([#14516](https://github.com/can1357/oh-my-pi/issues/14516), [#14572](https://github.com/can1357/oh-my-pi/pull/14572) by [@oleg494](https://github.com/oleg494))
19
+ - Fixed the auth gateway's `POST /v1/audio/speech` answering 502 for requests the speech adapter rejects (unsupported format, over-long input); they now get 400 `invalid_request_error` ([#14508](https://github.com/can1357/oh-my-pi/pull/14508) by [@F0Rextasy](https://github.com/F0Rextasy))
20
+ - Fixed a single-string tool call with two or more mislabelled string fields silently dispatching whichever field came first in the JSON; such ambiguous calls now fail validation and the model sees its own arguments back ([#14414](https://github.com/can1357/oh-my-pi/pull/14414) by [@F0Rextasy](https://github.com/F0Rextasy))
21
+ - Fixed GLM-5.2 on Ollama Cloud losing its reasoning between turns; Ollama Cloud now keeps assistant thinking in history for models verified to accept it ([#14440](https://github.com/can1357/oh-my-pi/pull/14440) by [@Dante-dan](https://github.com/Dante-dan))
22
+ - Fixed GPT Image generation and edits failing on gateways that validate request parameters, such as LiteLLM; `response_format` is no longer sent to GPT Image models ([#14619](https://github.com/can1357/oh-my-pi/pull/14619) by [@jimhester](https://github.com/jimhester))
23
+ - Fixed OpenRouter errors such as "Access denied by security policy" dropping the routed upstream's own explanation from `error.metadata.raw` ([#10906](https://github.com/can1357/oh-my-pi/issues/10906), [#14435](https://github.com/can1357/oh-my-pi/pull/14435) by [@F0Rextasy](https://github.com/F0Rextasy))
24
+ - Fixed GitHub Copilot usage reporting an AI-credit allowance as "Premium Requests" in request units; the unit each billing item reports is now honoured ([#13849](https://github.com/can1357/oh-my-pi/issues/13849), [#14501](https://github.com/can1357/oh-my-pi/pull/14501) by [@F0Rextasy](https://github.com/F0Rextasy))
25
+ - Fixed an OpenAI-compatible server's `finish_reason: "repetition"` or `stop_reason: "repetition_detected"` not being treated as a thinking loop, so the degenerate partial reasoning was kept and replayed into the next turn ([#12525](https://github.com/can1357/oh-my-pi/issues/12525), [#14535](https://github.com/can1357/oh-my-pi/pull/14535) by [@F0Rextasy](https://github.com/F0Rextasy))
26
+
5
27
  ## [18.11.1] - 2026-10-15
6
28
 
7
29
  ### Added
@@ -2357,49 +2379,4 @@
2357
2379
 
2358
2380
  - Added utility functions to strip schema descriptions for optimized LLM context usage
2359
2381
 
2360
- ## [16.0.10] - 2026-06-18
2361
-
2362
- ### Added
2363
-
2364
- - Replaced the old legacy XML-ish `pi` owned tool-calling dialect with the new sigil-delimited format (`§` call header with inline `key=value` scalars, `«…»` verbatim body fence for the dominant string argument, `¤` reasoning, `‡‡` tool result) using single-token markers that never occur in source code. Verbatim fences escalate Markdown-style (`««…»»`) so re-rendered history never collides with payload content, and the scanner gates a bare `§` on an exact known-tool name to avoid swallowing prose. Round-trips and streams through the existing scanner contract at ~46% fewer tokens than the legacy format on typical calls; selectable via `tools.format` or `PI_DIALECT=pi`.
2365
-
2366
- ### Changed
2367
-
2368
- - Updated `pi` dialect formatting to use a token-frugal, sigil-delimited format (`§`, `¤`, `‡‡`)
2369
- - Updated `pi` dialect body fences to automatically escalate when content contains fence markers
2370
- - Changed `pi` dialect tool results response format to `‡‡` blocks
2371
-
2372
- ### Fixed
2373
-
2374
- - Fixed Bedrock application inference profile ARNs to route requests to the ARN's region instead of the default Bedrock runtime region. ([#3004](https://github.com/can1357/oh-my-pi/issues/3004))
2375
-
2376
- ## [16.0.9] - 2026-06-18
2377
-
2378
- ### Fixed
2379
-
2380
- - Fixed OAuth login replacing all other active accounts for the same provider, allowing multiple OAuth accounts to coexist concurrently.
2381
- - Fixed legacy `api_key` credentials not being replaced/disabled atomically upon upgrading to OAuth login.
2382
- - Fixed a logic issue where AuthStorage lost session-to-credential stickiness upon CLI restarts, causing cold-starts for server-side prompt cache (KV cache) and wasting tokens.
2383
- - Fixed GitHub Copilot Responses requests rejecting image inputs that carry the `detail: "original"` hint with an HTTP 400 by degrading the hint to `"auto"` for hosts that do not support it; other hosts still preserve native-resolution frames (snapcompact). ([#2822](https://github.com/can1357/oh-my-pi/issues/2822))
2384
-
2385
- ## [16.0.8] - 2026-06-18
2386
-
2387
- ### Fixed
2388
-
2389
- - Improved reliability of auth-broker snapshot loading by implementing a robust manual schema check
2390
- - Fixed MCP tool argument validation to drop optional empty-string parameters before schema validation, matching the existing optional null handling and avoiding pattern/type failures for omitted model-filled fields. ([#2981](https://github.com/can1357/oh-my-pi/issues/2981))
2391
- - Fixed API-key credential replacement to hard-delete superseded disabled `api_key` rows so `auth_credentials` does not grow indefinitely after key rotation. ([#2941](https://github.com/can1357/oh-my-pi/issues/2941))
2392
- - Fixed Cursor provider streaming to close text blocks before tool calls so post-tool text opens a new content block and TUI transcript cards render inline instead of grouped near the bottom. ([#2924](https://github.com/can1357/oh-my-pi/issues/2924))
2393
-
2394
- ## [16.0.7] - 2026-06-18
2395
-
2396
- ### Changed
2397
-
2398
- - Switched Google OAuth callback hostname from `localhost` to `127.0.0.1` to prevent IPv6 loopback fallback delays and proxy routing interception.
2399
-
2400
- ### Fixed
2401
-
2402
- - Fixed OpenCode Go usage reporting to synthesize `/usage` limits from OMP-observed request costs for the 5h, weekly, and monthly provider caps. ([#2942](https://github.com/can1357/oh-my-pi/issues/2942))
2403
- - Fixed MiniMax Anthropic-compatible requests to serialize adaptive thinking without an invalid Anthropic `output_config.effort` tier ([#2928](https://github.com/can1357/oh-my-pi/issues/2928)).
2404
-
2405
- Older entries are archived in [packages/ai/CHANGELOG.md@592e894eb29e](https://github.com/can1357/oh-my-pi/blob/592e894eb29ed3db2ee2dae71ec7d72ac13c64c8/packages/ai/CHANGELOG.md).
2382
+ Older entries are archived in [packages/ai/CHANGELOG.md@aa43b0057f82](https://github.com/can1357/oh-my-pi/blob/aa43b0057f82cb167459f8e6b41c5ec9f4e0f91d/packages/ai/CHANGELOG.md).
package/README.md CHANGED
@@ -565,6 +565,8 @@ if (message.stopReason === "error" || message.stopReason === "aborted") {
565
565
  }
566
566
  ```
567
567
 
568
+ If the provider discards partial output by returning an error message with empty `content`, the final result remains empty. Leaked-thinking healing does not reconstruct discarded blocks from earlier streaming deltas.
569
+
568
570
  ### Aborting Requests
569
571
 
570
572
  The abort signal allows you to cancel in-progress requests. Aborted requests have `stopReason === 'aborted'`:
@@ -0,0 +1,67 @@
1
+ import type { SessionsApi } from "../auth/types.js";
2
+ import type { Api, Model } from "../types.js";
3
+ /** One scoped client. */
4
+ export interface AuthGatewayClient {
5
+ /** Operator-chosen name, logged with the client's requests. */
6
+ name: string;
7
+ /** Hex SHA-256 digest of the client's bearer token. */
8
+ tokenSha256: string;
9
+ /** `provider/id` or `provider/*` grants; absent allows every served model, `[]` none. */
10
+ models?: readonly string[];
11
+ /** Provider id → OAuth identity keys the client's requests to that provider may use. */
12
+ accounts?: Readonly<Record<string, readonly string[]>>;
13
+ }
14
+ /** An admitted scoped client, as the gateway's routes see it. */
15
+ export interface AuthGatewayClientScope {
16
+ readonly name: string;
17
+ /** False when the client may call every served model. */
18
+ readonly limitsModels: boolean;
19
+ /** Whether the client may call `model`. */
20
+ allows(model: Model<Api>): boolean;
21
+ /**
22
+ * Session id the client's credential lookups for `provider` resolve under:
23
+ * its own restricted session when `accounts` lists the provider, otherwise
24
+ * the request's `sessionId`. Throws for a provider `accounts` omits, and for
25
+ * a bound provider once the restriction is lifted, so a lookup can never
26
+ * resolve unrestricted on the client's behalf.
27
+ */
28
+ credentialSessionId(provider: string, sessionId: string): string;
29
+ /**
30
+ * Hold the client's account restriction while a request uses it; call the
31
+ * returned release once the request can no longer resolve a credential. A
32
+ * reload that retires the client lifts the restriction only after every
33
+ * hold is released. Take the first hold in the same tick as
34
+ * `authenticate`, before a reload can retire the client.
35
+ */
36
+ retain(): () => void;
37
+ }
38
+ /**
39
+ * Parse a clients file: a JSON object mapping each client name to
40
+ * `{ tokenSha256, models?, accounts? }`. Any malformed entry or unknown field
41
+ * rejects the whole file, since a misspelled `models` would otherwise allow
42
+ * every model. Errors name the offending field, never its value.
43
+ */
44
+ export declare function parseAuthGatewayClients(text: string): AuthGatewayClient[];
45
+ /** Hex SHA-256 digest of a bearer token, as `tokenSha256` records it. */
46
+ export declare function hashAuthGatewayToken(token: string): string;
47
+ type RestrictionSessions = Pick<SessionsApi, "restrict" | "unrestrict">;
48
+ /**
49
+ * The scoped clients one gateway admits, looked up by the digest of the
50
+ * presented bearer.
51
+ */
52
+ export declare class AuthGatewayClientTable {
53
+ #private;
54
+ constructor(sessions: RestrictionSessions, gatewayTokens: Iterable<string>);
55
+ /**
56
+ * Replace the admitted clients. An invalid table throws and changes
57
+ * nothing. An unchanged client keeps its scope; a new or changed one is
58
+ * restricted before its token is admitted; the scopes left out are retired.
59
+ * Ignored once the table is closed.
60
+ */
61
+ set(clients: readonly AuthGatewayClient[]): void;
62
+ /** The client `token` authenticates, if any. */
63
+ authenticate(token: string): AuthGatewayClientScope | undefined;
64
+ /** Admit no more clients and retire every scope. */
65
+ close(): void;
66
+ }
67
+ export {};
@@ -3,6 +3,7 @@ import type { AuthStorage } from "../auth-storage.js";
3
3
  import { type GatewayErrorClassification } from "../error/gateway.js";
4
4
  import type { Api, FetchImpl, Model, Usage } from "../types.js";
5
5
  import type { ClientUsageIdentity } from "../usage.js";
6
+ import type { AuthGatewayClientScope } from "./clients.js";
6
7
  import type { AuthGatewayServerOptions } from "./types.js";
7
8
  export type ModelResolver = (modelId: string) => Model<Api> | undefined;
8
9
  /** What the gateway's routes need, whatever transport carries the requests. */
@@ -21,7 +22,15 @@ export interface AuthGatewayRouteOptions {
21
22
  excludeProviders?: ReadonlySet<string>;
22
23
  /** Upstream transport for every provider call; defaults to global `fetch`. Test seam. */
23
24
  fetch?: FetchImpl;
25
+ /**
26
+ * The scoped client that sent this request, set per request by the HTTP
27
+ * server: it narrows `resolveModel` / `listModels`, and every credential
28
+ * lookup below resolves under its {@link AuthGatewayClientScope.credentialSessionId}.
29
+ */
30
+ client?: AuthGatewayClientScope;
24
31
  }
32
+ /** The route options a request's credential lookups read. */
33
+ export type AuthGatewayCredentialOptions = Pick<AuthGatewayRouteOptions, "storage" | "client">;
25
34
  /** The HTTP server's options: the routes' plus its listener and inbound auth. */
26
35
  export interface AuthGatewayBootOptions extends AuthGatewayServerOptions, AuthGatewayRouteOptions {
27
36
  }
@@ -37,13 +46,20 @@ export declare function normalizeClientSessionKey(clientKey: string | undefined)
37
46
  *
38
47
  * `markUsageLimitReached` and the auth-retry resolver switch a session to a
39
48
  * sibling credential, so the provider state retained for that session can
40
- * outlive the account that taught it. OAuth rows expose an account id / email
41
- * that survives token refresh — fingerprinting the bearer instead would look
49
+ * outlive the account that taught it. A stored credential keeps its durable
50
+ * row id across token refreshes — fingerprinting the bearer instead would look
42
51
  * like a rotation every time a token refreshes and discard the retained
43
- * lessons for nothing. Key-based rows fall back to a hash of the key, never
44
- * the key itself: this value is held for the lifetime of the entry.
52
+ * lessons for nothing. The id comes from the selection itself, not the
53
+ * session's current pin, which a concurrent request on the same session may
54
+ * have moved. Unstored keys fall back to a hash of the key, never the key
55
+ * itself: this value is held for the lifetime of the entry.
45
56
  */
46
- export declare function resolveGatewayAccount(storage: AuthStorage, provider: string, sessionId: string, apiKey: string): string;
57
+ export declare function resolveGatewayAccount(resolved: ResolvedApiKey): string;
58
+ /** Who sent a request and which stored credential it started on, for the `auth-gateway request` log line. */
59
+ export declare function gatewayRequestLogFields(opts: AuthGatewayCredentialOptions, resolved: ResolvedApiKey): {
60
+ client?: string;
61
+ credentialId?: number;
62
+ };
47
63
  /**
48
64
  * Resolve the credential for one request from broker-backed storage.
49
65
  *
@@ -53,7 +69,7 @@ export declare function resolveGatewayAccount(storage: AuthStorage, provider: st
53
69
  * stored token before dispatch. Storage failures map through
54
70
  * {@link classifyGatewayError}; a provider without any credential is a 401.
55
71
  */
56
- export declare function resolveGatewayApiKey(storage: AuthStorage, model: Model<Api>, sessionId: string, signal: AbortSignal, peer: string): Promise<ResolvedApiKey | GatewayErrorClassification>;
72
+ export declare function resolveGatewayApiKey(opts: AuthGatewayCredentialOptions, model: Model<Api>, sessionId: string, signal: AbortSignal, peer: string): Promise<ResolvedApiKey | GatewayErrorClassification>;
57
73
  /**
58
74
  * Build the {@link ApiKeyResolver} handed to a pi-ai client for a gateway
59
75
  * request. Drives the central a/b/c auth-retry policy server-side:
@@ -67,11 +83,13 @@ export declare function resolveGatewayApiKey(storage: AuthStorage, model: Model<
67
83
  * `lastKey` tracks the most recent bearer so the switch step invalidates the
68
84
  * credential that actually failed; a retry context's `previousKey` wins, since
69
85
  * concurrent operations sharing one resolver (split judgment chunks) each know
70
- * the bearer they sent. `onResolvedKey` observes every rotation; routes that
71
- * retain provider session state use it to re-key the account lease, one-shot
72
- * routes pass `undefined`.
86
+ * the bearer they sent. Every step resolves under the scoped client's
87
+ * credential session when `opts.client` binds the provider, so refresh,
88
+ * blocking and sibling rotation stay inside its accounts. `onResolvedKey`
89
+ * observes every rotation; routes that retain provider session state use it
90
+ * to re-key the account lease, one-shot routes pass `undefined`.
73
91
  */
74
- export declare function buildGatewayApiKeyResolver(storage: AuthStorage, model: Model<Api>, sessionId: string, initialKey: ResolvedApiKey, requestSignal: AbortSignal, format: string, peer: string, onResolvedKey?: (apiKey: string) => void): ApiKeyResolver;
92
+ export declare function buildGatewayApiKeyResolver(opts: AuthGatewayCredentialOptions, model: Model<Api>, sessionId: string, initialKey: ResolvedApiKey, requestSignal: AbortSignal, format: string, peer: string, onResolvedKey?: (resolved: ResolvedApiKey) => void): ApiKeyResolver;
75
93
  /**
76
94
  * Attribute one settled upstream request to the originating client via the
77
95
  * broker's observed-usage channel (`AuthStorage.usage.observe`, batched
@@ -24,6 +24,8 @@ export declare function hasMisplacedBearer(req: Request, url: URL, tokens: Reado
24
24
  * of the longer input so length itself doesn't leak via timing.
25
25
  */
26
26
  export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean;
27
+ /** The token of an `Authorization: Bearer <token>` header, trimmed; `undefined` without one. */
28
+ export declare function readBearerToken(req: Request): string | undefined;
27
29
  export declare function isAuthorized(req: Request, tokens: ReadonlySet<string>): boolean;
28
30
  /**
29
31
  * Extract allow-listed passthrough headers from an inbound request. Keys are
@@ -1,3 +1,4 @@
1
+ export * from "./clients.js";
1
2
  export * from "./dispatch.js";
2
3
  export * from "./http.js";
3
4
  export * from "./session-state.js";
@@ -27,12 +27,17 @@
27
27
  * answers the `/v1/*` routes for any transport: {@link startAuthGateway}
28
28
  * serves them over HTTP behind bearer auth, `stdio.ts` over JSON lines.
29
29
  */
30
+ import { type AuthGatewayClientScope } from "./clients.js";
30
31
  import { type AuthGatewayBootOptions, type AuthGatewayRouteOptions } from "./dispatch.js";
31
32
  import type { AuthGatewayServerHandle } from "./types.js";
32
33
  /** The gateway's `/v1/*` routes, for a transport that has already admitted the caller. */
33
34
  export interface AuthGatewayRouter {
34
- /** Answers one request; `peer` names the caller in logs. Never rejects: a crashed route answers 500. */
35
- route(req: Request, peer: string): Promise<Response>;
35
+ /**
36
+ * Answers one request; `peer` names the caller in logs, `client` is the
37
+ * scoped client the transport admitted, if any. Never rejects: a crashed
38
+ * route answers 500.
39
+ */
40
+ route(req: Request, peer: string, client?: AuthGatewayClientScope): Promise<Response>;
36
41
  /**
37
42
  * Closes the retained provider session state (Codex WebSockets, GitLab Duo
38
43
  * workflows), whose sockets and timers would otherwise keep the process alive.
@@ -40,8 +45,8 @@ export interface AuthGatewayRouter {
40
45
  close(): void;
41
46
  }
42
47
  /**
43
- * The gateway's routes over `opts`, owning their per-session provider state:
48
+ * The gateway's routes over `hostOpts`, owning their per-session provider state:
44
49
  * two routers in one process never share (or tear down) each other's.
45
50
  */
46
- export declare function createAuthGatewayRouter(opts: AuthGatewayRouteOptions): AuthGatewayRouter;
51
+ export declare function createAuthGatewayRouter(hostOpts: AuthGatewayRouteOptions): AuthGatewayRouter;
47
52
  export declare function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServerHandle;
@@ -77,6 +77,12 @@ export interface AuthGatewaySessionStateRequest {
77
77
  * resolves a sibling credential.
78
78
  */
79
79
  account: string;
80
+ /**
81
+ * Name of the scoped gateway client that sent the request, if any. A
82
+ * client's conversations never share retained state with another client's,
83
+ * or with unscoped traffic, even when their keys or histories match.
84
+ */
85
+ client?: string;
80
86
  }
81
87
  /**
82
88
  * Bounded per-session provider state, owned by one gateway server instance.
@@ -1,4 +1,5 @@
1
1
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
2
+ import type { AuthGatewayClient } from "./clients.js";
2
3
  import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Context, OpenAIResponseInclude, ServiceTier, TokenTaskBudget } from "../types.js";
3
4
  /**
4
5
  * Wire types for the omp auth-gateway.
@@ -123,8 +124,14 @@ export interface AuthGatewayFormatModule {
123
124
  export interface AuthGatewayServerOptions {
124
125
  /** Listen address. Default `127.0.0.1:4000`. */
125
126
  bind?: string;
126
- /** Accept any of these bearer tokens. Empty allows unauthenticated calls. */
127
+ /** Accept any of these bearer tokens. Empty allows unauthenticated calls unless `clients` is set. */
127
128
  bearerTokens: string[];
129
+ /**
130
+ * Scoped clients, each admitted by its own bearer and limited to its models
131
+ * and accounts (see `clients.ts`). Any table, even an empty one, requires a
132
+ * bearer on every request. Throws at startup when invalid.
133
+ */
134
+ clients?: readonly AuthGatewayClient[];
128
135
  /** Honor forwarded peer headers only when the connecting proxy is trusted. Default false. */
129
136
  trustProxyHeaders?: boolean;
130
137
  /** Version surfaced on `/healthz`. */
@@ -135,4 +142,11 @@ export interface AuthGatewayServerHandle {
135
142
  port: number;
136
143
  hostname: string;
137
144
  close(): Promise<void>;
145
+ /**
146
+ * Replace the scoped clients. A removed or changed token is refused from
147
+ * the next request on; a request already in flight keeps the models and
148
+ * accounts it was admitted with, retries included, until it finishes.
149
+ * Throws, changing nothing, when the table is invalid. Turns bearer auth on.
150
+ */
151
+ setClients(clients: readonly AuthGatewayClient[]): void;
138
152
  }
@@ -3,7 +3,8 @@ import { type ApiKey } from "../auth-retry.js";
3
3
  import * as AIError from "../error/index.js";
4
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
- import type { JudgmentWire } from "./wire.js";
6
+ import type { JudgmentImageAssets, JudgmentWire } from "./wire.js";
7
+ export type { JudgmentImageAssets } from "./wire.js";
7
8
  export declare const TYPESAFE_PROVIDER = "typesafe";
8
9
  export declare const TYPESAFE_DEFAULT_MODEL = "jev-latest";
9
10
  declare const JUDGMENT_WIRES: {
@@ -19,11 +20,16 @@ export declare function isJudgmentApi(api: Api): api is JudgmentApi;
19
20
  /** Whether both the native protocol and catalog model support image input. */
20
21
  export declare function nativeJudgmentSupportsImages(model: Model): boolean;
21
22
  /**
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.
23
+ * Whether `model`'s native judgment API takes these exact images: image support plus every documented
24
+ * per-dialect bound. A judge chain skips a candidate that would refuse them instead of failing on it.
25
+ */
26
+ export declare function nativeJudgmentAcceptsImages(model: Model, images: readonly ImageContent[]): boolean;
27
+ /**
28
+ * Reject images that `model`'s native judgment API cannot take — no image support, an unsupported or
29
+ * unreferenced asset, or a documented per-dialect cap exceeded — before any credential or upstream work.
24
30
  * @throws {AIError.ValidationError} naming the reason.
25
31
  */
26
- export declare function checkNativeJudgmentImages(model: Model, images: readonly ImageContent[]): void;
32
+ export declare function checkNativeJudgmentImages(model: Model, request: JudgmentRequest, imageAssets?: JudgmentImageAssets): void;
27
33
  /** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
28
34
  export declare function typesafeBaseUrl(): string;
29
35
  /** `TYPESAFE_DEFAULT_MODEL` when set, else {@link TYPESAFE_DEFAULT_MODEL}. */
@@ -52,13 +58,15 @@ export interface NativeJudgeOptions extends NativeJudgeConnection {
52
58
  */
53
59
  maxQuestions?: number;
54
60
  /**
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.
61
+ * Whether images are enabled for this judge. `false` disables image input; `true` enables
62
+ * dialects that require explicit opt-in but cannot enable a wire without image support.
57
63
  */
58
64
  supportsImages?: boolean;
59
65
  }
60
66
  /** Per-call options for {@link NativeJudge.judge}. */
61
67
  export interface NativeJudgeCallOptions extends JudgeOptions {
68
+ /** Assets keyed by their existing references; wires without asset transport reject them. */
69
+ imageAssets?: JudgmentImageAssets;
62
70
  /**
63
71
  * Called before a split judgment rejects, with the summed usage of the chunks that had already
64
72
  * succeeded: billed upstream, but their answers are discarded with the failure. Not called when
@@ -73,7 +81,7 @@ export declare class JudgmentApiError extends AIError.ProviderHttpError {
73
81
  export declare class NativeJudge implements Judge {
74
82
  #private;
75
83
  readonly label: string;
76
- /** True when the selected native wire and model both accept image inputs. */
84
+ /** True when the selected native wire and model or direct-call option permit image input. */
77
85
  readonly supportsImages?: boolean;
78
86
  readonly api: JudgmentApi;
79
87
  readonly provider: string;
@@ -92,4 +100,3 @@ export declare class NativeJudge implements Judge {
92
100
  * @throws ConfigurationError when the model's API is not a judgment API.
93
101
  */
94
102
  export declare function createNativeJudge(model: Model, connection: NativeJudgeConnection): NativeJudge;
95
- export {};
@@ -1,7 +1,14 @@
1
- import { type JudgmentWire } from "./wire.js";
1
+ import { type JudgmentRequest } from "./types.js";
2
+ import { type JudgmentImageAssets, type JudgmentWire } from "./wire.js";
3
+ /**
4
+ * The subset of `assets` that `request` references. TypeSafe substitutes a reference wherever it
5
+ * occurs as text in a string of `state`, a question's `instructions`, or its `criteria` (never in
6
+ * object keys) and rejects a request carrying an unreferenced asset.
7
+ */
8
+ export declare function referencedImageAssets(request: JudgmentRequest, assets: JudgmentImageAssets): JudgmentImageAssets;
2
9
  /** TypeSafe's own route, shared by every System One server. */
3
10
  export declare const typesafeWire: JudgmentWire;
4
- /** OpenRouter Decisions: the System One body under `/api/alpha`. */
11
+ /** OpenRouter Decisions: System One body under `/api/alpha`, with image parts preserved in state. */
5
12
  export declare const openrouterDecisionsWire: JudgmentWire;
6
13
  /** Cloudflare Workers AI decision models (Clef) through the account-scoped `ai/run` route. */
7
14
  export declare const workersAiWire: JudgmentWire;
@@ -3,7 +3,7 @@
3
3
  * encodes, and decodes one request for each API it serves. Internal to the
4
4
  * judgment module; `native.ts` maps every judgment API to its dialect.
5
5
  */
6
- import type { Usage } from "../types.js";
6
+ import type { ImageContent, Usage } from "../types.js";
7
7
  import type { JudgmentRequest } from "./types.js";
8
8
  /** Where one attempt posts and which bearer token it sends. */
9
9
  export interface JudgmentTarget {
@@ -35,7 +35,23 @@ export interface JudgmentImageInput {
35
35
  maxTotalBytes?: number;
36
36
  /** Pixels per image, when the image header reports dimensions. */
37
37
  maxPixels?: number;
38
+ /** Minimum pixels per image, when the image header reports dimensions. */
39
+ minPixels?: number;
40
+ /** Inclusive bounds on each side, when the image header reports dimensions. */
41
+ minSide?: number;
42
+ maxSide?: number;
38
43
  }
44
+ /** Shared TypeSafe Jev/OpenRouter image limits (https://docs.typesafe.ai/images#limits); out-of-range images are rejected upstream, never resized. */
45
+ export declare const JEV_IMAGE_INPUT: {
46
+ readonly mimeTypes: readonly ["image/png", "image/jpeg"];
47
+ readonly maxBytes: number;
48
+ readonly minPixels: 78400;
49
+ readonly maxPixels: 2458624;
50
+ readonly minSide: 280;
51
+ readonly maxSide: 1568;
52
+ };
53
+ /** Image assets kept separately from judgment state for asset-aware native wires. */
54
+ export type JudgmentImageAssets = Readonly<Record<string, ImageContent>>;
39
55
  /** How one judgment API addresses, encodes, and decodes a request. */
40
56
  export interface JudgmentWire {
41
57
  /**
@@ -43,10 +59,16 @@ export interface JudgmentWire {
43
59
  * Throws when the credential cannot address the endpoint (e.g. no account id).
44
60
  */
45
61
  target(baseUrl: string, model: string, key: string): JudgmentTarget;
46
- /** Request body for `model`. */
47
- encode(request: JudgmentRequest, model: string): unknown;
62
+ /** Request body for `model`, with optional asset context for dialects that preserve references. */
63
+ encode(request: JudgmentRequest, model: string, imageAssets?: JudgmentImageAssets): unknown;
64
+ /** Headers this dialect adds to requests that carry images. */
65
+ imageHeaders?: Readonly<Record<string, string>>;
48
66
  /** Image input this dialect accepts; absent means text-only. */
49
67
  images?: JudgmentImageInput;
68
+ /** Whether raw asset maps can be transported without loss by this dialect. */
69
+ supportsImageAssets?: boolean;
70
+ /** Whether image support must be enabled explicitly for direct NativeJudge construction. */
71
+ imagesRequireOptIn?: boolean;
50
72
  /** The response's answers and usage; `undefined` when the payload is not this API's response shape. */
51
73
  decode(payload: unknown): DecodedJudgment | undefined;
52
74
  }
@@ -399,6 +399,22 @@ interface StrictToolsRetryContext {
399
399
  strictToolsApplied: boolean;
400
400
  tools: Tool[] | undefined;
401
401
  }
402
+ /**
403
+ * Read OpenRouter's `error.metadata.raw` from a captured non-2xx body.
404
+ *
405
+ * OpenRouter routes to a different upstream per request, so its top-level
406
+ * `error.message` is a generic policy sentence ("Access denied by security
407
+ * policy") and the only place the routed provider's own words appear is
408
+ * `error.metadata.raw`. Providers that proxy through OpenRouter also hang the
409
+ * upstream provider name there.
410
+ *
411
+ * The detail lives in the decoded body captured on the thrown
412
+ * {@link AIError.OpenAIHttpError}, not on the error object itself: no error
413
+ * class in this package carries an `error` property, so reading
414
+ * `error.error.metadata.raw` unconditionally yielded `undefined` and the
415
+ * upstream detail was dropped from every surfaced message.
416
+ */
417
+ export declare function readOpenRouterRawMetadata(capturedErrorResponse: CapturedHttpErrorResponse | undefined): string | undefined;
402
418
  /** Decide whether an OpenAI-family request should retry once with non-strict tools. */
403
419
  export declare function shouldRetryWithoutStrictTools(error: unknown, capturedErrorResponse: CapturedHttpErrorResponse | undefined, context: StrictToolsRetryContext): boolean;
404
420
  export declare const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string>;
@@ -1,7 +1,9 @@
1
1
  import type { JudgmentRequest, JudgmentResult } from "../judgment/types.js";
2
+ import type { JudgmentImageAssets } from "../judgment/wire.js";
2
3
  export interface SystemOneParsedRequest {
3
4
  modelId: string;
4
5
  request: JudgmentRequest;
6
+ imageAssets?: JudgmentImageAssets;
5
7
  }
6
8
  /** `POST /v1/systemone` response body; `cost` (USD) appears only when the route billed one. */
7
9
  export interface SystemOneResponseBody {
@@ -15,10 +17,11 @@ export interface SystemOneResponseBody {
15
17
  }
16
18
  /**
17
19
  * Validate the routing-relevant parts of a System One request body, including
18
- * optional embedded base64 images.
20
+ * top-level embedded images and inline TypeSafe image assets.
19
21
  *
20
22
  * @throws {AIError.ValidationError} when required routing fields are missing
21
- * or malformed, or a supplied image is malformed.
23
+ * or malformed, supplied image data is invalid, or an asset uses multipart
24
+ * `part`, which this JSON ingress cannot read.
22
25
  */
23
26
  export declare function parseRequest(body: unknown): SystemOneParsedRequest;
24
27
  /** Re-encode a judgment result in TypeSafe's response shape. */
@@ -9,6 +9,10 @@ import type { Api, AssistantMessage, Message, Model } from "../types.js";
9
9
  * MUST not push a normalized id past this bound.
10
10
  */
11
11
  export declare const MAX_TOOL_CALL_ID_LENGTH = 64;
12
+ /** Replace unsupported tool-call ID characters and cap the result for strict replay providers. */
13
+ export declare function normalizeToolCallId(id: string, maxLength?: number): string;
14
+ /** Whether `id` already satisfies the strict 1–64 `[a-zA-Z0-9_-]` tool-call ID contract. */
15
+ export declare function isValidToolCallId(id: string): boolean;
12
16
  /**
13
17
  * OpenAI Responses-family APIs mint composite tool ids (`call_id|item_id`);
14
18
  * opaque Chat Completions ids do not (openai-completions preserves same-model
@@ -1,4 +1,5 @@
1
1
  import type { ResponseInput } from "./providers/openai-responses-wire.js";
2
+ export { normalizeToolCallId } from "./providers/transform-messages.js";
2
3
  import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types.js";
3
4
  export { isRecord } from "@oh-my-pi/pi-utils";
4
5
  /**
@@ -9,7 +10,6 @@ export { isRecord } from "@oh-my-pi/pi-utils";
9
10
  */
10
11
  export declare function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined;
11
12
  export declare function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[];
12
- export declare function normalizeToolCallId(id: string): string;
13
13
  type ResponsesToolItemIdPrefix = "fc" | "ctc";
14
14
  /** Preserve opaque call IDs for Responses replay while normalizing or generating the separate item ID. */
15
15
  export declare function normalizeResponsesToolCallId(id: string, itemPrefix?: ResponsesToolItemIdPrefix): {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.11.1",
3
+ "version": "18.11.2",
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.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"
158
+ "@oh-my-pi/omptype": "18.11.2",
159
+ "@oh-my-pi/pi-catalog": "18.11.2",
160
+ "@oh-my-pi/pi-natives": "18.11.2",
161
+ "@oh-my-pi/pi-utils": "18.11.2",
162
+ "@oh-my-pi/pi-wire": "18.11.2"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"