@oh-my-pi/pi-ai 16.4.8 → 16.5.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/auth-broker/remote-store.d.ts +2 -0
  3. package/dist/types/auth-broker/wire-schemas.d.ts +28 -0
  4. package/dist/types/auth-gateway/http.d.ts +18 -1
  5. package/dist/types/auth-retry.d.ts +44 -20
  6. package/dist/types/auth-storage.d.ts +51 -10
  7. package/dist/types/error/flags.d.ts +1 -0
  8. package/dist/types/index.d.ts +1 -0
  9. package/dist/types/providers/google-auth.d.ts +0 -8
  10. package/dist/types/providers/google-shared.d.ts +0 -13
  11. package/dist/types/providers/openai-chat-server-schema.d.ts +3 -3
  12. package/dist/types/providers/openai-shared.d.ts +2 -0
  13. package/dist/types/registry/oauth/types.d.ts +8 -0
  14. package/dist/types/types.d.ts +0 -16
  15. package/dist/types/usage/cursor.d.ts +3 -0
  16. package/dist/types/usage/openai-codex-reset.d.ts +1 -1
  17. package/dist/types/usage/zai.d.ts +2 -1
  18. package/dist/types/usage.d.ts +4 -0
  19. package/package.json +7 -7
  20. package/src/auth-broker/discover.ts +22 -3
  21. package/src/auth-broker/remote-store.ts +103 -10
  22. package/src/auth-broker/wire-schemas.ts +4 -0
  23. package/src/auth-gateway/http.ts +35 -2
  24. package/src/auth-gateway/server.ts +26 -4
  25. package/src/auth-retry.ts +191 -53
  26. package/src/auth-storage.ts +614 -109
  27. package/src/error/auth-classify.ts +2 -1
  28. package/src/error/flags.ts +10 -0
  29. package/src/error/provider.ts +7 -6
  30. package/src/index.ts +1 -0
  31. package/src/providers/cursor.ts +10 -1
  32. package/src/providers/google-auth.ts +0 -20
  33. package/src/providers/google-shared.ts +0 -13
  34. package/src/providers/google-vertex.ts +62 -108
  35. package/src/providers/google.ts +11 -51
  36. package/src/providers/openai-chat-server-schema.ts +2 -1
  37. package/src/providers/openai-codex/request-transformer.ts +1 -1
  38. package/src/providers/openai-codex-responses.ts +54 -27
  39. package/src/providers/openai-shared.ts +6 -3
  40. package/src/registry/oauth/anthropic.ts +57 -24
  41. package/src/registry/oauth/oauth.html +1 -1
  42. package/src/registry/oauth/types.ts +8 -0
  43. package/src/stream.ts +19 -33
  44. package/src/types.ts +0 -16
  45. package/src/usage/cursor.ts +192 -0
  46. package/src/usage/openai-codex-reset.ts +6 -2
  47. package/src/usage/zai.ts +49 -6
  48. package/src/usage.ts +4 -0
  49. package/dist/types/providers/google-interactions.d.ts +0 -65
  50. package/src/providers/google-interactions.ts +0 -753
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.4.8",
4
+ "version": "16.5.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,15 +37,15 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "16.4.8",
42
- "@oh-my-pi/pi-utils": "16.4.8",
43
- "@oh-my-pi/pi-wire": "16.4.8",
44
- "arktype": "2.2.2",
40
+ "@bufbuild/protobuf": "^2.12.1",
41
+ "@oh-my-pi/pi-catalog": "16.5.1",
42
+ "@oh-my-pi/pi-utils": "16.5.1",
43
+ "@oh-my-pi/pi-wire": "16.5.1",
44
+ "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
47
47
  "devDependencies": {
48
- "@bufbuild/protoc-gen-es": "^2.12.0",
48
+ "@bufbuild/protoc-gen-es": "^2.12.1",
49
49
  "@types/bun": "^1.3.14"
50
50
  },
51
51
  "engines": {
@@ -72,6 +72,26 @@ interface ConfigSnapshot {
72
72
  token?: string;
73
73
  }
74
74
 
75
+ /**
76
+ * Resolve a dotted config key (e.g. `auth.broker.url`) against a parsed YAML
77
+ * record, accepting both nested form (`auth: { broker: { url } }`) and the
78
+ * legacy flat literal-dot key (`"auth.broker.url": ...`). Nested wins when both
79
+ * are present. Returns the value only when it is a string.
80
+ */
81
+ function readDottedString(record: Record<string, unknown>, dottedKey: string): string | undefined {
82
+ let current: unknown = record;
83
+ for (const segment of dottedKey.split(".")) {
84
+ if (current === null || typeof current !== "object" || Array.isArray(current)) {
85
+ current = undefined;
86
+ break;
87
+ }
88
+ current = (current as Record<string, unknown>)[segment];
89
+ }
90
+ if (typeof current === "string") return current;
91
+ const flat = record[dottedKey];
92
+ return typeof flat === "string" ? flat : undefined;
93
+ }
94
+
75
95
  async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
76
96
  for (const filename of MAIN_CONFIG_FILENAMES) {
77
97
  const configPath = path.join(agentDir, filename);
@@ -80,9 +100,8 @@ async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
80
100
  const parsed = YAML.parse(raw);
81
101
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
82
102
  const record = parsed as Record<string, unknown>;
83
- const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
84
- const token =
85
- typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
103
+ const url = readDottedString(record, "auth.broker.url");
104
+ const token = readDottedString(record, "auth.broker.token");
86
105
  return { url, token };
87
106
  } catch (err) {
88
107
  if (isEnoent(err)) continue;
@@ -143,14 +143,24 @@ interface UsageCacheEntry {
143
143
 
144
144
  function usageOverlayKey(
145
145
  provider: Provider,
146
- ids: { accountId?: string; email?: string; projectId?: string },
146
+ ids: { accountId?: string; email?: string; projectId?: string; orgId?: string },
147
147
  ): string | undefined {
148
+ // Org first: one account email can hold several organizations (Anthropic
149
+ // Team seat + personal Max), each with its own limit pools. Keying the
150
+ // overlay by account/email would merge the two pools' header ingests.
151
+ // But the org alone is not enough either: two Team members share the org
152
+ // id while drawing on per-user pools, so the key stays qualified by the
153
+ // member's own base identity whenever one is known.
154
+ let base: string | undefined;
148
155
  const accountId = ids.accountId?.trim().toLowerCase();
149
- if (accountId) return `${provider}\0account:${accountId}`;
150
156
  const email = ids.email?.trim().toLowerCase();
151
- if (email) return `${provider}\0email:${email}`;
152
157
  const projectId = ids.projectId?.trim().toLowerCase();
153
- if (projectId) return `${provider}\0project:${projectId}`;
158
+ if (accountId) base = `account:${accountId}`;
159
+ else if (email) base = `email:${email}`;
160
+ else if (projectId) base = `project:${projectId}`;
161
+ const orgId = ids.orgId?.trim().toLowerCase();
162
+ if (orgId) return base ? `${provider}\0org:${orgId}|${base}` : `${provider}\0org:${orgId}`;
163
+ if (base) return `${provider}\0${base}`;
154
164
  return undefined;
155
165
  }
156
166
 
@@ -787,6 +797,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
787
797
  this.#cache.set(key, { value, expiresAtSec });
788
798
  }
789
799
 
800
+ /** Drop all cache rows whose keys start with the supplied prefix. */
801
+ deleteCachePrefix(prefix: string): void {
802
+ for (const key of this.#cache.keys()) {
803
+ if (key.startsWith(prefix)) this.#cache.delete(key);
804
+ }
805
+ }
806
+
790
807
  cleanExpiredCache(): void {
791
808
  const nowSec = Math.floor(Date.now() / 1000);
792
809
  for (const [key, entry] of this.#cache) {
@@ -987,12 +1004,55 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
987
1004
  * usage data" (ranking proceeds without a usage signal for this credential).
988
1005
  */
989
1006
  function matchUsageReport(reports: UsageReport[], provider: Provider, credential: OAuthCredential): UsageReport | null {
990
- const candidates = reports.filter(report => report.provider === provider);
991
- if (candidates.length === 0) return null;
992
- if (candidates.length === 1) return candidates[0];
1007
+ const all = reports.filter(report => report.provider === provider);
1008
+ if (all.length === 0) return null;
1009
+ // Org precedence, decisive on EITHER side: an org-scoped credential may
1010
+ // only take its own org's report, and an org-less (legacy) credential may
1011
+ // only take org-less reports — the shared email/account would otherwise
1012
+ // hand one subscription the OTHER subscription's pool (e.g. mark healthy
1013
+ // Max exhausted via Team's report, or rank a legacy row on a sibling's
1014
+ // numbers).
1015
+ const orgId = credential.orgId?.trim().toLowerCase();
993
1016
  const accountId = credential.accountId?.trim().toLowerCase();
994
1017
  const email = credential.email?.trim().toLowerCase();
995
1018
  const projectId = credential.projectId?.trim().toLowerCase();
1019
+ if (orgId) {
1020
+ const sameOrg: UsageReport[] = [];
1021
+ let sawReportOrg = false;
1022
+ for (const report of all) {
1023
+ const metaOrg = readMetadataString((report.metadata ?? {}) as Record<string, unknown>, "orgId");
1024
+ if (metaOrg) {
1025
+ sawReportOrg = true;
1026
+ if (metaOrg.toLowerCase() === orgId) sameOrg.push(report);
1027
+ }
1028
+ }
1029
+ // Org-attributed reports exist: the shared org is a GATE, not a match.
1030
+ // Two Team members share the org id while drawing on per-user pools,
1031
+ // so the credential's own base identity must still line up inside the
1032
+ // same-org subset — a lone sibling report is NOT ours. An org-only
1033
+ // credential (no base identifiers) takes the lone same-org report and
1034
+ // treats several as ambiguous. None in our org → "no usage data"
1035
+ // rather than mis-attributing another org's pool.
1036
+ if (sawReportOrg) {
1037
+ if (accountId || email || projectId) {
1038
+ for (const report of sameOrg) {
1039
+ if (reportMatchesIdentity(report, accountId, email, projectId)) return report;
1040
+ }
1041
+ return null;
1042
+ }
1043
+ return sameOrg.length === 1 ? sameOrg[0]! : null;
1044
+ }
1045
+ // No surviving report carries an org at all: presence mismatch is a
1046
+ // non-match too — the sole org-less report may be a legacy sibling
1047
+ // row's pool, and handing it to a scoped credential would rank/block
1048
+ // on the wrong quota. "No usage data" degrades gracefully instead.
1049
+ return null;
1050
+ }
1051
+ const candidates = all.filter(
1052
+ report => !readMetadataString((report.metadata ?? {}) as Record<string, unknown>, "orgId"),
1053
+ );
1054
+ if (candidates.length === 0) return null;
1055
+ if (all.length === 1 && candidates.length === 1) return candidates[0];
996
1056
  for (const report of candidates) {
997
1057
  if (reportMatchesIdentity(report, accountId, email, projectId)) return report;
998
1058
  }
@@ -1000,15 +1060,48 @@ function matchUsageReport(reports: UsageReport[], provider: Provider, credential
1000
1060
  }
1001
1061
 
1002
1062
  function findMatchingReportIndex(reports: UsageReport[], overlay: UsageReport): number {
1003
- const candidates = reports
1063
+ const all = reports
1004
1064
  .map((report, index) => ({ report, index }))
1005
1065
  .filter(candidate => candidate.report.provider === overlay.provider);
1006
- if (candidates.length === 0) return -1;
1007
- if (candidates.length === 1) return candidates[0]!.index;
1066
+ if (all.length === 0) return -1;
1008
1067
  const metadata = (overlay.metadata ?? {}) as Record<string, unknown>;
1068
+ // Org precedence — mirror matchUsageReport: an org-attributed overlay may
1069
+ // only merge into a report of the SAME org, and an org-less overlay may
1070
+ // only merge into an org-less report. Within the same org the overlay's
1071
+ // base identity must still match — two Team members' reports share the
1072
+ // org id but must not swallow each other's header ingests.
1073
+ const overlayOrg = readMetadataString(metadata, "orgId")?.toLowerCase();
1009
1074
  const accountId = readMetadataString(metadata, "accountId")?.toLowerCase();
1010
1075
  const email = readMetadataString(metadata, "email")?.toLowerCase();
1011
1076
  const projectId = readMetadataString(metadata, "projectId")?.toLowerCase();
1077
+ if (overlayOrg) {
1078
+ const sameOrg: { report: UsageReport; index: number }[] = [];
1079
+ let sawReportOrg = false;
1080
+ for (const candidate of all) {
1081
+ const candidateOrg = readMetadataString((candidate.report.metadata ?? {}) as Record<string, unknown>, "orgId");
1082
+ if (candidateOrg) {
1083
+ sawReportOrg = true;
1084
+ if (candidateOrg.toLowerCase() === overlayOrg) sameOrg.push(candidate);
1085
+ }
1086
+ }
1087
+ if (sawReportOrg) {
1088
+ if (accountId || email || projectId) {
1089
+ for (const candidate of sameOrg) {
1090
+ if (reportMatchesIdentity(candidate.report, accountId, email, projectId)) return candidate.index;
1091
+ }
1092
+ return -1;
1093
+ }
1094
+ return sameOrg.length === 1 ? sameOrg[0]!.index : -1;
1095
+ }
1096
+ // Presence mismatch — mirror matchUsageReport: an org-scoped overlay
1097
+ // never merges into an org-less report; it becomes its own report row.
1098
+ return -1;
1099
+ }
1100
+ const candidates = all.filter(
1101
+ candidate => !readMetadataString((candidate.report.metadata ?? {}) as Record<string, unknown>, "orgId"),
1102
+ );
1103
+ if (candidates.length === 0) return -1;
1104
+ if (all.length === 1 && candidates.length === 1) return candidates[0]!.index;
1012
1105
  for (const candidate of candidates) {
1013
1106
  if (reportMatchesIdentity(candidate.report, accountId, email, projectId)) return candidate.index;
1014
1107
  }
@@ -32,6 +32,8 @@ export const oauthCredentialSchema = type({
32
32
  "projectId?": "string",
33
33
  "email?": "string",
34
34
  "accountId?": "string",
35
+ "orgId?": "string",
36
+ "orgName?": "string",
35
37
  });
36
38
 
37
39
  /** OAuth credential as it appears in broker snapshots — refresh replaced with sentinel. */
@@ -45,6 +47,8 @@ export const remoteOauthCredentialSchema = type({
45
47
  "projectId?": "string",
46
48
  "email?": "string",
47
49
  "accountId?": "string",
50
+ "orgId?": "string",
51
+ "orgName?": "string",
48
52
  });
49
53
 
50
54
  export const apiKeyCredentialSchema = type({
@@ -5,19 +5,50 @@
5
5
  * and peer-resolution logic.
6
6
  */
7
7
  import { timingSafeEqual as nodeTimingSafeEqual } from "node:crypto";
8
+ import type { Api, AssistantMessage, Model } from "../types";
8
9
 
9
10
  const JSON_HEADERS = {
10
11
  "Content-Type": "application/json",
11
12
  "X-Content-Type-Options": "nosniff",
12
13
  } as const;
13
14
 
14
- export function json(status: number, body: unknown): Response {
15
+ export function json(status: number, body: unknown, headers?: Record<string, string>): Response {
15
16
  return new Response(JSON.stringify(body) ?? "null", {
16
17
  status,
17
- headers: JSON_HEADERS,
18
+ headers: headers ? { ...JSON_HEADERS, ...headers } : JSON_HEADERS,
18
19
  });
19
20
  }
20
21
 
22
+ /**
23
+ * Diagnostic response headers for translated inference requests, mirroring the
24
+ * names existing gateway-aware clients already parse: `x-request-id` /
25
+ * `request-id` (surfaced as `_request_id` by the OpenAI and Anthropic SDKs,
26
+ * matches the gateway log line), LiteLLM's model-resolution and cost headers,
27
+ * and OpenAI's `openai-processing-ms`. Model/request-id headers are always
28
+ * present; `message` — the final assistant message, available only on
29
+ * non-streaming responses — adds the computed cost, and `startedAt` the wall
30
+ * time. Streaming responses send headers before usage exists, so they carry
31
+ * only the identity headers.
32
+ */
33
+ export function gatewayResponseHeaders(
34
+ model: Model<Api>,
35
+ info: { requestId: string; message?: AssistantMessage; startedAt?: number },
36
+ ): Record<string, string> {
37
+ const headers: Record<string, string> = {
38
+ "x-request-id": info.requestId,
39
+ "request-id": info.requestId,
40
+ "x-litellm-model-id": model.id,
41
+ };
42
+ if (model.baseUrl) headers["x-litellm-model-api-base"] = model.baseUrl;
43
+ if (info.message) headers["x-litellm-response-cost"] = info.message.usage.cost.total.toString();
44
+ if (info.startedAt !== undefined) {
45
+ const elapsed = (performance.now() - info.startedAt).toFixed(0);
46
+ headers["x-litellm-response-duration-ms"] = elapsed;
47
+ headers["openai-processing-ms"] = elapsed;
48
+ }
49
+ return headers;
50
+ }
51
+
21
52
  export function resolvePeer(req: Request): string {
22
53
  const fwd = req.headers.get("x-forwarded-for");
23
54
  if (fwd) return fwd.split(",")[0].trim();
@@ -165,6 +196,8 @@ const CORS_HEADERS: Record<string, string> = {
165
196
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
166
197
  "Access-Control-Allow-Headers":
167
198
  "authorization, content-type, anthropic-version, anthropic-beta, openai-organization, openai-project, x-stainless-*, x-api-key",
199
+ "Access-Control-Expose-Headers":
200
+ "x-request-id, request-id, x-litellm-model-id, x-litellm-model-api-base, x-litellm-response-cost, x-litellm-response-duration-ms, openai-processing-ms",
168
201
  "Access-Control-Max-Age": "86400",
169
202
  };
170
203
 
@@ -22,6 +22,7 @@ import { Effort } from "@oh-my-pi/pi-catalog/effort";
22
22
  import { extractHttpStatusFromError, extractRetryHint, logger } from "@oh-my-pi/pi-utils";
23
23
  import type { ApiKeyResolver } from "../auth-retry";
24
24
  import type { AuthStorage } from "../auth-storage";
25
+ import * as AIError from "../error";
25
26
  import { classifyGatewayError } from "../error/gateway";
26
27
  import { isUsageLimitOutcome } from "../error/rate-limit";
27
28
  import * as anthropicMessages from "../providers/anthropic-messages-server";
@@ -32,7 +33,15 @@ import { completeSimple, streamSimple } from "../stream";
32
33
  import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "../types";
33
34
  import { deterministicUuid } from "../utils/deterministic-id";
34
35
  import { parseBind } from "../utils/parse-bind";
35
- import { captureRequestHeaders, corsHeaders, isAuthorized, json, resolvePeer, withCors } from "./http";
36
+ import {
37
+ captureRequestHeaders,
38
+ corsHeaders,
39
+ gatewayResponseHeaders,
40
+ isAuthorized,
41
+ json,
42
+ resolvePeer,
43
+ withCors,
44
+ } from "./http";
36
45
  import type {
37
46
  AuthGatewayServerHandle,
38
47
  AuthGatewayServerOptions,
@@ -227,7 +236,8 @@ async function refreshGatewayApiKeyAfterAuthError(
227
236
  peer: string,
228
237
  ): Promise<string | undefined> {
229
238
  const message = error instanceof Error ? error.message : String(error);
230
- if (isUsageLimitOutcome(extractHttpStatusFromError(error), message)) {
239
+ const status = extractHttpStatusFromError(error);
240
+ if (AIError.isUsageLimit(error) || isUsageLimitOutcome(status, message)) {
231
241
  const retryAfterMs = extractRetryHint(undefined, message);
232
242
  const { switched, retryAtMs } = await storage.markUsageLimitReached(provider, sessionId, {
233
243
  retryAfterMs,
@@ -334,6 +344,8 @@ async function handleFormatEndpoint(
334
344
  req: Request,
335
345
  peer: string,
336
346
  ): Promise<Response> {
347
+ const startedAt = performance.now();
348
+ const requestId = crypto.randomUUID();
337
349
  const controller = mirrorRequestAbort(req);
338
350
  if (controller.signal.aborted) return clientClosedResponse(route);
339
351
 
@@ -430,6 +442,7 @@ async function handleFormatEndpoint(
430
442
  );
431
443
 
432
444
  logger.info("auth-gateway request", {
445
+ requestId,
433
446
  format: route.label,
434
447
  model: parsed.modelId,
435
448
  resolvedProvider: model.provider,
@@ -458,7 +471,11 @@ async function handleFormatEndpoint(
458
471
  const classified = classifyGatewayError(errorMessage);
459
472
  return route.module.formatError(classified.status, classified.type, errorMessage);
460
473
  }
461
- return json(200, route.module.encodeResponse(message, parsed.modelId));
474
+ return json(
475
+ 200,
476
+ route.module.encodeResponse(message, parsed.modelId),
477
+ gatewayResponseHeaders(model, { requestId, message, startedAt }),
478
+ );
462
479
  } catch (error) {
463
480
  if (controller.signal.aborted) return clientClosedResponse(route);
464
481
  const classified = classifyGatewayError(error);
@@ -493,6 +510,7 @@ async function handleFormatEndpoint(
493
510
  return new Response(sseStream, {
494
511
  status: 200,
495
512
  headers: {
513
+ ...gatewayResponseHeaders(model, { requestId }),
496
514
  "Content-Type": "text/event-stream; charset=utf-8",
497
515
  "Cache-Control": "no-cache",
498
516
  Connection: "keep-alive",
@@ -519,6 +537,8 @@ async function handleFormatEndpoint(
519
537
  * path.
520
538
  */
521
539
  async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, peer: string): Promise<Response> {
540
+ const startedAt = performance.now();
541
+ const requestId = crypto.randomUUID();
522
542
  const controller = mirrorRequestAbort(req);
523
543
  const aborted = (): Response => piNative.formatError(499, "request_aborted", "client closed request");
524
544
  if (controller.signal.aborted) return aborted();
@@ -605,6 +625,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
605
625
  streamOpts.sessionId ??= sessionId;
606
626
 
607
627
  logger.info("auth-gateway request", {
628
+ requestId,
608
629
  format: "pi-native",
609
630
  model: parsed.modelId,
610
631
  resolvedProvider: model.provider,
@@ -633,7 +654,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
633
654
  const classified = classifyGatewayError(errorMessage);
634
655
  return piNative.formatError(classified.status, classified.type, errorMessage);
635
656
  }
636
- return json(200, { message });
657
+ return json(200, { message }, gatewayResponseHeaders(model, { requestId, message, startedAt }));
637
658
  } catch (error) {
638
659
  if (controller.signal.aborted) return aborted();
639
660
  const classified = classifyGatewayError(error);
@@ -664,6 +685,7 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
664
685
  return new Response(sseStream, {
665
686
  status: 200,
666
687
  headers: {
688
+ ...gatewayResponseHeaders(model, { requestId }),
667
689
  "Content-Type": "text/event-stream; charset=utf-8",
668
690
  "Cache-Control": "no-cache",
669
691
  Connection: "keep-alive",