@jini-ai/daemon 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
- package/dist/agent-executor/antigravity-permissions.js +64 -0
- package/dist/agent-executor/contracts.d.ts +31 -6
- package/dist/agent-executor/event-translation.d.ts +6 -1
- package/dist/agent-executor/event-translation.js +1 -1
- package/dist/agent-executor/events.d.ts +1 -0
- package/dist/agent-executor/events.js +85 -74
- package/dist/agent-executor/launch.d.ts +4 -2
- package/dist/agent-executor/launch.js +306 -217
- package/dist/agent-executor/mcp-bridge-probe.js +51 -35
- package/dist/agent-executor/mcp-bridge.d.ts +18 -8
- package/dist/agent-executor/mcp-bridge.js +105 -65
- package/dist/agent-executor/run-output.d.ts +20 -0
- package/dist/agent-executor/run-output.js +80 -0
- package/dist/attachment-content.js +18 -15
- package/dist/core/read-only-tools.d.ts +26 -3
- package/dist/core/read-only-tools.js +38 -5
- package/dist/core/tool-audit.d.ts +3 -1
- package/dist/core/tool-audit.js +35 -22
- package/dist/core/tool-candidates.d.ts +8 -0
- package/dist/core/tool-candidates.js +6 -0
- package/dist/core/tool-gaps.d.ts +60 -0
- package/dist/core/tool-gaps.js +199 -0
- package/dist/delegated-tool-bridge.d.ts +1 -1
- package/dist/delegated-tool-bridge.js +161 -122
- package/dist/event-log.d.ts +45 -2
- package/dist/event-log.js +83 -4
- package/dist/http/attachments.d.ts +26 -1
- package/dist/http/attachments.js +129 -65
- package/dist/http/connectors.d.ts +1 -32
- package/dist/http/connectors.js +13 -68
- package/dist/http/delegated-tools.d.ts +5 -1
- package/dist/http/delegated-tools.js +29 -29
- package/dist/http/host-tools.d.ts +1 -0
- package/dist/http/host-tools.js +64 -59
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/media.js +48 -69
- package/dist/http/memory.js +45 -36
- package/dist/http/model-proxy-fields.d.ts +23 -0
- package/dist/http/model-proxy-fields.js +29 -0
- package/dist/http/model-proxy.js +193 -395
- package/dist/http/research-tavily.adapter.js +72 -49
- package/dist/http/research.js +33 -23
- package/dist/http/resource-request.d.ts +37 -0
- package/dist/http/resource-request.js +59 -0
- package/dist/http/routines.js +49 -90
- package/dist/http/runs.d.ts +7 -5
- package/dist/http/runs.js +21 -7
- package/dist/http/terminals.js +23 -48
- package/dist/http/tool-catalog.d.ts +5 -0
- package/dist/http/xai.d.ts +11 -1
- package/dist/http/xai.js +125 -124
- package/dist/legacy-data-migration.js +41 -24
- package/dist/run/core/retry.js +51 -35
- package/dist/run/diagnostics/diagnostics.js +5 -17
- package/dist/run-credentials.js +49 -27
- package/dist/run-lifecycle.d.ts +12 -2
- package/dist/run-lifecycle.js +47 -26
- package/dist/store/agent-sessions/sqlite.d.ts +6 -0
- package/dist/store/agent-sessions/sqlite.js +6 -4
- package/dist/surface-exchanges.js +2 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.js +26 -33
- package/dist/tool-audit.d.ts +1 -0
- package/dist/tool-audit.js +1 -0
- package/dist/tool-executor.d.ts +24 -3
- package/dist/tool-executor.js +277 -139
- package/dist/tool-recovery/recovery.js +64 -35
- package/dist/tool-recovery/stack.d.ts +2 -1
- package/dist/tool-recovery/stack.js +1 -1
- package/package.json +10 -10
package/dist/http/model-proxy.js
CHANGED
|
@@ -89,15 +89,31 @@ import { sendApiError } from '@jini-ai/http-kit';
|
|
|
89
89
|
import { guardSameOrigin } from '@jini-ai/http-kit';
|
|
90
90
|
import { createSseChannel } from '@jini-ai/http-kit';
|
|
91
91
|
import { err, ok } from '@jini-ai/http-kit';
|
|
92
|
+
import { firstOptionalFieldError, isArray, isNumber, isRecord, isString, nonEmptyString, pickDefined, requiredFieldError, } from './model-proxy-fields.js';
|
|
92
93
|
function defaultInternalErrorSink(context) {
|
|
93
94
|
// eslint-disable-next-line no-console
|
|
94
95
|
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, context.error);
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
const TOOLS_CHECK = ['tools', isArray, 'tools must be an array when provided'];
|
|
98
|
+
const SYSTEM_CHECK = ['system', isString, 'system must be a string when provided'];
|
|
99
|
+
/** The shared optional fields, in the order `parseCommon` reports them. */
|
|
100
|
+
const COMMON_OPTIONAL_CHECKS = [
|
|
101
|
+
['baseUrl', isString, 'baseUrl must be a string when provided'],
|
|
102
|
+
['temperature', isNumber, 'temperature must be a number when provided'],
|
|
103
|
+
['maxTokens', isNumber, 'maxTokens must be a number when provided'],
|
|
104
|
+
['maxToolTurns', isNumber, 'maxToolTurns must be a number when provided'],
|
|
105
|
+
['extraHeaders', isRecord, 'extraHeaders must be an object when provided'],
|
|
106
|
+
];
|
|
107
|
+
/** The required shared fields, in the order `parseCommon` reports them. */
|
|
108
|
+
function firstRequiredFieldError(body) {
|
|
109
|
+
if (!nonEmptyString(body.apiKey))
|
|
110
|
+
return requiredFieldError('apiKey', 'apiKey must be a non-empty string', 'required non-empty string');
|
|
111
|
+
if (!nonEmptyString(body.model))
|
|
112
|
+
return requiredFieldError('model', 'model must be a non-empty string', 'required non-empty string');
|
|
113
|
+
if (!Array.isArray(body.messages) || body.messages.length === 0) {
|
|
114
|
+
return requiredFieldError('messages', 'messages must be a non-empty array', 'required non-empty array');
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
101
117
|
}
|
|
102
118
|
/**
|
|
103
119
|
* Validates only the transport-level shape every proxied request shares
|
|
@@ -123,164 +139,101 @@ function nonEmptyString(value) {
|
|
|
123
139
|
function parseCommon(body) {
|
|
124
140
|
if (!isRecord(body))
|
|
125
141
|
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
return err({ error
|
|
129
|
-
}
|
|
130
|
-
const model = nonEmptyString(body.model);
|
|
131
|
-
if (!model) {
|
|
132
|
-
return err({ error: validationError({ message: 'model must be a non-empty string' }, { issues: [{ path: 'model', message: 'required non-empty string' }] }) });
|
|
133
|
-
}
|
|
134
|
-
if (!Array.isArray(body.messages) || body.messages.length === 0) {
|
|
135
|
-
return err({ error: validationError({ message: 'messages must be a non-empty array' }, { issues: [{ path: 'messages', message: 'required non-empty array' }] }) });
|
|
136
|
-
}
|
|
137
|
-
if (body.baseUrl !== undefined && typeof body.baseUrl !== 'string') {
|
|
138
|
-
return err({ error: validationError({ message: 'baseUrl must be a string when provided' }) });
|
|
139
|
-
}
|
|
140
|
-
if (body.temperature !== undefined && typeof body.temperature !== 'number') {
|
|
141
|
-
return err({ error: validationError({ message: 'temperature must be a number when provided' }) });
|
|
142
|
-
}
|
|
143
|
-
if (body.maxTokens !== undefined && typeof body.maxTokens !== 'number') {
|
|
144
|
-
return err({ error: validationError({ message: 'maxTokens must be a number when provided' }) });
|
|
145
|
-
}
|
|
146
|
-
if (body.maxToolTurns !== undefined && typeof body.maxToolTurns !== 'number') {
|
|
147
|
-
return err({ error: validationError({ message: 'maxToolTurns must be a number when provided' }) });
|
|
148
|
-
}
|
|
149
|
-
if (body.extraHeaders !== undefined && !isRecord(body.extraHeaders)) {
|
|
150
|
-
return err({ error: validationError({ message: 'extraHeaders must be an object when provided' }) });
|
|
151
|
-
}
|
|
142
|
+
const error = firstRequiredFieldError(body) ?? firstOptionalFieldError(body, COMMON_OPTIONAL_CHECKS);
|
|
143
|
+
if (error)
|
|
144
|
+
return err({ error });
|
|
152
145
|
return ok({
|
|
153
146
|
value: {
|
|
154
|
-
apiKey,
|
|
155
|
-
model,
|
|
147
|
+
apiKey: body.apiKey,
|
|
148
|
+
model: body.model,
|
|
156
149
|
messages: body.messages,
|
|
157
|
-
...(body
|
|
158
|
-
|
|
159
|
-
...(body.maxTokens !== undefined ? { maxTokens: body.maxTokens } : {}),
|
|
160
|
-
...(body.maxToolTurns !== undefined ? { maxToolTurns: body.maxToolTurns } : {}),
|
|
161
|
-
...(body.extraHeaders !== undefined ? { extraHeaders: body.extraHeaders } : {}),
|
|
162
|
-
}
|
|
150
|
+
...pickDefined(body, COMMON_OPTIONAL_CHECKS.map(([field]) => field)),
|
|
151
|
+
},
|
|
163
152
|
});
|
|
164
153
|
}
|
|
154
|
+
/** Shared tail of every provider parser: rejects the first badly-typed provider-specific optional field, otherwise returns `base` plus `required` plus whichever of those optional fields are present. */
|
|
155
|
+
function extendParsed({ base, record, checks }, { required = {} } = {}) {
|
|
156
|
+
const error = firstOptionalFieldError(record, checks);
|
|
157
|
+
if (error)
|
|
158
|
+
return err({ error });
|
|
159
|
+
const optional = pickDefined(record, checks.map(([field]) => field));
|
|
160
|
+
return ok({ value: { ...base, ...required, ...optional } });
|
|
161
|
+
}
|
|
162
|
+
const ANTHROPIC_OPTIONAL_CHECKS = [
|
|
163
|
+
['apiVersion', isString, 'apiVersion must be a string when provided'],
|
|
164
|
+
SYSTEM_CHECK,
|
|
165
|
+
TOOLS_CHECK,
|
|
166
|
+
];
|
|
167
|
+
function isPositiveFiniteNumber(value) {
|
|
168
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
169
|
+
}
|
|
165
170
|
function parseAnthropicProxyRequest(body) {
|
|
166
171
|
const common = parseCommon(body);
|
|
167
172
|
if (!common.ok)
|
|
168
173
|
return common;
|
|
169
174
|
const record = body;
|
|
170
|
-
if (
|
|
171
|
-
return err({ error:
|
|
172
|
-
}
|
|
173
|
-
if (record.apiVersion !== undefined && typeof record.apiVersion !== 'string') {
|
|
174
|
-
return err({ error: validationError({ message: 'apiVersion must be a string when provided' }) });
|
|
175
|
-
}
|
|
176
|
-
if (record.system !== undefined && typeof record.system !== 'string') {
|
|
177
|
-
return err({ error: validationError({ message: 'system must be a string when provided' }) });
|
|
175
|
+
if (!isPositiveFiniteNumber(record.maxTokens)) {
|
|
176
|
+
return err({ error: requiredFieldError('maxTokens', 'maxTokens must be a positive number', 'required positive number') });
|
|
178
177
|
}
|
|
179
|
-
|
|
180
|
-
return err({ error: validationError({ message: 'tools must be an array when provided' }) });
|
|
181
|
-
}
|
|
182
|
-
return ok({
|
|
183
|
-
value: {
|
|
184
|
-
...common.value,
|
|
185
|
-
maxTokens: record.maxTokens,
|
|
186
|
-
...(record.apiVersion !== undefined ? { apiVersion: record.apiVersion } : {}),
|
|
187
|
-
...(record.system !== undefined ? { system: record.system } : {}),
|
|
188
|
-
...(record.tools !== undefined ? { tools: record.tools } : {}),
|
|
189
|
-
}
|
|
190
|
-
});
|
|
178
|
+
return extendParsed({ base: common.value, record, checks: ANTHROPIC_OPTIONAL_CHECKS }, { required: { maxTokens: record.maxTokens } });
|
|
191
179
|
}
|
|
192
|
-
function
|
|
180
|
+
function parseToolsProxyRequest(body) {
|
|
193
181
|
const common = parseCommon(body);
|
|
194
182
|
if (!common.ok)
|
|
195
183
|
return common;
|
|
196
|
-
|
|
197
|
-
if (record.tools !== undefined && !Array.isArray(record.tools)) {
|
|
198
|
-
return err({ error: validationError({ message: 'tools must be an array when provided' }) });
|
|
199
|
-
}
|
|
200
|
-
return ok({
|
|
201
|
-
value: {
|
|
202
|
-
...common.value,
|
|
203
|
-
...(record.tools !== undefined ? { tools: record.tools } : {}),
|
|
204
|
-
}
|
|
205
|
-
});
|
|
184
|
+
return extendParsed({ base: common.value, record: body, checks: [TOOLS_CHECK] });
|
|
206
185
|
}
|
|
207
186
|
function parseAzureProxyRequest(body) {
|
|
208
187
|
const common = parseCommon(body);
|
|
209
188
|
if (!common.ok)
|
|
210
189
|
return common;
|
|
211
190
|
const record = body;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
return err({ error:
|
|
191
|
+
const { baseUrl } = common.value;
|
|
192
|
+
// Truthiness only, unlike `apiVersion`'s trim check: a whitespace-only baseUrl passes here and
|
|
193
|
+
// fails inside the turn-runner instead (pinned as "reported, not fixed").
|
|
194
|
+
if (!baseUrl) {
|
|
195
|
+
return err({ error: requiredFieldError('baseUrl', 'baseUrl must be a non-empty string', 'required non-empty string') });
|
|
217
196
|
}
|
|
218
|
-
|
|
219
|
-
|
|
197
|
+
const apiVersion = nonEmptyString(record.apiVersion);
|
|
198
|
+
if (!apiVersion) {
|
|
199
|
+
return err({ error: requiredFieldError('apiVersion', 'apiVersion must be a non-empty string', 'required non-empty string') });
|
|
220
200
|
}
|
|
221
|
-
return
|
|
222
|
-
value: {
|
|
223
|
-
...common.value,
|
|
224
|
-
baseUrl: common.value.baseUrl,
|
|
225
|
-
apiVersion: record.apiVersion,
|
|
226
|
-
...(record.tools !== undefined ? { tools: record.tools } : {}),
|
|
227
|
-
}
|
|
228
|
-
});
|
|
201
|
+
return extendParsed({ base: common.value, record, checks: [TOOLS_CHECK] }, { required: { baseUrl, apiVersion } });
|
|
229
202
|
}
|
|
203
|
+
const GOOGLE_OPTIONAL_CHECKS = [
|
|
204
|
+
SYSTEM_CHECK,
|
|
205
|
+
TOOLS_CHECK,
|
|
206
|
+
['maxOutputTokens', isNumber, 'maxOutputTokens must be a number when provided'],
|
|
207
|
+
];
|
|
230
208
|
/** `parsed.messages` carries Gemini's own `contents: [{role, parts}]` shape at the call site below (cast, same convention as `parsed.messages as unknown as readonly AnthropicMessageParam[]`) — the HTTP JSON body schema stays uniform across all five providers; only each turn-runner's own field name differs. */
|
|
231
209
|
function parseGoogleProxyRequest(body) {
|
|
232
210
|
const common = parseCommon(body);
|
|
233
211
|
if (!common.ok)
|
|
234
212
|
return common;
|
|
235
|
-
|
|
236
|
-
if (record.system !== undefined && typeof record.system !== 'string') {
|
|
237
|
-
return err({ error: validationError({ message: 'system must be a string when provided' }) });
|
|
238
|
-
}
|
|
239
|
-
if (record.tools !== undefined && !Array.isArray(record.tools)) {
|
|
240
|
-
return err({ error: validationError({ message: 'tools must be an array when provided' }) });
|
|
241
|
-
}
|
|
242
|
-
if (record.maxOutputTokens !== undefined && typeof record.maxOutputTokens !== 'number') {
|
|
243
|
-
return err({ error: validationError({ message: 'maxOutputTokens must be a number when provided' }) });
|
|
244
|
-
}
|
|
245
|
-
return ok({
|
|
246
|
-
value: {
|
|
247
|
-
...common.value,
|
|
248
|
-
...(record.system !== undefined ? { system: record.system } : {}),
|
|
249
|
-
...(record.tools !== undefined ? { tools: record.tools } : {}),
|
|
250
|
-
...(record.maxOutputTokens !== undefined ? { maxOutputTokens: record.maxOutputTokens } : {}),
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
function parseOllamaProxyRequest(body) {
|
|
255
|
-
const common = parseCommon(body);
|
|
256
|
-
if (!common.ok)
|
|
257
|
-
return common;
|
|
258
|
-
const record = body;
|
|
259
|
-
if (record.tools !== undefined && !Array.isArray(record.tools)) {
|
|
260
|
-
return err({ error: validationError({ message: 'tools must be an array when provided' }) });
|
|
261
|
-
}
|
|
262
|
-
return ok({
|
|
263
|
-
value: {
|
|
264
|
-
...common.value,
|
|
265
|
-
...(record.tools !== undefined ? { tools: record.tools } : {}),
|
|
266
|
-
}
|
|
267
|
-
});
|
|
213
|
+
return extendParsed({ base: common.value, record: body, checks: GOOGLE_OPTIONAL_CHECKS });
|
|
268
214
|
}
|
|
269
215
|
/** Wraps a raw turn-runner event for `sse.ts`'s generic channel: `kind` drives both the wire `event:` field and `createSseChannel`'s `isEndEvent` auto-close, `data` preserves the exact event shape agent-runtime produced. */
|
|
270
216
|
function toStreamEvent(seq, event) {
|
|
271
217
|
return { opaqueCursor: String(seq), kind: event.type, data: event };
|
|
272
218
|
}
|
|
219
|
+
/** Answers a failure that happened before any SSE stream existed: a redacted JSON 500 if nothing was sent yet, otherwise just end the response (matching `mountJsonRoute`'s own SEC-005 catch). */
|
|
220
|
+
function respondInternalError(res, correlationId) {
|
|
221
|
+
if (!res.headersSent) {
|
|
222
|
+
sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
223
|
+
}
|
|
224
|
+
else if (!res.writableEnded) {
|
|
225
|
+
res.end();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
273
228
|
/**
|
|
274
229
|
* The actual request handler body, shared by every fixed-path route and the `:provider`
|
|
275
230
|
* catch-all: same-origin guard, request parsing, SSE channel lifecycle, and the SEC-005 catch-all
|
|
276
231
|
* for a turn-runner promise that rejects outright (see module doc). Provider-specific pieces
|
|
277
|
-
* (`provider`, `parse`, `run`) are injected
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* from `req.params` instead of a fixed string, without duplicating the SSE-wiring/error-handling
|
|
281
|
-
* code a second time.
|
|
232
|
+
* (`provider`, `entry.parse`, `entry.run`) are injected, so the generic catch-all route below can
|
|
233
|
+
* invoke the identical logic with a `provider` value read from `req.params` instead of a fixed
|
|
234
|
+
* string, without duplicating the SSE-wiring/error-handling code a second time.
|
|
282
235
|
*/
|
|
283
|
-
async function runProxyStream(req, res, adapter, provider, onInternalError,
|
|
236
|
+
async function runProxyStream({ req, res, adapter, provider, onInternalError, entry }) {
|
|
284
237
|
// Both `channel` and `seq` are declared ahead of the try so the catch block below can still
|
|
285
238
|
// reach them however far execution got — including a throw from `guardSameOrigin`/`parse`,
|
|
286
239
|
// before either exists yet.
|
|
@@ -292,14 +245,15 @@ async function runProxyStream(req, res, adapter, provider, onInternalError, pars
|
|
|
292
245
|
sendApiError({ res, status: 403, error: origin.error });
|
|
293
246
|
return;
|
|
294
247
|
}
|
|
295
|
-
const parsed = parse(req.body);
|
|
248
|
+
const parsed = entry.parse(req.body);
|
|
296
249
|
if (!parsed.ok) {
|
|
297
250
|
sendApiError({ res, status: 400, error: parsed.error });
|
|
298
251
|
return;
|
|
299
252
|
}
|
|
300
|
-
|
|
301
|
-
channel
|
|
302
|
-
|
|
253
|
+
const openChannel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
|
|
254
|
+
channel = openChannel;
|
|
255
|
+
openChannel.open();
|
|
256
|
+
await entry.run(parsed.value, (event) => openChannel.enqueue({ event: toStreamEvent(seq++, event) }));
|
|
303
257
|
// No `channel.end()` call here: every reachable exit path inside every turn-runner emits
|
|
304
258
|
// exactly one `'end'`-kind event before its promise resolves (the duplicate-end-event fix —
|
|
305
259
|
// see each provider module's own `turn-end-guard.ts` usage, and each function's own
|
|
@@ -308,176 +262,132 @@ async function runProxyStream(req, res, adapter, provider, onInternalError, pars
|
|
|
308
262
|
}
|
|
309
263
|
catch (error) {
|
|
310
264
|
// This catch now covers the whole handler, not just `run(...)`: neither caller of this
|
|
311
|
-
// function (
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
265
|
+
// function (the fixed routes, `registerGenericProxyStreamRoute`'s catch-all) awaits or
|
|
266
|
+
// `.catch()`s the promise this function returns — exactly how Express itself invokes every
|
|
267
|
+
// handler. `guardSameOrigin`/`parse` are Result-returning by contract and don't throw today,
|
|
268
|
+
// but nothing enforces that; if either ever did, the throw used to become an unhandled
|
|
269
|
+
// rejection with no process-level guard anywhere in this package's path, which is Node's
|
|
270
|
+
// documented crash-the-process default. See the paired regression test in
|
|
317
271
|
// `__tests__/model-proxy.test.ts`.
|
|
318
272
|
const correlationId = randomUUID();
|
|
319
273
|
onInternalError({ provider, correlationId, error });
|
|
320
|
-
if (channel) {
|
|
321
|
-
// `enqueue` is a documented no-op once the channel is already closed (`sse.ts`), so these
|
|
322
|
-
// two synthetic events are safe to send unconditionally: normally the channel is still open
|
|
323
|
-
// here (an exception escaping `run` — e.g. a caller-supplied `executeTool` throwing — means
|
|
324
|
-
// no `'end'` event was ever emitted for this connection), and the synthetic `'end'`-kind
|
|
325
|
-
// event's own `isEndEvent` match closes the channel via the same path as a real one.
|
|
326
|
-
channel.enqueue({
|
|
327
|
-
event: toStreamEvent(seq++, {
|
|
328
|
-
type: 'error',
|
|
329
|
-
message: 'an internal error occurred',
|
|
330
|
-
code: correlationId,
|
|
331
|
-
})
|
|
332
|
-
});
|
|
333
|
-
channel.enqueue({ event: toStreamEvent(seq++, { type: 'end', reason: 'error' }) });
|
|
334
|
-
}
|
|
335
|
-
else if (!res.headersSent) {
|
|
274
|
+
if (!channel) {
|
|
336
275
|
// The SSE channel was never opened (the throw happened before it), so no stream headers
|
|
337
276
|
// were ever committed — a normal JSON error response is still possible here, matching how
|
|
338
277
|
// `mountJsonRoute`'s own SEC-005 catch responds to a pre-response failure.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
else if (!res.writableEnded) {
|
|
342
|
-
res.end();
|
|
278
|
+
respondInternalError(res, correlationId);
|
|
279
|
+
return;
|
|
343
280
|
}
|
|
281
|
+
// `enqueue` is a documented no-op once the channel is already closed (`sse.ts`), so these
|
|
282
|
+
// two synthetic events are safe to send unconditionally: normally the channel is still open
|
|
283
|
+
// here (an exception escaping `run` — e.g. a caller-supplied `executeTool` throwing — means
|
|
284
|
+
// no `'end'` event was ever emitted for this connection), and the synthetic `'end'`-kind
|
|
285
|
+
// event's own `isEndEvent` match closes the channel via the same path as a real one.
|
|
286
|
+
channel.enqueue({ event: toStreamEvent(seq++, { type: 'error', message: 'an internal error occurred', code: correlationId }) });
|
|
287
|
+
channel.enqueue({ event: toStreamEvent(seq++, { type: 'end', reason: 'error' }) });
|
|
344
288
|
}
|
|
345
289
|
}
|
|
346
|
-
/**
|
|
347
|
-
function
|
|
348
|
-
|
|
290
|
+
/** Erases one provider's concrete parse/run types into the registry's shared entry shape — the single cast every entry needs. */
|
|
291
|
+
function defineProvider(entry) {
|
|
292
|
+
return entry;
|
|
293
|
+
}
|
|
294
|
+
/** The test-seam transport options every turn-runner accepts; each is forwarded only when the host supplied it (see `ModelProxyHttpDeps.fetchImpl`). */
|
|
295
|
+
function transportOptions(deps) {
|
|
296
|
+
return {
|
|
297
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
298
|
+
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** `tools` cast to the turn-runner's own tool-definition type, forwarded only when present (the parser never inspects tool contents — see `parseCommon`). */
|
|
302
|
+
function toolsOption(tools) {
|
|
303
|
+
return tools !== undefined ? { tools: tools } : {};
|
|
349
304
|
}
|
|
350
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* Builds the `provider name -> {parse, run}` table that both the five fixed routes and the generic
|
|
307
|
+
* catch-all dispatch through. Each entry's `run` closure captures `deps` for the optional
|
|
308
|
+
* `executeTool` wiring and the transport test seams. `executeTool` is a fresh wrapper that calls
|
|
309
|
+
* `deps.<provider>ExecuteTool` as a method at call time, so the host function sees exactly one
|
|
310
|
+
* argument.
|
|
311
|
+
*/
|
|
351
312
|
function buildProxyProviderRegistry(deps) {
|
|
352
313
|
return {
|
|
353
|
-
anthropic: {
|
|
314
|
+
anthropic: defineProvider({
|
|
354
315
|
parse: parseAnthropicProxyRequest,
|
|
355
|
-
run: (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
...(parsed
|
|
366
|
-
...(parsed.
|
|
367
|
-
...(
|
|
368
|
-
...(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
? { executeTool: (call) => deps.anthropicExecuteTool(call) }
|
|
373
|
-
: {})
|
|
374
|
-
})),
|
|
375
|
-
},
|
|
376
|
-
openai: {
|
|
377
|
-
parse: parseOpenAiProxyRequest,
|
|
378
|
-
run: ((parsed, onEvent) => runOpenAiToolTurn({
|
|
379
|
-
apiKey: parsed.apiKey,
|
|
380
|
-
model: parsed.model,
|
|
381
|
-
messages: parsed.messages,
|
|
382
|
-
onEvent
|
|
383
|
-
}, {
|
|
384
|
-
...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
|
|
385
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
386
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
387
|
-
...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
|
|
388
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
389
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
390
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
391
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
392
|
-
...(deps.openaiExecuteTool
|
|
393
|
-
? { executeTool: (call) => deps.openaiExecuteTool(call) }
|
|
394
|
-
: {})
|
|
395
|
-
})),
|
|
396
|
-
},
|
|
397
|
-
azure: {
|
|
316
|
+
run: (parsed, onEvent) => runAnthropicToolTurn({ apiKey: parsed.apiKey, model: parsed.model, maxTokens: parsed.maxTokens, messages: parsed.messages, onEvent }, {
|
|
317
|
+
...pickDefined(parsed, ['baseUrl', 'apiVersion', 'system', 'temperature', 'maxToolTurns', 'extraHeaders']),
|
|
318
|
+
...toolsOption(parsed.tools),
|
|
319
|
+
...transportOptions(deps),
|
|
320
|
+
...(deps.anthropicExecuteTool ? { executeTool: (call) => deps.anthropicExecuteTool(call) } : {}),
|
|
321
|
+
}),
|
|
322
|
+
}),
|
|
323
|
+
openai: defineProvider({
|
|
324
|
+
parse: parseToolsProxyRequest,
|
|
325
|
+
run: (parsed, onEvent) => runOpenAiToolTurn({ apiKey: parsed.apiKey, model: parsed.model, messages: parsed.messages, onEvent }, {
|
|
326
|
+
...pickDefined(parsed, ['baseUrl', 'temperature', 'maxTokens', 'maxToolTurns', 'extraHeaders']),
|
|
327
|
+
...toolsOption(parsed.tools),
|
|
328
|
+
...transportOptions(deps),
|
|
329
|
+
...(deps.openaiExecuteTool ? { executeTool: (call) => deps.openaiExecuteTool(call) } : {}),
|
|
330
|
+
}),
|
|
331
|
+
}),
|
|
332
|
+
azure: defineProvider({
|
|
398
333
|
parse: parseAzureProxyRequest,
|
|
399
|
-
run: (
|
|
400
|
-
apiKey: parsed.apiKey,
|
|
401
|
-
baseUrl: parsed.baseUrl,
|
|
402
|
-
model: parsed.model,
|
|
403
|
-
messages: parsed.messages,
|
|
404
|
-
onEvent
|
|
405
|
-
}, {
|
|
334
|
+
run: (parsed, onEvent) => runAzureToolTurn({ apiKey: parsed.apiKey, baseUrl: parsed.baseUrl, model: parsed.model, messages: parsed.messages, onEvent }, {
|
|
406
335
|
apiVersion: parsed.apiVersion,
|
|
407
|
-
...(parsed
|
|
408
|
-
...(parsed.
|
|
409
|
-
...(
|
|
410
|
-
...(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
...(deps.azureExecuteTool
|
|
415
|
-
? { executeTool: (call) => deps.azureExecuteTool(call) }
|
|
416
|
-
: {})
|
|
417
|
-
})),
|
|
418
|
-
},
|
|
419
|
-
google: {
|
|
336
|
+
...pickDefined(parsed, ['temperature', 'maxTokens', 'maxToolTurns', 'extraHeaders']),
|
|
337
|
+
...toolsOption(parsed.tools),
|
|
338
|
+
...transportOptions(deps),
|
|
339
|
+
...(deps.azureExecuteTool ? { executeTool: (call) => deps.azureExecuteTool(call) } : {}),
|
|
340
|
+
}),
|
|
341
|
+
}),
|
|
342
|
+
google: defineProvider({
|
|
420
343
|
parse: parseGoogleProxyRequest,
|
|
421
|
-
run: (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
},
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
...(parsed
|
|
432
|
-
...(parsed.
|
|
433
|
-
...(
|
|
434
|
-
...(deps.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
? { executeTool: (call) => deps.googleExecuteTool(call) }
|
|
438
|
-
: {})
|
|
439
|
-
})),
|
|
440
|
-
},
|
|
441
|
-
ollama: {
|
|
442
|
-
parse: parseOllamaProxyRequest,
|
|
443
|
-
run: ((parsed, onEvent) => runOllamaToolTurn({
|
|
444
|
-
apiKey: parsed.apiKey,
|
|
445
|
-
model: parsed.model,
|
|
446
|
-
messages: parsed.messages,
|
|
447
|
-
onEvent
|
|
448
|
-
}, {
|
|
449
|
-
...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
|
|
450
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
451
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
452
|
-
...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
|
|
453
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
454
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
455
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
456
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
457
|
-
...(deps.ollamaExecuteTool
|
|
458
|
-
? { executeTool: (call) => deps.ollamaExecuteTool(call) }
|
|
459
|
-
: {})
|
|
460
|
-
})),
|
|
461
|
-
},
|
|
344
|
+
run: (parsed, onEvent) => runGoogleToolTurn({ apiKey: parsed.apiKey, model: parsed.model, contents: parsed.messages, onEvent }, {
|
|
345
|
+
...pickDefined(parsed, ['baseUrl', 'system', 'temperature', 'maxOutputTokens', 'maxToolTurns', 'extraHeaders']),
|
|
346
|
+
...toolsOption(parsed.tools),
|
|
347
|
+
...transportOptions(deps),
|
|
348
|
+
...(deps.googleExecuteTool ? { executeTool: (call) => deps.googleExecuteTool(call) } : {}),
|
|
349
|
+
}),
|
|
350
|
+
}),
|
|
351
|
+
ollama: defineProvider({
|
|
352
|
+
parse: parseToolsProxyRequest,
|
|
353
|
+
run: (parsed, onEvent) => runOllamaToolTurn({ apiKey: parsed.apiKey, model: parsed.model, messages: parsed.messages, onEvent }, {
|
|
354
|
+
...pickDefined(parsed, ['baseUrl', 'temperature', 'maxTokens', 'maxToolTurns', 'extraHeaders']),
|
|
355
|
+
...toolsOption(parsed.tools),
|
|
356
|
+
...transportOptions(deps),
|
|
357
|
+
...(deps.ollamaExecuteTool ? { executeTool: (call) => deps.ollamaExecuteTool(call) } : {}),
|
|
358
|
+
}),
|
|
359
|
+
}),
|
|
462
360
|
};
|
|
463
361
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
'azure',
|
|
468
|
-
'google',
|
|
469
|
-
'ollama',
|
|
470
|
-
]);
|
|
362
|
+
/** Fixed-route registration order; load-bearing, since Express matches in registration order and the catch-all must come after all five. */
|
|
363
|
+
const PROXY_PROVIDERS = ['anthropic', 'openai', 'azure', 'google', 'ollama'];
|
|
364
|
+
const RECOGNIZED_PROVIDERS = new Set(PROXY_PROVIDERS);
|
|
471
365
|
function isRecognizedProvider(value) {
|
|
472
366
|
return RECOGNIZED_PROVIDERS.has(value);
|
|
473
367
|
}
|
|
368
|
+
/** Reports a throw from the catch-all before `runProxyStream` took over. */
|
|
369
|
+
function reportCatchAllError(providerParam, onInternalError, error) {
|
|
370
|
+
const correlationId = randomUUID();
|
|
371
|
+
// `onInternalError`'s contract only accepts the 5 recognized provider literals; a throw
|
|
372
|
+
// this early (e.g. `guardSameOrigin`) may have happened before the provider name was even
|
|
373
|
+
// validated, so an unrecognized/empty `providerParam` logs directly instead of lying to
|
|
374
|
+
// that typed contract with a provider that was never actually confirmed.
|
|
375
|
+
if (isRecognizedProvider(providerParam)) {
|
|
376
|
+
onInternalError({ provider: providerParam, correlationId, error });
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// eslint-disable-next-line no-console
|
|
380
|
+
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, error);
|
|
381
|
+
}
|
|
382
|
+
return correlationId;
|
|
383
|
+
}
|
|
474
384
|
/**
|
|
475
385
|
* Mounts `POST /api/proxy/:provider/stream` — see module doc's "the `:provider` catch-all's
|
|
476
386
|
* relationship to the five fixed routes" section for why an unrecognized-but-registry-present
|
|
477
387
|
* provider name is the only branch of this handler real HTTP traffic can reach for the five known
|
|
478
388
|
* provider ids.
|
|
479
389
|
*/
|
|
480
|
-
function registerGenericProxyStreamRoute(app, adapter, onInternalError, registry) {
|
|
390
|
+
function registerGenericProxyStreamRoute({ app, adapter, onInternalError, registry }) {
|
|
481
391
|
app.post('/api/proxy/:provider/stream', async (req, res) => {
|
|
482
392
|
// Unlike the five fixed routes, this catch-all's own `guardSameOrigin` call sits ahead of
|
|
483
393
|
// `runProxyStream` (the registry lookup below has to happen first, to even know which
|
|
@@ -498,145 +408,33 @@ function registerGenericProxyStreamRoute(app, adapter, onInternalError, registry
|
|
|
498
408
|
sendApiError({ res, status: 403, error: origin.error });
|
|
499
409
|
return;
|
|
500
410
|
}
|
|
411
|
+
// Both checks are needed: `registry` is a plain object, so an inherited key such as
|
|
412
|
+
// `constructor` yields a truthy `entry` that only the recognized-provider set rejects.
|
|
501
413
|
const entry = registry[providerParam];
|
|
502
414
|
if (!entry || !isRecognizedProvider(providerParam)) {
|
|
503
415
|
sendApiError({ res, status: 400, error: createApiError({ code: 'BAD_REQUEST', message: `unknown provider: ${providerParam}` }) });
|
|
504
416
|
return;
|
|
505
417
|
}
|
|
506
|
-
await runProxyStream(req, res, adapter, providerParam, onInternalError, entry
|
|
418
|
+
await runProxyStream({ req, res, adapter, provider: providerParam, onInternalError, entry });
|
|
507
419
|
}
|
|
508
420
|
catch (error) {
|
|
509
421
|
// No SSE channel can exist yet at this point — `runProxyStream` owns opening one, and this
|
|
510
422
|
// catch only reaches errors thrown before that call. A plain JSON error response is always
|
|
511
423
|
// still possible, matching `runProxyStream`'s own "channel never opened" branch.
|
|
512
|
-
|
|
513
|
-
// `onInternalError`'s contract only accepts the 5 recognized provider literals; a throw
|
|
514
|
-
// this early (e.g. `guardSameOrigin`) may have happened before the provider name was even
|
|
515
|
-
// validated, so an unrecognized/empty `providerParam` logs directly instead of lying to
|
|
516
|
-
// that typed contract with a provider that was never actually confirmed.
|
|
517
|
-
if (isRecognizedProvider(providerParam)) {
|
|
518
|
-
onInternalError({ provider: providerParam, correlationId, error });
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
// eslint-disable-next-line no-console
|
|
522
|
-
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, error);
|
|
523
|
-
}
|
|
524
|
-
if (!res.headersSent) {
|
|
525
|
-
sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
|
|
526
|
-
}
|
|
527
|
-
else if (!res.writableEnded) {
|
|
528
|
-
res.end();
|
|
529
|
-
}
|
|
424
|
+
respondInternalError(res, reportCatchAllError(providerParam, onInternalError, error));
|
|
530
425
|
}
|
|
531
426
|
});
|
|
532
427
|
}
|
|
533
428
|
/** Mounts `POST /api/proxy/{anthropic,openai,azure,google,ollama}/stream` and the generic `POST /api/proxy/:provider/stream` catch-all. */
|
|
534
429
|
export function registerModelProxyRoutes({ app, deps, adapter }, _optional = {}) {
|
|
535
430
|
const onInternalError = deps.onInternalError ?? defaultInternalErrorSink;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
...(parsed.apiVersion !== undefined ? { apiVersion: parsed.apiVersion } : {}),
|
|
545
|
-
...(parsed.system !== undefined ? { system: parsed.system } : {}),
|
|
546
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
547
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
548
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
549
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
550
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
551
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
552
|
-
...(deps.anthropicExecuteTool
|
|
553
|
-
? {
|
|
554
|
-
executeTool: (call) => deps.anthropicExecuteTool(call),
|
|
555
|
-
}
|
|
556
|
-
: {})
|
|
557
|
-
}));
|
|
558
|
-
registerProxyStreamRoute(app, '/api/proxy/openai/stream', adapter, 'openai', onInternalError, parseOpenAiProxyRequest, (parsed, onEvent) => runOpenAiToolTurn({
|
|
559
|
-
apiKey: parsed.apiKey,
|
|
560
|
-
model: parsed.model,
|
|
561
|
-
messages: parsed.messages,
|
|
562
|
-
onEvent
|
|
563
|
-
}, {
|
|
564
|
-
...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
|
|
565
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
566
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
567
|
-
...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
|
|
568
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
569
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
570
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
571
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
572
|
-
...(deps.openaiExecuteTool
|
|
573
|
-
? {
|
|
574
|
-
executeTool: (call) => deps.openaiExecuteTool(call),
|
|
575
|
-
}
|
|
576
|
-
: {})
|
|
577
|
-
}));
|
|
578
|
-
registerProxyStreamRoute(app, '/api/proxy/azure/stream', adapter, 'azure', onInternalError, parseAzureProxyRequest, (parsed, onEvent) => runAzureToolTurn({
|
|
579
|
-
apiKey: parsed.apiKey,
|
|
580
|
-
baseUrl: parsed.baseUrl,
|
|
581
|
-
model: parsed.model,
|
|
582
|
-
messages: parsed.messages,
|
|
583
|
-
onEvent
|
|
584
|
-
}, {
|
|
585
|
-
apiVersion: parsed.apiVersion,
|
|
586
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
587
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
588
|
-
...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
|
|
589
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
590
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
591
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
592
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
593
|
-
...(deps.azureExecuteTool
|
|
594
|
-
? {
|
|
595
|
-
executeTool: (call) => deps.azureExecuteTool(call),
|
|
596
|
-
}
|
|
597
|
-
: {})
|
|
598
|
-
}));
|
|
599
|
-
registerProxyStreamRoute(app, '/api/proxy/google/stream', adapter, 'google', onInternalError, parseGoogleProxyRequest, (parsed, onEvent) => runGoogleToolTurn({
|
|
600
|
-
apiKey: parsed.apiKey,
|
|
601
|
-
model: parsed.model,
|
|
602
|
-
contents: parsed.messages,
|
|
603
|
-
onEvent
|
|
604
|
-
}, {
|
|
605
|
-
...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
|
|
606
|
-
...(parsed.system !== undefined ? { system: parsed.system } : {}),
|
|
607
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
608
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
609
|
-
...(parsed.maxOutputTokens !== undefined ? { maxOutputTokens: parsed.maxOutputTokens } : {}),
|
|
610
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
611
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
612
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
613
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
614
|
-
...(deps.googleExecuteTool
|
|
615
|
-
? {
|
|
616
|
-
executeTool: (call) => deps.googleExecuteTool(call),
|
|
617
|
-
}
|
|
618
|
-
: {})
|
|
619
|
-
}));
|
|
620
|
-
registerProxyStreamRoute(app, '/api/proxy/ollama/stream', adapter, 'ollama', onInternalError, parseOllamaProxyRequest, (parsed, onEvent) => runOllamaToolTurn({
|
|
621
|
-
apiKey: parsed.apiKey,
|
|
622
|
-
model: parsed.model,
|
|
623
|
-
messages: parsed.messages,
|
|
624
|
-
onEvent
|
|
625
|
-
}, {
|
|
626
|
-
...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
|
|
627
|
-
...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
|
|
628
|
-
...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
|
|
629
|
-
...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
|
|
630
|
-
...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
|
|
631
|
-
...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
|
|
632
|
-
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
633
|
-
...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
|
|
634
|
-
...(deps.ollamaExecuteTool
|
|
635
|
-
? {
|
|
636
|
-
executeTool: (call) => deps.ollamaExecuteTool(call),
|
|
637
|
-
}
|
|
638
|
-
: {})
|
|
639
|
-
}));
|
|
640
|
-
registerGenericProxyStreamRoute(app, adapter, onInternalError, buildProxyProviderRegistry(deps));
|
|
431
|
+
const registry = buildProxyProviderRegistry(deps);
|
|
432
|
+
// Each fixed route is a thin wrapper over `runProxyStream` with `provider` fixed, not read from
|
|
433
|
+
// `req.params`.
|
|
434
|
+
for (const provider of PROXY_PROVIDERS) {
|
|
435
|
+
const entry = registry[provider];
|
|
436
|
+
app.post(`/api/proxy/${provider}/stream`, (req, res) => runProxyStream({ req, res, adapter, provider, onInternalError, entry }));
|
|
437
|
+
}
|
|
438
|
+
registerGenericProxyStreamRoute({ app, adapter, onInternalError, registry });
|
|
641
439
|
}
|
|
642
440
|
//# sourceMappingURL=model-proxy.js.map
|