@gajae-code/ai 0.13.2 → 0.14.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +61 -2
  2. package/dist/types/auth-broker/client.d.ts +9 -1
  3. package/dist/types/auth-broker/redact.d.ts +7 -0
  4. package/dist/types/auth-broker/remote-store.d.ts +50 -9
  5. package/dist/types/auth-broker/types.d.ts +14 -0
  6. package/dist/types/auth-broker/wire-schemas.d.ts +25 -0
  7. package/dist/types/auth-storage.d.ts +200 -6
  8. package/dist/types/core.d.ts +1 -0
  9. package/dist/types/model-cache.d.ts +4 -1
  10. package/dist/types/model-manager.d.ts +11 -0
  11. package/dist/types/provider-models/openai-compat.d.ts +5 -0
  12. package/dist/types/provider-models/special.d.ts +3 -0
  13. package/dist/types/providers/anthropic.d.ts +31 -0
  14. package/dist/types/providers/cursor.d.ts +9 -1
  15. package/dist/types/providers/kiro-codewhisperer.d.ts +8 -0
  16. package/dist/types/providers/mock.d.ts +8 -0
  17. package/dist/types/providers/register-builtins.d.ts +1 -0
  18. package/dist/types/providers/transform-messages.d.ts +18 -0
  19. package/dist/types/types.d.ts +34 -8
  20. package/dist/types/usage/grok-cli.d.ts +5 -0
  21. package/dist/types/usage.d.ts +6 -0
  22. package/dist/types/utils/discovery/openai-compatible.d.ts +5 -0
  23. package/dist/types/utils/event-stream.d.ts +4 -2
  24. package/dist/types/utils/fallback-transport.d.ts +10 -0
  25. package/dist/types/utils/http-inspector.d.ts +1 -0
  26. package/dist/types/utils/idle-iterator.d.ts +13 -1
  27. package/dist/types/utils/json-parse.d.ts +19 -0
  28. package/dist/types/utils/oauth/callback-server.d.ts +13 -0
  29. package/dist/types/utils/oauth/kiro.d.ts +71 -0
  30. package/dist/types/utils/oauth/types.d.ts +1 -1
  31. package/dist/types/utils/parse-bind.d.ts +8 -5
  32. package/dist/types/utils/tool-call-healing.d.ts +7 -0
  33. package/dist/types/utils/tool-choice-capability.d.ts +11 -0
  34. package/package.json +3 -2
  35. package/src/auth-broker/client.ts +30 -0
  36. package/src/auth-broker/redact.ts +15 -0
  37. package/src/auth-broker/refresher.ts +4 -2
  38. package/src/auth-broker/remote-store.ts +693 -70
  39. package/src/auth-broker/server.ts +57 -12
  40. package/src/auth-broker/types.ts +16 -0
  41. package/src/auth-broker/wire-schemas.ts +21 -0
  42. package/src/auth-gateway/server.ts +84 -19
  43. package/src/auth-storage.ts +985 -41
  44. package/src/core.ts +1 -0
  45. package/src/model-cache.ts +23 -4
  46. package/src/model-manager.ts +70 -11
  47. package/src/model-thinking.ts +45 -1
  48. package/src/models.json +9604 -1932
  49. package/src/openai-completions-compat.ts +2 -1
  50. package/src/provider-models/descriptors.ts +7 -1
  51. package/src/provider-models/openai-compat.ts +52 -28
  52. package/src/provider-models/special.ts +12 -0
  53. package/src/providers/amazon-bedrock.ts +2 -1
  54. package/src/providers/anthropic.ts +831 -27
  55. package/src/providers/cursor.ts +83 -3
  56. package/src/providers/kiro-codewhisperer.ts +572 -0
  57. package/src/providers/mock.ts +15 -2
  58. package/src/providers/ollama.ts +9 -2
  59. package/src/providers/openai-codex-responses.ts +16 -9
  60. package/src/providers/openai-completions.ts +6 -1
  61. package/src/providers/openai-responses-shared.ts +180 -18
  62. package/src/providers/register-builtins.ts +24 -2
  63. package/src/providers/transform-messages.ts +64 -1
  64. package/src/stream.ts +25 -2
  65. package/src/types.ts +36 -7
  66. package/src/usage/grok-cli.ts +86 -1
  67. package/src/usage.ts +7 -0
  68. package/src/utils/discovery/openai-compatible.ts +89 -4
  69. package/src/utils/event-stream.ts +11 -2
  70. package/src/utils/fallback-transport.ts +44 -2
  71. package/src/utils/http-inspector.ts +1 -0
  72. package/src/utils/idle-iterator.ts +29 -6
  73. package/src/utils/json-parse.ts +80 -0
  74. package/src/utils/oauth/callback-server.ts +31 -1
  75. package/src/utils/oauth/index.ts +14 -1
  76. package/src/utils/oauth/kiro.ts +448 -0
  77. package/src/utils/oauth/synthetic.ts +2 -3
  78. package/src/utils/oauth/types.ts +1 -0
  79. package/src/utils/parse-bind.ts +27 -0
  80. package/src/utils/tool-call-healing.ts +13 -2
  81. package/src/utils/tool-choice-capability.ts +386 -6
@@ -10,12 +10,15 @@
10
10
  * the server only checks a bearer token against an allow-list per request.
11
11
  */
12
12
  import { logger } from "@gajae-code/utils";
13
+ import { timingSafeEqual } from "../auth-gateway/http";
13
14
  import type { AuthStorage } from "../auth-storage";
14
- import { parseBind } from "../utils/parse-bind";
15
+ import { assertAuthenticatedOrLoopback, parseBind } from "../utils/parse-bind";
16
+ import { cleanReason } from "./redact";
15
17
  import { AuthBrokerRefresher, type AuthBrokerRefresherSchedule } from "./refresher";
16
18
  import type {
17
19
  CredentialDisableResponse,
18
20
  CredentialIfAbsentUploadResponse,
21
+ CredentialMetadataResponse,
19
22
  CredentialRefreshRequest,
20
23
  CredentialRefreshResponse,
21
24
  CredentialUploadResponse,
@@ -88,7 +91,13 @@ function isAuthorized(req: Request, tokens: ReadonlySet<string>): boolean {
88
91
  if (!header) return false;
89
92
  const match = header.match(/^Bearer\s+(.+)$/i);
90
93
  if (!match) return false;
91
- return tokens.has(match[1].trim());
94
+ const presented = new TextEncoder().encode(match[1].trim());
95
+ let ok = false;
96
+ for (const token of tokens) {
97
+ const expected = new TextEncoder().encode(token);
98
+ if (timingSafeEqual(presented, expected)) ok = true;
99
+ }
100
+ return ok;
92
101
  }
93
102
 
94
103
  /**
@@ -105,7 +114,12 @@ async function parseBody<T>(
105
114
  try {
106
115
  raw = await req.text();
107
116
  } catch (error) {
108
- return { ok: false, response: json(400, { error: `Invalid request body: ${String(error)}` }) };
117
+ return {
118
+ ok: false,
119
+ response: json(400, {
120
+ error: `Invalid request body: ${cleanReason(error) ?? "request body could not be read"}`,
121
+ }),
122
+ };
109
123
  }
110
124
  if (raw.length === 0 && !options.allowEmpty) {
111
125
  return { ok: false, response: json(400, { error: "Request body required" }) };
@@ -114,7 +128,12 @@ async function parseBody<T>(
114
128
  try {
115
129
  parsed = raw.length === 0 ? {} : JSON.parse(raw);
116
130
  } catch (error) {
117
- return { ok: false, response: json(400, { error: `Invalid JSON body: ${String(error)}` }) };
131
+ return {
132
+ ok: false,
133
+ response: json(400, {
134
+ error: `Invalid JSON body: ${cleanReason(error) ?? "request body could not be parsed"}`,
135
+ }),
136
+ };
118
137
  }
119
138
  const result = schema.safeParse(parsed);
120
139
  if (!result.success) {
@@ -284,6 +303,22 @@ function buildSnapshot(storage: AuthStorage, refresher: AuthBrokerRefresher | un
284
303
  };
285
304
  }
286
305
 
306
+ /** Build the payload-free credential inventory projection for the metadata route. */
307
+ function buildCredentialMetadata(storage: AuthStorage): CredentialMetadataResponse {
308
+ const credentials = storage.listCredentialInventory().map(record => ({
309
+ id: record.id,
310
+ provider: record.provider,
311
+ type: record.credentialKind,
312
+ identity: record.identityLabel,
313
+ disabledCause: record.disabledCause,
314
+ }));
315
+ return {
316
+ generation: storage.getGeneration(),
317
+ generatedAt: Date.now(),
318
+ credentials,
319
+ };
320
+ }
321
+
287
322
  async function serveSnapshot(
288
323
  req: Request,
289
324
  url: URL,
@@ -505,6 +540,7 @@ function serveSnapshotStream(
505
540
  export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServerHandle {
506
541
  const bind = parseBind(opts.bind ?? DEFAULT_AUTH_BROKER_BIND);
507
542
  const tokens = new Set<string>(opts.bearerTokens);
543
+ assertAuthenticatedOrLoopback(bind, tokens.size, "auth-broker");
508
544
  const version = opts.version;
509
545
  const streamKeepaliveMs = opts.streamKeepaliveMs ?? DEFAULT_STREAM_KEEPALIVE_MS;
510
546
 
@@ -536,6 +572,15 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
536
572
  logger.info("auth-broker request unauthorized", { method: req.method, path: pathname, peer });
537
573
  return json(401, { error: "unauthorized" });
538
574
  }
575
+ if (req.method === "GET" && pathname === "/v1/credentials/metadata") {
576
+ await opts.storage.reload();
577
+ const body = buildCredentialMetadata(opts.storage);
578
+ logger.info("auth-broker credential metadata served", {
579
+ generation: body.generation,
580
+ status: "ok",
581
+ });
582
+ return json(200, body);
583
+ }
539
584
  if (req.method === "GET" && pathname === "/v1/snapshot/stream") {
540
585
  return serveSnapshotStream(req, opts.storage, refresher, peer, streamKeepaliveMs);
541
586
  }
@@ -557,7 +602,7 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
557
602
  logger.info("auth-broker usage served", { peer, reports: trimmed.length });
558
603
  return json(200, { generatedAt: Date.now(), reports: trimmed });
559
604
  } catch (error) {
560
- const message = error instanceof Error ? error.message : String(error);
605
+ const message = cleanReason(error) ?? "Usage fetch failed";
561
606
  logger.warn("auth-broker usage fetch failed", { peer, error: message });
562
607
  return json(502, { error: message });
563
608
  }
@@ -579,9 +624,9 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
579
624
  });
580
625
  return json(200, body);
581
626
  } catch (error) {
582
- const message = error instanceof Error ? error.message : String(error);
627
+ const message = cleanReason(error) ?? "Credential refresh failed";
583
628
  logger.warn("auth-broker refresh failed", { id, peer, error: message });
584
- const status = message.includes("No credential with id") ? 404 : 500;
629
+ const status = message.startsWith("No credential with id") ? 404 : 500;
585
630
  return json(status, { error: message });
586
631
  }
587
632
  }
@@ -594,10 +639,10 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
594
639
  parsed.data.cause && parsed.data.cause.length > 0 ? parsed.data.cause : "disabled via auth-broker";
595
640
  const ok = opts.storage.disableCredentialById(id, cause);
596
641
  if (!ok) {
597
- logger.info("auth-broker disable miss", { id, peer, cause });
642
+ logger.info("auth-broker disable miss", { id, peer });
598
643
  return json(404, { error: `No credential with id=${id}` });
599
644
  }
600
- logger.info("auth-broker credential disabled", { id, peer, cause });
645
+ logger.info("auth-broker credential disabled", { id, peer });
601
646
  const response: CredentialDisableResponse = { ok: true };
602
647
  return json(200, response);
603
648
  }
@@ -622,7 +667,7 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
622
667
  };
623
668
  return json(200, response);
624
669
  } catch (error) {
625
- const message = error instanceof Error ? error.message : String(error);
670
+ const message = cleanReason(error) ?? "Credential import failed";
626
671
  logger.warn("auth-broker upload-if-absent failed", { provider, type: credential.type, peer });
627
672
  return json(500, { error: message });
628
673
  }
@@ -647,7 +692,7 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
647
692
  const response: CredentialUploadResponse = { entries };
648
693
  return json(200, response);
649
694
  } catch (error) {
650
- const message = error instanceof Error ? error.message : String(error);
695
+ const message = cleanReason(error) ?? "Credential upload failed";
651
696
  logger.warn("auth-broker upload failed", { provider, peer, error: message });
652
697
  return json(500, { error: message });
653
698
  }
@@ -657,7 +702,7 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
657
702
  logger.error("auth-broker handler crashed", {
658
703
  method: req.method,
659
704
  path: pathname,
660
- error: String(error),
705
+ error: cleanReason(error) ?? "internal error",
661
706
  });
662
707
  return json(500, { error: "internal error" });
663
708
  }
@@ -15,6 +15,22 @@ import type {
15
15
  } from "../auth-storage";
16
16
  import type { UsageReport } from "../usage";
17
17
 
18
+ /** A single credential metadata projection; secret-bearing fields are intentionally absent. */
19
+ export interface CredentialMetadataRecord {
20
+ id: number;
21
+ provider: string;
22
+ type: "oauth" | "api_key";
23
+ identity: string | null;
24
+ disabledCause: string | null;
25
+ }
26
+
27
+ /** GET /v1/credentials/metadata response body. */
28
+ export interface CredentialMetadataResponse {
29
+ generation: number;
30
+ generatedAt: number;
31
+ credentials: CredentialMetadataRecord[];
32
+ }
33
+
18
34
  /** GET /v1/healthz response body. */
19
35
  export interface HealthzResponse {
20
36
  ok: boolean;
@@ -109,6 +109,27 @@ export const snapshotResponseSchema = z
109
109
  })
110
110
  .strict();
111
111
 
112
+ // ─── Credential metadata ───────────────────────────────────────────────────
113
+
114
+ /** Closed redacted projection: no token, key, identity object, or extension fields. */
115
+ export const credentialMetadataRecordSchema = z
116
+ .object({
117
+ id: z.number().int(),
118
+ provider: z.string().min(1),
119
+ type: z.enum(["oauth", "api_key"]),
120
+ identity: z.string().nullable(),
121
+ disabledCause: z.string().nullable(),
122
+ })
123
+ .strict();
124
+
125
+ export const credentialMetadataResponseSchema = z
126
+ .object({
127
+ generation: z.number().int().nonnegative(),
128
+ generatedAt: z.number().finite().nonnegative(),
129
+ credentials: z.array(credentialMetadataRecordSchema),
130
+ })
131
+ .strict();
132
+
112
133
  // ─── Snapshot stream (SSE) ────────────────────────────────────────────────
113
134
 
114
135
  /** First frame on connect — full snapshot embedded inline with a `kind` tag. */
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { logger } from "@gajae-code/utils";
22
+ import { cleanReason } from "../auth-broker/redact";
22
23
  import type { AuthStorage } from "../auth-storage";
23
24
  import { Effort } from "../model-thinking";
24
25
  import * as anthropicMessages from "../providers/anthropic-messages-server";
@@ -26,9 +27,17 @@ import * as openaiChat from "../providers/openai-chat-server";
26
27
  import * as openaiResponses from "../providers/openai-responses-server";
27
28
  import * as piNative from "../providers/pi-native-server";
28
29
  import { streamSimple } from "../stream";
29
- import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "../types";
30
+ import type {
31
+ Api,
32
+ AssistantMessage,
33
+ AssistantMessageEvent,
34
+ AssistantMessageEventStream,
35
+ Context,
36
+ Model,
37
+ SimpleStreamOptions,
38
+ } from "../types";
30
39
  import { beginAttempt, classifyFallbackTrigger } from "../utils/fallback-transport";
31
- import { parseBind } from "../utils/parse-bind";
40
+ import { assertAuthenticatedOrLoopback, parseBind } from "../utils/parse-bind";
32
41
  import {
33
42
  captureRequestHeaders,
34
43
  corsHeaders,
@@ -204,8 +213,9 @@ function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: Abort
204
213
  * format emits its native envelope shape.
205
214
  */
206
215
  function classifyGatewayError(err: unknown): { status: number; type: string; message: string } {
207
- const message = err instanceof Error ? err.message : String(err);
208
- const lower = message.toLowerCase();
216
+ const rawMessage = err instanceof Error ? err.message : String(err);
217
+ const message = cleanReason(rawMessage) ?? "Upstream request failed";
218
+ const lower = rawMessage.toLowerCase();
209
219
 
210
220
  // Custom pi-ai errors may attach a numeric `status` property; honor it
211
221
  // when present and pick the matching tag.
@@ -242,6 +252,48 @@ function classifyGatewayError(err: unknown): { status: number; type: string; mes
242
252
  return { status: 502, type: "upstream_error", message };
243
253
  }
244
254
 
255
+ function redactGatewayError(error: unknown): Error {
256
+ const redacted = new Error(cleanReason(error) ?? "Upstream request failed");
257
+ if (error instanceof Error) {
258
+ redacted.name = error.name;
259
+ const status = (error as { status?: unknown }).status;
260
+ if (typeof status === "number") (redacted as Error & { status: number }).status = status;
261
+ }
262
+ return redacted;
263
+ }
264
+
265
+ function redactGatewayMessage(message: AssistantMessage): AssistantMessage {
266
+ if (message.errorMessage === undefined) return message;
267
+ return { ...message, errorMessage: cleanReason(message.errorMessage) ?? "Upstream request failed" };
268
+ }
269
+
270
+ /**
271
+ * Protect all gateway SSE encoders from upstream error text. Provider wire
272
+ * modules format `error` events themselves, so sanitize at this boundary and
273
+ * also convert iterator failures to bounded errors before their catch blocks.
274
+ */
275
+ function redactGatewayStream(events: AssistantMessageEventStream): AssistantMessageEventStream {
276
+ async function* redactedEvents(): AsyncGenerator<AssistantMessageEvent> {
277
+ try {
278
+ for await (const event of events) {
279
+ if (event.type === "error") yield { ...event, error: redactGatewayMessage(event.error) };
280
+ else yield event;
281
+ }
282
+ } catch (error) {
283
+ throw redactGatewayError(error);
284
+ }
285
+ }
286
+ const result = redactedEvents() as unknown as AssistantMessageEventStream;
287
+ result.result = async () => {
288
+ try {
289
+ return redactGatewayMessage(await events.result());
290
+ } catch (error) {
291
+ throw redactGatewayError(error);
292
+ }
293
+ };
294
+ return result;
295
+ }
296
+
245
297
  async function refreshGatewayApiKeyAfterAuthError(
246
298
  storage: AuthStorage,
247
299
  model: Model<Api>,
@@ -257,7 +309,7 @@ async function refreshGatewayApiKeyAfterAuthError(
257
309
  format,
258
310
  provider,
259
311
  peer,
260
- error: error instanceof Error ? error.message : String(error),
312
+ error: cleanReason(error) ?? "Upstream request failed",
261
313
  });
262
314
  return storage.getApiKey(provider, undefined, { modelId: model.id, signal });
263
315
  }
@@ -308,7 +360,7 @@ async function markManagedGatewayCredentialFailure(
308
360
  format,
309
361
  provider: model.provider,
310
362
  peer,
311
- error: markError instanceof Error ? markError.message : String(markError),
363
+ error: cleanReason(markError) ?? "Credential bookkeeping failed",
312
364
  });
313
365
  }
314
366
  }
@@ -382,7 +434,11 @@ async function handleFormatEndpoint(
382
434
  body = await req.json();
383
435
  } catch (error) {
384
436
  if (controller.signal.aborted) return clientClosedResponse(route);
385
- return route.module.formatError(400, "invalid_request_error", `Invalid JSON body: ${String(error)}`);
437
+ return route.module.formatError(
438
+ 400,
439
+ "invalid_request_error",
440
+ `Invalid JSON body: ${cleanReason(error) ?? "request body could not be parsed"}`,
441
+ );
386
442
  }
387
443
  if (controller.signal.aborted) return clientClosedResponse(route);
388
444
 
@@ -438,7 +494,7 @@ async function handleFormatEndpoint(
438
494
  parsed = route.module.parseRequest(body, req.headers);
439
495
  } catch (error) {
440
496
  if (controller.signal.aborted) return clientClosedResponse(route);
441
- const message = error instanceof Error ? error.message : String(error);
497
+ const message = cleanReason(error) ?? "Request validation failed";
442
498
  return route.module.formatError(400, "invalid_request_error", message);
443
499
  }
444
500
  // Merge gateway-captured passthrough headers under the parser's own
@@ -511,6 +567,7 @@ async function handleFormatEndpoint(
511
567
  ),
512
568
  );
513
569
  }
570
+ events = redactGatewayStream(events);
514
571
 
515
572
  if (!parsed.stream) {
516
573
  try {
@@ -520,17 +577,18 @@ async function handleFormatEndpoint(
520
577
  const errorMessage =
521
578
  message.errorMessage ??
522
579
  (message.stopReason === "aborted" ? "Request was aborted" : "Upstream request failed");
580
+ const safeErrorMessage = cleanReason(errorMessage) ?? "Upstream request failed";
523
581
  logger.warn("auth-gateway non-streaming failed", {
524
582
  format: route.label,
525
583
  reason: message.stopReason,
526
- error: errorMessage,
584
+ error: safeErrorMessage,
527
585
  peer,
528
586
  });
529
587
  if (message.stopReason === "aborted") {
530
- return route.module.formatError(499, "request_aborted", errorMessage);
588
+ return route.module.formatError(499, "request_aborted", safeErrorMessage);
531
589
  }
532
- const classified = classifyGatewayError(new Error(errorMessage));
533
- return route.module.formatError(classified.status, classified.type, errorMessage);
590
+ const classified = classifyGatewayError(new Error(safeErrorMessage));
591
+ return route.module.formatError(classified.status, classified.type, classified.message);
534
592
  }
535
593
  return json(200, route.module.encodeResponse(message, parsed.modelId));
536
594
  } catch (error) {
@@ -585,7 +643,11 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
585
643
  body = await req.json();
586
644
  } catch (error) {
587
645
  if (controller.signal.aborted) return aborted();
588
- return piNative.formatError(400, "invalid_request_error", `Invalid JSON body: ${String(error)}`);
646
+ return piNative.formatError(
647
+ 400,
648
+ "invalid_request_error",
649
+ `Invalid JSON body: ${cleanReason(error) ?? "request body could not be parsed"}`,
650
+ );
589
651
  }
590
652
  if (controller.signal.aborted) return aborted();
591
653
 
@@ -594,7 +656,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
594
656
  parsed = piNative.parseRequest(body, req.headers);
595
657
  } catch (error) {
596
658
  if (controller.signal.aborted) return aborted();
597
- const message = error instanceof Error ? error.message : String(error);
659
+ const message = cleanReason(error) ?? "Request validation failed";
598
660
  return piNative.formatError(400, "invalid_request_error", message);
599
661
  }
600
662
 
@@ -699,6 +761,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
699
761
  ),
700
762
  );
701
763
  }
764
+ events = redactGatewayStream(events);
702
765
 
703
766
  if (!parsed.stream) {
704
767
  try {
@@ -708,17 +771,18 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
708
771
  const errorMessage =
709
772
  message.errorMessage ??
710
773
  (message.stopReason === "aborted" ? "Request was aborted" : "Upstream request failed");
774
+ const safeErrorMessage = cleanReason(errorMessage) ?? "Upstream request failed";
711
775
  logger.warn("auth-gateway non-streaming failed", {
712
776
  format: "pi-native",
713
777
  reason: message.stopReason,
714
- error: errorMessage,
778
+ error: safeErrorMessage,
715
779
  peer,
716
780
  });
717
781
  if (message.stopReason === "aborted") {
718
- return piNative.formatError(499, "request_aborted", errorMessage);
782
+ return piNative.formatError(499, "request_aborted", safeErrorMessage);
719
783
  }
720
- const classified = classifyGatewayError(new Error(errorMessage));
721
- return piNative.formatError(classified.status, classified.type, errorMessage);
784
+ const classified = classifyGatewayError(new Error(safeErrorMessage));
785
+ return piNative.formatError(classified.status, classified.type, classified.message);
722
786
  }
723
787
  return json(200, { message });
724
788
  } catch (error) {
@@ -787,6 +851,7 @@ function handleModelsList(opts: AuthGatewayBootOptions): Response {
787
851
  export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServerHandle {
788
852
  const bind = parseBind(opts.bind ?? DEFAULT_AUTH_GATEWAY_BIND);
789
853
  const tokens = new Set<string>(opts.bearerTokens);
854
+ assertAuthenticatedOrLoopback(bind, tokens.size, "auth-gateway");
790
855
  const version = opts.version;
791
856
 
792
857
  const server = Bun.serve({
@@ -859,7 +924,7 @@ export function startAuthGateway(opts: AuthGatewayBootOptions): AuthGatewayServe
859
924
  method: req.method,
860
925
  path: pathname,
861
926
  peer,
862
- error: String(error),
927
+ error: cleanReason(error) ?? "internal error",
863
928
  });
864
929
  return withCors(json(500, { error: "internal error" }), req);
865
930
  }