babelgate 0.0.0 → 1.1.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 (46) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +467 -1
  4. package/dist/app.js +40 -0
  5. package/dist/cli.js +203 -0
  6. package/dist/config/env-config.js +99 -0
  7. package/dist/index.js +60 -0
  8. package/dist/middleware/error-handler.js +158 -0
  9. package/dist/middleware/request-logger.js +66 -0
  10. package/dist/middleware/require-api-key.js +80 -0
  11. package/dist/middleware/resolve-source.js +28 -0
  12. package/dist/routes/proxy.js +74 -0
  13. package/dist/services/http-client.js +220 -0
  14. package/dist/services/proxy.service.js +329 -0
  15. package/dist/services/stream-pipeline.js +119 -0
  16. package/dist/services/upstream-errors.js +34 -0
  17. package/dist/transformers/adapters/anthropic.adapter.js +292 -0
  18. package/dist/transformers/adapters/anthropic.messages.js +261 -0
  19. package/dist/transformers/adapters/anthropic.stream.js +378 -0
  20. package/dist/transformers/adapters/gemini.adapter.js +418 -0
  21. package/dist/transformers/adapters/gemini.schema.js +61 -0
  22. package/dist/transformers/adapters/gemini.stream.js +341 -0
  23. package/dist/transformers/adapters/gemini.tools.js +246 -0
  24. package/dist/transformers/adapters/index.js +141 -0
  25. package/dist/transformers/adapters/ollama.adapter.js +288 -0
  26. package/dist/transformers/adapters/ollama.stream.js +294 -0
  27. package/dist/transformers/adapters/ollama.tools.js +107 -0
  28. package/dist/transformers/adapters/openai.adapter.js +460 -0
  29. package/dist/transformers/adapters/openai.stream.js +229 -0
  30. package/dist/transformers/index.js +110 -0
  31. package/dist/types/anthropic.types.js +2 -0
  32. package/dist/types/canonical.schema.js +160 -0
  33. package/dist/types/canonical.types.js +8 -0
  34. package/dist/types/gemini.types.js +2 -0
  35. package/dist/types/ollama.types.js +4 -0
  36. package/dist/types/openai.types.js +2 -0
  37. package/dist/utils/canonical-error.js +77 -0
  38. package/dist/utils/errors.js +95 -0
  39. package/dist/utils/ids.js +17 -0
  40. package/dist/utils/logger.js +34 -0
  41. package/dist/utils/model-map.js +39 -0
  42. package/dist/utils/sse.js +147 -0
  43. package/dist/utils/stream-reassemble.js +104 -0
  44. package/dist/utils/token-estimate.js +36 -0
  45. package/dist/utils/tool-args.js +26 -0
  46. package/package.json +55 -7
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransformError = void 0;
4
+ exports.proxyRequest = proxyRequest;
5
+ const env_config_1 = require("../config/env-config");
6
+ const transformers_1 = require("../transformers");
7
+ const adapters_1 = require("../transformers/adapters");
8
+ const http_client_1 = require("./http-client");
9
+ const stream_pipeline_1 = require("./stream-pipeline");
10
+ const model_map_1 = require("../utils/model-map");
11
+ const token_estimate_1 = require("../utils/token-estimate");
12
+ const upstream_errors_1 = require("./upstream-errors");
13
+ const canonical_schema_1 = require("../types/canonical.schema");
14
+ const errors_1 = require("../utils/errors");
15
+ Object.defineProperty(exports, "TransformError", { enumerable: true, get: function () { return errors_1.TransformError; } });
16
+ const logger_1 = require("../utils/logger");
17
+ /**
18
+ * Attach the spec §3.3 client-facing rendering to an upstream failure.
19
+ * Only real upstream error statuses (≥ 400) go through the mapping
20
+ * table — timeouts, transport failures and non-JSON 2xx replies already
21
+ * carry their final client-facing message.
22
+ */
23
+ function toClientFacing(err, target) {
24
+ if (err.upstreamStatus !== undefined && err.upstreamStatus >= 400) {
25
+ err.clientError = (0, upstream_errors_1.toClientError)(target, err.upstreamStatus, err.upstreamBody, err.retryAfterSeconds);
26
+ }
27
+ return err;
28
+ }
29
+ /**
30
+ * The "<method>" in a "/v1beta/models/<m>:<method>" path — the text
31
+ * after the last ":" of the percent-decoded path, the same parsing the
32
+ * route's isSupportedPath uses.
33
+ */
34
+ function methodFromPath(path) {
35
+ let decoded = path;
36
+ try {
37
+ decoded = decodeURIComponent(path);
38
+ }
39
+ catch {
40
+ // malformed escapes — fall back to the raw path
41
+ }
42
+ const colon = decoded.lastIndexOf(":");
43
+ return colon === -1 ? decoded : decoded.slice(colon + 1);
44
+ }
45
+ /**
46
+ * Provider-specific auth headers.
47
+ */
48
+ function buildAuthHeaders(provider, apiKey) {
49
+ if (!apiKey)
50
+ return {};
51
+ switch (provider) {
52
+ case "openai":
53
+ return { Authorization: `Bearer ${apiKey}` };
54
+ case "anthropic":
55
+ return {
56
+ "x-api-key": apiKey,
57
+ "anthropic-version": "2023-06-01",
58
+ };
59
+ case "gemini":
60
+ return { "x-goog-api-key": apiKey };
61
+ case "ollama":
62
+ return {};
63
+ }
64
+ }
65
+ /**
66
+ * Full proxy orchestration:
67
+ * path match → config → canonical boundary validation →
68
+ * transform → upstream call → transform back.
69
+ *
70
+ * Returns null when the path doesn't belong to the configured source
71
+ * provider (route should call next() → 404).
72
+ * Throws ValidationError | UpstreamError | TransformError | ConfigurationError.
73
+ */
74
+ async function proxyRequest(path, method, body, requestId, opts) {
75
+ // Cheap pre-check: only recognized provider API paths are proxied.
76
+ const isProviderPath = Object.values(transformers_1.PROVIDER_ROUTES)
77
+ .flat()
78
+ .some((r) => r.method === method && path.startsWith(r.pathPrefix));
79
+ if (!isProviderPath)
80
+ return null;
81
+ const config = (0, env_config_1.getConfig)();
82
+ const sourceApi = config.sourceApi;
83
+ const targetApi = config.targetApi;
84
+ // Match the incoming path to a route on the configured source provider
85
+ const route = (0, adapters_1.findSourceRoute)(sourceApi, method, path);
86
+ if (!route)
87
+ return null;
88
+ if (route.kind === "models") {
89
+ return proxyModelList(route, config, sourceApi, targetApi, requestId);
90
+ }
91
+ // Gemini paths carry a ":<method>" suffix — anything that isn't a
92
+ // method this route serves is refused before conversion, before any
93
+ // upstream call (spec §3.2, E11).
94
+ if (route.isSupportedPath?.(path) === false) {
95
+ throw new errors_1.NotSupportedError(`${methodFromPath(path)} is not supported`);
96
+ }
97
+ // Source → Canonical. The adapter may throw on malformed input —
98
+ // that's a client error (400), not a server error (500).
99
+ let canonical;
100
+ try {
101
+ canonical = transformers_1.adapters[sourceApi].toCanonicalRequest(body);
102
+ }
103
+ catch (err) {
104
+ throw new errors_1.ValidationError(`Malformed ${sourceApi} request: ${err instanceof Error ? err.message : "invalid body"}`, { requestId });
105
+ }
106
+ // Boundary validation: malformed client request → 400, not 500.
107
+ const parsed = canonical_schema_1.canonicalRequestSchema.safeParse(canonical);
108
+ if (!parsed.success) {
109
+ throw new errors_1.ValidationError("Malformed request body", {
110
+ requestId,
111
+ issues: parsed.error.issues.map((i) => ({
112
+ path: i.path.join("."),
113
+ message: i.message,
114
+ })),
115
+ });
116
+ }
117
+ // The model comes from the source path when the provider puts it there
118
+ // (Gemini), otherwise from the canonical request. An explicitly empty
119
+ // model in the path (e.g. /v1beta/models/:generateContent) is a client
120
+ // error, not something to send upstream.
121
+ const modelFromPath = route.modelFromPath?.(path);
122
+ if (modelFromPath === "") {
123
+ throw new errors_1.ValidationError(`Malformed ${sourceApi} request path: model is empty`, { requestId });
124
+ }
125
+ // §3.5: count_tokens is answered locally — a chars/4 estimate with the
126
+ // estimate header, never an upstream call.
127
+ if (route.kind === "count_tokens") {
128
+ logger_1.logger.info({ requestId, sourceApi }, "count_tokens answered locally");
129
+ return {
130
+ body: { input_tokens: (0, token_estimate_1.estimateInputTokens)(parsed.data) },
131
+ headers: { "x-babelgate-token-estimate": "true" },
132
+ };
133
+ }
134
+ // §3.5: MODEL_MAP rewrites the upstream-bound model; replies and
135
+ // stream encoders keep the name the client asked for.
136
+ const requestedModel = modelFromPath ?? parsed.data.model;
137
+ const model = (0, model_map_1.applyModelMap)(requestedModel, config.modelMap);
138
+ parsed.data.model = model;
139
+ // §3.5: TARGET_CONTEXT_WINDOW → canonical contextWindow (Ollama num_ctx).
140
+ if (config.targetContextWindow !== undefined) {
141
+ parsed.data.config = {
142
+ ...parsed.data.config,
143
+ contextWindow: config.targetContextWindow,
144
+ };
145
+ }
146
+ // Streaming (spec §3.2): a `stream` body flag on the JSON-body sources,
147
+ // the dedicated streaming endpoint on Gemini — which carries no body
148
+ // flag, so canonical.stream is set here for the target adapter's
149
+ // own streaming marker (openai stream_options, anthropic/ollama
150
+ // stream:true).
151
+ if (parsed.data.stream === true || route.isStreamPath?.(path) === true) {
152
+ parsed.data.stream = true;
153
+ return proxyChatStream(parsed.data, body, model, requestedModel, config, sourceApi, targetApi, requestId, opts);
154
+ }
155
+ const targetBody = transformers_1.adapters[targetApi].fromCanonicalRequest(parsed.data);
156
+ const targetRoute = transformers_1.PROVIDER_ROUTES[targetApi].find((r) => r.kind === "chat");
157
+ const url = `${config.targetBaseUrl}${targetRoute.requestPath(model)}`;
158
+ logger_1.logger.info({ requestId, sourceApi, targetApi, url, model, requestedModel }, "Forwarding request to upstream");
159
+ const upstream = await (0, http_client_1.postJson)({
160
+ url,
161
+ headers: buildAuthHeaders(targetApi, config.targetApiKey),
162
+ body: targetBody,
163
+ timeoutMs: config.upstreamTimeoutMs,
164
+ retries: config.upstreamRetries,
165
+ requestId,
166
+ }).catch((err) => {
167
+ throw err instanceof errors_1.UpstreamError
168
+ ? toClientFacing(err, targetApi)
169
+ : err;
170
+ });
171
+ if (upstream.status < 200 || upstream.status >= 300) {
172
+ const err = new errors_1.UpstreamError(`Upstream ${targetApi} returned ${upstream.status}`, upstream.status, upstream.body);
173
+ err.retryAfterSeconds = upstream.retryAfterSeconds;
174
+ throw toClientFacing(err, targetApi);
175
+ }
176
+ // Target → Canonical → Source. §3.5: replies carry the model name the
177
+ // client asked for, not the mapped name sent upstream.
178
+ const canonicalResp = transformers_1.adapters[targetApi].toCanonicalResponse(upstream.body, model);
179
+ canonicalResp.model = requestedModel;
180
+ // Adapter bug detection (non-production only): canonical response
181
+ // should always be schema-valid
182
+ if (process.env.NODE_ENV !== "production") {
183
+ const respParsed = canonical_schema_1.canonicalResponseSchema.safeParse(canonicalResp);
184
+ if (!respParsed.success) {
185
+ logger_1.logger.error({ requestId, issues: respParsed.error.issues }, "Canonical response failed schema validation — adapter bug");
186
+ }
187
+ }
188
+ logger_1.logger.info({
189
+ requestId,
190
+ sourceApi,
191
+ targetApi,
192
+ model,
193
+ requestedModel,
194
+ inputTokens: canonicalResp.usage?.inputTokens,
195
+ outputTokens: canonicalResp.usage?.outputTokens,
196
+ finishReason: canonicalResp.finishReason,
197
+ upstreamDurationMs: upstream.durationMs,
198
+ }, "Request completed");
199
+ const sourceBody = transformers_1.adapters[sourceApi].fromCanonicalResponse(canonicalResp);
200
+ return { body: sourceBody };
201
+ }
202
+ /**
203
+ * Streaming chat flow (spec §3.2): target decoder + source encoder,
204
+ * postStream for upstream bytes, runStreamPipeline for the pipe.
205
+ * All four adapters stream since v1.1 Task 7 — the v1.0.1
206
+ * streaming-refusal path is gone. A Gemini target uses the route's
207
+ * streamRequestPath (`:streamGenerateContent?alt=sse`); the others
208
+ * stream on the chat path.
209
+ */
210
+ async function proxyChatStream(canonical, sourceRequest, model, requestedModel, config, sourceApi, targetApi, requestId, opts) {
211
+ const decoder = transformers_1.adapters[targetApi].createStreamDecoder(model);
212
+ const encoder = transformers_1.adapters[sourceApi].createStreamEncoder({
213
+ sourceRequest,
214
+ query: opts?.query ?? {},
215
+ requestedModel,
216
+ });
217
+ const targetBody = transformers_1.adapters[targetApi].fromCanonicalRequest(canonical);
218
+ const targetRoute = transformers_1.PROVIDER_ROUTES[targetApi].find((r) => r.kind === "chat");
219
+ const url = `${config.targetBaseUrl}${targetRoute.streamRequestPath?.(model) ?? targetRoute.requestPath(model)}`;
220
+ logger_1.logger.info({ requestId, sourceApi, targetApi, url, model, requestedModel, stream: true }, "Forwarding request to upstream");
221
+ // Client disconnect aborts the upstream request; upstreamAc lets the
222
+ // pipeline's end (terminal event, failure) tear it down too — a bare
223
+ // body cancel doesn't reach the request on every transport.
224
+ const upstreamAc = new AbortController();
225
+ const upstream = await (0, http_client_1.postStream)({
226
+ url,
227
+ headers: buildAuthHeaders(targetApi, config.targetApiKey),
228
+ body: targetBody,
229
+ timeoutMs: config.upstreamTimeoutMs,
230
+ retries: config.upstreamRetries,
231
+ requestId,
232
+ signal: opts?.signal
233
+ ? AbortSignal.any([opts.signal, upstreamAc.signal])
234
+ : upstreamAc.signal,
235
+ }).catch((err) => {
236
+ throw err instanceof errors_1.UpstreamError
237
+ ? toClientFacing(err, targetApi)
238
+ : err;
239
+ });
240
+ // Watch the event stream for the completion log line — the pipeline
241
+ // yields only encoded bytes, so the decoder is wrapped. Chunk contents
242
+ // are never logged (§3.2); only usage and the finish reason are.
243
+ const observed = [];
244
+ const recordingDecoder = {
245
+ feed(chunk) {
246
+ const events = decoder.feed(chunk);
247
+ observed.push(...events);
248
+ return events;
249
+ },
250
+ end() {
251
+ const events = decoder.end();
252
+ observed.push(...events);
253
+ return events;
254
+ },
255
+ };
256
+ const signal = opts?.signal ?? new AbortController().signal;
257
+ const chunks = (async function* () {
258
+ try {
259
+ yield* (0, stream_pipeline_1.runStreamPipeline)({
260
+ upstream: upstream.body,
261
+ decoder: recordingDecoder,
262
+ encoder,
263
+ idleMs: config.upstreamStreamIdleMs,
264
+ signal,
265
+ });
266
+ }
267
+ finally {
268
+ // Whatever ended the pipe — terminal event, in-stream error,
269
+ // client disconnect — the upstream request is done with.
270
+ upstreamAc.abort();
271
+ }
272
+ const last = observed.at(-1);
273
+ if (last?.type === "message_end") {
274
+ const usageEvent = observed.find((e) => e.type === "usage");
275
+ logger_1.logger.info({
276
+ requestId,
277
+ sourceApi,
278
+ targetApi,
279
+ model,
280
+ requestedModel,
281
+ stream: true,
282
+ inputTokens: usageEvent?.type === "usage"
283
+ ? usageEvent.usage.inputTokens
284
+ : undefined,
285
+ outputTokens: usageEvent?.type === "usage"
286
+ ? usageEvent.usage.outputTokens
287
+ : undefined,
288
+ finishReason: last.finishReason,
289
+ upstreamDurationMs: upstream.durationMs,
290
+ }, "Request completed");
291
+ }
292
+ })();
293
+ return { stream: { contentType: encoder.contentType, chunks } };
294
+ }
295
+ /**
296
+ * GET /models flow — no request body, response transforms through
297
+ * canonical like chat does.
298
+ */
299
+ async function proxyModelList(route, config, sourceApi, targetApi, requestId) {
300
+ const targetRoute = transformers_1.PROVIDER_ROUTES[targetApi].find((r) => r.kind === "models");
301
+ const url = `${config.targetBaseUrl}${targetRoute.requestPath()}`;
302
+ logger_1.logger.info({ requestId, sourceApi, targetApi, url }, "Forwarding models list request to upstream");
303
+ const upstream = await (0, http_client_1.getJson)({
304
+ url,
305
+ headers: buildAuthHeaders(targetApi, config.targetApiKey),
306
+ timeoutMs: config.upstreamTimeoutMs,
307
+ retries: config.upstreamRetries,
308
+ requestId,
309
+ }).catch((err) => {
310
+ throw err instanceof errors_1.UpstreamError
311
+ ? toClientFacing(err, targetApi)
312
+ : err;
313
+ });
314
+ if (upstream.status < 200 || upstream.status >= 300) {
315
+ const err = new errors_1.UpstreamError(`Upstream ${targetApi} returned ${upstream.status}`, upstream.status, upstream.body);
316
+ err.retryAfterSeconds = upstream.retryAfterSeconds;
317
+ throw toClientFacing(err, targetApi);
318
+ }
319
+ const canonical = transformers_1.adapters[targetApi].toCanonicalModelList(upstream.body);
320
+ if (process.env.NODE_ENV !== "production") {
321
+ const parsed = canonical_schema_1.canonicalModelListSchema.safeParse(canonical);
322
+ if (!parsed.success) {
323
+ logger_1.logger.error({ requestId, issues: parsed.error.issues }, "Canonical model list failed schema validation — adapter bug");
324
+ }
325
+ }
326
+ logger_1.logger.info({ requestId, sourceApi, targetApi, modelCount: canonical.models.length }, "Models list completed");
327
+ const sourceBody = transformers_1.adapters[sourceApi].fromCanonicalModelList(canonical);
328
+ return { body: sourceBody };
329
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runStreamPipeline = runStreamPipeline;
4
+ const canonical_error_1 = require("../utils/canonical-error");
5
+ /**
6
+ * upstream bytes → decoder → canonical events → encoder → client bytes
7
+ * (spec §3.2). Provider-agnostic; the adapters own all dialect knowledge.
8
+ *
9
+ * Termination rules: an upstream read error, undecodable data or an idle
10
+ * gap longer than `idleMs` ends the stream with the source dialect's
11
+ * in-stream error form (kind `timeout` for idle, `upstream` otherwise) —
12
+ * the client never sees a truncated message followed by silence. An
13
+ * encoded `message_end` or `error` event is terminal: the encoder's tail
14
+ * is written, the upstream read is cancelled and the pipe ends, even
15
+ * when the upstream keeps its body open. Aborting `signal` cancels the
16
+ * upstream read and ends the pipe quietly.
17
+ */
18
+ async function* runStreamPipeline(opts) {
19
+ const { upstream, decoder, encoder, idleMs, signal } = opts;
20
+ const reader = upstream.getReader();
21
+ const cancel = () => {
22
+ reader.cancel().catch(() => { });
23
+ };
24
+ signal.addEventListener("abort", cancel, { once: true });
25
+ /** Terminal events never produce empty output — the encoder writes the
26
+ * provider's in-stream error form, then the stream ends. */
27
+ function* fail(kind, message) {
28
+ yield encoder.encode({
29
+ type: "error",
30
+ error: (0, canonical_error_1.makeCanonicalError)(kind, message),
31
+ });
32
+ const tail = encoder.end();
33
+ if (tail !== "")
34
+ yield tail;
35
+ }
36
+ /** Yields encoded bytes; returns true at the first terminal event
37
+ * (message_end or error — nothing may be encoded after it). */
38
+ function* encodeAll(events) {
39
+ for (const event of events) {
40
+ const encoded = encoder.encode(event);
41
+ if (encoded !== "")
42
+ yield encoded;
43
+ if (event.type === "message_end" || event.type === "error") {
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+ try {
50
+ for (;;) {
51
+ if (signal.aborted) {
52
+ cancel();
53
+ return;
54
+ }
55
+ let timer;
56
+ let chunk;
57
+ try {
58
+ chunk = await Promise.race([
59
+ reader.read(),
60
+ new Promise((resolve) => {
61
+ timer = setTimeout(() => resolve("idle"), idleMs);
62
+ }),
63
+ ]);
64
+ }
65
+ catch {
66
+ clearTimeout(timer);
67
+ cancel();
68
+ yield* fail("upstream", "Upstream stream read failed");
69
+ return;
70
+ }
71
+ clearTimeout(timer);
72
+ if (signal.aborted) {
73
+ cancel();
74
+ return;
75
+ }
76
+ if (chunk === "idle") {
77
+ cancel();
78
+ yield* fail("timeout", `Upstream stream idle for over ${idleMs}ms`);
79
+ return;
80
+ }
81
+ let events;
82
+ if (chunk.done) {
83
+ try {
84
+ events = decoder.end();
85
+ }
86
+ catch {
87
+ yield* fail("upstream", "Upstream sent a malformed stream");
88
+ return;
89
+ }
90
+ yield* encodeAll(events);
91
+ const tail = encoder.end();
92
+ if (tail !== "")
93
+ yield tail;
94
+ return;
95
+ }
96
+ try {
97
+ events = decoder.feed(chunk.value);
98
+ }
99
+ catch {
100
+ cancel();
101
+ yield* fail("upstream", "Upstream sent undecodable stream data");
102
+ return;
103
+ }
104
+ if (yield* encodeAll(events)) {
105
+ // Terminal event: end now even if the upstream keeps the body
106
+ // open — nothing may follow message_end or error.
107
+ const tail = encoder.end();
108
+ if (tail !== "")
109
+ yield tail;
110
+ cancel();
111
+ return;
112
+ }
113
+ }
114
+ }
115
+ finally {
116
+ signal.removeEventListener("abort", cancel);
117
+ cancel();
118
+ }
119
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toClientError = toClientError;
4
+ const transformers_1 = require("../transformers");
5
+ const canonical_error_1 = require("../utils/canonical-error");
6
+ /**
7
+ * The spec §3.3 upstream → client mapping, applied row by row.
8
+ *
9
+ * The *target* adapter parses the upstream body for its kind and message
10
+ * (`toCanonicalError` never throws; unrecognised bodies get the fallback
11
+ * message). This function then decides what the client sees:
12
+ *
13
+ * - authentication/permission → 502 "upstream" with the credentials
14
+ * message (D1: the upstream rejected the *proxy's* key — a 401 would
15
+ * wrongly tell the client its own key is bad). Decided on the parsed
16
+ * kind, not the raw status, because e.g. Gemini reports a bad key as
17
+ * 400 + API_KEY_INVALID.
18
+ * - 400/422 → 400 invalid_request, 404 → not_found, 413 →
19
+ * request_too_large, 429 → rate_limit, 503/529 → unavailable — the
20
+ * message passes through, `Retry-After` too when present.
21
+ * - every other 5xx → 502 upstream (kindFromStatus calls a bare 500
22
+ * "internal", but for the client it is an upstream condition, not a
23
+ * proxy bug).
24
+ */
25
+ function toClientError(target, upstreamStatus, upstreamBody, retryAfterSeconds) {
26
+ const upstream = transformers_1.adapters[target].toCanonicalError(upstreamStatus, upstreamBody);
27
+ if (upstream.kind === "authentication" || upstream.kind === "permission") {
28
+ return (0, canonical_error_1.makeCanonicalError)("upstream", `Upstream ${target} rejected the proxy's credentials (${upstreamStatus})`);
29
+ }
30
+ const kind = upstreamStatus >= 500 && upstream.kind !== "unavailable"
31
+ ? "upstream"
32
+ : upstream.kind;
33
+ return (0, canonical_error_1.makeCanonicalError)(kind, upstream.message, retryAfterSeconds);
34
+ }