babelgate 1.1.2 → 1.2.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/README.md +116 -25
- package/dist/app.js +5 -5
- package/dist/cli.js +84 -43
- package/dist/config/env-config.js +61 -10
- package/dist/config/gateway.schema.js +50 -0
- package/dist/config/yaml-config.js +164 -0
- package/dist/index.js +6 -4
- package/dist/middleware/detect-source.js +20 -0
- package/dist/middleware/error-handler.js +8 -0
- package/dist/routes/proxy.js +3 -0
- package/dist/routing/detect-source.js +33 -0
- package/dist/routing/fallback.js +37 -0
- package/dist/routing/plan-route.js +47 -0
- package/dist/routing/registry.js +47 -0
- package/dist/routing/route-log.js +8 -0
- package/dist/routing/rules.js +35 -0
- package/dist/routing/table.js +32 -0
- package/dist/services/attempt-targets.js +91 -0
- package/dist/services/chat-stream.service.js +131 -0
- package/dist/services/chat.service.js +91 -0
- package/dist/services/models.service.js +160 -0
- package/dist/services/proxy.service.js +39 -231
- package/dist/transformers/adapters/gemini.adapter.js +7 -3
- package/dist/transformers/adapters/gemini.tools.js +13 -3
- package/dist/transformers/adapters/index.js +1 -2
- package/dist/utils/errors.js +6 -0
- package/dist/utils/model-map.js +7 -8
- package/package.json +2 -1
|
@@ -5,27 +5,15 @@ exports.proxyRequest = proxyRequest;
|
|
|
5
5
|
const env_config_1 = require("../config/env-config");
|
|
6
6
|
const transformers_1 = require("../transformers");
|
|
7
7
|
const adapters_1 = require("../transformers/adapters");
|
|
8
|
-
const
|
|
9
|
-
const stream_pipeline_1 = require("./stream-pipeline");
|
|
10
|
-
const model_map_1 = require("../utils/model-map");
|
|
8
|
+
const plan_route_1 = require("../routing/plan-route");
|
|
11
9
|
const token_estimate_1 = require("../utils/token-estimate");
|
|
12
|
-
const upstream_errors_1 = require("./upstream-errors");
|
|
13
10
|
const canonical_schema_1 = require("../types/canonical.schema");
|
|
14
11
|
const errors_1 = require("../utils/errors");
|
|
15
12
|
Object.defineProperty(exports, "TransformError", { enumerable: true, get: function () { return errors_1.TransformError; } });
|
|
16
13
|
const logger_1 = require("../utils/logger");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
14
|
+
const chat_service_1 = require("./chat.service");
|
|
15
|
+
const chat_stream_service_1 = require("./chat-stream.service");
|
|
16
|
+
const models_service_1 = require("./models.service");
|
|
29
17
|
/**
|
|
30
18
|
* The "<method>" in a "/v1beta/models/<m>:<method>" path — the text
|
|
31
19
|
* after the last ":" of the percent-decoded path, the same parsing the
|
|
@@ -43,32 +31,14 @@ function methodFromPath(path) {
|
|
|
43
31
|
return colon === -1 ? decoded : decoded.slice(colon + 1);
|
|
44
32
|
}
|
|
45
33
|
/**
|
|
46
|
-
*
|
|
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:
|
|
34
|
+
* Thin entry over the routing layer (spec §3.1):
|
|
67
35
|
* path match → config → canonical boundary validation →
|
|
68
|
-
*
|
|
36
|
+
* count_tokens → planRoute → chat/chat-stream/models services.
|
|
69
37
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
38
|
+
* The client format comes from `opts.source` (the detect-source
|
|
39
|
+
* middleware's read of the request path, spec §3.2). Returns null when
|
|
40
|
+
* it is unset or the path doesn't belong to that source provider
|
|
41
|
+
* (route should call next() → 404).
|
|
72
42
|
* Throws ValidationError | UpstreamError | TransformError | ConfigurationError.
|
|
73
43
|
*/
|
|
74
44
|
async function proxyRequest(path, method, body, requestId, opts) {
|
|
@@ -79,14 +49,15 @@ async function proxyRequest(path, method, body, requestId, opts) {
|
|
|
79
49
|
if (!isProviderPath)
|
|
80
50
|
return null;
|
|
81
51
|
const config = (0, env_config_1.getConfig)();
|
|
82
|
-
const sourceApi =
|
|
83
|
-
|
|
84
|
-
|
|
52
|
+
const sourceApi = opts?.source;
|
|
53
|
+
if (!sourceApi)
|
|
54
|
+
return null;
|
|
55
|
+
// Match the incoming path to a route on the detected source provider
|
|
85
56
|
const route = (0, adapters_1.findSourceRoute)(sourceApi, method, path);
|
|
86
57
|
if (!route)
|
|
87
58
|
return null;
|
|
88
59
|
if (route.kind === "models") {
|
|
89
|
-
return
|
|
60
|
+
return (0, models_service_1.proxyModelList)(sourceApi, route, config, requestId);
|
|
90
61
|
}
|
|
91
62
|
// Gemini paths carry a ":<method>" suffix — anything that isn't a
|
|
92
63
|
// method this route serves is refused before conversion, before any
|
|
@@ -122,27 +93,37 @@ async function proxyRequest(path, method, body, requestId, opts) {
|
|
|
122
93
|
if (modelFromPath === "") {
|
|
123
94
|
throw new errors_1.ValidationError(`Malformed ${sourceApi} request path: model is empty`, { requestId });
|
|
124
95
|
}
|
|
96
|
+
// The chars/4 input estimate is computed once — count_tokens answers
|
|
97
|
+
// with it (§3.5) and routing `when` conditions consume it (§3.3).
|
|
98
|
+
const estimatedInputTokens = (0, token_estimate_1.estimateInputTokens)(parsed.data);
|
|
125
99
|
// §3.5: count_tokens is answered locally — a chars/4 estimate with the
|
|
126
100
|
// estimate header, never an upstream call.
|
|
127
101
|
if (route.kind === "count_tokens") {
|
|
128
102
|
logger_1.logger.info({ requestId, sourceApi }, "count_tokens answered locally");
|
|
129
103
|
return {
|
|
130
|
-
body: { input_tokens:
|
|
104
|
+
body: { input_tokens: estimatedInputTokens },
|
|
131
105
|
headers: { "x-babelgate-token-estimate": "true" },
|
|
132
106
|
};
|
|
133
107
|
}
|
|
134
|
-
// §3.
|
|
135
|
-
//
|
|
108
|
+
// §3.3: resolve the model to an ordered list of targets — env mode
|
|
109
|
+
// produces MODEL_MAP routes then the single default provider.
|
|
136
110
|
const requestedModel = modelFromPath ?? parsed.data.model;
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
111
|
+
const plan = (0, plan_route_1.planRoute)({
|
|
112
|
+
requestedModel,
|
|
113
|
+
estimatedInputTokens,
|
|
114
|
+
routing: config.routing,
|
|
115
|
+
providerNames: new Set(config.registry.names()),
|
|
116
|
+
});
|
|
117
|
+
const input = {
|
|
118
|
+
source: sourceApi,
|
|
119
|
+
sourceBody: body,
|
|
120
|
+
canonical: parsed.data,
|
|
121
|
+
requestedModel,
|
|
122
|
+
plan,
|
|
123
|
+
config,
|
|
124
|
+
requestId,
|
|
125
|
+
opts,
|
|
126
|
+
};
|
|
146
127
|
// Streaming (spec §3.2): a `stream` body flag on the JSON-body sources,
|
|
147
128
|
// the dedicated streaming endpoint on Gemini — which carries no body
|
|
148
129
|
// flag, so canonical.stream is set here for the target adapter's
|
|
@@ -150,180 +131,7 @@ async function proxyRequest(path, method, body, requestId, opts) {
|
|
|
150
131
|
// stream:true).
|
|
151
132
|
if (parsed.data.stream === true || route.isStreamPath?.(path) === true) {
|
|
152
133
|
parsed.data.stream = true;
|
|
153
|
-
return
|
|
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
|
-
}
|
|
134
|
+
return (0, chat_stream_service_1.proxyChatStream)(input);
|
|
325
135
|
}
|
|
326
|
-
|
|
327
|
-
const sourceBody = transformers_1.adapters[sourceApi].fromCanonicalModelList(canonical);
|
|
328
|
-
return { body: sourceBody };
|
|
136
|
+
return (0, chat_service_1.proxyChat)(input);
|
|
329
137
|
}
|
|
@@ -181,7 +181,7 @@ function fromCanonicalRequest(request) {
|
|
|
181
181
|
}
|
|
182
182
|
if (calls.length > 0) {
|
|
183
183
|
parts = parts.filter((p) => !("text" in p && p.text === ""));
|
|
184
|
-
parts.push(...calls.map(gemini_tools_1.functionCallPart));
|
|
184
|
+
parts.push(...calls.map((call) => (0, gemini_tools_1.functionCallPart)(call)));
|
|
185
185
|
}
|
|
186
186
|
contents.push({ role: "model", parts });
|
|
187
187
|
}
|
|
@@ -326,9 +326,10 @@ function fromCanonicalResponse(response) {
|
|
|
326
326
|
? response.message.content
|
|
327
327
|
: "";
|
|
328
328
|
const toolCalls = response.message.toolCalls ?? [];
|
|
329
|
+
const callPart = (call) => (0, gemini_tools_1.functionCallPart)(call, { includeId: true });
|
|
329
330
|
const parts = toolCalls.length > 0 && content === ""
|
|
330
|
-
? toolCalls.map(
|
|
331
|
-
: [{ text: content }, ...toolCalls.map(
|
|
331
|
+
? toolCalls.map(callPart)
|
|
332
|
+
: [{ text: content }, ...toolCalls.map(callPart)];
|
|
332
333
|
return {
|
|
333
334
|
candidates: [
|
|
334
335
|
{
|
|
@@ -340,6 +341,9 @@ function fromCanonicalResponse(response) {
|
|
|
340
341
|
index: 0,
|
|
341
342
|
},
|
|
342
343
|
],
|
|
344
|
+
// §3.3: replies carry the requested model name — on the wire that's
|
|
345
|
+
// modelVersion, matching the stream encoder.
|
|
346
|
+
modelVersion: response.model,
|
|
343
347
|
usageMetadata: response.usage
|
|
344
348
|
? {
|
|
345
349
|
promptTokenCount: response.usage.inputTokens,
|
|
@@ -156,14 +156,24 @@ function functionResponseContent(res) {
|
|
|
156
156
|
return JSON.stringify(response ?? {});
|
|
157
157
|
}
|
|
158
158
|
// ============ ENCODE: Canonical → Gemini ============
|
|
159
|
-
/**
|
|
160
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Canonical tool call → Gemini functionCall part. `functionCall.id`
|
|
161
|
+
* is a documented optional field: the request encoder drops it
|
|
162
|
+
* (default — requests stay byte-identical), while the response
|
|
163
|
+
* encoder passes `includeId` so the client can echo
|
|
164
|
+
* `functionResponse.id` back, matching the stream encoder's shape.
|
|
165
|
+
*/
|
|
166
|
+
function functionCallPart(call, opts = {}) {
|
|
161
167
|
const parsed = (0, tool_args_1.parseToolArguments)(call.function.arguments);
|
|
162
168
|
if (!parsed.valid) {
|
|
163
169
|
logger_1.logger.warn("Tool call arguments are not valid JSON; sending _invalid_arguments wrapper");
|
|
164
170
|
}
|
|
165
171
|
return {
|
|
166
|
-
functionCall: {
|
|
172
|
+
functionCall: {
|
|
173
|
+
...(opts.includeId === true && { id: call.id }),
|
|
174
|
+
name: call.function.name,
|
|
175
|
+
args: parsed.args,
|
|
176
|
+
},
|
|
167
177
|
};
|
|
168
178
|
}
|
|
169
179
|
/** Canonical tool message → Gemini functionResponse part. */
|
|
@@ -122,8 +122,7 @@ exports.PROVIDER_ROUTES = {
|
|
|
122
122
|
};
|
|
123
123
|
/**
|
|
124
124
|
* Match an incoming request to a route on the given source provider
|
|
125
|
-
* (method + path prefix). Used by
|
|
126
|
-
* proxy.service.
|
|
125
|
+
* (method + path prefix). Used by proxy.service.
|
|
127
126
|
*/
|
|
128
127
|
function findSourceRoute(source, method, path) {
|
|
129
128
|
return exports.PROVIDER_ROUTES[source].find((r) => r.method === method && path.startsWith(r.pathPrefix));
|
package/dist/utils/errors.js
CHANGED
|
@@ -6,6 +6,12 @@ class ProxyError extends Error {
|
|
|
6
6
|
statusCode;
|
|
7
7
|
code;
|
|
8
8
|
context;
|
|
9
|
+
/**
|
|
10
|
+
* Extra response headers the error-handler sets before rendering —
|
|
11
|
+
* e.g. `x-babelgate-attempts` on errors after upstream attempts
|
|
12
|
+
* (spec §3.4).
|
|
13
|
+
*/
|
|
14
|
+
responseHeaders;
|
|
9
15
|
constructor(message, statusCode = 500, code = "PROXY_ERROR", context) {
|
|
10
16
|
super(message);
|
|
11
17
|
this.statusCode = statusCode;
|
package/dist/utils/model-map.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.globMatch = void 0;
|
|
3
4
|
exports.parseModelMap = parseModelMap;
|
|
4
5
|
exports.applyModelMap = applyModelMap;
|
|
5
6
|
const errors_1 = require("./errors");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return new RegExp(`^${escaped}$`);
|
|
12
|
-
}
|
|
7
|
+
const rules_1 = require("../routing/rules");
|
|
8
|
+
// The glob matcher moved to routing/rules.ts with the routing layer;
|
|
9
|
+
// re-exported so existing model-map imports keep working.
|
|
10
|
+
var rules_2 = require("../routing/rules");
|
|
11
|
+
Object.defineProperty(exports, "globMatch", { enumerable: true, get: function () { return rules_2.globMatch; } });
|
|
13
12
|
/**
|
|
14
13
|
* Comma-separated `glob=model` entries → ordered rules. An entry that
|
|
15
14
|
* isn't `glob=model` (empty pattern or model, no `=`) is a configuration
|
|
@@ -32,7 +31,7 @@ function parseModelMap(value) {
|
|
|
32
31
|
/** First matching rule wins; no match leaves the model unchanged. */
|
|
33
32
|
function applyModelMap(model, rules) {
|
|
34
33
|
for (const rule of rules) {
|
|
35
|
-
if (
|
|
34
|
+
if ((0, rules_1.globMatch)(rule.pattern, model))
|
|
36
35
|
return rule.model;
|
|
37
36
|
}
|
|
38
37
|
return model;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelgate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"bin": {
|
|
5
5
|
"babelgate": "dist/cli.js"
|
|
6
6
|
},
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"express": "^5.1.0",
|
|
48
48
|
"pino": "^10.4.0",
|
|
49
49
|
"pino-http": "^11.0.0",
|
|
50
|
+
"yaml": "^2.9.1",
|
|
50
51
|
"zod": "^4.6.5"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|