@jxsuite/server 4.3.0 → 4.4.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 +1 -1
- package/package.json +5 -4
- package/src/ai-api.ts +44 -14
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "Jx development server with live reload, server-side proxy, and studio integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,19 +41,20 @@
|
|
|
41
41
|
"upgrade": "bun update --latest && bun install"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@jxsuite/ai": "^0.
|
|
44
|
+
"@jxsuite/ai": "^0.39.1",
|
|
45
45
|
"@jxsuite/catalog": "^0.1.0",
|
|
46
|
-
"@jxsuite/collab": "^0.
|
|
46
|
+
"@jxsuite/collab": "^0.11.0",
|
|
47
47
|
"@jxsuite/compiler": "^4.0.2",
|
|
48
48
|
"@jxsuite/create": "^1.3.12",
|
|
49
49
|
"@jxsuite/import": "^0.40.4",
|
|
50
|
-
"@jxsuite/protocol": "^2.
|
|
50
|
+
"@jxsuite/protocol": "^2.6.0",
|
|
51
51
|
"@jxsuite/runtime": "^4.0.2",
|
|
52
52
|
"@jxsuite/schema": "^2.3.0",
|
|
53
53
|
"@jxsuite/site": "^2.0.2",
|
|
54
54
|
"@jxsuite/starters": "^1.8.2",
|
|
55
55
|
"chokidar": "^5.0.0",
|
|
56
56
|
"kysely": "^0.29.6",
|
|
57
|
+
"oxfmt": "^0.70.0",
|
|
57
58
|
"zod": "^4.6.5"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
package/src/ai-api.ts
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ai-api.js — AI proxy endpoints for Jx Studio
|
|
3
3
|
*
|
|
4
|
-
* Handles /__studio/ai/chat (SSE streaming proxy to OpenAI) and /__studio/ai/models.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles /__studio/ai/chat (SSE streaming proxy to OpenAI) and /__studio/ai/models. The server
|
|
5
|
+
* acts as a thin proxy: validates the request shape, forwards to OpenAI, normalizes the SSE stream
|
|
6
|
+
* into StreamEvent-compatible format, and pipes back.
|
|
7
7
|
*
|
|
8
8
|
* The wire half of that (reading the body, the upstream request, the stream normalizer, the SSE
|
|
9
9
|
* framing, the problem responses) is `@jxsuite/ai/gateway`, which every backend serving these
|
|
10
10
|
* routes can share. What stays here is this server's POLICY, which no other host shares:
|
|
11
11
|
*
|
|
12
12
|
* API key flow:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* 1. Request header X-Api-Key or Authorization: Bearer <key>
|
|
15
|
+
* 2. Fallback: OPENAI_API_KEY env var — attached ONLY to the env/default base URL, never to a
|
|
16
|
+
* caller-supplied X-Api-Base-URL (prevents exfiltrating the server key to a chosen endpoint)
|
|
17
|
+
* 3. If neither → 401 with error message
|
|
17
18
|
*
|
|
18
19
|
* Base URL flow:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* 1. Request header X-Api-Base-URL — allowed only alongside a header API key
|
|
22
|
+
* 2. Fallback: OPENAI_BASE_URL env var
|
|
23
|
+
* 3. Default: https://api.openai.com/v1
|
|
24
|
+
*
|
|
25
|
+
* A base URL resolving to a cloud metadata / link-local host is refused (SSRF defense).
|
|
26
|
+
*
|
|
27
|
+
* Prompt-cache affinity: a request carrying a valid X-Jx-Ai-Session header (the client's
|
|
28
|
+
* conversation id) is forwarded with `sessionAffinity` set to a hash of it, scoped to this server
|
|
29
|
+
* ("local"). The gateway sends that hash only to an upstream that routes on it (OpenAI, Workers
|
|
30
|
+
* AI); the raw id never leaves this process. An absent or malformed header just means no hint: the
|
|
31
|
+
* request is still served.
|
|
23
32
|
*
|
|
24
33
|
* The model catalogue for /models is this server's too: its defaults, and how it reads the
|
|
25
34
|
* upstream's own listing.
|
|
@@ -27,8 +36,11 @@
|
|
|
27
36
|
* @license MIT
|
|
28
37
|
*/
|
|
29
38
|
import {
|
|
39
|
+
AI_SESSION_HEADER,
|
|
40
|
+
affinityKey,
|
|
30
41
|
createChatHandler,
|
|
31
42
|
extractUpstreamErrorMessage,
|
|
43
|
+
isAiSessionId,
|
|
32
44
|
modelsResponse,
|
|
33
45
|
problemResponse,
|
|
34
46
|
} from "@jxsuite/ai/gateway";
|
|
@@ -43,6 +55,12 @@ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
|
43
55
|
/** The model a chat request that names none is forwarded with. */
|
|
44
56
|
const DEFAULT_MODEL = "gpt-4o";
|
|
45
57
|
|
|
58
|
+
/**
|
|
59
|
+
* This server's namespace for affinity keys. Every dev/desktop server shares it: their keys never
|
|
60
|
+
* meet a hosted backend's, because that one hashes under its own scope.
|
|
61
|
+
*/
|
|
62
|
+
const AFFINITY_SCOPE = "local";
|
|
63
|
+
|
|
46
64
|
/** A model entry from the upstream `/models` listing. */
|
|
47
65
|
interface ModelEntry {
|
|
48
66
|
id: string;
|
|
@@ -151,9 +169,10 @@ function refuse(status: number, message: string): GatewayRefusal {
|
|
|
151
169
|
|
|
152
170
|
/**
|
|
153
171
|
* This server's upstream for a request, or its refusal: the key-provenance and SSRF rules above,
|
|
154
|
-
* applied per request (the environment is read each time, so a key set later is honoured).
|
|
172
|
+
* applied per request (the environment is read each time, so a key set later is honoured). A valid
|
|
173
|
+
* session header adds the hashed affinity key; an invalid one is ignored rather than refused.
|
|
155
174
|
*/
|
|
156
|
-
function resolveUpstream(req: Request): Upstream | GatewayRefusal {
|
|
175
|
+
async function resolveUpstream(req: Request): Promise<Upstream | GatewayRefusal> {
|
|
157
176
|
const { apiKey, baseUrl, reject } = getConfig(req);
|
|
158
177
|
if (reject) {
|
|
159
178
|
return refuse(reject.status, reject.message);
|
|
@@ -164,7 +183,18 @@ function resolveUpstream(req: Request): Upstream | GatewayRefusal {
|
|
|
164
183
|
"No API key configured. Set OPENAI_API_KEY env var or send X-Api-Key header.",
|
|
165
184
|
);
|
|
166
185
|
}
|
|
167
|
-
|
|
186
|
+
const upstream: Upstream = {
|
|
187
|
+
apiKey,
|
|
188
|
+
baseUrl,
|
|
189
|
+
defaultModel: DEFAULT_MODEL,
|
|
190
|
+
family: "openai-compat",
|
|
191
|
+
managed: false,
|
|
192
|
+
};
|
|
193
|
+
const sessionId = req.headers.get(AI_SESSION_HEADER);
|
|
194
|
+
if (isAiSessionId(sessionId)) {
|
|
195
|
+
return { ...upstream, sessionAffinity: await affinityKey(AFFINITY_SCOPE, sessionId) };
|
|
196
|
+
}
|
|
197
|
+
return upstream;
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
// ─── /__studio/ai/chat — SSE streaming proxy ───────────────────────────────
|