@jini-ai/daemon 0.3.7 → 0.5.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.
Files changed (159) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +50 -8
  3. package/dist/agent-executor/contracts.d.ts +736 -0
  4. package/dist/agent-executor/contracts.js +28 -0
  5. package/dist/agent-executor/event-translation.d.ts +87 -0
  6. package/dist/agent-executor/event-translation.js +230 -0
  7. package/dist/agent-executor/events.d.ts +190 -0
  8. package/dist/agent-executor/events.js +848 -0
  9. package/dist/agent-executor/handoff.d.ts +18 -0
  10. package/dist/agent-executor/handoff.js +16 -0
  11. package/dist/agent-executor/index.d.ts +98 -0
  12. package/dist/agent-executor/index.js +98 -0
  13. package/dist/agent-executor/launch.d.ts +375 -0
  14. package/dist/agent-executor/launch.js +1001 -0
  15. package/dist/agent-executor/mcp-bridge.d.ts +338 -0
  16. package/dist/agent-executor/mcp-bridge.js +835 -0
  17. package/dist/agent-executor/values.d.ts +5 -0
  18. package/dist/agent-executor/values.js +13 -0
  19. package/dist/agent-executor.d.ts +382 -177
  20. package/dist/agent-executor.js +408 -344
  21. package/dist/close-status.d.ts +11 -8
  22. package/dist/close-status.js +31 -19
  23. package/dist/continuation/continuation-transport.d.ts +3 -1
  24. package/dist/continuation/continuation-transport.js +1 -1
  25. package/dist/continuation/journal.d.ts +12 -5
  26. package/dist/continuation/journal.js +4 -4
  27. package/dist/continuation/run-scoped-context-store.d.ts +13 -4
  28. package/dist/continuation/run-scoped-context-store.js +6 -6
  29. package/dist/continuation/run-start-handler.d.ts +1 -1
  30. package/dist/continuation/run-start-handler.js +3 -16
  31. package/dist/delegated-tool-bridge.d.ts +7 -3
  32. package/dist/delegated-tool-bridge.js +32 -31
  33. package/dist/event-log.d.ts +7 -5
  34. package/dist/event-log.js +12 -11
  35. package/dist/frontend-capability-tools.d.ts +1 -1
  36. package/dist/frontend-capability-tools.js +3 -2
  37. package/dist/frontend-session-registry.d.ts +32 -9
  38. package/dist/frontend-session-registry.js +12 -11
  39. package/dist/http/active-context.d.ts +105 -0
  40. package/dist/http/active-context.js +67 -0
  41. package/dist/http/agents.d.ts +68 -0
  42. package/dist/http/agents.js +12 -0
  43. package/dist/http/attachments.d.ts +618 -0
  44. package/dist/http/attachments.js +1034 -0
  45. package/dist/http/cancel-owned-runs.d.ts +49 -0
  46. package/dist/http/cancel-owned-runs.js +35 -0
  47. package/dist/http/component-catalog.d.ts +84 -0
  48. package/dist/http/component-catalog.js +64 -0
  49. package/dist/http/connectors.d.ts +209 -0
  50. package/dist/http/connectors.js +409 -0
  51. package/dist/http/daemon-status.d.ts +76 -0
  52. package/dist/http/daemon-status.js +55 -0
  53. package/dist/http/db-ops.d.ts +123 -0
  54. package/dist/http/db-ops.js +189 -0
  55. package/dist/http/delegated-tools.d.ts +113 -0
  56. package/dist/http/delegated-tools.js +230 -0
  57. package/dist/http/frontend-control.d.ts +136 -0
  58. package/dist/http/frontend-control.js +79 -0
  59. package/dist/http/frontend-sessions.d.ts +110 -0
  60. package/dist/http/frontend-sessions.js +180 -0
  61. package/dist/http/health.d.ts +95 -0
  62. package/dist/http/health.js +40 -0
  63. package/dist/http/host-tools.d.ts +214 -0
  64. package/dist/http/host-tools.js +329 -0
  65. package/dist/http/index.d.ts +52 -0
  66. package/dist/http/index.js +27 -0
  67. package/dist/http/media.d.ts +146 -0
  68. package/dist/http/media.js +346 -0
  69. package/dist/http/memory.d.ts +265 -0
  70. package/dist/http/memory.js +328 -0
  71. package/dist/http/model-proxy.d.ts +49 -0
  72. package/dist/http/model-proxy.js +642 -0
  73. package/dist/http/remote-run-events.d.ts +70 -0
  74. package/dist/http/remote-run-events.js +224 -0
  75. package/dist/http/research.d.ts +48 -0
  76. package/dist/http/research.js +315 -0
  77. package/dist/http/route-manifest.d.ts +35 -0
  78. package/dist/http/route-manifest.js +103 -0
  79. package/dist/http/routines.d.ts +120 -0
  80. package/dist/http/routines.js +298 -0
  81. package/dist/http/run-credentials.d.ts +73 -0
  82. package/dist/http/run-credentials.js +71 -0
  83. package/dist/http/runs.d.ts +91 -0
  84. package/dist/http/runs.js +221 -0
  85. package/dist/http/terminals.d.ts +138 -0
  86. package/dist/http/terminals.js +325 -0
  87. package/dist/http/tool-catalog.d.ts +78 -0
  88. package/dist/http/tool-catalog.js +66 -0
  89. package/dist/http/workspace-root.d.ts +77 -0
  90. package/dist/http/workspace-root.js +63 -0
  91. package/dist/http/xai.d.ts +90 -0
  92. package/dist/http/xai.js +655 -0
  93. package/dist/image-prompt-delivery.d.ts +13 -3
  94. package/dist/image-prompt-delivery.js +5 -5
  95. package/dist/index.d.ts +556 -4
  96. package/dist/index.js +556 -4
  97. package/dist/legacy-data-migration.d.ts +59 -8
  98. package/dist/legacy-data-migration.js +38 -33
  99. package/dist/read-only-tools.d.ts +130 -0
  100. package/dist/read-only-tools.js +115 -0
  101. package/dist/remote-tool-bridge.d.ts +11 -5
  102. package/dist/remote-tool-bridge.js +15 -15
  103. package/dist/routines/index.d.ts +1 -1
  104. package/dist/routines/index.js +1 -1
  105. package/dist/routines/routine-store.d.ts +34 -12
  106. package/dist/routines/routine-store.js +21 -20
  107. package/dist/routines/schedule.d.ts +48 -10
  108. package/dist/routines/schedule.js +21 -21
  109. package/dist/routines/scheduler.d.ts +32 -11
  110. package/dist/routines/scheduler.js +61 -57
  111. package/dist/routines/types.d.ts +10 -3
  112. package/dist/run/core/result.d.ts +6 -2
  113. package/dist/run/core/result.js +3 -3
  114. package/dist/run/core/retry.d.ts +17 -4
  115. package/dist/run/core/retry.js +7 -11
  116. package/dist/run/diagnostics/diagnostics.d.ts +14 -4
  117. package/dist/run/diagnostics/diagnostics.js +7 -7
  118. package/dist/run/index.d.ts +1 -1
  119. package/dist/run/index.js +1 -1
  120. package/dist/run-credentials.d.ts +138 -0
  121. package/dist/run-credentials.js +152 -0
  122. package/dist/run-lifecycle.d.ts +92 -33
  123. package/dist/run-lifecycle.js +86 -73
  124. package/dist/scheduler.d.ts +8 -0
  125. package/dist/scheduler.js +2 -0
  126. package/dist/session-coordination.d.ts +115 -0
  127. package/dist/session-coordination.js +111 -0
  128. package/dist/store/agent-sessions/errors.d.ts +8 -0
  129. package/dist/store/agent-sessions/errors.js +10 -0
  130. package/dist/store/agent-sessions/index.d.ts +12 -0
  131. package/dist/store/agent-sessions/index.js +19 -0
  132. package/dist/store/agent-sessions/pglite.d.ts +12 -0
  133. package/dist/store/agent-sessions/pglite.js +7 -0
  134. package/dist/store/agent-sessions/ports.d.ts +24 -0
  135. package/dist/store/agent-sessions/ports.js +2 -0
  136. package/dist/store/agent-sessions/postgres.d.ts +12 -0
  137. package/dist/store/agent-sessions/postgres.js +7 -0
  138. package/dist/store/agent-sessions/schema.d.ts +3 -0
  139. package/dist/store/agent-sessions/schema.js +23 -0
  140. package/dist/store/agent-sessions/sql.d.ts +35 -0
  141. package/dist/store/agent-sessions/sql.js +59 -0
  142. package/dist/store/agent-sessions/sqlite.d.ts +117 -0
  143. package/dist/store/agent-sessions/sqlite.js +128 -0
  144. package/dist/store/event-log/sqlite.d.ts +75 -0
  145. package/dist/store/event-log/sqlite.js +230 -0
  146. package/dist/surface-exchanges.d.ts +97 -0
  147. package/dist/surface-exchanges.js +153 -0
  148. package/dist/terminal-session.d.ts +43 -11
  149. package/dist/terminal-session.js +22 -20
  150. package/dist/tokens.js +4 -4
  151. package/dist/tool-audit.d.ts +114 -0
  152. package/dist/tool-audit.js +95 -0
  153. package/dist/tool-executor.d.ts +22 -7
  154. package/dist/tool-executor.js +16 -16
  155. package/dist/tool-result-media.d.ts +3 -1
  156. package/dist/tool-result-media.js +1 -1
  157. package/dist/tool-result-surfaces.d.ts +3 -1
  158. package/dist/tool-result-surfaces.js +1 -1
  159. package/package.json +127 -9
@@ -0,0 +1,642 @@
1
+ /**
2
+ * @module model-proxy
3
+ *
4
+ * `POST /api/proxy/{anthropic,openai,azure,google,ollama}/stream`, plus a
5
+ * generic `POST /api/proxy/:provider/stream` catch-all — thin SSE route
6
+ * wrappers over `@jini-ai/agent-runtime`'s `run{Anthropic,OpenAi,Azure,Google,
7
+ * Ollama}ToolTurn` wire-adapter/turn-runners. Implements the placement
8
+ * decision in `ADS-memory/reports/proposals/
9
+ * PROP-http-route-packs-chat-model-proxy-2026-07-21.md`: OD's
10
+ * `apps/daemon/src/routes/chat.ts` (2267 lines) genuinely is the largest
11
+ * reusable route pack this package's routes-classification table ever
12
+ * found, but its provider-specific wire-protocol knowledge (Anthropic
13
+ * Messages API / OpenAI Chat Completions SSE parsing, tool-call-fragment
14
+ * accumulation, the role-marker-guard contamination loop) does not belong
15
+ * in generic HTTP transport. The provider-specific implementation stays in
16
+ * the runtime, following the proposal to extend `@jini-ai/agent-runtime`'s existing `providers/` pattern instead.
17
+ * This module is the `@jini-ai/daemon/http` transport half of that split: request parsing,
18
+ * same-origin enforcement, and SSE transport (`sse.ts`, the same primitive
19
+ * `runs.ts`/`memory.ts` already consume) — no knowledge of what an
20
+ * Anthropic `content_block_delta`, an OpenAI `tool_calls[].function.
21
+ * arguments` fragment, or a Gemini `candidates[0].content.parts[]` entry
22
+ * looks like lives here.
23
+ *
24
+ * **2026-07-21 pass**: Anthropic and OpenAI only, per the proposal's own
25
+ * phased recommendation ("the two most-used... deferring Azure/Google/
26
+ * Ollama as mechanical repeats once the pattern is proven").
27
+ *
28
+ * **2026-07-22 pass (this addition)**: picks up exactly that deferred item
29
+ * — Azure, Google, and Ollama proxy routes are now built, each a thin
30
+ * sibling `registerProxyStreamRoute` call following the identical request-
31
+ * parsing/SSE-wiring shape, once `@jini-ai/agent-runtime` grew the matching
32
+ * per-vendor turn-runner (see that package's `archived provenance ledger`'s own
33
+ * 2026-07-22 dated section). Also adds the generic `POST
34
+ * /api/proxy/:provider/stream` catch-all described below.
35
+ *
36
+ * **OpenRouter is still not built — open, not rejected.** Evaluated as part
37
+ * of the OD route-parity audit this pass drew on; the user wants to discuss
38
+ * OpenRouter's specific placement (gateway vs. native-provider shape) further
39
+ * before it gets ported. Flagged here explicitly, the same way the
40
+ * 2026-07-21 pass flagged Azure/Google/Ollama, so it isn't mistaken for a
41
+ * decision that's already been made either way.
42
+ *
43
+ * **Deliberately excluded regardless of this decision** (confirmed
44
+ * OD-PRODUCT per the proposal): `POST /api/runs/:id/feedback` and the two
45
+ * "Critique Theater" routes. Not touched by this module at all.
46
+ *
47
+ * **BYOK, not server-held credentials.** Every request carries its own
48
+ * `apiKey` (and optional `baseUrl`/`extraHeaders`) in the POST body, exactly
49
+ * like `@jini-ai/agent-runtime`'s existing `providers/model-catalog.ts` BYOK
50
+ * surface — this route never stores or reads a server-side credential.
51
+ * `apiKey` is required for all five providers, including Ollama — an earlier
52
+ * version of this module made it optional for Ollama on a "local install
53
+ * needs no auth" rationale that a live comparison against a running Open
54
+ * Design daemon proved doesn't match OD's real behavior (OD's default
55
+ * Ollama target is Ollama Cloud, which does require a key; see
56
+ * `@jini-ai/agent-runtime`'s `ollama-chat.ts` module doc for the full
57
+ * corrected design).
58
+ *
59
+ * **Tool execution is out of scope for this pass.** `ModelProxyHttpDeps`'s
60
+ * `executeTool` hooks are optional and default to unset, matching
61
+ * `packages/deploy/archived provenance ledger`'s precedent for "deferred real
62
+ * `ToolExecutor` wiring." With no executor supplied, a turn still streams
63
+ * back any `tool_use`/`tool_calls` the model requests (so a caller can act
64
+ * on them itself and start a fresh request with the results appended to
65
+ * `messages`) — the turn-runner just does not attempt the server-side
66
+ * tool-execution loop. See `anthropic-messages.ts`/`openai-chat.ts`'s own
67
+ * docs for that behavior.
68
+ *
69
+ * **The `:provider` catch-all's relationship to the five fixed routes.**
70
+ * `registerModelProxyRoutes` registers the five literal-path routes first,
71
+ * then the generic `/api/proxy/:provider/stream` catch-all last. Express
72
+ * matches registered routes in registration order, so a request to
73
+ * `/api/proxy/anthropic/stream` (etc.) is always served by its own fixed,
74
+ * strongly-typed handler — the catch-all's own lookup-table entry for
75
+ * `'anthropic'` is real, tested code, but is only reachable via real HTTP
76
+ * traffic for a provider name that has no dedicated fixed route (a typo, or
77
+ * a future provider added to the registry before its own literal route
78
+ * lands). This is deliberate, not dead code left behind by accident: the
79
+ * catch-all exists for callers that want one parameterized endpoint rather
80
+ * than hardcoding five URLs, and for forward-compatibility, without taking
81
+ * over routing for the five providers that already have a dedicated, more
82
+ * specifically-typed path.
83
+ */
84
+ import { randomUUID } from 'node:crypto';
85
+ import { runAnthropicToolTurn, runAzureToolTurn, runGoogleToolTurn, runOllamaToolTurn, runOpenAiToolTurn, } from '@jini-ai/agent-runtime';
86
+ import { createApiError } from '@jini-ai/protocol';
87
+ import { validationError } from '@jini-ai/http-kit';
88
+ import { sendApiError } from '@jini-ai/http-kit';
89
+ import { guardSameOrigin } from '@jini-ai/http-kit';
90
+ import { createSseChannel } from '@jini-ai/http-kit';
91
+ import { err, ok } from '@jini-ai/http-kit';
92
+ function defaultInternalErrorSink(context) {
93
+ // eslint-disable-next-line no-console
94
+ console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, context.error);
95
+ }
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;
101
+ }
102
+ /**
103
+ * Validates only the transport-level shape every proxied request shares
104
+ * (non-empty `apiKey`/`model`, a `messages` array, and the optional numeric/
105
+ * record fields' primitive types) — never the *contents* of `messages` or
106
+ * `tools`, which is provider wire-protocol knowledge this package does not
107
+ * have (see module doc). A malformed message/tool shape is instead rejected
108
+ * by the real provider API and surfaces as a normal `'error'` SSE event,
109
+ * exactly like any other upstream rejection.
110
+ *
111
+ * SECURITY: `apiKey` travels in this JSON body, not an `Authorization` header
112
+ * — a deliberate BYOK design choice (see module doc), not an oversight, but
113
+ * one that only stays safe because every caller of this function is reached
114
+ * through `runProxyStream`/`registerGenericProxyStreamRoute`, both of which
115
+ * call `guardSameOrigin` before this parser ever runs (confirmed 2026-08-23;
116
+ * see the "rejects a cross-origin request with 403 before touching fetch"
117
+ * regression test per provider, plus the catch-all's own, in
118
+ * `__tests__/model-proxy.test.ts` — each proves the credential is never
119
+ * forwarded to `fetch` for a rejected cross-origin request). Do not loosen or
120
+ * remove that same-origin enforcement without first moving `apiKey` to a
121
+ * header — a body-carried credential is not safe to accept cross-origin.
122
+ */
123
+ function parseCommon(body) {
124
+ if (!isRecord(body))
125
+ 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
+ }
152
+ return ok({
153
+ value: {
154
+ apiKey,
155
+ model,
156
+ 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
+ }
163
+ });
164
+ }
165
+ function parseAnthropicProxyRequest(body) {
166
+ const common = parseCommon(body);
167
+ if (!common.ok)
168
+ return common;
169
+ 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' }) });
178
+ }
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
+ });
191
+ }
192
+ function parseOpenAiProxyRequest(body) {
193
+ const common = parseCommon(body);
194
+ if (!common.ok)
195
+ 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
+ });
206
+ }
207
+ function parseAzureProxyRequest(body) {
208
+ const common = parseCommon(body);
209
+ if (!common.ok)
210
+ return common;
211
+ 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' }] }) });
217
+ }
218
+ if (record.tools !== undefined && !Array.isArray(record.tools)) {
219
+ return err({ error: validationError({ message: 'tools must be an array when provided' }) });
220
+ }
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
+ });
229
+ }
230
+ /** `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
+ function parseGoogleProxyRequest(body) {
232
+ const common = parseCommon(body);
233
+ if (!common.ok)
234
+ 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
+ });
268
+ }
269
+ /** 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
+ function toStreamEvent(seq, event) {
271
+ return { opaqueCursor: String(seq), kind: event.type, data: event };
272
+ }
273
+ /**
274
+ * The actual request handler body, shared by every fixed-path route and the `:provider`
275
+ * catch-all: same-origin guard, request parsing, SSE channel lifecycle, and the SEC-005 catch-all
276
+ * 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.
282
+ */
283
+ async function runProxyStream(req, res, adapter, provider, onInternalError, parse, run) {
284
+ // Both `channel` and `seq` are declared ahead of the try so the catch block below can still
285
+ // reach them however far execution got — including a throw from `guardSameOrigin`/`parse`,
286
+ // before either exists yet.
287
+ let seq = 0;
288
+ let channel;
289
+ try {
290
+ const origin = guardSameOrigin({ req, origin: adapter });
291
+ if (!origin.ok) {
292
+ sendApiError({ res, status: 403, error: origin.error });
293
+ return;
294
+ }
295
+ const parsed = parse(req.body);
296
+ if (!parsed.ok) {
297
+ sendApiError({ res, status: 400, error: parsed.error });
298
+ return;
299
+ }
300
+ channel = createSseChannel({ res }, { isEndEvent: ({ event }) => event.kind === 'end' });
301
+ channel.open();
302
+ await run(parsed.value, (event) => channel.enqueue({ event: toStreamEvent(seq++, event) }));
303
+ // No `channel.end()` call here: every reachable exit path inside every turn-runner emits
304
+ // exactly one `'end'`-kind event before its promise resolves (the duplicate-end-event fix —
305
+ // see each provider module's own `turn-end-guard.ts` usage, and each function's own
306
+ // `while (true)` loop, which cannot exit without first calling `emitEnd`), and `isEndEvent`
307
+ // above already auto-closed the channel the instant that event was enqueued.
308
+ }
309
+ catch (error) {
310
+ // 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
317
+ // `__tests__/model-proxy.test.ts`.
318
+ const correlationId = randomUUID();
319
+ 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) {
336
+ // The SSE channel was never opened (the throw happened before it), so no stream headers
337
+ // were ever committed — a normal JSON error response is still possible here, matching how
338
+ // `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();
343
+ }
344
+ }
345
+ }
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));
349
+ }
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. */
351
+ function buildProxyProviderRegistry(deps) {
352
+ return {
353
+ anthropic: {
354
+ 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: {
398
+ 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
+ }, {
406
+ 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: {
420
+ 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
+ },
462
+ };
463
+ }
464
+ const RECOGNIZED_PROVIDERS = new Set([
465
+ 'anthropic',
466
+ 'openai',
467
+ 'azure',
468
+ 'google',
469
+ 'ollama',
470
+ ]);
471
+ function isRecognizedProvider(value) {
472
+ return RECOGNIZED_PROVIDERS.has(value);
473
+ }
474
+ /**
475
+ * Mounts `POST /api/proxy/:provider/stream` — see module doc's "the `:provider` catch-all's
476
+ * relationship to the five fixed routes" section for why an unrecognized-but-registry-present
477
+ * provider name is the only branch of this handler real HTTP traffic can reach for the five known
478
+ * provider ids.
479
+ */
480
+ function registerGenericProxyStreamRoute(app, adapter, onInternalError, registry) {
481
+ app.post('/api/proxy/:provider/stream', async (req, res) => {
482
+ // Unlike the five fixed routes, this catch-all's own `guardSameOrigin` call sits ahead of
483
+ // `runProxyStream` (the registry lookup below has to happen first, to even know which
484
+ // `provider` string to hand `onInternalError`) — so it needs its own guard against the same
485
+ // failure class `runProxyStream`'s catch already documents: `guardSameOrigin` is
486
+ // Result-returning by contract but not guaranteed never to throw (`isLocalSameOrigin` really
487
+ // does throw on a malformed `JINI_ALLOWED_ORIGINS` entry — see `origin-validation.ts`), and
488
+ // this handler's returned promise is never awaited or `.catch()`-ed by Express, so an
489
+ // uncaught throw here becomes an unhandled rejection with no process-level guard. See the
490
+ // paired regression test in `__tests__/model-proxy.test.ts`.
491
+ //
492
+ // Read ahead of the try: plain property/index access on `req.params`/`registry`, never
493
+ // throws, and the catch below needs it regardless of how far execution got.
494
+ const providerParam = req.params.provider ?? '';
495
+ try {
496
+ const origin = guardSameOrigin({ req, origin: adapter });
497
+ if (!origin.ok) {
498
+ sendApiError({ res, status: 403, error: origin.error });
499
+ return;
500
+ }
501
+ const entry = registry[providerParam];
502
+ if (!entry || !isRecognizedProvider(providerParam)) {
503
+ sendApiError({ res, status: 400, error: createApiError({ code: 'BAD_REQUEST', message: `unknown provider: ${providerParam}` }) });
504
+ return;
505
+ }
506
+ await runProxyStream(req, res, adapter, providerParam, onInternalError, entry.parse, entry.run);
507
+ }
508
+ catch (error) {
509
+ // No SSE channel can exist yet at this point — `runProxyStream` owns opening one, and this
510
+ // catch only reaches errors thrown before that call. A plain JSON error response is always
511
+ // 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
+ }
530
+ }
531
+ });
532
+ }
533
+ /** Mounts `POST /api/proxy/{anthropic,openai,azure,google,ollama}/stream` and the generic `POST /api/proxy/:provider/stream` catch-all. */
534
+ export function registerModelProxyRoutes({ app, deps, adapter }, _optional = {}) {
535
+ 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));
641
+ }
642
+ //# sourceMappingURL=model-proxy.js.map