@jini-ai/http-kit 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +107 -0
  3. package/dist/active-context.d.ts +99 -0
  4. package/dist/active-context.d.ts.map +1 -0
  5. package/dist/active-context.js +77 -0
  6. package/dist/active-context.js.map +1 -0
  7. package/dist/adapter.d.ts +36 -0
  8. package/dist/adapter.d.ts.map +1 -0
  9. package/dist/adapter.js +92 -0
  10. package/dist/adapter.js.map +1 -0
  11. package/dist/agents.d.ts +55 -0
  12. package/dist/agents.d.ts.map +1 -0
  13. package/dist/agents.js +23 -0
  14. package/dist/agents.js.map +1 -0
  15. package/dist/api-security-middleware.d.ts +146 -0
  16. package/dist/api-security-middleware.d.ts.map +1 -0
  17. package/dist/api-security-middleware.js +276 -0
  18. package/dist/api-security-middleware.js.map +1 -0
  19. package/dist/attachments.d.ts +317 -0
  20. package/dist/attachments.d.ts.map +1 -0
  21. package/dist/attachments.js +664 -0
  22. package/dist/attachments.js.map +1 -0
  23. package/dist/cancel-owned-runs.d.ts +44 -0
  24. package/dist/cancel-owned-runs.d.ts.map +1 -0
  25. package/dist/cancel-owned-runs.js +35 -0
  26. package/dist/cancel-owned-runs.js.map +1 -0
  27. package/dist/compat.d.ts +22 -0
  28. package/dist/compat.d.ts.map +1 -0
  29. package/dist/compat.js +17 -0
  30. package/dist/compat.js.map +1 -0
  31. package/dist/component-catalog.d.ts +74 -0
  32. package/dist/component-catalog.d.ts.map +1 -0
  33. package/dist/component-catalog.js +74 -0
  34. package/dist/component-catalog.js.map +1 -0
  35. package/dist/connectors.d.ts +165 -0
  36. package/dist/connectors.d.ts.map +1 -0
  37. package/dist/connectors.js +474 -0
  38. package/dist/connectors.js.map +1 -0
  39. package/dist/daemon-status.d.ts +72 -0
  40. package/dist/daemon-status.d.ts.map +1 -0
  41. package/dist/daemon-status.js +60 -0
  42. package/dist/daemon-status.js.map +1 -0
  43. package/dist/db-ops.d.ts +116 -0
  44. package/dist/db-ops.d.ts.map +1 -0
  45. package/dist/db-ops.js +191 -0
  46. package/dist/db-ops.js.map +1 -0
  47. package/dist/delegated-tools.d.ts +46 -0
  48. package/dist/delegated-tools.d.ts.map +1 -0
  49. package/dist/delegated-tools.js +155 -0
  50. package/dist/delegated-tools.js.map +1 -0
  51. package/dist/express/run-stream.d.ts +15 -0
  52. package/dist/express/run-stream.d.ts.map +1 -0
  53. package/dist/express/run-stream.js +11 -0
  54. package/dist/express/run-stream.js.map +1 -0
  55. package/dist/express-index.d.ts +32 -0
  56. package/dist/express-index.d.ts.map +1 -0
  57. package/dist/express-index.js +14 -0
  58. package/dist/express-index.js.map +1 -0
  59. package/dist/fastify/adapter.d.ts +31 -0
  60. package/dist/fastify/adapter.d.ts.map +1 -0
  61. package/dist/fastify/adapter.js +64 -0
  62. package/dist/fastify/adapter.js.map +1 -0
  63. package/dist/fastify/agents.d.ts +13 -0
  64. package/dist/fastify/agents.d.ts.map +1 -0
  65. package/dist/fastify/agents.js +7 -0
  66. package/dist/fastify/agents.js.map +1 -0
  67. package/dist/fastify/api-security-middleware.d.ts +64 -0
  68. package/dist/fastify/api-security-middleware.d.ts.map +1 -0
  69. package/dist/fastify/api-security-middleware.js +139 -0
  70. package/dist/fastify/api-security-middleware.js.map +1 -0
  71. package/dist/fastify/compat.d.ts +22 -0
  72. package/dist/fastify/compat.d.ts.map +1 -0
  73. package/dist/fastify/compat.js +16 -0
  74. package/dist/fastify/compat.js.map +1 -0
  75. package/dist/fastify/daemon-status.d.ts +22 -0
  76. package/dist/fastify/daemon-status.d.ts.map +1 -0
  77. package/dist/fastify/daemon-status.js +9 -0
  78. package/dist/fastify/daemon-status.js.map +1 -0
  79. package/dist/fastify/host-tools.d.ts +13 -0
  80. package/dist/fastify/host-tools.d.ts.map +1 -0
  81. package/dist/fastify/host-tools.js +8 -0
  82. package/dist/fastify/host-tools.js.map +1 -0
  83. package/dist/fastify/index.d.ts +36 -0
  84. package/dist/fastify/index.d.ts.map +1 -0
  85. package/dist/fastify/index.js +18 -0
  86. package/dist/fastify/index.js.map +1 -0
  87. package/dist/fastify/local-daemon-request.d.ts +43 -0
  88. package/dist/fastify/local-daemon-request.d.ts.map +1 -0
  89. package/dist/fastify/local-daemon-request.js +155 -0
  90. package/dist/fastify/local-daemon-request.js.map +1 -0
  91. package/dist/fastify/origin.d.ts +21 -0
  92. package/dist/fastify/origin.d.ts.map +1 -0
  93. package/dist/fastify/origin.js +14 -0
  94. package/dist/fastify/origin.js.map +1 -0
  95. package/dist/fastify/request.d.ts +20 -0
  96. package/dist/fastify/request.d.ts.map +1 -0
  97. package/dist/fastify/request.js +25 -0
  98. package/dist/fastify/request.js.map +1 -0
  99. package/dist/fastify/response.d.ts +20 -0
  100. package/dist/fastify/response.d.ts.map +1 -0
  101. package/dist/fastify/response.js +41 -0
  102. package/dist/fastify/response.js.map +1 -0
  103. package/dist/fastify/route-registration-guard.d.ts +70 -0
  104. package/dist/fastify/route-registration-guard.d.ts.map +1 -0
  105. package/dist/fastify/route-registration-guard.js +69 -0
  106. package/dist/fastify/route-registration-guard.js.map +1 -0
  107. package/dist/fastify/run-stream.d.ts +18 -0
  108. package/dist/fastify/run-stream.d.ts.map +1 -0
  109. package/dist/fastify/run-stream.js +10 -0
  110. package/dist/fastify/run-stream.js.map +1 -0
  111. package/dist/fastify/runs.d.ts +17 -0
  112. package/dist/fastify/runs.d.ts.map +1 -0
  113. package/dist/fastify/runs.js +33 -0
  114. package/dist/fastify/runs.js.map +1 -0
  115. package/dist/frontend-control.d.ts +131 -0
  116. package/dist/frontend-control.d.ts.map +1 -0
  117. package/dist/frontend-control.js +77 -0
  118. package/dist/frontend-control.js.map +1 -0
  119. package/dist/frontend-sessions.d.ts +100 -0
  120. package/dist/frontend-sessions.d.ts.map +1 -0
  121. package/dist/frontend-sessions.js +180 -0
  122. package/dist/frontend-sessions.js.map +1 -0
  123. package/dist/health.d.ts +91 -0
  124. package/dist/health.d.ts.map +1 -0
  125. package/dist/health.js +53 -0
  126. package/dist/health.js.map +1 -0
  127. package/dist/host-tools.d.ts +174 -0
  128. package/dist/host-tools.d.ts.map +1 -0
  129. package/dist/host-tools.js +338 -0
  130. package/dist/host-tools.js.map +1 -0
  131. package/dist/index.d.ts +86 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +40 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/local-daemon-request.d.ts +44 -0
  136. package/dist/local-daemon-request.d.ts.map +1 -0
  137. package/dist/local-daemon-request.js +145 -0
  138. package/dist/local-daemon-request.js.map +1 -0
  139. package/dist/media.d.ts +131 -0
  140. package/dist/media.d.ts.map +1 -0
  141. package/dist/media.js +352 -0
  142. package/dist/media.js.map +1 -0
  143. package/dist/memory.d.ts +230 -0
  144. package/dist/memory.d.ts.map +1 -0
  145. package/dist/memory.js +331 -0
  146. package/dist/memory.js.map +1 -0
  147. package/dist/model-proxy.d.ts +45 -0
  148. package/dist/model-proxy.d.ts.map +1 -0
  149. package/dist/model-proxy.js +619 -0
  150. package/dist/model-proxy.js.map +1 -0
  151. package/dist/origin-validation.d.ts +64 -0
  152. package/dist/origin-validation.d.ts.map +1 -0
  153. package/dist/origin-validation.js +226 -0
  154. package/dist/origin-validation.js.map +1 -0
  155. package/dist/origin.d.ts +32 -0
  156. package/dist/origin.d.ts.map +1 -0
  157. package/dist/origin.js +14 -0
  158. package/dist/origin.js.map +1 -0
  159. package/dist/pack-http.d.ts +30 -0
  160. package/dist/pack-http.d.ts.map +1 -0
  161. package/dist/pack-http.js +12 -0
  162. package/dist/pack-http.js.map +1 -0
  163. package/dist/raw-sse.d.ts +44 -0
  164. package/dist/raw-sse.d.ts.map +1 -0
  165. package/dist/raw-sse.js +93 -0
  166. package/dist/raw-sse.js.map +1 -0
  167. package/dist/remote-run-events.d.ts +66 -0
  168. package/dist/remote-run-events.d.ts.map +1 -0
  169. package/dist/remote-run-events.js +230 -0
  170. package/dist/remote-run-events.js.map +1 -0
  171. package/dist/request.d.ts +19 -0
  172. package/dist/request.d.ts.map +1 -0
  173. package/dist/request.js +25 -0
  174. package/dist/request.js.map +1 -0
  175. package/dist/research.d.ts +42 -0
  176. package/dist/research.d.ts.map +1 -0
  177. package/dist/research.js +316 -0
  178. package/dist/research.js.map +1 -0
  179. package/dist/response.d.ts +14 -0
  180. package/dist/response.d.ts.map +1 -0
  181. package/dist/response.js +45 -0
  182. package/dist/response.js.map +1 -0
  183. package/dist/route-manifest.d.ts +31 -0
  184. package/dist/route-manifest.d.ts.map +1 -0
  185. package/dist/route-manifest.js +103 -0
  186. package/dist/route-manifest.js.map +1 -0
  187. package/dist/route-registration-guard.d.ts +65 -0
  188. package/dist/route-registration-guard.d.ts.map +1 -0
  189. package/dist/route-registration-guard.js +71 -0
  190. package/dist/route-registration-guard.js.map +1 -0
  191. package/dist/routines.d.ts +114 -0
  192. package/dist/routines.d.ts.map +1 -0
  193. package/dist/routines.js +320 -0
  194. package/dist/routines.js.map +1 -0
  195. package/dist/run-stream.d.ts +60 -0
  196. package/dist/run-stream.d.ts.map +1 -0
  197. package/dist/run-stream.js +108 -0
  198. package/dist/run-stream.js.map +1 -0
  199. package/dist/runs.d.ts +79 -0
  200. package/dist/runs.d.ts.map +1 -0
  201. package/dist/runs.js +232 -0
  202. package/dist/runs.js.map +1 -0
  203. package/dist/sse.d.ts +139 -0
  204. package/dist/sse.d.ts.map +1 -0
  205. package/dist/sse.js +157 -0
  206. package/dist/sse.js.map +1 -0
  207. package/dist/terminals.d.ts +125 -0
  208. package/dist/terminals.d.ts.map +1 -0
  209. package/dist/terminals.js +349 -0
  210. package/dist/terminals.js.map +1 -0
  211. package/dist/tool-catalog.d.ts +68 -0
  212. package/dist/tool-catalog.d.ts.map +1 -0
  213. package/dist/tool-catalog.js +68 -0
  214. package/dist/tool-catalog.js.map +1 -0
  215. package/dist/types.d.ts +58 -0
  216. package/dist/types.d.ts.map +1 -0
  217. package/dist/types.js +5 -0
  218. package/dist/types.js.map +1 -0
  219. package/dist/workspace-root.d.ts +69 -0
  220. package/dist/workspace-root.d.ts.map +1 -0
  221. package/dist/workspace-root.js +63 -0
  222. package/dist/workspace-root.js.map +1 -0
  223. package/dist/xai.d.ts +86 -0
  224. package/dist/xai.d.ts.map +1 -0
  225. package/dist/xai.js +668 -0
  226. package/dist/xai.js.map +1 -0
  227. package/package.json +58 -0
@@ -0,0 +1,619 @@
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 `@jini-ai/http-kit` — this package has zero AI-provider knowledge anywhere
16
+ * else in its surface, and that proposal recommended keeping it that way by
17
+ * extending `@jini-ai/agent-runtime`'s existing `providers/` pattern instead.
18
+ * This module is the `@jini-ai/http-kit` half of that split: request parsing,
19
+ * same-origin enforcement, and SSE transport (`sse.ts`, the same primitive
20
+ * `runs.ts`/`memory.ts` already consume) — no knowledge of what an
21
+ * Anthropic `content_block_delta`, an OpenAI `tool_calls[].function.
22
+ * arguments` fragment, or a Gemini `candidates[0].content.parts[]` entry
23
+ * looks like lives here.
24
+ *
25
+ * **2026-07-21 pass**: Anthropic and OpenAI only, per the proposal's own
26
+ * phased recommendation ("the two most-used... deferring Azure/Google/
27
+ * Ollama as mechanical repeats once the pattern is proven").
28
+ *
29
+ * **2026-07-22 pass (this addition)**: picks up exactly that deferred item
30
+ * — Azure, Google, and Ollama proxy routes are now built, each a thin
31
+ * sibling `registerProxyStreamRoute` call following the identical request-
32
+ * parsing/SSE-wiring shape, once `@jini-ai/agent-runtime` grew the matching
33
+ * per-vendor turn-runner (see that package's `source-map.md`'s own
34
+ * 2026-07-22 dated section). Also adds the generic `POST
35
+ * /api/proxy/:provider/stream` catch-all described below.
36
+ *
37
+ * **OpenRouter is still not built — open, not rejected.** Evaluated as part
38
+ * of the OD route-parity audit this pass drew on; the user wants to discuss
39
+ * OpenRouter's specific placement (gateway vs. native-provider shape) further
40
+ * before it gets ported. Flagged here explicitly, the same way the
41
+ * 2026-07-21 pass flagged Azure/Google/Ollama, so it isn't mistaken for a
42
+ * decision that's already been made either way.
43
+ *
44
+ * **Deliberately excluded regardless of this decision** (confirmed
45
+ * OD-PRODUCT per the proposal): `POST /api/runs/:id/feedback` and the two
46
+ * "Critique Theater" routes. Not touched by this module at all.
47
+ *
48
+ * **BYOK, not server-held credentials.** Every request carries its own
49
+ * `apiKey` (and optional `baseUrl`/`extraHeaders`) in the POST body, exactly
50
+ * like `@jini-ai/agent-runtime`'s existing `providers/model-catalog.ts` BYOK
51
+ * surface — this route never stores or reads a server-side credential.
52
+ * `apiKey` is required for all five providers, including Ollama — an earlier
53
+ * version of this module made it optional for Ollama on a "local install
54
+ * needs no auth" rationale that a live comparison against a running Open
55
+ * Design daemon proved doesn't match OD's real behavior (OD's default
56
+ * Ollama target is Ollama Cloud, which does require a key; see
57
+ * `@jini-ai/agent-runtime`'s `ollama-chat.ts` module doc for the full
58
+ * corrected design).
59
+ *
60
+ * **Tool execution is out of scope for this pass.** `ModelProxyHttpDeps`'s
61
+ * `executeTool` hooks are optional and default to unset, matching
62
+ * `packages/deploy/source-map.md`'s precedent for "deferred real
63
+ * `ToolExecutor` wiring." With no executor supplied, a turn still streams
64
+ * back any `tool_use`/`tool_calls` the model requests (so a caller can act
65
+ * on them itself and start a fresh request with the results appended to
66
+ * `messages`) — the turn-runner just does not attempt the server-side
67
+ * tool-execution loop. See `anthropic-messages.ts`/`openai-chat.ts`'s own
68
+ * docs for that behavior.
69
+ *
70
+ * **The `:provider` catch-all's relationship to the five fixed routes.**
71
+ * `registerModelProxyRoutes` registers the five literal-path routes first,
72
+ * then the generic `/api/proxy/:provider/stream` catch-all last. Express
73
+ * matches registered routes in registration order, so a request to
74
+ * `/api/proxy/anthropic/stream` (etc.) is always served by its own fixed,
75
+ * strongly-typed handler — the catch-all's own lookup-table entry for
76
+ * `'anthropic'` is real, tested code, but is only reachable via real HTTP
77
+ * traffic for a provider name that has no dedicated fixed route (a typo, or
78
+ * a future provider added to the registry before its own literal route
79
+ * lands). This is deliberate, not dead code left behind by accident: the
80
+ * catch-all exists for callers that want one parameterized endpoint rather
81
+ * than hardcoding five URLs, and for forward-compatibility, without taking
82
+ * over routing for the five providers that already have a dedicated, more
83
+ * specifically-typed path.
84
+ */
85
+ import { randomUUID } from 'node:crypto';
86
+ import { runAnthropicToolTurn, runAzureToolTurn, runGoogleToolTurn, runOllamaToolTurn, runOpenAiToolTurn, } from '@jini-ai/agent-runtime';
87
+ import { createApiError } from '@jini-ai/protocol';
88
+ import { validationError } from './request.js';
89
+ import { sendApiError } from './response.js';
90
+ import { guardSameOrigin } from './origin.js';
91
+ import { createSseChannel } from './sse.js';
92
+ import { err, ok } from './types.js';
93
+ function defaultInternalErrorSink(context) {
94
+ // eslint-disable-next-line no-console
95
+ console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, context.error);
96
+ }
97
+ function isRecord(value) {
98
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
99
+ }
100
+ function nonEmptyString(value) {
101
+ return typeof value === 'string' && value.trim().length > 0 ? value : null;
102
+ }
103
+ /**
104
+ * Validates only the transport-level shape every proxied request shares
105
+ * (non-empty `apiKey`/`model`, a `messages` array, and the optional numeric/
106
+ * record fields' primitive types) — never the *contents* of `messages` or
107
+ * `tools`, which is provider wire-protocol knowledge this package does not
108
+ * have (see module doc). A malformed message/tool shape is instead rejected
109
+ * by the real provider API and surfaces as a normal `'error'` SSE event,
110
+ * exactly like any other upstream rejection.
111
+ *
112
+ * SECURITY: `apiKey` travels in this JSON body, not an `Authorization` header
113
+ * — a deliberate BYOK design choice (see module doc), not an oversight, but
114
+ * one that only stays safe because every caller of this function is reached
115
+ * through `runProxyStream`/`registerGenericProxyStreamRoute`, both of which
116
+ * call `guardSameOrigin` before this parser ever runs (confirmed 2026-08-23;
117
+ * see the "rejects a cross-origin request with 403 before touching fetch"
118
+ * regression test per provider, plus the catch-all's own, in
119
+ * `__tests__/model-proxy.test.ts` — each proves the credential is never
120
+ * forwarded to `fetch` for a rejected cross-origin request). Do not loosen or
121
+ * remove that same-origin enforcement without first moving `apiKey` to a
122
+ * header — a body-carried credential is not safe to accept cross-origin.
123
+ */
124
+ function parseCommon(body) {
125
+ if (!isRecord(body))
126
+ return err(validationError('body must be a JSON object'));
127
+ const apiKey = nonEmptyString(body.apiKey);
128
+ if (!apiKey) {
129
+ return err(validationError('apiKey must be a non-empty string', [{ path: 'apiKey', message: 'required non-empty string' }]));
130
+ }
131
+ const model = nonEmptyString(body.model);
132
+ if (!model) {
133
+ return err(validationError('model must be a non-empty string', [{ path: 'model', message: 'required non-empty string' }]));
134
+ }
135
+ if (!Array.isArray(body.messages) || body.messages.length === 0) {
136
+ return err(validationError('messages must be a non-empty array', [{ path: 'messages', message: 'required non-empty array' }]));
137
+ }
138
+ if (body.baseUrl !== undefined && typeof body.baseUrl !== 'string') {
139
+ return err(validationError('baseUrl must be a string when provided'));
140
+ }
141
+ if (body.temperature !== undefined && typeof body.temperature !== 'number') {
142
+ return err(validationError('temperature must be a number when provided'));
143
+ }
144
+ if (body.maxTokens !== undefined && typeof body.maxTokens !== 'number') {
145
+ return err(validationError('maxTokens must be a number when provided'));
146
+ }
147
+ if (body.maxToolTurns !== undefined && typeof body.maxToolTurns !== 'number') {
148
+ return err(validationError('maxToolTurns must be a number when provided'));
149
+ }
150
+ if (body.extraHeaders !== undefined && !isRecord(body.extraHeaders)) {
151
+ return err(validationError('extraHeaders must be an object when provided'));
152
+ }
153
+ return ok({
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
+ function parseAnthropicProxyRequest(body) {
165
+ const common = parseCommon(body);
166
+ if (!common.ok)
167
+ return common;
168
+ const record = body;
169
+ if (typeof record.maxTokens !== 'number' || !Number.isFinite(record.maxTokens) || record.maxTokens <= 0) {
170
+ return err(validationError('maxTokens must be a positive number', [{ path: 'maxTokens', message: 'required positive number' }]));
171
+ }
172
+ if (record.apiVersion !== undefined && typeof record.apiVersion !== 'string') {
173
+ return err(validationError('apiVersion must be a string when provided'));
174
+ }
175
+ if (record.system !== undefined && typeof record.system !== 'string') {
176
+ return err(validationError('system must be a string when provided'));
177
+ }
178
+ if (record.tools !== undefined && !Array.isArray(record.tools)) {
179
+ return err(validationError('tools must be an array when provided'));
180
+ }
181
+ return ok({
182
+ ...common.value,
183
+ maxTokens: record.maxTokens,
184
+ ...(record.apiVersion !== undefined ? { apiVersion: record.apiVersion } : {}),
185
+ ...(record.system !== undefined ? { system: record.system } : {}),
186
+ ...(record.tools !== undefined ? { tools: record.tools } : {}),
187
+ });
188
+ }
189
+ function parseOpenAiProxyRequest(body) {
190
+ const common = parseCommon(body);
191
+ if (!common.ok)
192
+ return common;
193
+ const record = body;
194
+ if (record.tools !== undefined && !Array.isArray(record.tools)) {
195
+ return err(validationError('tools must be an array when provided'));
196
+ }
197
+ return ok({
198
+ ...common.value,
199
+ ...(record.tools !== undefined ? { tools: record.tools } : {}),
200
+ });
201
+ }
202
+ function parseAzureProxyRequest(body) {
203
+ const common = parseCommon(body);
204
+ if (!common.ok)
205
+ return common;
206
+ const record = body;
207
+ if (!common.value.baseUrl) {
208
+ return err(validationError('baseUrl must be a non-empty string', [{ path: 'baseUrl', message: 'required non-empty string' }]));
209
+ }
210
+ if (typeof record.apiVersion !== 'string' || record.apiVersion.trim().length === 0) {
211
+ return err(validationError('apiVersion must be a non-empty string', [{ path: 'apiVersion', message: 'required non-empty string' }]));
212
+ }
213
+ if (record.tools !== undefined && !Array.isArray(record.tools)) {
214
+ return err(validationError('tools must be an array when provided'));
215
+ }
216
+ return ok({
217
+ ...common.value,
218
+ baseUrl: common.value.baseUrl,
219
+ apiVersion: record.apiVersion,
220
+ ...(record.tools !== undefined ? { tools: record.tools } : {}),
221
+ });
222
+ }
223
+ /** `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. */
224
+ function parseGoogleProxyRequest(body) {
225
+ const common = parseCommon(body);
226
+ if (!common.ok)
227
+ return common;
228
+ const record = body;
229
+ if (record.system !== undefined && typeof record.system !== 'string') {
230
+ return err(validationError('system must be a string when provided'));
231
+ }
232
+ if (record.tools !== undefined && !Array.isArray(record.tools)) {
233
+ return err(validationError('tools must be an array when provided'));
234
+ }
235
+ if (record.maxOutputTokens !== undefined && typeof record.maxOutputTokens !== 'number') {
236
+ return err(validationError('maxOutputTokens must be a number when provided'));
237
+ }
238
+ return ok({
239
+ ...common.value,
240
+ ...(record.system !== undefined ? { system: record.system } : {}),
241
+ ...(record.tools !== undefined ? { tools: record.tools } : {}),
242
+ ...(record.maxOutputTokens !== undefined ? { maxOutputTokens: record.maxOutputTokens } : {}),
243
+ });
244
+ }
245
+ function parseOllamaProxyRequest(body) {
246
+ const common = parseCommon(body);
247
+ if (!common.ok)
248
+ return common;
249
+ const record = body;
250
+ if (record.tools !== undefined && !Array.isArray(record.tools)) {
251
+ return err(validationError('tools must be an array when provided'));
252
+ }
253
+ return ok({
254
+ ...common.value,
255
+ ...(record.tools !== undefined ? { tools: record.tools } : {}),
256
+ });
257
+ }
258
+ /** 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. */
259
+ function toStreamEvent(seq, event) {
260
+ return { opaqueCursor: String(seq), kind: event.type, data: event };
261
+ }
262
+ /**
263
+ * The actual request handler body, shared by every fixed-path route and the `:provider`
264
+ * catch-all: same-origin guard, request parsing, SSE channel lifecycle, and the SEC-005 catch-all
265
+ * for a turn-runner promise that rejects outright (see module doc). Provider-specific pieces
266
+ * (`provider`, `parse`, `run`) are injected — extracted into its own function (rather than living
267
+ * inline inside `registerProxyStreamRoute`'s `app.post` callback, as it did before this pass) so
268
+ * the generic catch-all route below can invoke the identical logic with a `provider` value read
269
+ * from `req.params` instead of a fixed string, without duplicating the SSE-wiring/error-handling
270
+ * code a second time.
271
+ */
272
+ async function runProxyStream(req, res, adapter, provider, onInternalError, parse, run) {
273
+ // Both `channel` and `seq` are declared ahead of the try so the catch block below can still
274
+ // reach them however far execution got — including a throw from `guardSameOrigin`/`parse`,
275
+ // before either exists yet.
276
+ let seq = 0;
277
+ let channel;
278
+ try {
279
+ const origin = guardSameOrigin(req, adapter);
280
+ if (!origin.ok) {
281
+ sendApiError(res, 403, origin.error);
282
+ return;
283
+ }
284
+ const parsed = parse(req.body);
285
+ if (!parsed.ok) {
286
+ sendApiError(res, 400, parsed.error);
287
+ return;
288
+ }
289
+ channel = createSseChannel(res, { isEndEvent: (event) => event.kind === 'end' });
290
+ channel.open();
291
+ await run(parsed.value, (event) => channel.enqueue(toStreamEvent(seq++, event)));
292
+ // No `channel.end()` call here: every reachable exit path inside every turn-runner emits
293
+ // exactly one `'end'`-kind event before its promise resolves (the duplicate-end-event fix —
294
+ // see each provider module's own `turn-end-guard.ts` usage, and each function's own
295
+ // `while (true)` loop, which cannot exit without first calling `emitEnd`), and `isEndEvent`
296
+ // above already auto-closed the channel the instant that event was enqueued.
297
+ }
298
+ catch (error) {
299
+ // This catch now covers the whole handler, not just `run(...)`: neither caller of this
300
+ // function (`registerProxyStreamRoute`'s fixed routes, `registerGenericProxyStreamRoute`'s
301
+ // catch-all) awaits or `.catch()`s the promise this function returns — exactly how Express
302
+ // itself invokes every handler. `guardSameOrigin`/`parse` are Result-returning by contract and
303
+ // don't throw today, but nothing enforces that; if either ever did, the throw used to become
304
+ // an unhandled rejection with no process-level guard anywhere in this package's path, which is
305
+ // Node's documented crash-the-process default. See the paired regression test in
306
+ // `__tests__/model-proxy.test.ts`.
307
+ const correlationId = randomUUID();
308
+ onInternalError({ provider, correlationId, error });
309
+ if (channel) {
310
+ // `enqueue` is a documented no-op once the channel is already closed (`sse.ts`), so these
311
+ // two synthetic events are safe to send unconditionally: normally the channel is still open
312
+ // here (an exception escaping `run` — e.g. a caller-supplied `executeTool` throwing — means
313
+ // no `'end'` event was ever emitted for this connection), and the synthetic `'end'`-kind
314
+ // event's own `isEndEvent` match closes the channel via the same path as a real one.
315
+ channel.enqueue(toStreamEvent(seq++, {
316
+ type: 'error',
317
+ message: 'an internal error occurred',
318
+ code: correlationId,
319
+ }));
320
+ channel.enqueue(toStreamEvent(seq++, { type: 'end', reason: 'error' }));
321
+ }
322
+ else if (!res.headersSent) {
323
+ // The SSE channel was never opened (the throw happened before it), so no stream headers
324
+ // were ever committed — a normal JSON error response is still possible here, matching how
325
+ // `mountJsonRoute`'s own SEC-005 catch responds to a pre-response failure.
326
+ sendApiError(res, 500, createApiError('INTERNAL_ERROR', 'an internal error occurred', { requestId: correlationId }));
327
+ }
328
+ else if (!res.writableEnded) {
329
+ res.end();
330
+ }
331
+ }
332
+ }
333
+ /** 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`. */
334
+ function registerProxyStreamRoute(app, path, adapter, provider, onInternalError, parse, run) {
335
+ app.post(path, (req, res) => runProxyStream(req, res, adapter, provider, onInternalError, parse, run));
336
+ }
337
+ /** 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. */
338
+ function buildProxyProviderRegistry(deps) {
339
+ return {
340
+ anthropic: {
341
+ parse: parseAnthropicProxyRequest,
342
+ run: ((parsed, onEvent) => runAnthropicToolTurn({
343
+ apiKey: parsed.apiKey,
344
+ model: parsed.model,
345
+ maxTokens: parsed.maxTokens,
346
+ messages: parsed.messages,
347
+ onEvent,
348
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
349
+ ...(parsed.apiVersion !== undefined ? { apiVersion: parsed.apiVersion } : {}),
350
+ ...(parsed.system !== undefined ? { system: parsed.system } : {}),
351
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
352
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
353
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
354
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
355
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
356
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
357
+ ...(deps.anthropicExecuteTool
358
+ ? { executeTool: (call) => deps.anthropicExecuteTool(call) }
359
+ : {}),
360
+ })),
361
+ },
362
+ openai: {
363
+ parse: parseOpenAiProxyRequest,
364
+ run: ((parsed, onEvent) => runOpenAiToolTurn({
365
+ apiKey: parsed.apiKey,
366
+ model: parsed.model,
367
+ messages: parsed.messages,
368
+ onEvent,
369
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
370
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
371
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
372
+ ...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
373
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
374
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
375
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
376
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
377
+ ...(deps.openaiExecuteTool
378
+ ? { executeTool: (call) => deps.openaiExecuteTool(call) }
379
+ : {}),
380
+ })),
381
+ },
382
+ azure: {
383
+ parse: parseAzureProxyRequest,
384
+ run: ((parsed, onEvent) => runAzureToolTurn({
385
+ apiKey: parsed.apiKey,
386
+ baseUrl: parsed.baseUrl,
387
+ model: parsed.model,
388
+ apiVersion: parsed.apiVersion,
389
+ messages: parsed.messages,
390
+ onEvent,
391
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
392
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
393
+ ...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
394
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
395
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
396
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
397
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
398
+ ...(deps.azureExecuteTool
399
+ ? { executeTool: (call) => deps.azureExecuteTool(call) }
400
+ : {}),
401
+ })),
402
+ },
403
+ google: {
404
+ parse: parseGoogleProxyRequest,
405
+ run: ((parsed, onEvent) => runGoogleToolTurn({
406
+ apiKey: parsed.apiKey,
407
+ model: parsed.model,
408
+ contents: parsed.messages,
409
+ onEvent,
410
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
411
+ ...(parsed.system !== undefined ? { system: parsed.system } : {}),
412
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
413
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
414
+ ...(parsed.maxOutputTokens !== undefined ? { maxOutputTokens: parsed.maxOutputTokens } : {}),
415
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
416
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
417
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
418
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
419
+ ...(deps.googleExecuteTool
420
+ ? { executeTool: (call) => deps.googleExecuteTool(call) }
421
+ : {}),
422
+ })),
423
+ },
424
+ ollama: {
425
+ parse: parseOllamaProxyRequest,
426
+ run: ((parsed, onEvent) => runOllamaToolTurn({
427
+ apiKey: parsed.apiKey,
428
+ model: parsed.model,
429
+ messages: parsed.messages,
430
+ onEvent,
431
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
432
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
433
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
434
+ ...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
435
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
436
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
437
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
438
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
439
+ ...(deps.ollamaExecuteTool
440
+ ? { executeTool: (call) => deps.ollamaExecuteTool(call) }
441
+ : {}),
442
+ })),
443
+ },
444
+ };
445
+ }
446
+ const RECOGNIZED_PROVIDERS = new Set([
447
+ 'anthropic',
448
+ 'openai',
449
+ 'azure',
450
+ 'google',
451
+ 'ollama',
452
+ ]);
453
+ function isRecognizedProvider(value) {
454
+ return RECOGNIZED_PROVIDERS.has(value);
455
+ }
456
+ /**
457
+ * Mounts `POST /api/proxy/:provider/stream` — see module doc's "the `:provider` catch-all's
458
+ * relationship to the five fixed routes" section for why an unrecognized-but-registry-present
459
+ * provider name is the only branch of this handler real HTTP traffic can reach for the five known
460
+ * provider ids.
461
+ */
462
+ function registerGenericProxyStreamRoute(app, adapter, onInternalError, registry) {
463
+ app.post('/api/proxy/:provider/stream', async (req, res) => {
464
+ // Unlike the five fixed routes, this catch-all's own `guardSameOrigin` call sits ahead of
465
+ // `runProxyStream` (the registry lookup below has to happen first, to even know which
466
+ // `provider` string to hand `onInternalError`) — so it needs its own guard against the same
467
+ // failure class `runProxyStream`'s catch already documents: `guardSameOrigin` is
468
+ // Result-returning by contract but not guaranteed never to throw (`isLocalSameOrigin` really
469
+ // does throw on a malformed `JINI_ALLOWED_ORIGINS` entry — see `origin-validation.ts`), and
470
+ // this handler's returned promise is never awaited or `.catch()`-ed by Express, so an
471
+ // uncaught throw here becomes an unhandled rejection with no process-level guard. See the
472
+ // paired regression test in `__tests__/model-proxy.test.ts`.
473
+ //
474
+ // Read ahead of the try: plain property/index access on `req.params`/`registry`, never
475
+ // throws, and the catch below needs it regardless of how far execution got.
476
+ const providerParam = req.params.provider ?? '';
477
+ try {
478
+ const origin = guardSameOrigin(req, adapter);
479
+ if (!origin.ok) {
480
+ sendApiError(res, 403, origin.error);
481
+ return;
482
+ }
483
+ const entry = registry[providerParam];
484
+ if (!entry || !isRecognizedProvider(providerParam)) {
485
+ sendApiError(res, 400, createApiError('BAD_REQUEST', `unknown provider: ${providerParam}`));
486
+ return;
487
+ }
488
+ await runProxyStream(req, res, adapter, providerParam, onInternalError, entry.parse, entry.run);
489
+ }
490
+ catch (error) {
491
+ // No SSE channel can exist yet at this point — `runProxyStream` owns opening one, and this
492
+ // catch only reaches errors thrown before that call. A plain JSON error response is always
493
+ // still possible, matching `runProxyStream`'s own "channel never opened" branch.
494
+ const correlationId = randomUUID();
495
+ // `onInternalError`'s contract only accepts the 5 recognized provider literals; a throw
496
+ // this early (e.g. `guardSameOrigin`) may have happened before the provider name was even
497
+ // validated, so an unrecognized/empty `providerParam` logs directly instead of lying to
498
+ // that typed contract with a provider that was never actually confirmed.
499
+ if (isRecognizedProvider(providerParam)) {
500
+ onInternalError({ provider: providerParam, correlationId, error });
501
+ }
502
+ else {
503
+ // eslint-disable-next-line no-console
504
+ console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, error);
505
+ }
506
+ if (!res.headersSent) {
507
+ sendApiError(res, 500, createApiError('INTERNAL_ERROR', 'an internal error occurred', { requestId: correlationId }));
508
+ }
509
+ else if (!res.writableEnded) {
510
+ res.end();
511
+ }
512
+ }
513
+ });
514
+ }
515
+ /** Mounts `POST /api/proxy/{anthropic,openai,azure,google,ollama}/stream` and the generic `POST /api/proxy/:provider/stream` catch-all. */
516
+ export function registerModelProxyRoutes(app, deps, adapter) {
517
+ const onInternalError = deps.onInternalError ?? defaultInternalErrorSink;
518
+ registerProxyStreamRoute(app, '/api/proxy/anthropic/stream', adapter, 'anthropic', onInternalError, parseAnthropicProxyRequest, (parsed, onEvent) => runAnthropicToolTurn({
519
+ apiKey: parsed.apiKey,
520
+ model: parsed.model,
521
+ maxTokens: parsed.maxTokens,
522
+ messages: parsed.messages,
523
+ onEvent,
524
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
525
+ ...(parsed.apiVersion !== undefined ? { apiVersion: parsed.apiVersion } : {}),
526
+ ...(parsed.system !== undefined ? { system: parsed.system } : {}),
527
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
528
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
529
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
530
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
531
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
532
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
533
+ ...(deps.anthropicExecuteTool
534
+ ? {
535
+ executeTool: (call) => deps.anthropicExecuteTool(call),
536
+ }
537
+ : {}),
538
+ }));
539
+ registerProxyStreamRoute(app, '/api/proxy/openai/stream', adapter, 'openai', onInternalError, parseOpenAiProxyRequest, (parsed, onEvent) => runOpenAiToolTurn({
540
+ apiKey: parsed.apiKey,
541
+ model: parsed.model,
542
+ messages: parsed.messages,
543
+ onEvent,
544
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
545
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
546
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
547
+ ...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
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.openaiExecuteTool
553
+ ? {
554
+ executeTool: (call) => deps.openaiExecuteTool(call),
555
+ }
556
+ : {}),
557
+ }));
558
+ registerProxyStreamRoute(app, '/api/proxy/azure/stream', adapter, 'azure', onInternalError, parseAzureProxyRequest, (parsed, onEvent) => runAzureToolTurn({
559
+ apiKey: parsed.apiKey,
560
+ baseUrl: parsed.baseUrl,
561
+ model: parsed.model,
562
+ apiVersion: parsed.apiVersion,
563
+ messages: parsed.messages,
564
+ onEvent,
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.azureExecuteTool
573
+ ? {
574
+ executeTool: (call) => deps.azureExecuteTool(call),
575
+ }
576
+ : {}),
577
+ }));
578
+ registerProxyStreamRoute(app, '/api/proxy/google/stream', adapter, 'google', onInternalError, parseGoogleProxyRequest, (parsed, onEvent) => runGoogleToolTurn({
579
+ apiKey: parsed.apiKey,
580
+ model: parsed.model,
581
+ contents: parsed.messages,
582
+ onEvent,
583
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
584
+ ...(parsed.system !== undefined ? { system: parsed.system } : {}),
585
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
586
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
587
+ ...(parsed.maxOutputTokens !== undefined ? { maxOutputTokens: parsed.maxOutputTokens } : {}),
588
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
589
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
590
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
591
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
592
+ ...(deps.googleExecuteTool
593
+ ? {
594
+ executeTool: (call) => deps.googleExecuteTool(call),
595
+ }
596
+ : {}),
597
+ }));
598
+ registerProxyStreamRoute(app, '/api/proxy/ollama/stream', adapter, 'ollama', onInternalError, parseOllamaProxyRequest, (parsed, onEvent) => runOllamaToolTurn({
599
+ apiKey: parsed.apiKey,
600
+ model: parsed.model,
601
+ messages: parsed.messages,
602
+ onEvent,
603
+ ...(parsed.baseUrl !== undefined ? { baseUrl: parsed.baseUrl } : {}),
604
+ ...(parsed.tools !== undefined ? { tools: parsed.tools } : {}),
605
+ ...(parsed.temperature !== undefined ? { temperature: parsed.temperature } : {}),
606
+ ...(parsed.maxTokens !== undefined ? { maxTokens: parsed.maxTokens } : {}),
607
+ ...(parsed.maxToolTurns !== undefined ? { maxToolTurns: parsed.maxToolTurns } : {}),
608
+ ...(parsed.extraHeaders !== undefined ? { extraHeaders: parsed.extraHeaders } : {}),
609
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
610
+ ...(deps.dnsLookup ? { dnsLookup: deps.dnsLookup } : {}),
611
+ ...(deps.ollamaExecuteTool
612
+ ? {
613
+ executeTool: (call) => deps.ollamaExecuteTool(call),
614
+ }
615
+ : {}),
616
+ }));
617
+ registerGenericProxyStreamRoute(app, adapter, onInternalError, buildProxyProviderRegistry(deps));
618
+ }
619
+ //# sourceMappingURL=model-proxy.js.map