libfx 0.0.7-dev.609.g1f98d14929a0 → 0.0.7-dev.630.gc07d0e40d42d
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/README.md +49 -10
- package/browser.js +2 -1
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +189 -2
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/node.js +19 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,11 @@ filesystem read when imported.
|
|
|
19
19
|
import { createFxAgent } from "libfx";
|
|
20
20
|
|
|
21
21
|
const agent = await createFxAgent({
|
|
22
|
-
|
|
22
|
+
apiKey: process.env.AI_GATEWAY_API_KEY,
|
|
23
|
+
model: "google/gemini-2.5-flash-lite",
|
|
24
|
+
onEvent(event) {
|
|
25
|
+
if (event.type === "transport.response") console.log(event.elapsedMs);
|
|
26
|
+
},
|
|
23
27
|
});
|
|
24
28
|
|
|
25
29
|
const turn = agent.prompt("Explain this project.");
|
|
@@ -33,6 +37,20 @@ const checkpoint = await agent.checkpoint();
|
|
|
33
37
|
await agent.close();
|
|
34
38
|
```
|
|
35
39
|
|
|
40
|
+
`apiKey` is required. `model` is optional and defaults to fx's built-in model.
|
|
41
|
+
Agent configuration uses named options; `env` is reserved for
|
|
42
|
+
`createFxTerminal()`.
|
|
43
|
+
|
|
44
|
+
The host selects the model. Agent creation and prompting do not fetch the
|
|
45
|
+
Gateway model catalog.
|
|
46
|
+
|
|
47
|
+
`onEvent` receives runtime diagnostics separately from model output. Transport
|
|
48
|
+
events report request start, response status and elapsed time, safe Gateway
|
|
49
|
+
request metadata, and failures. Credentials and raw headers are never included.
|
|
50
|
+
|
|
51
|
+
libfx makes at most one automatic retry after a retryable transport failure and
|
|
52
|
+
only before model output or tool effects escape. Cancellation prevents a retry.
|
|
53
|
+
|
|
36
54
|
`prompt(input, { signal? })` accepts a string or text/resource blocks. It
|
|
37
55
|
returns an async iterable of normalized events:
|
|
38
56
|
|
|
@@ -46,17 +64,36 @@ opaque, bounded, versioned bytes. Restore them only when creating a fresh
|
|
|
46
64
|
agent:
|
|
47
65
|
|
|
48
66
|
```js
|
|
49
|
-
const restored = await createFxAgent({
|
|
67
|
+
const restored = await createFxAgent({ apiKey, model, checkpoint });
|
|
50
68
|
```
|
|
51
69
|
|
|
52
70
|
The checkpoint contains conversation history and usage only. The host owns
|
|
53
71
|
durable storage and must resupply models, credentials, instructions, tools,
|
|
54
72
|
MCP clients, and skill records.
|
|
55
73
|
|
|
74
|
+
## Models
|
|
75
|
+
|
|
76
|
+
Model discovery is explicit and does not create an Agent or load native or Wasm
|
|
77
|
+
artifacts:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { listModels } from "libfx";
|
|
81
|
+
|
|
82
|
+
const models = await listModels({
|
|
83
|
+
apiKey: process.env.AI_GATEWAY_API_KEY,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`listModels()` performs one bounded Gateway request and returns sorted, unique
|
|
88
|
+
language-model IDs. It accepts the same optional `fetch` override as the Agent
|
|
89
|
+
API.
|
|
90
|
+
|
|
56
91
|
## JavaScript tools and instructions
|
|
57
92
|
|
|
58
93
|
```js
|
|
59
94
|
const agent = await createFxAgent({
|
|
95
|
+
apiKey,
|
|
96
|
+
model,
|
|
60
97
|
instructions: "Keep answers concise.",
|
|
61
98
|
tools: [{
|
|
62
99
|
name: "lookup",
|
|
@@ -70,14 +107,15 @@ const agent = await createFxAgent({
|
|
|
70
107
|
return database.get(input.key, { signal });
|
|
71
108
|
},
|
|
72
109
|
}],
|
|
73
|
-
env,
|
|
74
110
|
});
|
|
75
111
|
```
|
|
76
112
|
|
|
77
113
|
The JavaScript host is the authority for tool effects. The same descriptors,
|
|
78
114
|
schemas, cancellation, results, and events are used by N-API and WebAssembly.
|
|
79
115
|
Instructions are limited to 64 KiB of UTF-8 text, including text assembled by
|
|
80
|
-
the MCP and skills adapters.
|
|
116
|
+
the MCP and skills adapters. They are the complete host-owned system context:
|
|
117
|
+
libfx adds no hidden base prompt, and omitting `instructions` sends no system
|
|
118
|
+
message.
|
|
81
119
|
|
|
82
120
|
## MCP
|
|
83
121
|
|
|
@@ -94,9 +132,10 @@ const mcp = await createMcpAdapter(client, {
|
|
|
94
132
|
});
|
|
95
133
|
|
|
96
134
|
const agent = await createFxAgent({
|
|
135
|
+
apiKey,
|
|
136
|
+
model,
|
|
97
137
|
tools: mcp.tools,
|
|
98
138
|
instructions: mcp.instructions,
|
|
99
|
-
env,
|
|
100
139
|
});
|
|
101
140
|
|
|
102
141
|
// ...
|
|
@@ -115,15 +154,15 @@ import { createSkillsAdapter } from "libfx/skills";
|
|
|
115
154
|
|
|
116
155
|
const record = await loadSkillFile("./skills/review/SKILL.md");
|
|
117
156
|
const skills = createSkillsAdapter([record]);
|
|
118
|
-
const agent = await createFxAgent({ ...skills
|
|
157
|
+
const agent = await createFxAgent({ apiKey, model, ...skills });
|
|
119
158
|
```
|
|
120
159
|
|
|
121
160
|
## Backends
|
|
122
161
|
|
|
123
162
|
```js
|
|
124
|
-
await createFxAgent({ backend: "auto" }); // native, then Wasm fallback
|
|
125
|
-
await createFxAgent({ backend: "native" }); // require N-API
|
|
126
|
-
await createFxAgent({ backend: "wasm" }); // require Wasm + JSPI
|
|
163
|
+
await createFxAgent({ apiKey, backend: "auto" }); // native, then Wasm fallback
|
|
164
|
+
await createFxAgent({ apiKey, backend: "native" }); // require N-API
|
|
165
|
+
await createFxAgent({ apiKey, backend: "wasm" }); // require Wasm + JSPI
|
|
127
166
|
```
|
|
128
167
|
|
|
129
168
|
Within one JavaScript realm, libfx compiles each stable Wasm source once and
|
|
@@ -156,7 +195,7 @@ stores remain terminal-only host integrations.
|
|
|
156
195
|
|
|
157
196
|
## Security
|
|
158
197
|
|
|
159
|
-
Treat `nativeAddon` and `
|
|
198
|
+
Treat `nativeAddon` and `gatewayChatUrl` as trusted host
|
|
160
199
|
configuration. Do not embed long-lived credentials in public browser code.
|
|
161
200
|
Host tool functions, MCP clients, and skill loaders retain their own authority;
|
|
162
201
|
libfx validates and sequences them but does not grant operating-system access.
|
package/browser.js
CHANGED
|
@@ -3,11 +3,12 @@ import {
|
|
|
3
3
|
createFxTerminal as createWasmTerminal,
|
|
4
4
|
encodeXtermKeyEvent,
|
|
5
5
|
fxSdkApiVersion,
|
|
6
|
+
listModels,
|
|
6
7
|
supportsJspi,
|
|
7
8
|
xtermAdapter,
|
|
8
9
|
} from "./fx-sdk.js";
|
|
9
10
|
|
|
10
|
-
export { encodeXtermKeyEvent, fxSdkApiVersion, supportsJspi, xtermAdapter };
|
|
11
|
+
export { encodeXtermKeyEvent, fxSdkApiVersion, listModels, supportsJspi, xtermAdapter };
|
|
11
12
|
export const libfxApiVersion = 2;
|
|
12
13
|
|
|
13
14
|
const defaultCoreWasm = new URL("./fx-core.wasm", import.meta.url).href;
|
package/fx-core.wasm
CHANGED
|
Binary file
|
package/fx-sdk.js
CHANGED
|
@@ -5,8 +5,145 @@ const workspaceInfoLimit = 4 * 1024;
|
|
|
5
5
|
const workspaceCommandLimit = 64 * 1024;
|
|
6
6
|
const workspaceOutputLimit = 64 * 1024;
|
|
7
7
|
const maxInstructionsBytes = 64 * 1024;
|
|
8
|
+
const maxApiKeyBytes = 64 * 1024;
|
|
9
|
+
const maxModelBytes = 1024;
|
|
10
|
+
const maxUrlBytes = 16 * 1024;
|
|
11
|
+
const maxModelCatalogBytes = 4 * 1024 * 1024;
|
|
12
|
+
const maxModelCatalogEntries = 10_000;
|
|
8
13
|
const streamReadsPerTaskYield = 32;
|
|
9
14
|
|
|
15
|
+
function boundedString(value, name, maxBytes, required) {
|
|
16
|
+
if (value === undefined && !required) return undefined;
|
|
17
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
18
|
+
throw new TypeError(`${name} ${required ? "is required and " : ""}must be a non-empty string`);
|
|
19
|
+
}
|
|
20
|
+
if (encoder.encode(value).length > maxBytes) {
|
|
21
|
+
throw new RangeError(`${name} exceeds the ${maxBytes} byte libfx limit`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateGatewayChatUrl(value) {
|
|
27
|
+
if (value === undefined) return;
|
|
28
|
+
boundedString(value, "gatewayChatUrl", maxUrlBytes, false);
|
|
29
|
+
let url;
|
|
30
|
+
try { url = new URL(value); } catch { throw new TypeError("gatewayChatUrl must be a valid URL"); }
|
|
31
|
+
if (url.username || url.password || url.hash) {
|
|
32
|
+
throw new TypeError("gatewayChatUrl must not contain credentials or a fragment");
|
|
33
|
+
}
|
|
34
|
+
if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
|
|
35
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
|
|
36
|
+
if (url.protocol !== "http:" || !loopback || !url.port) {
|
|
37
|
+
throw new TypeError("gatewayChatUrl must use the canonical Gateway or explicit loopback HTTP");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeAgentOptions(value) {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
43
|
+
throw new TypeError("createFxAgent() options must be an object");
|
|
44
|
+
}
|
|
45
|
+
const options = { ...value };
|
|
46
|
+
if (Object.hasOwn(options, "env")) {
|
|
47
|
+
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
48
|
+
}
|
|
49
|
+
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
50
|
+
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
51
|
+
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
52
|
+
return options;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function agentEnvironment(options) {
|
|
56
|
+
return {
|
|
57
|
+
AI_GATEWAY_API_KEY: options.apiKey,
|
|
58
|
+
...(options.model === undefined ? {} : { FX_MODEL: options.model }),
|
|
59
|
+
...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function cancelResponseBody(response) {
|
|
64
|
+
try {
|
|
65
|
+
await response.body?.cancel();
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readBoundedResponseText(response, limit) {
|
|
70
|
+
const declared = Number(response.headers.get("content-length"));
|
|
71
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
72
|
+
await cancelResponseBody(response);
|
|
73
|
+
throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
|
|
74
|
+
}
|
|
75
|
+
if (!response.body) {
|
|
76
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
77
|
+
if (bytes.length > limit) throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
|
|
78
|
+
return strictDecoder.decode(bytes);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const reader = response.body.getReader();
|
|
82
|
+
const chunks = [];
|
|
83
|
+
let total = 0;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const { done, value } = await reader.read();
|
|
86
|
+
if (done) break;
|
|
87
|
+
if (!value?.length) continue;
|
|
88
|
+
total += value.length;
|
|
89
|
+
if (total > limit) {
|
|
90
|
+
try {
|
|
91
|
+
await reader.cancel();
|
|
92
|
+
} catch {}
|
|
93
|
+
throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
|
|
94
|
+
}
|
|
95
|
+
chunks.push(value);
|
|
96
|
+
}
|
|
97
|
+
const bytes = new Uint8Array(total);
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const chunk of chunks) {
|
|
100
|
+
bytes.set(chunk, offset);
|
|
101
|
+
offset += chunk.length;
|
|
102
|
+
}
|
|
103
|
+
return strictDecoder.decode(bytes);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function listModels(options = {}) {
|
|
107
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
108
|
+
throw new TypeError("listModels() options must be an object");
|
|
109
|
+
}
|
|
110
|
+
const apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
111
|
+
const fetchModels = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
112
|
+
if (typeof fetchModels !== "function") throw new TypeError("fetch is unavailable");
|
|
113
|
+
const response = await fetchModels("https://ai-gateway.vercel.sh/coding-agent/v1/models", {
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
await cancelResponseBody(response);
|
|
119
|
+
throw new Error(`model catalog request failed with HTTP ${response.status}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let catalog;
|
|
123
|
+
try {
|
|
124
|
+
catalog = JSON.parse(await readBoundedResponseText(response, maxModelCatalogBytes));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof RangeError) throw error;
|
|
127
|
+
throw new TypeError("model catalog response is malformed");
|
|
128
|
+
}
|
|
129
|
+
if (!catalog || typeof catalog !== "object" || !Array.isArray(catalog.data)) {
|
|
130
|
+
throw new TypeError("model catalog response is malformed");
|
|
131
|
+
}
|
|
132
|
+
if (catalog.data.length > maxModelCatalogEntries) {
|
|
133
|
+
throw new RangeError(`model catalog exceeds the ${maxModelCatalogEntries} entry libfx limit`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ids = new Set();
|
|
137
|
+
for (const entry of catalog.data) {
|
|
138
|
+
if (!entry || typeof entry !== "object") continue;
|
|
139
|
+
if (typeof entry.type === "string" && entry.type.toLowerCase() !== "language") continue;
|
|
140
|
+
if (typeof entry.id !== "string" || entry.id.length === 0) continue;
|
|
141
|
+
if (encoder.encode(entry.id).length > maxModelBytes) continue;
|
|
142
|
+
ids.add(entry.id);
|
|
143
|
+
}
|
|
144
|
+
return [...ids].sort();
|
|
145
|
+
}
|
|
146
|
+
|
|
10
147
|
function validWorkspacePath(path) {
|
|
11
148
|
if (typeof path !== "string" || !path.startsWith("/") || path.includes("\0")) return false;
|
|
12
149
|
if (strictDecoder.decode(encoder.encode(path)) !== path) return false;
|
|
@@ -1014,7 +1151,7 @@ function base64ToBytes(value) {
|
|
|
1014
1151
|
}
|
|
1015
1152
|
|
|
1016
1153
|
export async function createFxAgent(options = {}) {
|
|
1017
|
-
options =
|
|
1154
|
+
options = normalizeAgentOptions(options);
|
|
1018
1155
|
const hostTools = normalizeHostTools(options.tools);
|
|
1019
1156
|
const instructions = normalizeInstructions(options.instructions);
|
|
1020
1157
|
const initialCheckpoint = checkpointBytes(options.checkpoint);
|
|
@@ -1026,6 +1163,49 @@ export async function createFxAgent(options = {}) {
|
|
|
1026
1163
|
const emit = (type, detail = {}) => {
|
|
1027
1164
|
try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
|
|
1028
1165
|
};
|
|
1166
|
+
const hostFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
1167
|
+
const transportFetch = async (input, init = {}) => {
|
|
1168
|
+
const method = String(init.method ?? input?.method ?? "GET").toUpperCase();
|
|
1169
|
+
let endpoint = String(input?.url ?? input);
|
|
1170
|
+
try {
|
|
1171
|
+
const url = new URL(endpoint);
|
|
1172
|
+
endpoint = `${url.origin}${url.pathname}`;
|
|
1173
|
+
} catch {}
|
|
1174
|
+
for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
|
|
1175
|
+
const startedAt = performance.now();
|
|
1176
|
+
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1177
|
+
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1178
|
+
try {
|
|
1179
|
+
if (!hostFetch) throw new TypeError("fetch is unavailable");
|
|
1180
|
+
const response = await hostFetch(input, init);
|
|
1181
|
+
const headers = response.headers;
|
|
1182
|
+
emit("transport.response", {
|
|
1183
|
+
attempt,
|
|
1184
|
+
status: response.status,
|
|
1185
|
+
elapsedMs: performance.now() - startedAt,
|
|
1186
|
+
requestId: headers.get("x-vercel-id"),
|
|
1187
|
+
generationId: headers.get("x-generation-id"),
|
|
1188
|
+
model: headers.get("x-model-id") ?? options.model,
|
|
1189
|
+
provider: headers.get("x-vercel-ai-gateway-provider") ?? headers.get("x-ai-gateway-provider"),
|
|
1190
|
+
});
|
|
1191
|
+
return response;
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
const errorName = error instanceof Error ? error.name : "Error";
|
|
1194
|
+
const elapsedMs = performance.now() - startedAt;
|
|
1195
|
+
emit("transport.error", { attempt, elapsedMs, error: errorName });
|
|
1196
|
+
if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1197
|
+
if (attemptIndex === 1) throw error;
|
|
1198
|
+
emit("transport.retry", {
|
|
1199
|
+
attempt,
|
|
1200
|
+
nextAttempt: attempt + 1,
|
|
1201
|
+
elapsedMs,
|
|
1202
|
+
error: errorName,
|
|
1203
|
+
});
|
|
1204
|
+
if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
throw new Error("transport retry exhausted");
|
|
1208
|
+
};
|
|
1029
1209
|
const executeHostTool = async (name, input, requestedSessionId) => {
|
|
1030
1210
|
const execute = hostTools.executors.get(name);
|
|
1031
1211
|
const turn = requestedSessionId === undefined || requestedSessionId === sessionId
|
|
@@ -1047,7 +1227,13 @@ export async function createFxAgent(options = {}) {
|
|
|
1047
1227
|
return { content, isError, cancelled: controller.signal.aborted };
|
|
1048
1228
|
};
|
|
1049
1229
|
emit("runtime.start");
|
|
1050
|
-
const runtimeOptions = {
|
|
1230
|
+
const runtimeOptions = {
|
|
1231
|
+
...options,
|
|
1232
|
+
fetch: transportFetch,
|
|
1233
|
+
args: ["acp"],
|
|
1234
|
+
env: agentEnvironment(options),
|
|
1235
|
+
hostToolExecutor: executeHostTool,
|
|
1236
|
+
};
|
|
1051
1237
|
const runtime = options.runtimeFactory
|
|
1052
1238
|
? await options.runtimeFactory(runtimeOptions)
|
|
1053
1239
|
: await instantiate(runtimeOptions);
|
|
@@ -1219,6 +1405,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1219
1405
|
const turn = {
|
|
1220
1406
|
push(update) { const waiter = waiters.shift(); if (waiter) waiter({ value: update, done: false }); else queue.push(update); },
|
|
1221
1407
|
toolControllers,
|
|
1408
|
+
transportAttempts: 0,
|
|
1222
1409
|
cancel() {
|
|
1223
1410
|
if (finished || cancelled) return;
|
|
1224
1411
|
cancelled = true;
|
package/fx-term.wasm
CHANGED
|
Binary file
|
package/libfx.darwin-arm64.node
CHANGED
|
Binary file
|
package/libfx.darwin-x64.node
CHANGED
|
Binary file
|
package/libfx.linux-arm64.node
CHANGED
|
Binary file
|
package/libfx.linux-x64.node
CHANGED
|
Binary file
|
package/node.js
CHANGED
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
createFxTerminal as createWasmTerminal,
|
|
9
9
|
encodeXtermKeyEvent,
|
|
10
10
|
fxSdkApiVersion,
|
|
11
|
+
listModels,
|
|
11
12
|
supportsJspi,
|
|
12
13
|
xtermAdapter,
|
|
13
14
|
} from "./fx-sdk.js";
|
|
14
15
|
|
|
15
|
-
export { encodeXtermKeyEvent, fxSdkApiVersion, supportsJspi, xtermAdapter };
|
|
16
|
+
export { encodeXtermKeyEvent, fxSdkApiVersion, listModels, supportsJspi, xtermAdapter };
|
|
16
17
|
export const libfxApiVersion = 2;
|
|
17
18
|
|
|
18
19
|
const fetchOperationStale = 0;
|
|
@@ -67,9 +68,8 @@ function validateNativeBackend(backend) {
|
|
|
67
68
|
const actualVersion = backend.libfxApiVersion ?? "missing";
|
|
68
69
|
throw new Error(`native addon API version ${actualVersion} is incompatible with libfx API version ${libfxApiVersion}`);
|
|
69
70
|
}
|
|
70
|
-
if (typeof backend.
|
|
71
|
-
|
|
72
|
-
throw new Error("native addon must export createFxAgent(), createCore(), or createFxTerminal()");
|
|
71
|
+
if (typeof backend.createCore !== "function" && typeof backend.createFxTerminal !== "function") {
|
|
72
|
+
throw new Error("native addon must export createCore() or createFxTerminal()");
|
|
73
73
|
}
|
|
74
74
|
return backend;
|
|
75
75
|
}
|
|
@@ -120,26 +120,8 @@ function wasmBytes(input) {
|
|
|
120
120
|
return pending;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function validateGatewayChatUrl(value) {
|
|
124
|
-
if (value === undefined) return;
|
|
125
|
-
if (typeof value !== "string") throw new TypeError("FX_GATEWAY_CHAT_URL must be a string");
|
|
126
|
-
let url;
|
|
127
|
-
try { url = new URL(value); } catch { throw new TypeError("FX_GATEWAY_CHAT_URL must be a valid URL"); }
|
|
128
|
-
if (url.username || url.password || url.hash) {
|
|
129
|
-
throw new TypeError("FX_GATEWAY_CHAT_URL must not contain credentials or a fragment");
|
|
130
|
-
}
|
|
131
|
-
if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
|
|
132
|
-
const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
|
|
133
|
-
if (url.protocol !== "http:" || !loopback || !url.port) {
|
|
134
|
-
throw new TypeError("FX_GATEWAY_CHAT_URL must use the canonical Gateway or explicit loopback HTTP");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
123
|
function createNativeCoreRuntime(addon, options) {
|
|
139
|
-
const apiKey = options
|
|
140
|
-
const model = options.env?.FX_MODEL;
|
|
141
|
-
const gatewayChatUrl = options.env?.FX_GATEWAY_CHAT_URL;
|
|
142
|
-
validateGatewayChatUrl(gatewayChatUrl);
|
|
124
|
+
const { apiKey, model, gatewayChatUrl } = options;
|
|
143
125
|
const core = addon.createCore({
|
|
144
126
|
apiKey,
|
|
145
127
|
home: options.home ?? homedir(),
|
|
@@ -258,7 +240,6 @@ function createNativeAgent(addon, options) {
|
|
|
258
240
|
|
|
259
241
|
async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
|
|
260
242
|
const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
|
|
261
|
-
validateGatewayChatUrl(runtimeOptions.env?.FX_GATEWAY_CHAT_URL);
|
|
262
243
|
if (!new Set(["auto", "native", "wasm"]).has(backend)) {
|
|
263
244
|
throw new TypeError('backend must be "auto", "native", or "wasm"');
|
|
264
245
|
}
|
|
@@ -268,14 +249,11 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
268
249
|
if (backend !== "wasm") {
|
|
269
250
|
const native = await resolveNativeBackend(nativeAddon);
|
|
270
251
|
nativeError = native.error;
|
|
271
|
-
if (typeof native.backend?.[nativeMethod] === "function"
|
|
272
|
-
(surface === "agent" && typeof native.backend?.createCore === "function")) {
|
|
252
|
+
if (typeof native.backend?.[nativeMethod] === "function") {
|
|
273
253
|
nativeAttempted = true;
|
|
274
254
|
try {
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
return await createNativeAgent(native.backend, runtimeOptions);
|
|
255
|
+
if (surface === "agent") return await createNativeAgent(native.backend, runtimeOptions);
|
|
256
|
+
return await native.backend[nativeMethod](runtimeOptions);
|
|
279
257
|
} catch (error) {
|
|
280
258
|
nativeError = error;
|
|
281
259
|
if (backend === "native") throw error;
|
|
@@ -298,8 +276,17 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
298
276
|
});
|
|
299
277
|
}
|
|
300
278
|
|
|
301
|
-
export function createFxAgent(options = {}) {
|
|
302
|
-
|
|
279
|
+
export async function createFxAgent(options = {}) {
|
|
280
|
+
if (options != null && Object.hasOwn(Object(options), "env")) {
|
|
281
|
+
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
282
|
+
}
|
|
283
|
+
return createWithFallback(
|
|
284
|
+
"agent",
|
|
285
|
+
"createCore",
|
|
286
|
+
createWasmAgent,
|
|
287
|
+
defaultCoreWasm,
|
|
288
|
+
options,
|
|
289
|
+
);
|
|
303
290
|
}
|
|
304
291
|
|
|
305
292
|
export function createFxTerminal(options = {}) {
|