@linxiraos/pi-ai 1.2.1 → 1.3.1
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 +98 -0
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/auth/cascade.d.ts +5 -3
- package/dist/types/auth/pool.d.ts +3 -3
- package/dist/types/auth/select.d.ts +5 -1
- package/dist/types/auth/types.d.ts +15 -5
- package/dist/types/auth/usage-cache.d.ts +3 -1
- package/dist/types/auth-gateway/clients.d.ts +67 -0
- package/dist/types/auth-gateway/dispatch.d.ts +28 -10
- package/dist/types/auth-gateway/http.d.ts +2 -0
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/server.d.ts +9 -4
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/dialect/coercion.d.ts +6 -0
- package/dist/types/dialect/deepseek.d.ts +13 -0
- package/dist/types/error/classes.d.ts +1 -1
- package/dist/types/error/flags.d.ts +15 -0
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/error/network-cause.d.ts +40 -0
- package/dist/types/images/shared.d.ts +26 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/judgment/native.d.ts +62 -3
- package/dist/types/judgment/systemone.d.ts +9 -2
- package/dist/types/judgment/text.d.ts +11 -5
- package/dist/types/judgment/types.d.ts +37 -6
- package/dist/types/judgment/wire.d.ts +18 -3
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-images.d.ts +7 -0
- package/dist/types/providers/anthropic-signature.d.ts +12 -2
- package/dist/types/providers/anthropic-state.d.ts +7 -0
- package/dist/types/providers/anthropic.d.ts +21 -6
- package/dist/types/providers/connection-prewarm.d.ts +15 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -0
- package/dist/types/providers/cursor.d.ts +7 -0
- package/dist/types/providers/ollama.d.ts +6 -1
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/providers/openai-responses.d.ts +9 -0
- package/dist/types/providers/openai-shared.d.ts +30 -0
- package/dist/types/providers/systemone-server.d.ts +7 -3
- package/dist/types/providers/transform-messages.d.ts +28 -5
- package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
- package/dist/types/stream.d.ts +0 -2
- package/dist/types/types.d.ts +33 -9
- package/dist/types/usage/muse-code.d.ts +1 -1
- package/dist/types/utils/dsml-leak.d.ts +17 -13
- package/dist/types/utils/narration.d.ts +9 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/dist/types/utils/request-body.d.ts +11 -0
- package/dist/types/utils/request-compression.d.ts +21 -0
- package/dist/types/utils/tool-call-arguments.d.ts +2 -1
- package/dist/types/utils/transport-fetch.d.ts +8 -0
- package/dist/types/utils.d.ts +1 -1
- package/package.json +7 -7
- package/src/auth/cascade.ts +23 -13
- package/src/auth/pool.ts +3 -3
- package/src/auth/refresh.ts +39 -12
- package/src/auth/select.ts +8 -2
- package/src/auth/types.ts +16 -5
- package/src/auth/usage-cache.ts +23 -7
- package/src/auth/usage.ts +2 -4
- package/src/auth-broker/remote-store.ts +31 -6
- package/src/auth-gateway/clients.ts +318 -0
- package/src/auth-gateway/dispatch.ts +66 -35
- package/src/auth-gateway/http.ts +11 -5
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +4 -10
- package/src/auth-gateway/routes/images.ts +6 -10
- package/src/auth-gateway/routes/rerank.ts +4 -10
- package/src/auth-gateway/routes/speech.ts +10 -10
- package/src/auth-gateway/routes/systemone.ts +22 -12
- package/src/auth-gateway/routes/transcriptions.ts +4 -2
- package/src/auth-gateway/routes/video.ts +12 -15
- package/src/auth-gateway/server.ts +136 -53
- package/src/auth-gateway/session-state.ts +14 -0
- package/src/auth-gateway/types.ts +16 -2
- package/src/dialect/coercion.ts +16 -0
- package/src/dialect/deepseek.ts +344 -81
- package/src/dialect/hermes.ts +7 -11
- package/src/dialect/qwen3.ts +3 -11
- package/src/error/classes.ts +3 -2
- package/src/error/flags.ts +24 -3
- package/src/error/index.ts +1 -0
- package/src/error/network-cause.ts +179 -0
- package/src/images/codex-format-directive.md +3 -0
- package/src/images/google-antigravity.ts +2 -1
- package/src/images/google-generative-ai.ts +4 -2
- package/src/images/openai-hosted.ts +25 -2
- package/src/images/openai-images.ts +11 -6
- package/src/images/shared.ts +77 -3
- package/src/index.ts +1 -0
- package/src/judgment/chat.ts +17 -1
- package/src/judgment/native.ts +238 -12
- package/src/judgment/openai-decisions.ts +30 -6
- package/src/judgment/systemone.ts +153 -11
- package/src/judgment/text.ts +26 -7
- package/src/judgment/types.ts +46 -6
- package/src/judgment/wire.ts +19 -3
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +4 -1
- package/src/providers/anthropic-client.ts +1 -1
- package/src/providers/anthropic-images.ts +263 -0
- package/src/providers/anthropic-messages-server.ts +84 -1
- package/src/providers/anthropic-signature.ts +35 -8
- package/src/providers/anthropic-state.ts +13 -0
- package/src/providers/anthropic.ts +479 -397
- package/src/providers/connection-prewarm.ts +78 -0
- package/src/providers/cowork-fetch.ts +5 -1
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +19 -0
- package/src/providers/cursor.ts +53 -13
- package/src/providers/devin.ts +10 -4
- package/src/providers/factory-droid/gemini.ts +9 -3
- package/src/providers/google-gemini-cli.ts +11 -1
- package/src/providers/google-shared.ts +2 -6
- package/src/providers/images-server.ts +6 -1
- package/src/providers/ollama.ts +23 -5
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server.ts +29 -0
- package/src/providers/openai-codex/access-programs.ts +2 -1
- package/src/providers/openai-codex-responses.ts +49 -82
- package/src/providers/openai-completions.ts +117 -19
- package/src/providers/openai-responses.ts +25 -2
- package/src/providers/openai-shared.ts +36 -5
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +72 -12
- package/src/providers/transform-messages.ts +104 -55
- package/src/registry/oauth/google-antigravity.ts +1 -1
- package/src/stream.ts +13 -73
- package/src/types.ts +33 -9
- package/src/usage/claude.ts +2 -9
- package/src/usage/github-copilot.ts +26 -4
- package/src/usage/muse-code.ts +115 -29
- package/src/usage/openai-codex.ts +13 -7
- package/src/usage/opencode-go.ts +7 -8
- package/src/usage/xai-oauth.ts +2 -9
- package/src/utils/dsml-leak.ts +88 -33
- package/src/utils/leaked-thinking-stream.ts +10 -0
- package/src/utils/narration.ts +18 -0
- package/src/utils/openai-http.ts +27 -18
- package/src/utils/request-body.ts +27 -0
- package/src/utils/request-compression.ts +202 -0
- package/src/utils/request-debug.ts +23 -9
- package/src/utils/schema/dereference.ts +51 -18
- package/src/utils/schema/draft.ts +10 -2
- package/src/utils/schema/json-schema-validator.ts +4 -10
- package/src/utils/schema/normalize.ts +6 -9
- package/src/utils/tool-call-arguments.ts +2 -1
- package/src/utils/transport-fetch.ts +37 -0
- package/src/utils/validation.ts +26 -16
- package/src/utils.ts +38 -23
|
@@ -16,6 +16,7 @@ import { isUsageLimitOutcome } from "../error/rate-limit";
|
|
|
16
16
|
import type { Api, FetchImpl, Model, Usage } from "../types";
|
|
17
17
|
import type { ClientUsageIdentity } from "../usage";
|
|
18
18
|
import { extractProviderRetryHint } from "../utils/retry-after";
|
|
19
|
+
import type { AuthGatewayClientScope } from "./clients";
|
|
19
20
|
import type { AuthGatewayServerOptions } from "./types";
|
|
20
21
|
|
|
21
22
|
export type ModelResolver = (modelId: string) => Model<Api> | undefined;
|
|
@@ -36,8 +37,17 @@ export interface AuthGatewayRouteOptions {
|
|
|
36
37
|
excludeProviders?: ReadonlySet<string>;
|
|
37
38
|
/** Upstream transport for every provider call; defaults to global `fetch`. Test seam. */
|
|
38
39
|
fetch?: FetchImpl;
|
|
40
|
+
/**
|
|
41
|
+
* The scoped client that sent this request, set per request by the HTTP
|
|
42
|
+
* server: it narrows `resolveModel` / `listModels`, and every credential
|
|
43
|
+
* lookup below resolves under its {@link AuthGatewayClientScope.credentialSessionId}.
|
|
44
|
+
*/
|
|
45
|
+
client?: AuthGatewayClientScope;
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
/** The route options a request's credential lookups read. */
|
|
49
|
+
export type AuthGatewayCredentialOptions = Pick<AuthGatewayRouteOptions, "storage" | "client">;
|
|
50
|
+
|
|
41
51
|
/** The HTTP server's options: the routes' plus its listener and inbound auth. */
|
|
42
52
|
export interface AuthGatewayBootOptions extends AuthGatewayServerOptions, AuthGatewayRouteOptions {}
|
|
43
53
|
|
|
@@ -56,28 +66,28 @@ export function normalizeClientSessionKey(clientKey: string | undefined): string
|
|
|
56
66
|
*
|
|
57
67
|
* `markUsageLimitReached` and the auth-retry resolver switch a session to a
|
|
58
68
|
* sibling credential, so the provider state retained for that session can
|
|
59
|
-
* outlive the account that taught it.
|
|
60
|
-
*
|
|
69
|
+
* outlive the account that taught it. A stored credential keeps its durable
|
|
70
|
+
* row id across token refreshes — fingerprinting the bearer instead would look
|
|
61
71
|
* like a rotation every time a token refreshes and discard the retained
|
|
62
|
-
* lessons for nothing.
|
|
63
|
-
*
|
|
72
|
+
* lessons for nothing. The id comes from the selection itself, not the
|
|
73
|
+
* session's current pin, which a concurrent request on the same session may
|
|
74
|
+
* have moved. Unstored keys fall back to a hash of the key, never the key
|
|
75
|
+
* itself: this value is held for the lifetime of the entry.
|
|
64
76
|
*/
|
|
65
|
-
export function resolveGatewayAccount(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
return `key:${Bun.hash(apiKey).toString(36)}`;
|
|
77
|
+
export function resolveGatewayAccount(resolved: ResolvedApiKey): string {
|
|
78
|
+
if (resolved.credentialId !== undefined) return `credential:${resolved.credentialId}`;
|
|
79
|
+
return `key:${Bun.hash(resolved.apiKey).toString(36)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Who sent a request and which stored credential it started on, for the `auth-gateway request` log line. */
|
|
83
|
+
export function gatewayRequestLogFields(
|
|
84
|
+
opts: AuthGatewayCredentialOptions,
|
|
85
|
+
resolved: ResolvedApiKey,
|
|
86
|
+
): { client?: string; credentialId?: number } {
|
|
87
|
+
const fields: { client?: string; credentialId?: number } = {};
|
|
88
|
+
if (opts.client) fields.client = opts.client.name;
|
|
89
|
+
if (resolved.credentialId !== undefined) fields.credentialId = resolved.credentialId;
|
|
90
|
+
return fields;
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
/**
|
|
@@ -90,7 +100,7 @@ export function resolveGatewayAccount(
|
|
|
90
100
|
* {@link classifyGatewayError}; a provider without any credential is a 401.
|
|
91
101
|
*/
|
|
92
102
|
export async function resolveGatewayApiKey(
|
|
93
|
-
|
|
103
|
+
opts: AuthGatewayCredentialOptions,
|
|
94
104
|
model: Model<Api>,
|
|
95
105
|
sessionId: string,
|
|
96
106
|
signal: AbortSignal,
|
|
@@ -98,7 +108,11 @@ export async function resolveGatewayApiKey(
|
|
|
98
108
|
): Promise<ResolvedApiKey | GatewayErrorClassification> {
|
|
99
109
|
let apiKey: ResolvedApiKey | undefined;
|
|
100
110
|
try {
|
|
101
|
-
apiKey = await storage.keys.getWithCredential(
|
|
111
|
+
apiKey = await opts.storage.keys.getWithCredential(
|
|
112
|
+
model.provider,
|
|
113
|
+
gatewayCredentialSessionId(opts, model, sessionId),
|
|
114
|
+
modelKeyOptions(model, signal),
|
|
115
|
+
);
|
|
102
116
|
} catch (error) {
|
|
103
117
|
const classified = classifyGatewayError(error);
|
|
104
118
|
logger.warn("auth-gateway getApiKey threw", { provider: model.provider, peer, error: classified.message });
|
|
@@ -197,20 +211,23 @@ function modelKeyOptions(model: Model<Api>, signal: AbortSignal): AuthApiKeyOpti
|
|
|
197
211
|
* `lastKey` tracks the most recent bearer so the switch step invalidates the
|
|
198
212
|
* credential that actually failed; a retry context's `previousKey` wins, since
|
|
199
213
|
* concurrent operations sharing one resolver (split judgment chunks) each know
|
|
200
|
-
* the bearer they sent.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
214
|
+
* the bearer they sent. Every step resolves under the scoped client's
|
|
215
|
+
* credential session when `opts.client` binds the provider, so refresh,
|
|
216
|
+
* blocking and sibling rotation stay inside its accounts. `onResolvedKey`
|
|
217
|
+
* observes every rotation; routes that retain provider session state use it
|
|
218
|
+
* to re-key the account lease, one-shot routes pass `undefined`.
|
|
203
219
|
*/
|
|
204
220
|
export function buildGatewayApiKeyResolver(
|
|
205
|
-
|
|
221
|
+
opts: AuthGatewayCredentialOptions,
|
|
206
222
|
model: Model<Api>,
|
|
207
223
|
sessionId: string,
|
|
208
224
|
initialKey: ResolvedApiKey,
|
|
209
225
|
requestSignal: AbortSignal,
|
|
210
226
|
format: string,
|
|
211
227
|
peer: string,
|
|
212
|
-
onResolvedKey?: (
|
|
228
|
+
onResolvedKey?: (resolved: ResolvedApiKey) => void,
|
|
213
229
|
): ApiKeyResolver {
|
|
230
|
+
const { storage } = opts;
|
|
214
231
|
let lastKey = initialKey.apiKey;
|
|
215
232
|
return async ({ lastChance, error, signal, previousKey }) => {
|
|
216
233
|
const sig = signal ?? requestSignal;
|
|
@@ -219,19 +236,23 @@ export function buildGatewayApiKeyResolver(
|
|
|
219
236
|
return initialKey;
|
|
220
237
|
}
|
|
221
238
|
if (!lastChance) {
|
|
222
|
-
const refreshed = await storage.keys.getWithCredential(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
const refreshed = await storage.keys.getWithCredential(
|
|
240
|
+
model.provider,
|
|
241
|
+
gatewayCredentialSessionId(opts, model, sessionId),
|
|
242
|
+
{
|
|
243
|
+
...modelKeyOptions(model, sig),
|
|
244
|
+
forceRefresh: true,
|
|
245
|
+
refreshReason: AIError.status(error) === 401 ? "auth-recovery" : undefined,
|
|
246
|
+
},
|
|
247
|
+
);
|
|
227
248
|
lastKey = refreshed?.apiKey ?? lastKey;
|
|
228
|
-
if (refreshed) onResolvedKey?.(refreshed
|
|
249
|
+
if (refreshed) onResolvedKey?.(refreshed);
|
|
229
250
|
return refreshed;
|
|
230
251
|
}
|
|
231
252
|
const next = await refreshGatewayApiKeyAfterAuthError(
|
|
232
253
|
storage,
|
|
233
254
|
model,
|
|
234
|
-
sessionId,
|
|
255
|
+
gatewayCredentialSessionId(opts, model, sessionId),
|
|
235
256
|
model.provider,
|
|
236
257
|
previousKey ?? lastKey,
|
|
237
258
|
error,
|
|
@@ -240,11 +261,21 @@ export function buildGatewayApiKeyResolver(
|
|
|
240
261
|
peer,
|
|
241
262
|
);
|
|
242
263
|
lastKey = next?.apiKey ?? lastKey;
|
|
243
|
-
if (next) onResolvedKey?.(next
|
|
264
|
+
if (next) onResolvedKey?.(next);
|
|
244
265
|
return next;
|
|
245
266
|
};
|
|
246
267
|
}
|
|
247
268
|
|
|
269
|
+
/**
|
|
270
|
+
* The session a request's credential lookup resolves under: a scoped client's
|
|
271
|
+
* own restricted session for a provider it binds. Asked on every lookup, not
|
|
272
|
+
* once per request, so a retry after the client's restriction is lifted fails
|
|
273
|
+
* instead of resolving unrestricted.
|
|
274
|
+
*/
|
|
275
|
+
function gatewayCredentialSessionId(opts: AuthGatewayCredentialOptions, model: Model<Api>, sessionId: string): string {
|
|
276
|
+
return opts.client?.credentialSessionId(model.provider, sessionId) ?? sessionId;
|
|
277
|
+
}
|
|
278
|
+
|
|
248
279
|
/**
|
|
249
280
|
* Attribute one settled upstream request to the originating client via the
|
|
250
281
|
* broker's observed-usage channel (`AuthStorage.usage.observe`, batched
|
package/src/auth-gateway/http.ts
CHANGED
|
@@ -113,13 +113,19 @@ export function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
113
113
|
|
|
114
114
|
const TOKEN_ENCODER = new TextEncoder();
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
/** The token of an `Authorization: Bearer <token>` header, trimmed; `undefined` without one. */
|
|
117
|
+
export function readBearerToken(req: Request): string | undefined {
|
|
118
118
|
const header = req.headers.get("authorization");
|
|
119
|
-
if (!header) return
|
|
119
|
+
if (!header) return undefined;
|
|
120
120
|
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
return match ? match[1].trim() : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isAuthorized(req: Request, tokens: ReadonlySet<string>): boolean {
|
|
125
|
+
if (tokens.size === 0) return true;
|
|
126
|
+
const token = readBearerToken(req);
|
|
127
|
+
if (token === undefined) return false;
|
|
128
|
+
const presented = TOKEN_ENCODER.encode(token);
|
|
123
129
|
// Iterate every allowed token regardless of early hits so the result
|
|
124
130
|
// timing reflects the full set, not the position of the match.
|
|
125
131
|
let ok = false;
|
|
@@ -6,6 +6,7 @@ import { deterministicUuid } from "../../utils/deterministic-id";
|
|
|
6
6
|
import {
|
|
7
7
|
type AuthGatewayRouteOptions,
|
|
8
8
|
buildGatewayApiKeyResolver,
|
|
9
|
+
gatewayRequestLogFields,
|
|
9
10
|
mirrorRequestAbort,
|
|
10
11
|
recordGatewayUsage,
|
|
11
12
|
resolveGatewayApiKey,
|
|
@@ -49,7 +50,7 @@ export async function handleEmbeddings(
|
|
|
49
50
|
|
|
50
51
|
const client = resolveClientIdentity(req.headers);
|
|
51
52
|
const sessionId = deterministicUuid(`embeddings\u0000${model.provider}/${model.id}`);
|
|
52
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
53
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
53
54
|
if (controller.signal.aborted) return aborted();
|
|
54
55
|
if ("status" in apiKey) {
|
|
55
56
|
return embeddings.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
@@ -63,19 +64,12 @@ export async function handleEmbeddings(
|
|
|
63
64
|
resolvedModel: model.id,
|
|
64
65
|
stream: false,
|
|
65
66
|
peer,
|
|
67
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
try {
|
|
69
71
|
const result = await embed(model, parsed.request, {
|
|
70
|
-
apiKey: buildGatewayApiKeyResolver(
|
|
71
|
-
bootOpts.storage,
|
|
72
|
-
model,
|
|
73
|
-
sessionId,
|
|
74
|
-
apiKey,
|
|
75
|
-
controller.signal,
|
|
76
|
-
"embeddings",
|
|
77
|
-
peer,
|
|
78
|
-
),
|
|
72
|
+
apiKey: buildGatewayApiKeyResolver(bootOpts, model, sessionId, apiKey, controller.signal, "embeddings", peer),
|
|
79
73
|
fetch: bootOpts.fetch,
|
|
80
74
|
signal: controller.signal,
|
|
81
75
|
});
|
|
@@ -2,11 +2,13 @@ import { calculateCost } from "@linxiraos/pi-catalog/models";
|
|
|
2
2
|
import { logger } from "@linxiraos/pi-utils";
|
|
3
3
|
import { classifyGatewayError } from "../../error/gateway";
|
|
4
4
|
import { generateImage } from "../../images";
|
|
5
|
+
import { validateImageInput } from "../../images/shared";
|
|
5
6
|
import * as imagesServer from "../../providers/images-server";
|
|
6
7
|
import { deterministicUuid } from "../../utils/deterministic-id";
|
|
7
8
|
import {
|
|
8
9
|
type AuthGatewayRouteOptions,
|
|
9
10
|
buildGatewayApiKeyResolver,
|
|
11
|
+
gatewayRequestLogFields,
|
|
10
12
|
mirrorRequestAbort,
|
|
11
13
|
recordGatewayUsage,
|
|
12
14
|
resolveGatewayApiKey,
|
|
@@ -49,6 +51,7 @@ async function handleImages(
|
|
|
49
51
|
let parsed: imagesServer.ImagesParsedRequest;
|
|
50
52
|
try {
|
|
51
53
|
parsed = await imagesServer.parseRequest(body, kind);
|
|
54
|
+
for (const image of parsed.request.inputImages ?? []) validateImageInput(image);
|
|
52
55
|
} catch (error) {
|
|
53
56
|
const message = error instanceof Error ? error.message : String(error);
|
|
54
57
|
return imagesServer.formatError(400, "invalid_request_error", message);
|
|
@@ -75,7 +78,7 @@ async function handleImages(
|
|
|
75
78
|
|
|
76
79
|
const client = resolveClientIdentity(req.headers);
|
|
77
80
|
const sessionId = deterministicUuid(`images\u0000${model.provider}/${model.id}`);
|
|
78
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
81
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
79
82
|
if (controller.signal.aborted) return aborted();
|
|
80
83
|
if ("status" in apiKey) return imagesServer.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
81
84
|
|
|
@@ -87,19 +90,12 @@ async function handleImages(
|
|
|
87
90
|
resolvedModel: model.id,
|
|
88
91
|
stream: false,
|
|
89
92
|
peer,
|
|
93
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
90
94
|
});
|
|
91
95
|
|
|
92
96
|
try {
|
|
93
97
|
const result = await generateImage(model, parsed.request, {
|
|
94
|
-
apiKey: buildGatewayApiKeyResolver(
|
|
95
|
-
bootOpts.storage,
|
|
96
|
-
model,
|
|
97
|
-
sessionId,
|
|
98
|
-
apiKey,
|
|
99
|
-
controller.signal,
|
|
100
|
-
"images",
|
|
101
|
-
peer,
|
|
102
|
-
),
|
|
98
|
+
apiKey: buildGatewayApiKeyResolver(bootOpts, model, sessionId, apiKey, controller.signal, "images", peer),
|
|
103
99
|
fetch: bootOpts.fetch,
|
|
104
100
|
signal: controller.signal,
|
|
105
101
|
});
|
|
@@ -6,6 +6,7 @@ import { deterministicUuid } from "../../utils/deterministic-id";
|
|
|
6
6
|
import {
|
|
7
7
|
type AuthGatewayRouteOptions,
|
|
8
8
|
buildGatewayApiKeyResolver,
|
|
9
|
+
gatewayRequestLogFields,
|
|
9
10
|
mirrorRequestAbort,
|
|
10
11
|
recordGatewayUsage,
|
|
11
12
|
resolveGatewayApiKey,
|
|
@@ -39,7 +40,7 @@ export async function handleRerank(bootOpts: AuthGatewayRouteOptions, req: Reque
|
|
|
39
40
|
|
|
40
41
|
const client = resolveClientIdentity(req.headers);
|
|
41
42
|
const sessionId = deterministicUuid(`rerank\u0000${model.provider}/${model.id}`);
|
|
42
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
43
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
43
44
|
if (controller.signal.aborted) return aborted();
|
|
44
45
|
if ("status" in apiKey) return rerankWire.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
45
46
|
|
|
@@ -51,19 +52,12 @@ export async function handleRerank(bootOpts: AuthGatewayRouteOptions, req: Reque
|
|
|
51
52
|
resolvedModel: model.id,
|
|
52
53
|
stream: false,
|
|
53
54
|
peer,
|
|
55
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
try {
|
|
57
59
|
const result = await rerank(model, parsed.request, {
|
|
58
|
-
apiKey: buildGatewayApiKeyResolver(
|
|
59
|
-
bootOpts.storage,
|
|
60
|
-
model,
|
|
61
|
-
sessionId,
|
|
62
|
-
apiKey,
|
|
63
|
-
controller.signal,
|
|
64
|
-
"rerank",
|
|
65
|
-
peer,
|
|
66
|
-
),
|
|
60
|
+
apiKey: buildGatewayApiKeyResolver(bootOpts, model, sessionId, apiKey, controller.signal, "rerank", peer),
|
|
67
61
|
fetch: bootOpts.fetch,
|
|
68
62
|
signal: controller.signal,
|
|
69
63
|
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { logger } from "@linxiraos/pi-utils";
|
|
2
2
|
import { classifyGatewayError } from "../../error/gateway";
|
|
3
|
+
import { ValidationError } from "../../error/validation";
|
|
3
4
|
import * as speechWire from "../../providers/speech-server";
|
|
4
5
|
import { isSpeechApi, synthesizeSpeech } from "../../speech";
|
|
5
6
|
import { deterministicUuid } from "../../utils/deterministic-id";
|
|
6
7
|
import {
|
|
7
8
|
type AuthGatewayRouteOptions,
|
|
8
9
|
buildGatewayApiKeyResolver,
|
|
10
|
+
gatewayRequestLogFields,
|
|
9
11
|
mirrorRequestAbort,
|
|
10
12
|
recordGatewayUsage,
|
|
11
13
|
resolveGatewayApiKey,
|
|
@@ -55,7 +57,7 @@ export async function handleSpeech(bootOpts: AuthGatewayRouteOptions, req: Reque
|
|
|
55
57
|
|
|
56
58
|
const client = resolveClientIdentity(req.headers);
|
|
57
59
|
const sessionId = deterministicUuid(`speech\u0000${model.provider}/${model.id}`);
|
|
58
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
60
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
59
61
|
if (controller.signal.aborted) return aborted();
|
|
60
62
|
if ("status" in apiKey) return speechWire.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
61
63
|
|
|
@@ -67,19 +69,12 @@ export async function handleSpeech(bootOpts: AuthGatewayRouteOptions, req: Reque
|
|
|
67
69
|
resolvedModel: model.id,
|
|
68
70
|
stream: false,
|
|
69
71
|
peer,
|
|
72
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
try {
|
|
73
76
|
const result = await synthesizeSpeech(model, parsed.request, {
|
|
74
|
-
apiKey: buildGatewayApiKeyResolver(
|
|
75
|
-
bootOpts.storage,
|
|
76
|
-
model,
|
|
77
|
-
sessionId,
|
|
78
|
-
apiKey,
|
|
79
|
-
controller.signal,
|
|
80
|
-
"speech",
|
|
81
|
-
peer,
|
|
82
|
-
),
|
|
77
|
+
apiKey: buildGatewayApiKeyResolver(bootOpts, model, sessionId, apiKey, controller.signal, "speech", peer),
|
|
83
78
|
fetch: bootOpts.fetch,
|
|
84
79
|
signal: controller.signal,
|
|
85
80
|
});
|
|
@@ -94,6 +89,11 @@ export async function handleSpeech(bootOpts: AuthGatewayRouteOptions, req: Reque
|
|
|
94
89
|
return response;
|
|
95
90
|
} catch (error) {
|
|
96
91
|
if (controller.signal.aborted) return aborted();
|
|
92
|
+
// Adapter validation is a client error: the wire schema admits values the
|
|
93
|
+
// adapter cannot serve, and the classifier would bill them as a 502.
|
|
94
|
+
if (error instanceof ValidationError) {
|
|
95
|
+
return speechWire.formatError(400, "invalid_request_error", error.message);
|
|
96
|
+
}
|
|
97
97
|
const classified = classifyGatewayError(error);
|
|
98
98
|
logger.warn("auth-gateway speech failed", { format: "speech", error: classified.message, peer });
|
|
99
99
|
return speechWire.formatError(classified.status, classified.type, classified.message);
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { calculateCost } from "@linxiraos/pi-catalog/models";
|
|
15
15
|
import { logger } from "@linxiraos/pi-utils";
|
|
16
|
+
import * as AIError from "../../error";
|
|
16
17
|
import { classifyGatewayError } from "../../error/gateway";
|
|
17
|
-
import { createNativeJudge, isJudgmentApi } from "../../judgment/native";
|
|
18
|
+
import { checkNativeJudgmentImages, createNativeJudge, isJudgmentApi } from "../../judgment/native";
|
|
18
19
|
import * as systemOne from "../../providers/systemone-server";
|
|
19
20
|
import { deterministicUuid } from "../../utils/deterministic-id";
|
|
20
21
|
import {
|
|
21
22
|
type AuthGatewayRouteOptions,
|
|
22
23
|
buildGatewayApiKeyResolver,
|
|
24
|
+
gatewayRequestLogFields,
|
|
23
25
|
mirrorRequestAbort,
|
|
24
26
|
recordGatewayUsage,
|
|
25
27
|
resolveGatewayApiKey,
|
|
@@ -66,26 +68,28 @@ export async function handleSystemOne(
|
|
|
66
68
|
`Model ${parsed.modelId} does not answer System One judgments`,
|
|
67
69
|
);
|
|
68
70
|
}
|
|
71
|
+
// Model capability and dialect caps are known locally: reject before credential lookup or refresh.
|
|
72
|
+
try {
|
|
73
|
+
checkNativeJudgmentImages(model, parsed.request, parsed.imageAssets);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return systemOne.formatError(
|
|
76
|
+
422,
|
|
77
|
+
"invalid_request_error",
|
|
78
|
+
error instanceof Error ? error.message : String(error),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
69
81
|
const client = resolveClientIdentity(req.headers);
|
|
70
82
|
// Judgments carry no conversation to derive a session from. One sticky
|
|
71
83
|
// session per model keeps credential selection stable across calls and
|
|
72
84
|
// still lets `markUsageLimitReached` hand off to a sibling account.
|
|
73
85
|
const sessionId = deterministicUuid(`systemone\u0000${model.provider}/${model.id}`);
|
|
74
86
|
|
|
75
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
87
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
76
88
|
if (controller.signal.aborted) return aborted();
|
|
77
89
|
if ("status" in apiKey) return systemOne.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
78
90
|
|
|
79
91
|
const judge = createNativeJudge(model, {
|
|
80
|
-
apiKey: buildGatewayApiKeyResolver(
|
|
81
|
-
bootOpts.storage,
|
|
82
|
-
model,
|
|
83
|
-
sessionId,
|
|
84
|
-
apiKey,
|
|
85
|
-
controller.signal,
|
|
86
|
-
"systemone",
|
|
87
|
-
peer,
|
|
88
|
-
),
|
|
92
|
+
apiKey: buildGatewayApiKeyResolver(bootOpts, model, sessionId, apiKey, controller.signal, "systemone", peer),
|
|
89
93
|
fetch: bootOpts.fetch,
|
|
90
94
|
});
|
|
91
95
|
|
|
@@ -97,11 +101,13 @@ export async function handleSystemOne(
|
|
|
97
101
|
resolvedModel: model.id,
|
|
98
102
|
stream: false,
|
|
99
103
|
peer,
|
|
104
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
100
105
|
});
|
|
101
106
|
|
|
102
107
|
try {
|
|
103
108
|
const result = await judge.judge(parsed.request, {
|
|
104
109
|
signal: controller.signal,
|
|
110
|
+
imageAssets: parsed.imageAssets,
|
|
105
111
|
// A split judgment that fails still billed the chunks that succeeded.
|
|
106
112
|
onPartialUsage: usage => {
|
|
107
113
|
if (usage.cost.total === 0) calculateCost(model, usage);
|
|
@@ -117,7 +123,11 @@ export async function handleSystemOne(
|
|
|
117
123
|
return json(200, body, gatewayResponseHeaders(model, { requestId, costUsd: result.usage.cost.total, startedAt }));
|
|
118
124
|
} catch (error) {
|
|
119
125
|
if (controller.signal.aborted) return aborted();
|
|
120
|
-
const
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
const classified =
|
|
128
|
+
error instanceof AIError.ValidationError
|
|
129
|
+
? { status: 422, type: "invalid_request_error", message }
|
|
130
|
+
: classifyGatewayError(error);
|
|
121
131
|
logger.warn("auth-gateway judgment failed", { format: "systemone", error: classified.message, peer });
|
|
122
132
|
return systemOne.formatError(classified.status, classified.type, classified.message);
|
|
123
133
|
}
|
|
@@ -6,6 +6,7 @@ import { deterministicUuid } from "../../utils/deterministic-id";
|
|
|
6
6
|
import {
|
|
7
7
|
type AuthGatewayRouteOptions,
|
|
8
8
|
buildGatewayApiKeyResolver,
|
|
9
|
+
gatewayRequestLogFields,
|
|
9
10
|
mirrorRequestAbort,
|
|
10
11
|
recordGatewayUsage,
|
|
11
12
|
resolveGatewayApiKey,
|
|
@@ -49,7 +50,7 @@ export async function handleTranscriptions(
|
|
|
49
50
|
|
|
50
51
|
const client = resolveClientIdentity(req.headers);
|
|
51
52
|
const sessionId = deterministicUuid(`transcriptions\u0000${model.provider}/${model.id}`);
|
|
52
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
53
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
53
54
|
if (controller.signal.aborted) return aborted();
|
|
54
55
|
if ("status" in apiKey) {
|
|
55
56
|
return transcriptions.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
@@ -63,12 +64,13 @@ export async function handleTranscriptions(
|
|
|
63
64
|
resolvedModel: model.id,
|
|
64
65
|
stream: false,
|
|
65
66
|
peer,
|
|
67
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
try {
|
|
69
71
|
const result = await transcribeAudio(model, parsed.request, {
|
|
70
72
|
apiKey: buildGatewayApiKeyResolver(
|
|
71
|
-
bootOpts
|
|
73
|
+
bootOpts,
|
|
72
74
|
model,
|
|
73
75
|
sessionId,
|
|
74
76
|
apiKey,
|
|
@@ -9,6 +9,7 @@ import { deterministicUuid } from "../../utils/deterministic-id";
|
|
|
9
9
|
import {
|
|
10
10
|
type AuthGatewayRouteOptions,
|
|
11
11
|
buildGatewayApiKeyResolver,
|
|
12
|
+
gatewayRequestLogFields,
|
|
12
13
|
mirrorRequestAbort,
|
|
13
14
|
resolveGatewayApiKey,
|
|
14
15
|
} from "../dispatch";
|
|
@@ -57,7 +58,7 @@ async function resolveVideoJob(
|
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
const sessionId = deterministicUuid(`video\u0000${model.provider}/${model.id}`);
|
|
60
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
61
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
61
62
|
if (controller.signal.aborted) return aborted();
|
|
62
63
|
if ("status" in apiKey) return videoServer.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
63
64
|
return { model, upstreamId: identity.upstreamId, sessionId, apiKey, controller };
|
|
@@ -66,7 +67,7 @@ async function resolveVideoJob(
|
|
|
66
67
|
function videoOptions(bootOpts: AuthGatewayRouteOptions, resolved: ResolvedVideoRequest, peer: string) {
|
|
67
68
|
return {
|
|
68
69
|
apiKey: buildGatewayApiKeyResolver(
|
|
69
|
-
bootOpts
|
|
70
|
+
bootOpts,
|
|
70
71
|
resolved.model,
|
|
71
72
|
resolved.sessionId,
|
|
72
73
|
resolved.apiKey,
|
|
@@ -80,11 +81,13 @@ function videoOptions(bootOpts: AuthGatewayRouteOptions, resolved: ResolvedVideo
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function logVideoRequest(
|
|
84
|
+
bootOpts: AuthGatewayRouteOptions,
|
|
83
85
|
requestId: string,
|
|
84
86
|
operation: "submit" | "poll" | "content",
|
|
85
|
-
|
|
87
|
+
resolved: ResolvedVideoRequest,
|
|
86
88
|
peer: string,
|
|
87
89
|
): void {
|
|
90
|
+
const { model } = resolved;
|
|
88
91
|
logger.info("auth-gateway request", {
|
|
89
92
|
requestId,
|
|
90
93
|
format: `video-${operation}`,
|
|
@@ -93,6 +96,7 @@ function logVideoRequest(
|
|
|
93
96
|
resolvedModel: model.id,
|
|
94
97
|
stream: operation === "content",
|
|
95
98
|
peer,
|
|
99
|
+
...gatewayRequestLogFields(bootOpts, resolved.apiKey),
|
|
96
100
|
});
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -140,7 +144,7 @@ export async function handleVideoSubmit(
|
|
|
140
144
|
);
|
|
141
145
|
}
|
|
142
146
|
const sessionId = deterministicUuid(`video\u0000${model.provider}/${model.id}`);
|
|
143
|
-
const apiKey = await resolveGatewayApiKey(bootOpts
|
|
147
|
+
const apiKey = await resolveGatewayApiKey(bootOpts, model, sessionId, controller.signal, peer);
|
|
144
148
|
if (controller.signal.aborted) return aborted();
|
|
145
149
|
if ("status" in apiKey) return videoServer.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
146
150
|
logger.info("auth-gateway request", {
|
|
@@ -151,18 +155,11 @@ export async function handleVideoSubmit(
|
|
|
151
155
|
resolvedModel: model.id,
|
|
152
156
|
stream: false,
|
|
153
157
|
peer,
|
|
158
|
+
...gatewayRequestLogFields(bootOpts, apiKey),
|
|
154
159
|
});
|
|
155
160
|
try {
|
|
156
161
|
const job = await submitVideo(model, parsed.request, {
|
|
157
|
-
apiKey: buildGatewayApiKeyResolver(
|
|
158
|
-
bootOpts.storage,
|
|
159
|
-
model,
|
|
160
|
-
sessionId,
|
|
161
|
-
apiKey,
|
|
162
|
-
controller.signal,
|
|
163
|
-
"video",
|
|
164
|
-
peer,
|
|
165
|
-
),
|
|
162
|
+
apiKey: buildGatewayApiKeyResolver(bootOpts, model, sessionId, apiKey, controller.signal, "video", peer),
|
|
166
163
|
fetch: bootOpts.fetch,
|
|
167
164
|
signal: controller.signal,
|
|
168
165
|
});
|
|
@@ -195,7 +192,7 @@ export async function handleVideoPoll(
|
|
|
195
192
|
const requestId = crypto.randomUUID();
|
|
196
193
|
const resolved = await resolveVideoJob(bootOpts, req, peer, gatewayId);
|
|
197
194
|
if (resolved instanceof Response) return resolved;
|
|
198
|
-
logVideoRequest(requestId, "poll", resolved
|
|
195
|
+
logVideoRequest(bootOpts, requestId, "poll", resolved, peer);
|
|
199
196
|
try {
|
|
200
197
|
const job = await pollVideo(resolved.model, resolved.upstreamId, videoOptions(bootOpts, resolved, peer));
|
|
201
198
|
recordCompletedUsage(bootOpts, resolved, req, job);
|
|
@@ -227,7 +224,7 @@ export async function handleVideoContent(
|
|
|
227
224
|
const requestId = crypto.randomUUID();
|
|
228
225
|
const resolved = await resolveVideoJob(bootOpts, req, peer, gatewayId);
|
|
229
226
|
if (resolved instanceof Response) return resolved;
|
|
230
|
-
logVideoRequest(requestId, "content", resolved
|
|
227
|
+
logVideoRequest(bootOpts, requestId, "content", resolved, peer);
|
|
231
228
|
try {
|
|
232
229
|
const content = await downloadVideo(resolved.model, resolved.upstreamId, videoOptions(bootOpts, resolved, peer));
|
|
233
230
|
const headers = new Headers(gatewayResponseHeaders(resolved.model, { requestId, startedAt }));
|