@jini-ai/daemon 0.6.8 → 0.6.10

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 (79) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
  3. package/dist/agent-executor/antigravity-permissions.js +64 -0
  4. package/dist/agent-executor/contracts.d.ts +37 -6
  5. package/dist/agent-executor/event-translation.d.ts +6 -1
  6. package/dist/agent-executor/event-translation.js +3 -1
  7. package/dist/agent-executor/events.d.ts +5 -0
  8. package/dist/agent-executor/events.js +143 -87
  9. package/dist/agent-executor/launch.d.ts +5 -3
  10. package/dist/agent-executor/launch.js +315 -217
  11. package/dist/agent-executor/mcp-bridge-probe.js +51 -35
  12. package/dist/agent-executor/mcp-bridge.d.ts +18 -8
  13. package/dist/agent-executor/mcp-bridge.js +105 -65
  14. package/dist/agent-executor/run-output.d.ts +20 -0
  15. package/dist/agent-executor/run-output.js +80 -0
  16. package/dist/attachment-content.js +18 -15
  17. package/dist/core/read-only-tools.d.ts +15 -0
  18. package/dist/core/read-only-tools.js +29 -1
  19. package/dist/core/tool-audit.d.ts +3 -1
  20. package/dist/core/tool-audit.js +35 -22
  21. package/dist/core/tool-gaps.d.ts +60 -0
  22. package/dist/core/tool-gaps.js +199 -0
  23. package/dist/delegated-result-correlation.d.ts +26 -0
  24. package/dist/delegated-result-correlation.js +68 -0
  25. package/dist/delegated-tool-bridge.d.ts +6 -14
  26. package/dist/delegated-tool-bridge.js +155 -155
  27. package/dist/event-log.d.ts +45 -2
  28. package/dist/event-log.js +83 -4
  29. package/dist/http/attachments.d.ts +26 -1
  30. package/dist/http/attachments.js +129 -65
  31. package/dist/http/connectors.d.ts +1 -32
  32. package/dist/http/connectors.js +13 -68
  33. package/dist/http/delegated-tools.d.ts +8 -1
  34. package/dist/http/delegated-tools.js +30 -30
  35. package/dist/http/host-tools.d.ts +1 -0
  36. package/dist/http/host-tools.js +64 -59
  37. package/dist/http/index.d.ts +2 -2
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/media.js +48 -69
  40. package/dist/http/memory.js +45 -36
  41. package/dist/http/model-proxy-fields.d.ts +23 -0
  42. package/dist/http/model-proxy-fields.js +29 -0
  43. package/dist/http/model-proxy.js +193 -395
  44. package/dist/http/research-tavily.adapter.js +72 -49
  45. package/dist/http/research.js +33 -23
  46. package/dist/http/resource-request.d.ts +37 -0
  47. package/dist/http/resource-request.js +59 -0
  48. package/dist/http/routines.js +49 -90
  49. package/dist/http/runs.d.ts +7 -5
  50. package/dist/http/runs.js +21 -7
  51. package/dist/http/terminals.js +23 -48
  52. package/dist/http/tool-catalog.d.ts +5 -0
  53. package/dist/http/xai.d.ts +11 -1
  54. package/dist/http/xai.js +125 -124
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +1 -0
  57. package/dist/legacy-data-migration.js +41 -24
  58. package/dist/run/core/retry.js +51 -35
  59. package/dist/run/diagnostics/diagnostics.js +5 -17
  60. package/dist/run-credentials.js +49 -27
  61. package/dist/run-lifecycle.d.ts +31 -2
  62. package/dist/run-lifecycle.js +49 -42
  63. package/dist/store/agent-sessions/sqlite.d.ts +6 -0
  64. package/dist/store/agent-sessions/sqlite.js +6 -4
  65. package/dist/surface-exchanges.js +2 -1
  66. package/dist/terminal-session.d.ts +1 -1
  67. package/dist/terminal-session.js +26 -33
  68. package/dist/tool-audit.d.ts +1 -0
  69. package/dist/tool-audit.js +1 -0
  70. package/dist/tool-executor.d.ts +24 -3
  71. package/dist/tool-executor.js +276 -139
  72. package/dist/tool-recovery/recovery.js +64 -35
  73. package/dist/tool-recovery/stack.d.ts +2 -1
  74. package/dist/tool-recovery/stack.js +1 -1
  75. package/dist/tool-result-diagnostics.d.ts +27 -0
  76. package/dist/tool-result-diagnostics.js +77 -0
  77. package/dist/tool-result-media.d.ts +27 -14
  78. package/dist/tool-result-media.js +51 -38
  79. package/package.json +14 -14
@@ -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
- function isRecord(value) {
97
- return typeof value === 'object' && value !== null && !Array.isArray(value);
98
- }
99
- function nonEmptyString(value) {
100
- return typeof value === 'string' && value.trim().length > 0 ? value : null;
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 apiKey = nonEmptyString(body.apiKey);
127
- if (!apiKey) {
128
- return err({ error: validationError({ message: 'apiKey must be a non-empty string' }, { issues: [{ path: 'apiKey', message: 'required non-empty string' }] }) });
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.baseUrl !== undefined ? { baseUrl: body.baseUrl } : {}),
158
- ...(body.temperature !== undefined ? { temperature: body.temperature } : {}),
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 (typeof record.maxTokens !== 'number' || !Number.isFinite(record.maxTokens) || record.maxTokens <= 0) {
171
- return err({ error: validationError({ message: 'maxTokens must be a positive number' }, { issues: [{ path: 'maxTokens', message: 'required positive number' }] }) });
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
- if (record.tools !== undefined && !Array.isArray(record.tools)) {
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 parseOpenAiProxyRequest(body) {
180
+ function parseToolsProxyRequest(body) {
193
181
  const common = parseCommon(body);
194
182
  if (!common.ok)
195
183
  return common;
196
- const record = body;
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
- if (!common.value.baseUrl) {
213
- return err({ error: validationError({ message: 'baseUrl must be a non-empty string' }, { issues: [{ path: 'baseUrl', message: 'required non-empty string' }] }) });
214
- }
215
- if (typeof record.apiVersion !== 'string' || record.apiVersion.trim().length === 0) {
216
- return err({ error: validationError({ message: 'apiVersion must be a non-empty string' }, { issues: [{ path: 'apiVersion', message: 'required non-empty string' }] }) });
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
- if (record.tools !== undefined && !Array.isArray(record.tools)) {
219
- return err({ error: validationError({ message: 'tools must be an array when provided' }) });
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 ok({
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
- const record = body;
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 — extracted into its own function (rather than living
278
- * inline inside `registerProxyStreamRoute`'s `app.post` callback, as it did before this pass) so
279
- * the generic catch-all route below can invoke the identical logic with a `provider` value read
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, parse, run) {
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
- channel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
301
- channel.open();
302
- await run(parsed.value, (event) => channel.enqueue({ event: toStreamEvent(seq++, event) }));
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 (`registerProxyStreamRoute`'s fixed routes, `registerGenericProxyStreamRoute`'s
312
- // catch-all) awaits or `.catch()`s the promise this function returns — exactly how Express
313
- // itself invokes every handler. `guardSameOrigin`/`parse` are Result-returning by contract and
314
- // don't throw today, but nothing enforces that; if either ever did, the throw used to become
315
- // an unhandled rejection with no process-level guard anywhere in this package's path, which is
316
- // Node's documented crash-the-process default. See the paired regression test in
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
- sendApiError({ res, status: 500, error: createApiError({ code: 'INTERNAL_ERROR', message: 'an internal error occurred' }, { requestId: correlationId }) });
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
- /** Registers one fixed-path provider route (`registerModelProxyRoutes`'s five calls below). Thin wrapper over {@link runProxyStream} — `provider` is a fixed string, not read from `req.params`. */
347
- function registerProxyStreamRoute(app, path, adapter, provider, onInternalError, parse, run) {
348
- app.post(path, (req, res) => runProxyStream(req, res, adapter, provider, onInternalError, parse, run));
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
- /** Builds the `provider name -> {parse, run}` table the generic catch-all dispatches through. Each entry's `run` closure captures `deps` for the same optional `executeTool` wiring the five fixed routes use. */
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: ((parsed, onEvent) => runAnthropicToolTurn({
356
- apiKey: parsed.apiKey,
357
- model: parsed.model,
358
- maxTokens: parsed.maxTokens,
359
- messages: parsed.messages,
360
- onEvent
361
- }, {
362
- ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
363
- ...(parsed.apiVersion !== undefined ? { apiVersion: parsed.apiVersion } : {}),
364
- ...(parsed.system !== undefined ? { system: parsed.system } : {}),
365
- ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
366
- ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
367
- ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
368
- ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
369
- ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
370
- ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
371
- ...(deps.anthropicExecuteTool
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: ((parsed, onEvent) => runAzureToolTurn({
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.tools !== undefined ? { tools: parsed.tools } : {}),
408
- ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
409
- ...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
410
- ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
411
- ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
412
- ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
413
- ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
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: ((parsed, onEvent) => runGoogleToolTurn({
422
- apiKey: parsed.apiKey,
423
- model: parsed.model,
424
- contents: parsed.messages,
425
- onEvent
426
- }, {
427
- ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
428
- ...(parsed.system !== undefined ? { system: parsed.system } : {}),
429
- ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
430
- ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
431
- ...(parsed.maxOutputTokens !== undefined ? { maxOutputTokens: parsed.maxOutputTokens } : {}),
432
- ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
433
- ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
434
- ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
435
- ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
436
- ...(deps.googleExecuteTool
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
- const RECOGNIZED_PROVIDERS = new Set([
465
- 'anthropic',
466
- 'openai',
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.parse, entry.run);
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
- const correlationId = randomUUID();
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
- registerProxyStreamRoute(app, '/api/proxy/anthropic/stream', adapter, 'anthropic', onInternalError, parseAnthropicProxyRequest, (parsed, onEvent) => runAnthropicToolTurn({
537
- apiKey: parsed.apiKey,
538
- model: parsed.model,
539
- maxTokens: parsed.maxTokens,
540
- messages: parsed.messages,
541
- onEvent
542
- }, {
543
- ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
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