@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.
- package/LICENSE +201 -0
- package/README.md +107 -0
- package/dist/active-context.d.ts +99 -0
- package/dist/active-context.d.ts.map +1 -0
- package/dist/active-context.js +77 -0
- package/dist/active-context.js.map +1 -0
- package/dist/adapter.d.ts +36 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +92 -0
- package/dist/adapter.js.map +1 -0
- package/dist/agents.d.ts +55 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +23 -0
- package/dist/agents.js.map +1 -0
- package/dist/api-security-middleware.d.ts +146 -0
- package/dist/api-security-middleware.d.ts.map +1 -0
- package/dist/api-security-middleware.js +276 -0
- package/dist/api-security-middleware.js.map +1 -0
- package/dist/attachments.d.ts +317 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +664 -0
- package/dist/attachments.js.map +1 -0
- package/dist/cancel-owned-runs.d.ts +44 -0
- package/dist/cancel-owned-runs.d.ts.map +1 -0
- package/dist/cancel-owned-runs.js +35 -0
- package/dist/cancel-owned-runs.js.map +1 -0
- package/dist/compat.d.ts +22 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +17 -0
- package/dist/compat.js.map +1 -0
- package/dist/component-catalog.d.ts +74 -0
- package/dist/component-catalog.d.ts.map +1 -0
- package/dist/component-catalog.js +74 -0
- package/dist/component-catalog.js.map +1 -0
- package/dist/connectors.d.ts +165 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +474 -0
- package/dist/connectors.js.map +1 -0
- package/dist/daemon-status.d.ts +72 -0
- package/dist/daemon-status.d.ts.map +1 -0
- package/dist/daemon-status.js +60 -0
- package/dist/daemon-status.js.map +1 -0
- package/dist/db-ops.d.ts +116 -0
- package/dist/db-ops.d.ts.map +1 -0
- package/dist/db-ops.js +191 -0
- package/dist/db-ops.js.map +1 -0
- package/dist/delegated-tools.d.ts +46 -0
- package/dist/delegated-tools.d.ts.map +1 -0
- package/dist/delegated-tools.js +155 -0
- package/dist/delegated-tools.js.map +1 -0
- package/dist/express/run-stream.d.ts +15 -0
- package/dist/express/run-stream.d.ts.map +1 -0
- package/dist/express/run-stream.js +11 -0
- package/dist/express/run-stream.js.map +1 -0
- package/dist/express-index.d.ts +32 -0
- package/dist/express-index.d.ts.map +1 -0
- package/dist/express-index.js +14 -0
- package/dist/express-index.js.map +1 -0
- package/dist/fastify/adapter.d.ts +31 -0
- package/dist/fastify/adapter.d.ts.map +1 -0
- package/dist/fastify/adapter.js +64 -0
- package/dist/fastify/adapter.js.map +1 -0
- package/dist/fastify/agents.d.ts +13 -0
- package/dist/fastify/agents.d.ts.map +1 -0
- package/dist/fastify/agents.js +7 -0
- package/dist/fastify/agents.js.map +1 -0
- package/dist/fastify/api-security-middleware.d.ts +64 -0
- package/dist/fastify/api-security-middleware.d.ts.map +1 -0
- package/dist/fastify/api-security-middleware.js +139 -0
- package/dist/fastify/api-security-middleware.js.map +1 -0
- package/dist/fastify/compat.d.ts +22 -0
- package/dist/fastify/compat.d.ts.map +1 -0
- package/dist/fastify/compat.js +16 -0
- package/dist/fastify/compat.js.map +1 -0
- package/dist/fastify/daemon-status.d.ts +22 -0
- package/dist/fastify/daemon-status.d.ts.map +1 -0
- package/dist/fastify/daemon-status.js +9 -0
- package/dist/fastify/daemon-status.js.map +1 -0
- package/dist/fastify/host-tools.d.ts +13 -0
- package/dist/fastify/host-tools.d.ts.map +1 -0
- package/dist/fastify/host-tools.js +8 -0
- package/dist/fastify/host-tools.js.map +1 -0
- package/dist/fastify/index.d.ts +36 -0
- package/dist/fastify/index.d.ts.map +1 -0
- package/dist/fastify/index.js +18 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/fastify/local-daemon-request.d.ts +43 -0
- package/dist/fastify/local-daemon-request.d.ts.map +1 -0
- package/dist/fastify/local-daemon-request.js +155 -0
- package/dist/fastify/local-daemon-request.js.map +1 -0
- package/dist/fastify/origin.d.ts +21 -0
- package/dist/fastify/origin.d.ts.map +1 -0
- package/dist/fastify/origin.js +14 -0
- package/dist/fastify/origin.js.map +1 -0
- package/dist/fastify/request.d.ts +20 -0
- package/dist/fastify/request.d.ts.map +1 -0
- package/dist/fastify/request.js +25 -0
- package/dist/fastify/request.js.map +1 -0
- package/dist/fastify/response.d.ts +20 -0
- package/dist/fastify/response.d.ts.map +1 -0
- package/dist/fastify/response.js +41 -0
- package/dist/fastify/response.js.map +1 -0
- package/dist/fastify/route-registration-guard.d.ts +70 -0
- package/dist/fastify/route-registration-guard.d.ts.map +1 -0
- package/dist/fastify/route-registration-guard.js +69 -0
- package/dist/fastify/route-registration-guard.js.map +1 -0
- package/dist/fastify/run-stream.d.ts +18 -0
- package/dist/fastify/run-stream.d.ts.map +1 -0
- package/dist/fastify/run-stream.js +10 -0
- package/dist/fastify/run-stream.js.map +1 -0
- package/dist/fastify/runs.d.ts +17 -0
- package/dist/fastify/runs.d.ts.map +1 -0
- package/dist/fastify/runs.js +33 -0
- package/dist/fastify/runs.js.map +1 -0
- package/dist/frontend-control.d.ts +131 -0
- package/dist/frontend-control.d.ts.map +1 -0
- package/dist/frontend-control.js +77 -0
- package/dist/frontend-control.js.map +1 -0
- package/dist/frontend-sessions.d.ts +100 -0
- package/dist/frontend-sessions.d.ts.map +1 -0
- package/dist/frontend-sessions.js +180 -0
- package/dist/frontend-sessions.js.map +1 -0
- package/dist/health.d.ts +91 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +53 -0
- package/dist/health.js.map +1 -0
- package/dist/host-tools.d.ts +174 -0
- package/dist/host-tools.d.ts.map +1 -0
- package/dist/host-tools.js +338 -0
- package/dist/host-tools.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/local-daemon-request.d.ts +44 -0
- package/dist/local-daemon-request.d.ts.map +1 -0
- package/dist/local-daemon-request.js +145 -0
- package/dist/local-daemon-request.js.map +1 -0
- package/dist/media.d.ts +131 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +352 -0
- package/dist/media.js.map +1 -0
- package/dist/memory.d.ts +230 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +331 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-proxy.d.ts +45 -0
- package/dist/model-proxy.d.ts.map +1 -0
- package/dist/model-proxy.js +619 -0
- package/dist/model-proxy.js.map +1 -0
- package/dist/origin-validation.d.ts +64 -0
- package/dist/origin-validation.d.ts.map +1 -0
- package/dist/origin-validation.js +226 -0
- package/dist/origin-validation.js.map +1 -0
- package/dist/origin.d.ts +32 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +14 -0
- package/dist/origin.js.map +1 -0
- package/dist/pack-http.d.ts +30 -0
- package/dist/pack-http.d.ts.map +1 -0
- package/dist/pack-http.js +12 -0
- package/dist/pack-http.js.map +1 -0
- package/dist/raw-sse.d.ts +44 -0
- package/dist/raw-sse.d.ts.map +1 -0
- package/dist/raw-sse.js +93 -0
- package/dist/raw-sse.js.map +1 -0
- package/dist/remote-run-events.d.ts +66 -0
- package/dist/remote-run-events.d.ts.map +1 -0
- package/dist/remote-run-events.js +230 -0
- package/dist/remote-run-events.js.map +1 -0
- package/dist/request.d.ts +19 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +25 -0
- package/dist/request.js.map +1 -0
- package/dist/research.d.ts +42 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +316 -0
- package/dist/research.js.map +1 -0
- package/dist/response.d.ts +14 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +45 -0
- package/dist/response.js.map +1 -0
- package/dist/route-manifest.d.ts +31 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +103 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/route-registration-guard.d.ts +65 -0
- package/dist/route-registration-guard.d.ts.map +1 -0
- package/dist/route-registration-guard.js +71 -0
- package/dist/route-registration-guard.js.map +1 -0
- package/dist/routines.d.ts +114 -0
- package/dist/routines.d.ts.map +1 -0
- package/dist/routines.js +320 -0
- package/dist/routines.js.map +1 -0
- package/dist/run-stream.d.ts +60 -0
- package/dist/run-stream.d.ts.map +1 -0
- package/dist/run-stream.js +108 -0
- package/dist/run-stream.js.map +1 -0
- package/dist/runs.d.ts +79 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +232 -0
- package/dist/runs.js.map +1 -0
- package/dist/sse.d.ts +139 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +157 -0
- package/dist/sse.js.map +1 -0
- package/dist/terminals.d.ts +125 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +349 -0
- package/dist/terminals.js.map +1 -0
- package/dist/tool-catalog.d.ts +68 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +68 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-root.d.ts +69 -0
- package/dist/workspace-root.d.ts.map +1 -0
- package/dist/workspace-root.js +63 -0
- package/dist/workspace-root.js.map +1 -0
- package/dist/xai.d.ts +86 -0
- package/dist/xai.d.ts.map +1 -0
- package/dist/xai.js +668 -0
- package/dist/xai.js.map +1 -0
- 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
|