@gajae-code/ai 0.15.5 → 0.16.0
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 +40 -0
- package/dist/types/adapter-internals/aws-region.d.ts +7 -0
- package/dist/types/auth-broker/client.d.ts +6 -2
- package/dist/types/auth-broker/remote-store.d.ts +14 -2
- package/dist/types/auth-broker/types.d.ts +6 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
- package/dist/types/auth-gateway/server.d.ts +39 -5
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/auth-storage.d.ts +92 -24
- package/dist/types/core.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/provider-models/openai-compat.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/providers/register-builtins.d.ts +12 -12
- package/dist/types/stream.d.ts +2 -1
- package/dist/types/types.d.ts +22 -2
- package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
- package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
- package/dist/types/utils/oauth/commandcode.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/retry.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +0 -5
- package/dist/types/utils/sqlite-errors.d.ts +4 -0
- package/package.json +3 -3
- package/src/adapter-internals/aws-region.d.ts +7 -0
- package/src/adapter-internals/aws-region.ts +14 -0
- package/src/auth-broker/client.ts +41 -13
- package/src/auth-broker/redact.ts +25 -1
- package/src/auth-broker/remote-store.ts +374 -115
- package/src/auth-broker/server.ts +131 -91
- package/src/auth-broker/types.ts +6 -0
- package/src/auth-broker/wire-schemas.ts +6 -0
- package/src/auth-gateway/server.ts +447 -79
- package/src/auth-gateway/types.ts +28 -2
- package/src/auth-storage.ts +672 -168
- package/src/cli.ts +1 -0
- package/src/core.ts +1 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +8 -0
- package/src/models.json +1224 -3
- package/src/provider-models/descriptors.ts +3 -1
- package/src/provider-models/openai-compat.ts +102 -1
- package/src/providers/amazon-bedrock.ts +5 -1
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +8 -2
- package/src/providers/aws-credentials.ts +6 -0
- package/src/providers/azure-openai-responses.ts +4 -1
- package/src/providers/cursor.ts +256 -101
- package/src/providers/gitlab-duo.ts +18 -1
- package/src/providers/google-gemini-cli.ts +3 -0
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-shared.ts +3 -0
- package/src/providers/kiro-api-key.ts +33 -8
- package/src/providers/kiro-codewhisperer.ts +28 -9
- package/src/providers/ollama.ts +3 -0
- package/src/providers/openai-codex-responses.d.ts +6 -0
- package/src/providers/openai-codex-responses.ts +37 -8
- package/src/providers/openai-completions.ts +11 -1
- package/src/providers/openai-responses.ts +10 -1
- package/src/providers/pi-native-client.ts +25 -1
- package/src/providers/pi-native-server.ts +24 -0
- package/src/providers/register-builtins.d.ts +12 -12
- package/src/providers/register-builtins.ts +16 -3
- package/src/stream.d.ts +2 -1
- package/src/stream.ts +175 -67
- package/src/types.d.ts +22 -2
- package/src/types.ts +27 -1
- package/src/utils/oauth/api-key-login.ts +13 -2
- package/src/utils/oauth/api-key-validation.ts +242 -41
- package/src/utils/oauth/commandcode.ts +17 -0
- package/src/utils/oauth/index.ts +20 -5
- package/src/utils/oauth/kiro.ts +91 -22
- package/src/utils/oauth/types.d.ts +1 -1
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/retry.d.ts +2 -0
- package/src/utils/retry.ts +15 -2
- package/src/utils/schema/dereference.ts +169 -49
- package/src/utils/schema/draft.ts +46 -23
- package/src/utils/schema/normalize.d.ts +0 -5
- package/src/utils/schema/normalize.ts +396 -119
- package/src/utils/schema/types.ts +3 -1
- package/src/utils/schema/zod-decontaminate.ts +83 -29
- package/src/utils/sqlite-errors.d.ts +4 -0
- package/src/utils/sqlite-errors.ts +13 -0
- package/src/utils/tool-choice-capability.ts +2 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gjc auth-gateway HTTP server.
|
|
3
3
|
*
|
|
4
|
-
* Accepts
|
|
4
|
+
* Accepts a provider-scoped provider-format request (OpenAI chat-completions, Anthropic
|
|
5
5
|
* messages, OpenAI Responses) and dispatches through pi-ai's `streamSimple()`
|
|
6
6
|
* — which handles credential injection, anthropic-beta headers, OpenAI code backend
|
|
7
7
|
* websocket transport, and all the per-provider intricacies. The gateway is
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* GET /healthz → unauth; ok + version
|
|
13
13
|
* GET /v1/usage → aggregated provider usage (5-min per-credential cache via AuthStorage)
|
|
14
14
|
* GET /v1/credentials/check → per-credential auth probe (diagnose 401s in a multi-account pool)
|
|
15
|
-
* GET /v1/models → list
|
|
15
|
+
* GET /v1/models → list models from the selected provider scope
|
|
16
16
|
* POST /v1/chat/completions → OpenAI chat-completions in/out
|
|
17
17
|
* POST /v1/messages → Anthropic messages in/out
|
|
18
18
|
* POST /v1/responses → OpenAI Responses in/out
|
|
@@ -32,8 +32,10 @@ import type {
|
|
|
32
32
|
AssistantMessage,
|
|
33
33
|
AssistantMessageEvent,
|
|
34
34
|
AssistantMessageEventStream,
|
|
35
|
+
AuthRetryCredential,
|
|
35
36
|
Context,
|
|
36
37
|
Model,
|
|
38
|
+
Provider,
|
|
37
39
|
SimpleStreamOptions,
|
|
38
40
|
} from "../types";
|
|
39
41
|
import { beginAttempt, classifyFallbackTrigger } from "../utils/fallback-transport";
|
|
@@ -53,7 +55,7 @@ import type {
|
|
|
53
55
|
AuthGatewayFormatModule as FormatModule,
|
|
54
56
|
AuthGatewayParsedRequest as ParsedFormatRequest,
|
|
55
57
|
} from "./types";
|
|
56
|
-
import { DEFAULT_AUTH_GATEWAY_BIND } from "./types";
|
|
58
|
+
import { AUTH_GATEWAY_PROVIDER_APIS, DEFAULT_AUTH_GATEWAY_BIND } from "./types";
|
|
57
59
|
|
|
58
60
|
// ParsedFormatRequest / ParsedFormatOptions / FormatModule come from ./types.
|
|
59
61
|
|
|
@@ -62,14 +64,267 @@ export type ModelResolver = (modelId: string) => Model<Api> | undefined;
|
|
|
62
64
|
export interface AuthGatewayBootOptions extends AuthGatewayServerOptions {
|
|
63
65
|
/** Source of credentials. Caller wires this to a broker-backed AuthStorage. */
|
|
64
66
|
storage: AuthStorage;
|
|
67
|
+
/**
|
|
68
|
+
* Current broker-backed scope authority. When supplied, this is checked on
|
|
69
|
+
* every request so a live broker snapshot removal immediately fails closed.
|
|
70
|
+
*/
|
|
71
|
+
hasProviderCredential: () => boolean;
|
|
72
|
+
/** Refresh the dispatch cache from the current broker snapshot before use. */
|
|
73
|
+
reloadProviderCredentials: (signal?: AbortSignal) => Promise<void>;
|
|
74
|
+
/** Confirm that the selected key is still present in the current authority snapshot. */
|
|
75
|
+
validateProviderCredential: (provider: string, apiKey: string) => boolean;
|
|
65
76
|
/**
|
|
66
77
|
* Resolve a client-requested model id to a pi-ai Model. Caller supplies
|
|
67
78
|
* this from a ModelRegistry (lives in `coding-agent` to avoid an inverse
|
|
68
79
|
* dependency in `pi-ai`).
|
|
69
80
|
*/
|
|
70
81
|
resolveModel: ModelResolver;
|
|
71
|
-
/**
|
|
72
|
-
listModels
|
|
82
|
+
/** Supplier for the source-backed model catalog used by `/v1/models`. */
|
|
83
|
+
listModels: () => Iterable<Model<Api>>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface AuthGatewayModelCatalog {
|
|
87
|
+
readonly models: readonly Model<Api>[];
|
|
88
|
+
resolve(modelId: string): Model<Api> | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function modelApiForProvider(provider: Provider): Api | undefined {
|
|
92
|
+
return AUTH_GATEWAY_PROVIDER_APIS[provider];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a model can be served through the broker-backed auth gateway.
|
|
97
|
+
*
|
|
98
|
+
* Bedrock's credential chain is process-local AWS authority, not a broker
|
|
99
|
+
* credential. Advertising a native Bedrock model from this gateway would let
|
|
100
|
+
* direct callers bypass the broker boundary (and make readiness lie about a
|
|
101
|
+
* model the gateway cannot authenticate). Keep this predicate shared with the
|
|
102
|
+
* CLI readiness checks so every entry point applies the same fence.
|
|
103
|
+
*/
|
|
104
|
+
export function isAuthGatewayModelBrokerConsumable(model: Pick<Model<Api>, "api" | "transport">): boolean {
|
|
105
|
+
return (
|
|
106
|
+
model.api !== "bedrock-converse-stream" &&
|
|
107
|
+
model.api !== "google-vertex" &&
|
|
108
|
+
model.api !== "kiro-codewhisperer-stream" &&
|
|
109
|
+
model.transport !== "pi-native"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isModelInProviderScope(model: Model<Api>, provider: Provider): boolean {
|
|
114
|
+
if (model.provider !== provider) return false;
|
|
115
|
+
const expectedApi = modelApiForProvider(provider);
|
|
116
|
+
return expectedApi === undefined || model.api === expectedApi;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build an unambiguous, provider-scoped catalog.
|
|
121
|
+
*
|
|
122
|
+
* Models from other providers are intentionally ignored rather than allowed
|
|
123
|
+
* to compete for the same id. Duplicate ids within the selected provider are
|
|
124
|
+
* rejected because choosing either one would make request dispatch
|
|
125
|
+
* order-dependent.
|
|
126
|
+
*/
|
|
127
|
+
export function createAuthGatewayModelCatalog(
|
|
128
|
+
provider: Provider,
|
|
129
|
+
models: Iterable<Model<Api>>,
|
|
130
|
+
): AuthGatewayModelCatalog {
|
|
131
|
+
const byId = new Map<string, Model<Api>>();
|
|
132
|
+
for (const model of models) {
|
|
133
|
+
if (!isAuthGatewayModelBrokerConsumable(model)) continue;
|
|
134
|
+
if (!isModelInProviderScope(model, provider)) continue;
|
|
135
|
+
if (byId.has(model.id)) {
|
|
136
|
+
throw new Error(`Ambiguous auth-gateway model id ${model.id} for provider ${provider}`);
|
|
137
|
+
}
|
|
138
|
+
byId.set(model.id, model);
|
|
139
|
+
}
|
|
140
|
+
const scopedModels = [...byId.values()];
|
|
141
|
+
return {
|
|
142
|
+
models: scopedModels,
|
|
143
|
+
resolve: (modelId: string) => byId.get(modelId),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveScopedModel(
|
|
148
|
+
opts: AuthGatewayBootOptions,
|
|
149
|
+
catalog: AuthGatewayModelCatalog,
|
|
150
|
+
modelId: string,
|
|
151
|
+
): Model<Api> | undefined {
|
|
152
|
+
const catalogModel = catalog.resolve(modelId);
|
|
153
|
+
if (!catalogModel) return undefined;
|
|
154
|
+
const resolved = opts.resolveModel(modelId);
|
|
155
|
+
if (
|
|
156
|
+
!resolved ||
|
|
157
|
+
resolved !== catalogModel ||
|
|
158
|
+
resolved.id !== catalogModel.id ||
|
|
159
|
+
resolved.api !== catalogModel.api ||
|
|
160
|
+
!isModelInProviderScope(resolved, opts.providerScope.provider)
|
|
161
|
+
) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
return catalogModel;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hasProviderCredential(opts: AuthGatewayBootOptions): boolean {
|
|
168
|
+
return opts.hasProviderCredential();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type ProviderScopeAvailability = "available" | "absent" | "reload_failed";
|
|
172
|
+
|
|
173
|
+
async function providerScopeAvailability(
|
|
174
|
+
opts: AuthGatewayBootOptions,
|
|
175
|
+
signal?: AbortSignal,
|
|
176
|
+
): Promise<ProviderScopeAvailability> {
|
|
177
|
+
try {
|
|
178
|
+
await opts.reloadProviderCredentials(signal);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
logger.warn("auth-gateway provider snapshot reload failed", {
|
|
181
|
+
provider: opts.providerScope.provider,
|
|
182
|
+
error: cleanReason(error) ?? "snapshot reload failed",
|
|
183
|
+
});
|
|
184
|
+
return "reload_failed";
|
|
185
|
+
}
|
|
186
|
+
return hasProviderCredential(opts) ? "available" : "absent";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
class GatewayCredentialError extends Error {
|
|
190
|
+
readonly status: number;
|
|
191
|
+
readonly type: string;
|
|
192
|
+
|
|
193
|
+
constructor(status: number, type: string, message: string) {
|
|
194
|
+
super(message);
|
|
195
|
+
this.name = "GatewayCredentialError";
|
|
196
|
+
this.status = status;
|
|
197
|
+
this.type = type;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const credentialAuthorityTails = new WeakMap<AuthGatewayBootOptions, Promise<void>>();
|
|
202
|
+
|
|
203
|
+
interface GatewayCredentialLease {
|
|
204
|
+
apiKey: string;
|
|
205
|
+
release(): void;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function releaseGatewayCredentialLeaseOnAdmission(
|
|
209
|
+
events: Pick<AssistantMessageEventStream, "result">,
|
|
210
|
+
release: () => void,
|
|
211
|
+
signal?: AbortSignal,
|
|
212
|
+
): void {
|
|
213
|
+
let released = false;
|
|
214
|
+
const releaseOnce = (): void => {
|
|
215
|
+
if (released) return;
|
|
216
|
+
released = true;
|
|
217
|
+
signal?.removeEventListener("abort", releaseOnce);
|
|
218
|
+
release();
|
|
219
|
+
};
|
|
220
|
+
if (signal?.aborted) {
|
|
221
|
+
releaseOnce();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
signal?.addEventListener("abort", releaseOnce, { once: true });
|
|
225
|
+
// A deferred provider import can fail before the admission hook runs. Do
|
|
226
|
+
// not strand the authority lease in that case, but never wait for a
|
|
227
|
+
// successful stream's full response lifetime.
|
|
228
|
+
void events.result().then(releaseOnce, releaseOnce);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function resolveGatewayApiKey(
|
|
232
|
+
opts: AuthGatewayBootOptions,
|
|
233
|
+
model: Model<Api>,
|
|
234
|
+
peer: string,
|
|
235
|
+
signal: AbortSignal,
|
|
236
|
+
): Promise<string> {
|
|
237
|
+
try {
|
|
238
|
+
const scopeAvailability = await providerScopeAvailability(opts, signal);
|
|
239
|
+
if (scopeAvailability === "reload_failed") {
|
|
240
|
+
throw new GatewayCredentialError(503, "upstream_error", "Auth broker unavailable");
|
|
241
|
+
}
|
|
242
|
+
if (scopeAvailability !== "available") {
|
|
243
|
+
throw new GatewayCredentialError(
|
|
244
|
+
401,
|
|
245
|
+
"authentication_error",
|
|
246
|
+
`No credential available for provider ${model.provider}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const apiKey = await opts.storage.getApiKey(model.provider, undefined, { modelId: model.id, signal });
|
|
250
|
+
if (!apiKey || !opts.validateProviderCredential(model.provider, apiKey) || !hasProviderCredential(opts)) {
|
|
251
|
+
throw new GatewayCredentialError(
|
|
252
|
+
401,
|
|
253
|
+
"authentication_error",
|
|
254
|
+
`No credential available for provider ${model.provider}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return apiKey;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof GatewayCredentialError) throw error;
|
|
260
|
+
const classified = classifyGatewayError(error);
|
|
261
|
+
logger.warn("auth-gateway getApiKey threw", {
|
|
262
|
+
provider: model.provider,
|
|
263
|
+
peer,
|
|
264
|
+
error: classified.message,
|
|
265
|
+
});
|
|
266
|
+
throw new GatewayCredentialError(classified.status, classified.type, classified.message);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function acquireGatewayApiKey(
|
|
271
|
+
opts: AuthGatewayBootOptions,
|
|
272
|
+
model: Model<Api>,
|
|
273
|
+
peer: string,
|
|
274
|
+
signal: AbortSignal,
|
|
275
|
+
): Promise<GatewayCredentialLease> {
|
|
276
|
+
const previous = credentialAuthorityTails.get(opts) ?? Promise.resolve();
|
|
277
|
+
const deferred = Promise.withResolvers<void>();
|
|
278
|
+
const tail = previous.then(
|
|
279
|
+
() => deferred.promise,
|
|
280
|
+
() => deferred.promise,
|
|
281
|
+
);
|
|
282
|
+
credentialAuthorityTails.set(opts, tail);
|
|
283
|
+
let released = false;
|
|
284
|
+
const release = (): void => {
|
|
285
|
+
if (released) return;
|
|
286
|
+
released = true;
|
|
287
|
+
deferred.resolve();
|
|
288
|
+
};
|
|
289
|
+
void tail.then(() => {
|
|
290
|
+
if (credentialAuthorityTails.get(opts) === tail) credentialAuthorityTails.delete(opts);
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
294
|
+
const abort = Promise.withResolvers<never>();
|
|
295
|
+
const onAbort = (): void =>
|
|
296
|
+
abort.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
297
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
298
|
+
try {
|
|
299
|
+
await Promise.race([previous, abort.promise]);
|
|
300
|
+
} finally {
|
|
301
|
+
signal.removeEventListener("abort", onAbort);
|
|
302
|
+
}
|
|
303
|
+
const apiKey = await resolveGatewayApiKey(opts, model, peer, signal);
|
|
304
|
+
const dispatchTicket = await opts.storage.acquireCredentialDispatchTicket?.(model.provider, signal);
|
|
305
|
+
if (!opts.validateProviderCredential(model.provider, apiKey) || !hasProviderCredential(opts)) {
|
|
306
|
+
dispatchTicket?.release();
|
|
307
|
+
throw new GatewayCredentialError(
|
|
308
|
+
401,
|
|
309
|
+
"authentication_error",
|
|
310
|
+
`No credential available for provider ${model.provider}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
apiKey,
|
|
315
|
+
release: () => {
|
|
316
|
+
// The store-owned ticket orders remote snapshot authority against
|
|
317
|
+
// provider admission; the gateway tail independently orders local
|
|
318
|
+
// acquisitions without serializing response lifetimes.
|
|
319
|
+
dispatchTicket?.release();
|
|
320
|
+
release();
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
release();
|
|
325
|
+
if (error instanceof GatewayCredentialError) throw error;
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
73
328
|
}
|
|
74
329
|
|
|
75
330
|
// `parseBind` lives in ../utils/parse-bind so the gateway and broker can't
|
|
@@ -134,7 +389,15 @@ function deriveSessionId(modelId: string, context: Context): string {
|
|
|
134
389
|
}
|
|
135
390
|
|
|
136
391
|
function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: AbortSignal): SimpleStreamOptions {
|
|
137
|
-
|
|
392
|
+
// Gateway authority is acquired once per managed attempt. Provider-internal
|
|
393
|
+
// retries would otherwise resend a captured credential after the broker lease
|
|
394
|
+
// is released; replacement attempts must flow through onAuthError instead.
|
|
395
|
+
const opts: SimpleStreamOptions = {
|
|
396
|
+
signal,
|
|
397
|
+
requestMaxRetries: 0,
|
|
398
|
+
streamMaxRetries: 0,
|
|
399
|
+
disableProviderRetries: true,
|
|
400
|
+
};
|
|
138
401
|
const { options } = parsed;
|
|
139
402
|
// OpenAI code backend backend rejects `temperature` / `top_p` (per-model defaults only),
|
|
140
403
|
// so we drop them silently for that one provider. Every other unsupported
|
|
@@ -199,7 +462,7 @@ function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: Abort
|
|
|
199
462
|
previousResponseId: options.previousResponseId,
|
|
200
463
|
seed: options.seed,
|
|
201
464
|
hasLogitBias: options.logitBias !== undefined,
|
|
202
|
-
|
|
465
|
+
hasUser: options.user !== undefined,
|
|
203
466
|
hasResponseFormat: options.responseFormat !== undefined,
|
|
204
467
|
});
|
|
205
468
|
}
|
|
@@ -295,7 +558,7 @@ function redactGatewayStream(events: AssistantMessageEventStream): AssistantMess
|
|
|
295
558
|
}
|
|
296
559
|
|
|
297
560
|
async function refreshGatewayApiKeyAfterAuthError(
|
|
298
|
-
|
|
561
|
+
opts: AuthGatewayBootOptions,
|
|
299
562
|
model: Model<Api>,
|
|
300
563
|
provider: string,
|
|
301
564
|
oldKey: string,
|
|
@@ -303,15 +566,29 @@ async function refreshGatewayApiKeyAfterAuthError(
|
|
|
303
566
|
signal: AbortSignal,
|
|
304
567
|
format: string,
|
|
305
568
|
peer: string,
|
|
306
|
-
): Promise<
|
|
307
|
-
await storage.invalidateCredentialMatching(provider, oldKey, signal);
|
|
569
|
+
): Promise<AuthRetryCredential | undefined> {
|
|
570
|
+
await opts.storage.invalidateCredentialMatching(provider, oldKey, signal);
|
|
308
571
|
logger.debug("auth-gateway retrying provider request after credential invalidation", {
|
|
309
572
|
format,
|
|
310
573
|
provider,
|
|
311
574
|
peer,
|
|
312
575
|
error: cleanReason(error) ?? "Upstream request failed",
|
|
313
576
|
});
|
|
314
|
-
|
|
577
|
+
try {
|
|
578
|
+
const lease = await acquireGatewayApiKey(opts, model, peer, signal);
|
|
579
|
+
return { apiKey: lease.apiKey, onStreamCreated: lease.release } satisfies AuthRetryCredential;
|
|
580
|
+
} catch (resolutionError) {
|
|
581
|
+
if (resolutionError instanceof GatewayCredentialError) {
|
|
582
|
+
logger.debug("auth-gateway has no broker-authorized replacement credential", {
|
|
583
|
+
format,
|
|
584
|
+
provider,
|
|
585
|
+
peer,
|
|
586
|
+
status: resolutionError.status,
|
|
587
|
+
});
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
throw resolutionError;
|
|
591
|
+
}
|
|
315
592
|
}
|
|
316
593
|
|
|
317
594
|
/**
|
|
@@ -423,6 +700,7 @@ function mirrorRequestAbort(req: Request): AbortController {
|
|
|
423
700
|
async function handleFormatEndpoint(
|
|
424
701
|
route: { module: FormatModule; label: string },
|
|
425
702
|
bootOpts: AuthGatewayBootOptions,
|
|
703
|
+
catalog: AuthGatewayModelCatalog,
|
|
426
704
|
req: Request,
|
|
427
705
|
peer: string,
|
|
428
706
|
): Promise<Response> {
|
|
@@ -453,36 +731,11 @@ async function handleFormatEndpoint(
|
|
|
453
731
|
return route.module.formatError(400, "invalid_request_error", "Missing top-level `model` field");
|
|
454
732
|
}
|
|
455
733
|
|
|
456
|
-
const model = bootOpts
|
|
734
|
+
const model = resolveScopedModel(bootOpts, catalog, modelId);
|
|
457
735
|
if (!model) {
|
|
458
736
|
return route.module.formatError(404, "invalid_request_error", `Unknown model: ${modelId}`);
|
|
459
737
|
}
|
|
460
738
|
|
|
461
|
-
// pi-ai's stream() does NOT consult AuthStorage — the caller (us) is
|
|
462
|
-
// expected to resolve the credential and pass it as `options.apiKey`.
|
|
463
|
-
// For OAuth providers this returns the access token (refreshed via the
|
|
464
|
-
// broker override on AuthStorage when needed).
|
|
465
|
-
let apiKey: string | undefined;
|
|
466
|
-
try {
|
|
467
|
-
apiKey = await bootOpts.storage.getApiKey(model.provider, undefined, {
|
|
468
|
-
modelId: model.id,
|
|
469
|
-
signal: controller.signal,
|
|
470
|
-
});
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (controller.signal.aborted) return clientClosedResponse(route);
|
|
473
|
-
const classified = classifyGatewayError(error);
|
|
474
|
-
logger.warn("auth-gateway getApiKey threw", { provider: model.provider, peer, error: classified.message });
|
|
475
|
-
return route.module.formatError(classified.status, classified.type, classified.message);
|
|
476
|
-
}
|
|
477
|
-
if (controller.signal.aborted) return clientClosedResponse(route);
|
|
478
|
-
if (!apiKey) {
|
|
479
|
-
return route.module.formatError(
|
|
480
|
-
401,
|
|
481
|
-
"authentication_error",
|
|
482
|
-
`No credential available for provider ${model.provider}`,
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
739
|
// Parse + validate against the strict format schema, rebuild as gjc's
|
|
487
740
|
// canonical Context, dispatch through pi-ai's streamSimple, encode the
|
|
488
741
|
// canonical event stream back to the inbound format. There is no
|
|
@@ -508,13 +761,12 @@ async function handleFormatEndpoint(
|
|
|
508
761
|
if (controller.signal.aborted) return clientClosedResponse(route);
|
|
509
762
|
|
|
510
763
|
const streamOpts = buildStreamOptions(parsed, model.api, controller.signal);
|
|
511
|
-
streamOpts.apiKey = apiKey;
|
|
512
764
|
if (streamOpts.fallbackManaged) {
|
|
513
765
|
streamOpts.fallbackAttempt = beginAttempt(model.id, "auth-gateway");
|
|
514
766
|
} else {
|
|
515
767
|
streamOpts.onAuthError = (provider, oldKey, error) =>
|
|
516
768
|
refreshGatewayApiKeyAfterAuthError(
|
|
517
|
-
bootOpts
|
|
769
|
+
bootOpts,
|
|
518
770
|
model,
|
|
519
771
|
provider,
|
|
520
772
|
oldKey,
|
|
@@ -534,11 +786,36 @@ async function handleFormatEndpoint(
|
|
|
534
786
|
peer,
|
|
535
787
|
});
|
|
536
788
|
|
|
537
|
-
let
|
|
789
|
+
let apiKey: string;
|
|
790
|
+
let credentialLease: GatewayCredentialLease;
|
|
538
791
|
try {
|
|
792
|
+
credentialLease = await acquireGatewayApiKey(bootOpts, model, peer, controller.signal);
|
|
793
|
+
apiKey = credentialLease.apiKey;
|
|
794
|
+
streamOpts.apiKey = apiKey;
|
|
795
|
+
} catch (error) {
|
|
539
796
|
if (controller.signal.aborted) return clientClosedResponse(route);
|
|
797
|
+
if (error instanceof GatewayCredentialError) {
|
|
798
|
+
return route.module.formatError(error.status, error.type, error.message);
|
|
799
|
+
}
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let events: AssistantMessageEventStream;
|
|
804
|
+
let releasedAtAdmission = false;
|
|
805
|
+
const releaseAtAdmission = (): void => {
|
|
806
|
+
if (releasedAtAdmission) return;
|
|
807
|
+
releasedAtAdmission = true;
|
|
808
|
+
credentialLease.release();
|
|
809
|
+
};
|
|
810
|
+
streamOpts.onStreamCreated = releaseAtAdmission;
|
|
811
|
+
try {
|
|
812
|
+
if (controller.signal.aborted) {
|
|
813
|
+
credentialLease.release();
|
|
814
|
+
return clientClosedResponse(route);
|
|
815
|
+
}
|
|
540
816
|
events = streamSimple(model, parsed.context, streamOpts);
|
|
541
817
|
} catch (error) {
|
|
818
|
+
credentialLease.release();
|
|
542
819
|
if (streamOpts.fallbackManaged) {
|
|
543
820
|
await markManagedGatewayCredentialFailure(
|
|
544
821
|
bootOpts.storage,
|
|
@@ -554,6 +831,7 @@ async function handleFormatEndpoint(
|
|
|
554
831
|
logger.warn("auth-gateway streamSimple threw", { format: route.label, error: classified.message, peer });
|
|
555
832
|
return route.module.formatError(classified.status, classified.type, classified.message);
|
|
556
833
|
}
|
|
834
|
+
releaseGatewayCredentialLeaseOnAdmission(events, releaseAtAdmission, controller.signal);
|
|
557
835
|
if (streamOpts.fallbackManaged) {
|
|
558
836
|
events = observeManagedGatewayFailure(events, error =>
|
|
559
837
|
markManagedGatewayCredentialFailure(
|
|
@@ -633,7 +911,12 @@ async function handleFormatEndpoint(
|
|
|
633
911
|
* `parseRequest`/`encodeResponse`/`encodeStream` differ from the format-endpoint
|
|
634
912
|
* path.
|
|
635
913
|
*/
|
|
636
|
-
async function handlePiNative(
|
|
914
|
+
async function handlePiNative(
|
|
915
|
+
bootOpts: AuthGatewayBootOptions,
|
|
916
|
+
catalog: AuthGatewayModelCatalog,
|
|
917
|
+
req: Request,
|
|
918
|
+
peer: string,
|
|
919
|
+
): Promise<Response> {
|
|
637
920
|
const controller = mirrorRequestAbort(req);
|
|
638
921
|
const aborted = (): Response => piNative.formatError(499, "request_aborted", "client closed request");
|
|
639
922
|
if (controller.signal.aborted) return aborted();
|
|
@@ -660,43 +943,28 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
660
943
|
return piNative.formatError(400, "invalid_request_error", message);
|
|
661
944
|
}
|
|
662
945
|
|
|
663
|
-
const model = bootOpts
|
|
946
|
+
const model = resolveScopedModel(bootOpts, catalog, parsed.modelId);
|
|
664
947
|
if (!model) {
|
|
665
948
|
return piNative.formatError(404, "invalid_request_error", `Unknown model: ${parsed.modelId}`);
|
|
666
949
|
}
|
|
667
950
|
|
|
668
|
-
let apiKey: string | undefined;
|
|
669
|
-
try {
|
|
670
|
-
apiKey = await bootOpts.storage.getApiKey(model.provider, undefined, {
|
|
671
|
-
modelId: model.id,
|
|
672
|
-
signal: controller.signal,
|
|
673
|
-
});
|
|
674
|
-
} catch (error) {
|
|
675
|
-
if (controller.signal.aborted) return aborted();
|
|
676
|
-
const classified = classifyGatewayError(error);
|
|
677
|
-
logger.warn("auth-gateway getApiKey threw", { provider: model.provider, peer, error: classified.message });
|
|
678
|
-
return piNative.formatError(classified.status, classified.type, classified.message);
|
|
679
|
-
}
|
|
680
|
-
if (controller.signal.aborted) return aborted();
|
|
681
|
-
if (!apiKey) {
|
|
682
|
-
return piNative.formatError(
|
|
683
|
-
401,
|
|
684
|
-
"authentication_error",
|
|
685
|
-
`No credential available for provider ${model.provider}`,
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
951
|
// Build the SimpleStreamOptions actually handed to `streamSimple`. We
|
|
690
952
|
// trust the client's options (already allow-listed by `parseRequest`) and
|
|
691
953
|
// only inject server-controlled fields. The OpenAI code backend temperature/topP strip
|
|
692
954
|
// matches `buildStreamOptions` — OpenAI code backend rejects them with a 400.
|
|
693
|
-
const streamOpts: SimpleStreamOptions = {
|
|
955
|
+
const streamOpts: SimpleStreamOptions = {
|
|
956
|
+
...parsed.options,
|
|
957
|
+
signal: controller.signal,
|
|
958
|
+
requestMaxRetries: 0,
|
|
959
|
+
streamMaxRetries: 0,
|
|
960
|
+
disableProviderRetries: true,
|
|
961
|
+
};
|
|
694
962
|
if (streamOpts.fallbackManaged) {
|
|
695
963
|
streamOpts.fallbackAttempt = beginAttempt(model.id, "auth-gateway-pi-native");
|
|
696
964
|
} else {
|
|
697
965
|
streamOpts.onAuthError = (provider, oldKey, error) =>
|
|
698
966
|
refreshGatewayApiKeyAfterAuthError(
|
|
699
|
-
bootOpts
|
|
967
|
+
bootOpts,
|
|
700
968
|
model,
|
|
701
969
|
provider,
|
|
702
970
|
oldKey,
|
|
@@ -728,11 +996,36 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
728
996
|
peer,
|
|
729
997
|
});
|
|
730
998
|
|
|
731
|
-
let
|
|
999
|
+
let apiKey: string;
|
|
1000
|
+
let credentialLease: GatewayCredentialLease;
|
|
732
1001
|
try {
|
|
1002
|
+
credentialLease = await acquireGatewayApiKey(bootOpts, model, peer, controller.signal);
|
|
1003
|
+
apiKey = credentialLease.apiKey;
|
|
1004
|
+
streamOpts.apiKey = apiKey;
|
|
1005
|
+
} catch (error) {
|
|
733
1006
|
if (controller.signal.aborted) return aborted();
|
|
1007
|
+
if (error instanceof GatewayCredentialError) {
|
|
1008
|
+
return piNative.formatError(error.status, error.type, error.message);
|
|
1009
|
+
}
|
|
1010
|
+
throw error;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
let events: AssistantMessageEventStream;
|
|
1014
|
+
let releasedAtAdmission = false;
|
|
1015
|
+
const releaseAtAdmission = (): void => {
|
|
1016
|
+
if (releasedAtAdmission) return;
|
|
1017
|
+
releasedAtAdmission = true;
|
|
1018
|
+
credentialLease.release();
|
|
1019
|
+
};
|
|
1020
|
+
streamOpts.onStreamCreated = releaseAtAdmission;
|
|
1021
|
+
try {
|
|
1022
|
+
if (controller.signal.aborted) {
|
|
1023
|
+
credentialLease.release();
|
|
1024
|
+
return aborted();
|
|
1025
|
+
}
|
|
734
1026
|
events = streamSimple(model, parsed.context, streamOpts);
|
|
735
1027
|
} catch (error) {
|
|
1028
|
+
credentialLease.release();
|
|
736
1029
|
if (streamOpts.fallbackManaged) {
|
|
737
1030
|
await markManagedGatewayCredentialFailure(
|
|
738
1031
|
bootOpts.storage,
|
|
@@ -748,6 +1041,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
748
1041
|
logger.warn("auth-gateway streamSimple threw", { format: "pi-native", error: classified.message, peer });
|
|
749
1042
|
return piNative.formatError(classified.status, classified.type, classified.message);
|
|
750
1043
|
}
|
|
1044
|
+
releaseGatewayCredentialLeaseOnAdmission(events, releaseAtAdmission, controller.signal);
|
|
751
1045
|
if (streamOpts.fallbackManaged) {
|
|
752
1046
|
events = observeManagedGatewayFailure(events, error =>
|
|
753
1047
|
markManagedGatewayCredentialFailure(
|
|
@@ -812,8 +1106,12 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
|
|
|
812
1106
|
* failure) inside `AuthStorage`, so this handler is a thin wrapper that
|
|
813
1107
|
* surfaces the same data to HTTP callers (notably the macOS usage widget).
|
|
814
1108
|
*/
|
|
815
|
-
async function handleUsage(storage: AuthStorage, signal: AbortSignal): Promise<Response> {
|
|
816
|
-
const
|
|
1109
|
+
async function handleUsage(storage: AuthStorage, provider: Provider, signal: AbortSignal): Promise<Response> {
|
|
1110
|
+
const fetchedReports = await storage.fetchUsageReports?.({ provider, signal });
|
|
1111
|
+
if (fetchedReports === null || fetchedReports === undefined) {
|
|
1112
|
+
throw new Error("Usage unavailable.");
|
|
1113
|
+
}
|
|
1114
|
+
const reports = fetchedReports.filter(report => report.provider === provider);
|
|
817
1115
|
// Drop the heavy provider-specific `raw` payload — UI consumers only need
|
|
818
1116
|
// `limits` + `metadata`. Match the broker's `/v1/usage` shape so a single
|
|
819
1117
|
// client struct (Swift widget, llm-git, ...) works against either endpoint.
|
|
@@ -821,6 +1119,10 @@ async function handleUsage(storage: AuthStorage, signal: AbortSignal): Promise<R
|
|
|
821
1119
|
return json(200, { generatedAt: Date.now(), reports: trimmed });
|
|
822
1120
|
}
|
|
823
1121
|
|
|
1122
|
+
function emptyScopedCredentialsResponse(): Response {
|
|
1123
|
+
return json(200, { generatedAt: Date.now(), credentials: [] });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
824
1126
|
/**
|
|
825
1127
|
* Per-credential health probe surfaced on `GET /v1/credentials/check`. Tells
|
|
826
1128
|
* the caller exactly which row in their broker is producing 401s — the
|
|
@@ -832,14 +1134,28 @@ async function handleUsage(storage: AuthStorage, signal: AbortSignal): Promise<R
|
|
|
832
1134
|
* endpoints. For multi-account pools that's the difference between getting
|
|
833
1135
|
* a clean diagnosis and getting a 429 storm.
|
|
834
1136
|
*/
|
|
835
|
-
async function handleCredentialsCheck(
|
|
836
|
-
|
|
1137
|
+
async function handleCredentialsCheck(
|
|
1138
|
+
storage: AuthStorage,
|
|
1139
|
+
provider: Provider,
|
|
1140
|
+
signal: AbortSignal,
|
|
1141
|
+
): Promise<Response> {
|
|
1142
|
+
const credentials = (await storage.checkCredentials({ provider, signal }))
|
|
1143
|
+
.filter(row => row.provider === provider)
|
|
1144
|
+
.map(row => ({
|
|
1145
|
+
id: row.id,
|
|
1146
|
+
provider: row.provider,
|
|
1147
|
+
type: row.type,
|
|
1148
|
+
...(row.remoteRefresh ? { remoteRefresh: true as const } : {}),
|
|
1149
|
+
ok: row.ok,
|
|
1150
|
+
...(row.reason
|
|
1151
|
+
? { reason: row.ok === false ? "Credential check failed." : "Credential status unavailable." }
|
|
1152
|
+
: {}),
|
|
1153
|
+
}));
|
|
837
1154
|
return json(200, { generatedAt: Date.now(), credentials });
|
|
838
1155
|
}
|
|
839
1156
|
|
|
840
|
-
function handleModelsList(
|
|
841
|
-
const
|
|
842
|
-
const data = list.map(model => ({
|
|
1157
|
+
function handleModelsList(catalog: AuthGatewayModelCatalog): Response {
|
|
1158
|
+
const data = catalog.models.map(model => ({
|
|
843
1159
|
id: model.id,
|
|
844
1160
|
object: "model" as const,
|
|
845
1161
|
owned_by: model.provider,
|
|
@@ -849,7 +1165,21 @@ function handleModelsList(opts: AuthGatewayBootOptions): Response {
|
|
|
849
1165
|
}
|
|
850
1166
|
|
|
851
1167
|
export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServerHandle {
|
|
1168
|
+
const provider = opts.providerScope.provider;
|
|
1169
|
+
if (!isSafeProviderScope(provider)) {
|
|
1170
|
+
throw new Error("Auth gateway requires a valid provider scope");
|
|
1171
|
+
}
|
|
1172
|
+
if (!opts.reloadProviderCredentials || !opts.hasProviderCredential || !opts.validateProviderCredential) {
|
|
1173
|
+
throw new Error("Auth gateway requires live provider authority callbacks");
|
|
1174
|
+
}
|
|
852
1175
|
const bind = parseBind(opts.bind ?? DEFAULT_AUTH_GATEWAY_BIND);
|
|
1176
|
+
if (!hasProviderCredential(opts)) {
|
|
1177
|
+
throw new Error(`Auth gateway scope ${provider} has no enabled broker credential`);
|
|
1178
|
+
}
|
|
1179
|
+
const catalog = createAuthGatewayModelCatalog(provider, opts.listModels());
|
|
1180
|
+
if (catalog.models.length === 0) {
|
|
1181
|
+
throw new Error(`Auth gateway scope ${provider} has no source-backed models`);
|
|
1182
|
+
}
|
|
853
1183
|
const tokens = new Set<string>(opts.bearerTokens);
|
|
854
1184
|
assertAuthenticatedOrLoopback(bind, tokens.size, "auth-gateway");
|
|
855
1185
|
const version = opts.version;
|
|
@@ -868,7 +1198,7 @@ export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServe
|
|
|
868
1198
|
peer,
|
|
869
1199
|
origin: req.headers.get("origin"),
|
|
870
1200
|
});
|
|
871
|
-
return json(403, { error: "
|
|
1201
|
+
return json(403, { error: "no-auth rejects requests carrying Origin" });
|
|
872
1202
|
}
|
|
873
1203
|
// CORS preflight is always answered without auth — browsers send
|
|
874
1204
|
// preflights pre-authentication and a 401 here breaks the actual
|
|
@@ -889,31 +1219,61 @@ export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServe
|
|
|
889
1219
|
// Same shape as the broker's `/v1/usage`, so widget/llm-git speak to either with the
|
|
890
1220
|
// same client struct.
|
|
891
1221
|
if (req.method === "GET" && pathname === "/v1/usage") {
|
|
892
|
-
|
|
1222
|
+
const scopeAvailability = await providerScopeAvailability(opts, req.signal);
|
|
1223
|
+
if (scopeAvailability === "reload_failed") {
|
|
1224
|
+
return withCors(
|
|
1225
|
+
json(503, { error: { code: "broker_unavailable", message: "Auth broker unavailable." } }),
|
|
1226
|
+
req,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
if (scopeAvailability === "absent")
|
|
1230
|
+
return withCors(json(200, { generatedAt: Date.now(), reports: [] }), req);
|
|
1231
|
+
try {
|
|
1232
|
+
return withCors(await handleUsage(opts.storage, opts.providerScope.provider, req.signal), req);
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
logger.warn("auth-gateway scoped usage unavailable", {
|
|
1235
|
+
error: cleanReason(error) ?? "Usage unavailable.",
|
|
1236
|
+
});
|
|
1237
|
+
return withCors(
|
|
1238
|
+
json(503, { error: { code: "usage_unavailable", message: "Usage unavailable." } }),
|
|
1239
|
+
req,
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
893
1242
|
}
|
|
894
1243
|
|
|
895
1244
|
// Per-credential auth probe — diagnoses which row in a multi-account
|
|
896
1245
|
// pool is producing 401s. Aggregated `/v1/usage` silently drops failed
|
|
897
1246
|
// credentials, so we need a separate endpoint that captures errors.
|
|
898
1247
|
if (req.method === "GET" && pathname === "/v1/credentials/check") {
|
|
899
|
-
|
|
1248
|
+
const scopeAvailability = await providerScopeAvailability(opts, req.signal);
|
|
1249
|
+
if (scopeAvailability === "reload_failed") {
|
|
1250
|
+
return withCors(
|
|
1251
|
+
json(503, { error: { code: "broker_unavailable", message: "Auth broker unavailable." } }),
|
|
1252
|
+
req,
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
if (scopeAvailability === "absent") return withCors(emptyScopedCredentialsResponse(), req);
|
|
1256
|
+
return withCors(
|
|
1257
|
+
await handleCredentialsCheck(opts.storage, opts.providerScope.provider, req.signal),
|
|
1258
|
+
req,
|
|
1259
|
+
);
|
|
900
1260
|
}
|
|
901
1261
|
|
|
902
1262
|
// Provider-format dispatch.
|
|
903
1263
|
const formatRoute = FORMAT_ROUTES[pathname];
|
|
904
1264
|
if (formatRoute && req.method === "POST") {
|
|
905
|
-
return withCors(await handleFormatEndpoint(formatRoute, opts, req, peer), req);
|
|
1265
|
+
return withCors(await handleFormatEndpoint(formatRoute, opts, catalog, req, peer), req);
|
|
906
1266
|
}
|
|
907
1267
|
|
|
908
1268
|
// Pi-native fast path. Same auth + provider plumbing as the
|
|
909
1269
|
// foreign-wire routes, just without the wire-format translation.
|
|
910
1270
|
if (req.method === "POST" && pathname === "/v1/pi/stream") {
|
|
911
|
-
return withCors(await handlePiNative(opts, req, peer), req);
|
|
1271
|
+
return withCors(await handlePiNative(opts, catalog, req, peer), req);
|
|
912
1272
|
}
|
|
913
1273
|
|
|
914
1274
|
// Model catalog.
|
|
915
1275
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
916
|
-
return withCors(handleModelsList(
|
|
1276
|
+
return withCors(handleModelsList(catalog), req);
|
|
917
1277
|
}
|
|
918
1278
|
|
|
919
1279
|
// Route-table miss: no format module to defer to, so we emit a
|
|
@@ -945,3 +1305,11 @@ export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServe
|
|
|
945
1305
|
},
|
|
946
1306
|
};
|
|
947
1307
|
}
|
|
1308
|
+
|
|
1309
|
+
export function isSafeProviderScope(provider: unknown): provider is string {
|
|
1310
|
+
return (
|
|
1311
|
+
typeof provider === "string" &&
|
|
1312
|
+
provider === provider.trim() &&
|
|
1313
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(provider)
|
|
1314
|
+
);
|
|
1315
|
+
}
|