@love-moon/conductor-cli 0.2.38 → 0.2.40
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/bin/conductor-config.js +16 -0
- package/bin/conductor-fire.js +532 -155
- package/bin/conductor-serve-ai.js +145 -0
- package/bin/conductor.js +5 -1
- package/package.json +6 -6
- package/src/ai-manager-handlers.js +51 -47
- package/src/daemon.js +346 -125
- package/src/fire/resume.js +498 -107
- package/src/handoff-log-mask.js +64 -0
- package/src/runtime-backends.js +111 -18
- package/src/serve-ai/adapter.js +383 -0
- package/src/serve-ai/config.js +133 -0
- package/src/serve-ai/errors.js +28 -0
- package/src/serve-ai/image-handler.js +92 -0
- package/src/serve-ai/index.js +529 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { createAiSession } from "@love-moon/ai-sdk";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildChatTurn,
|
|
6
|
+
normalizeResponseFormat,
|
|
7
|
+
normalizeStructuredOutputResult,
|
|
8
|
+
toOpenAiChatCompletion,
|
|
9
|
+
} from "./adapter.js";
|
|
10
|
+
import { loadServeAiRuntimeConfig } from "./config.js";
|
|
11
|
+
import { sendJson, sendOpenAiError } from "./errors.js";
|
|
12
|
+
import { materializeImageInputs } from "./image-handler.js";
|
|
13
|
+
import {
|
|
14
|
+
listAdvertisedBackends,
|
|
15
|
+
normalizeRuntimeBackendAlias,
|
|
16
|
+
normalizeRuntimeBackendName,
|
|
17
|
+
resolveConfiguredRuntimeBackend,
|
|
18
|
+
isRuntimeSupportedBackend,
|
|
19
|
+
} from "../runtime-backends.js";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
22
|
+
const DEFAULT_PORT = 8787;
|
|
23
|
+
const DEFAULT_REQUEST_BODY_LIMIT = 25 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
function createHttpError(
|
|
26
|
+
message,
|
|
27
|
+
{ statusCode = 400, type = "invalid_request_error", code = "invalid_request", param = null } = {},
|
|
28
|
+
) {
|
|
29
|
+
const error = new Error(String(message || "request error"));
|
|
30
|
+
error.statusCode = statusCode;
|
|
31
|
+
error.openAiType = type;
|
|
32
|
+
error.openAiCode = code;
|
|
33
|
+
error.openAiParam = param;
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePositiveInt(value, fallback) {
|
|
38
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
39
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePort(value, fallback) {
|
|
43
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
44
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseCommandParts(commandLine) {
|
|
48
|
+
const input = String(commandLine || "").trim();
|
|
49
|
+
if (!input) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parts = [];
|
|
54
|
+
let current = "";
|
|
55
|
+
let quote = "";
|
|
56
|
+
let escaping = false;
|
|
57
|
+
let tokenStarted = false;
|
|
58
|
+
|
|
59
|
+
for (const char of input) {
|
|
60
|
+
if (escaping) {
|
|
61
|
+
current += char;
|
|
62
|
+
tokenStarted = true;
|
|
63
|
+
escaping = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (char === "\\") {
|
|
68
|
+
escaping = true;
|
|
69
|
+
tokenStarted = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (quote) {
|
|
74
|
+
if (char === quote) {
|
|
75
|
+
quote = "";
|
|
76
|
+
} else {
|
|
77
|
+
current += char;
|
|
78
|
+
}
|
|
79
|
+
tokenStarted = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (char === "'" || char === "\"") {
|
|
84
|
+
quote = char;
|
|
85
|
+
tokenStarted = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (/\s/.test(char)) {
|
|
90
|
+
if (tokenStarted) {
|
|
91
|
+
parts.push(current);
|
|
92
|
+
current = "";
|
|
93
|
+
tokenStarted = false;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
current += char;
|
|
99
|
+
tokenStarted = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (tokenStarted) {
|
|
103
|
+
parts.push(current);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function extractModelOptionFromCommandLine(commandLine) {
|
|
110
|
+
const parts = parseCommandParts(commandLine);
|
|
111
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
112
|
+
const token = String(parts[index] || "").trim();
|
|
113
|
+
if (!token) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (token === "--model") {
|
|
117
|
+
const next = String(parts[index + 1] || "").trim();
|
|
118
|
+
return next || "";
|
|
119
|
+
}
|
|
120
|
+
if (token.startsWith("--model=")) {
|
|
121
|
+
return token.slice("--model=".length).trim();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveAiSessionCommandLine(backend, allowCliList, env = process.env, sessionBackend = backend) {
|
|
128
|
+
const normalizedBackend = normalizeRuntimeBackendName(backend);
|
|
129
|
+
const normalizedSessionBackend = normalizeRuntimeBackendName(sessionBackend);
|
|
130
|
+
const envKeyByBackend = {
|
|
131
|
+
codex: "CONDUCTOR_CODEX_APP_SERVER_COMMAND",
|
|
132
|
+
opencode: "CONDUCTOR_OPENCODE_COMMAND",
|
|
133
|
+
kimi: "CONDUCTOR_KIMI_COMMAND",
|
|
134
|
+
};
|
|
135
|
+
const envKey = envKeyByBackend[normalizedSessionBackend];
|
|
136
|
+
const preferredEnvCommand = envKey && typeof env?.[envKey] === "string" ? env[envKey].trim() : "";
|
|
137
|
+
if (preferredEnvCommand) {
|
|
138
|
+
return preferredEnvCommand;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const configuredCommand =
|
|
142
|
+
allowCliList && typeof allowCliList === "object"
|
|
143
|
+
? typeof allowCliList[normalizedBackend] === "string"
|
|
144
|
+
? allowCliList[normalizedBackend].trim()
|
|
145
|
+
: typeof allowCliList[normalizedSessionBackend] === "string"
|
|
146
|
+
? allowCliList[normalizedSessionBackend].trim()
|
|
147
|
+
: ""
|
|
148
|
+
: "";
|
|
149
|
+
const daemonCommand =
|
|
150
|
+
typeof env?.CONDUCTOR_CLI_COMMAND === "string" ? env.CONDUCTOR_CLI_COMMAND.trim() : "";
|
|
151
|
+
const resolvedCommand = configuredCommand || daemonCommand;
|
|
152
|
+
if (!resolvedCommand) {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
if (normalizedSessionBackend === "codex") {
|
|
156
|
+
if (/\bapp-server\b/.test(resolvedCommand)) {
|
|
157
|
+
return resolvedCommand;
|
|
158
|
+
}
|
|
159
|
+
return `${resolvedCommand} app-server --listen stdio://`;
|
|
160
|
+
}
|
|
161
|
+
return resolvedCommand;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveAiSessionOptions(backend, allowCliList, env = process.env, sessionBackend = backend) {
|
|
165
|
+
const commandLine = resolveAiSessionCommandLine(backend, allowCliList, env, sessionBackend);
|
|
166
|
+
const model = extractModelOptionFromCommandLine(commandLine);
|
|
167
|
+
return model ? { model } : {};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseAuthorizationHeader(headerValue) {
|
|
171
|
+
const normalized = String(headerValue || "").trim();
|
|
172
|
+
if (!normalized) {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
const match = normalized.match(/^Bearer\s+(.+)$/i);
|
|
176
|
+
return match ? match[1].trim() : normalized;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readJsonBody(req, { maxBytes = DEFAULT_REQUEST_BODY_LIMIT } = {}) {
|
|
180
|
+
const chunks = [];
|
|
181
|
+
let totalBytes = 0;
|
|
182
|
+
for await (const chunk of req) {
|
|
183
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
184
|
+
totalBytes += buffer.byteLength;
|
|
185
|
+
if (totalBytes > maxBytes) {
|
|
186
|
+
throw createHttpError(`request body exceeds ${maxBytes} bytes`, {
|
|
187
|
+
statusCode: 413,
|
|
188
|
+
code: "body_too_large",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
chunks.push(buffer);
|
|
192
|
+
}
|
|
193
|
+
const rawBody = Buffer.concat(chunks).toString("utf8").trim();
|
|
194
|
+
if (!rawBody) {
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(rawBody);
|
|
199
|
+
} catch {
|
|
200
|
+
throw createHttpError("request body must be valid JSON", {
|
|
201
|
+
code: "invalid_json",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function resolveRequestedBackend(model, state) {
|
|
207
|
+
const requestedModel = String(model || state.defaultBackend || "").trim();
|
|
208
|
+
if (!requestedModel) {
|
|
209
|
+
throw createHttpError("model is required", {
|
|
210
|
+
code: "missing_model",
|
|
211
|
+
param: "model",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const normalizedRequestedModel = normalizeRuntimeBackendName(requestedModel);
|
|
216
|
+
const configured = await state.resolveConfiguredRuntimeBackend(normalizedRequestedModel, state.allowCliList, {
|
|
217
|
+
configFilePath: state.configFilePath,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (configured?.runtimeBackend) {
|
|
221
|
+
const commandLine = state.resolveAiSessionCommandLine(
|
|
222
|
+
normalizedRequestedModel,
|
|
223
|
+
state.allowCliList,
|
|
224
|
+
state.runtimeEnv,
|
|
225
|
+
configured.runtimeBackend,
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
requestedModel,
|
|
229
|
+
sessionBackend: configured.runtimeBackend,
|
|
230
|
+
commandLine,
|
|
231
|
+
sessionOptions: state.resolveAiSessionOptions(
|
|
232
|
+
normalizedRequestedModel,
|
|
233
|
+
state.allowCliList,
|
|
234
|
+
state.runtimeEnv,
|
|
235
|
+
configured.runtimeBackend,
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const normalizedBackend = await state.normalizeRuntimeBackendAlias(normalizedRequestedModel, {
|
|
241
|
+
configFilePath: state.configFilePath,
|
|
242
|
+
});
|
|
243
|
+
const isSupported = await state.isRuntimeSupportedBackend(normalizedBackend, {
|
|
244
|
+
configFilePath: state.configFilePath,
|
|
245
|
+
});
|
|
246
|
+
if (!isSupported) {
|
|
247
|
+
throw createHttpError(`unknown model: ${requestedModel}`, {
|
|
248
|
+
statusCode: 404,
|
|
249
|
+
code: "model_not_found",
|
|
250
|
+
param: "model",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
requestedModel,
|
|
256
|
+
sessionBackend: normalizedBackend,
|
|
257
|
+
commandLine: state.resolveAiSessionCommandLine(
|
|
258
|
+
normalizedRequestedModel,
|
|
259
|
+
state.allowCliList,
|
|
260
|
+
state.runtimeEnv,
|
|
261
|
+
normalizedBackend,
|
|
262
|
+
),
|
|
263
|
+
sessionOptions: state.resolveAiSessionOptions(
|
|
264
|
+
normalizedRequestedModel,
|
|
265
|
+
state.allowCliList,
|
|
266
|
+
state.runtimeEnv,
|
|
267
|
+
normalizedBackend,
|
|
268
|
+
),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function handleModelsRequest(_req, res, state) {
|
|
273
|
+
const advertised = await state.listAdvertisedBackends(state.allowCliList, {
|
|
274
|
+
configFilePath: state.configFilePath,
|
|
275
|
+
});
|
|
276
|
+
const models = new Set(advertised.supportedBackends || []);
|
|
277
|
+
if (state.defaultBackend) {
|
|
278
|
+
models.add(state.defaultBackend);
|
|
279
|
+
}
|
|
280
|
+
sendJson(res, 200, {
|
|
281
|
+
object: "list",
|
|
282
|
+
data: [...models].sort().map((id) => ({
|
|
283
|
+
id,
|
|
284
|
+
object: "model",
|
|
285
|
+
created: Math.floor(Date.now() / 1000),
|
|
286
|
+
owned_by: "conductor",
|
|
287
|
+
})),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function handleChatCompletionsRequest(req, res, state) {
|
|
292
|
+
const body = await readJsonBody(req, { maxBytes: state.requestBodyLimitBytes });
|
|
293
|
+
if (body.stream === true) {
|
|
294
|
+
sendOpenAiError(res, 400, "stream=true is not supported yet", {
|
|
295
|
+
code: "unsupported_stream",
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (Array.isArray(body.tools) && body.tools.length > 0) {
|
|
300
|
+
sendOpenAiError(res, 400, "tools are not supported yet", {
|
|
301
|
+
code: "unsupported_tools",
|
|
302
|
+
param: "tools",
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (body.n !== undefined && Number(body.n) !== 1) {
|
|
307
|
+
sendOpenAiError(res, 400, "only n=1 is supported", {
|
|
308
|
+
code: "unsupported_n",
|
|
309
|
+
param: "n",
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let responseFormat;
|
|
315
|
+
let chatTurn;
|
|
316
|
+
try {
|
|
317
|
+
responseFormat = normalizeResponseFormat(body.response_format);
|
|
318
|
+
chatTurn = buildChatTurn(body.messages);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw createHttpError(error?.message || "invalid request", {
|
|
321
|
+
code: "invalid_chat_request",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const backend = await resolveRequestedBackend(body.model, state);
|
|
325
|
+
let imageInputs;
|
|
326
|
+
try {
|
|
327
|
+
imageInputs = await state.materializeImageInputs(chatTurn.imageUrls, {
|
|
328
|
+
fetchImpl: state.fetchImpl,
|
|
329
|
+
});
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw createHttpError(error?.message || "failed to process image input", {
|
|
332
|
+
code: "invalid_image_input",
|
|
333
|
+
param: "messages",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let session = null;
|
|
338
|
+
try {
|
|
339
|
+
session = state.createAiSession(backend.sessionBackend, {
|
|
340
|
+
initialHistory: chatTurn.initialHistory,
|
|
341
|
+
initialImages: imageInputs.files,
|
|
342
|
+
cwd: state.cwd,
|
|
343
|
+
configFile: state.configFilePath,
|
|
344
|
+
env: state.runtimeEnv,
|
|
345
|
+
...(backend.sessionOptions || {}),
|
|
346
|
+
...(backend.commandLine ? { commandLine: backend.commandLine } : {}),
|
|
347
|
+
...(backend.sessionBackend === "codex" ? { ignoreCodexApiKey: true } : {}),
|
|
348
|
+
...(responseFormat.outputFormat ? { outputFormat: responseFormat.outputFormat } : {}),
|
|
349
|
+
...(responseFormat.jsonSchema ? { jsonSchema: responseFormat.jsonSchema, structuredOutput: true } : {}),
|
|
350
|
+
logger: {
|
|
351
|
+
log: (message) => {
|
|
352
|
+
state.logger.log?.(`[serve-ai] ${message}`);
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const rawResult = await session.runTurn(chatTurn.promptText, {
|
|
358
|
+
useInitialImages: imageInputs.files.length > 0,
|
|
359
|
+
...(responseFormat.jsonSchema ? { jsonSchema: responseFormat.jsonSchema } : {}),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
let result = rawResult;
|
|
363
|
+
try {
|
|
364
|
+
result = normalizeStructuredOutputResult(rawResult, responseFormat);
|
|
365
|
+
} catch {
|
|
366
|
+
throw createHttpError("backend did not return valid JSON for the requested response_format", {
|
|
367
|
+
statusCode: 502,
|
|
368
|
+
type: "server_error",
|
|
369
|
+
code: "invalid_backend_json",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
sendJson(
|
|
374
|
+
res,
|
|
375
|
+
200,
|
|
376
|
+
toOpenAiChatCompletion(result, {
|
|
377
|
+
model: backend.requestedModel,
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
} finally {
|
|
381
|
+
if (session && typeof session.close === "function") {
|
|
382
|
+
await session.close().catch(() => {});
|
|
383
|
+
}
|
|
384
|
+
await imageInputs?.cleanup?.();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function handleRequest(req, res, state) {
|
|
389
|
+
if (req.method === "OPTIONS") {
|
|
390
|
+
res.writeHead(204, {
|
|
391
|
+
"access-control-allow-origin": "*",
|
|
392
|
+
"access-control-allow-headers": "authorization, content-type",
|
|
393
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
394
|
+
"cache-control": "no-store",
|
|
395
|
+
});
|
|
396
|
+
res.end();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (state.apiKey) {
|
|
401
|
+
const providedKey = parseAuthorizationHeader(req.headers.authorization);
|
|
402
|
+
if (!providedKey || providedKey !== state.apiKey) {
|
|
403
|
+
sendOpenAiError(res, 401, "invalid api key", {
|
|
404
|
+
type: "authentication_error",
|
|
405
|
+
code: "invalid_api_key",
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const requestUrl = new URL(req.url || "/", "http://127.0.0.1");
|
|
412
|
+
if (req.method === "GET" && requestUrl.pathname === "/health") {
|
|
413
|
+
sendJson(res, 200, { ok: true });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (req.method === "GET" && requestUrl.pathname === "/v1/models") {
|
|
417
|
+
await handleModelsRequest(req, res, state);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (req.method === "POST" && requestUrl.pathname === "/v1/chat/completions") {
|
|
421
|
+
await handleChatCompletionsRequest(req, res, state);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
sendOpenAiError(res, 404, `route not found: ${req.method || "GET"} ${requestUrl.pathname}`, {
|
|
426
|
+
code: "not_found",
|
|
427
|
+
type: "invalid_request_error",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function startServeAiServer(options = {}, deps = {}) {
|
|
432
|
+
const runtimeConfig = await (deps.loadServeAiRuntimeConfig || loadServeAiRuntimeConfig)(options.configFile);
|
|
433
|
+
const defaults = runtimeConfig.defaults && typeof runtimeConfig.defaults === "object" ? runtimeConfig.defaults : {};
|
|
434
|
+
const runtimeEnv = {
|
|
435
|
+
...process.env,
|
|
436
|
+
...(runtimeConfig.envs && typeof runtimeConfig.envs === "object" ? runtimeConfig.envs : {}),
|
|
437
|
+
};
|
|
438
|
+
const host = String(
|
|
439
|
+
options.host || process.env.CONDUCTOR_SERVE_AI_HOST || defaults.host || DEFAULT_HOST,
|
|
440
|
+
).trim() || DEFAULT_HOST;
|
|
441
|
+
const port = normalizePort(
|
|
442
|
+
options.port ?? process.env.CONDUCTOR_SERVE_AI_PORT ?? defaults.port,
|
|
443
|
+
DEFAULT_PORT,
|
|
444
|
+
);
|
|
445
|
+
const defaultBackend = String(options.backend || defaults.backend || "codex").trim();
|
|
446
|
+
const apiKey = typeof options.apiKey === "string" && options.apiKey.trim()
|
|
447
|
+
? options.apiKey.trim()
|
|
448
|
+
: typeof defaults.api_key === "string" && defaults.api_key.trim()
|
|
449
|
+
? defaults.api_key.trim()
|
|
450
|
+
: "";
|
|
451
|
+
const cwd =
|
|
452
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
453
|
+
? options.cwd.trim()
|
|
454
|
+
: process.cwd();
|
|
455
|
+
|
|
456
|
+
const state = {
|
|
457
|
+
host,
|
|
458
|
+
port,
|
|
459
|
+
cwd,
|
|
460
|
+
apiKey,
|
|
461
|
+
defaultBackend,
|
|
462
|
+
configFilePath: runtimeConfig.activeConfigPath,
|
|
463
|
+
conductorConfigPath: runtimeConfig.conductorConfigPath,
|
|
464
|
+
serveAiConfigPath: runtimeConfig.serveAiConfigPath,
|
|
465
|
+
configSource: runtimeConfig.source,
|
|
466
|
+
allowCliList: runtimeConfig.allowCliList,
|
|
467
|
+
runtimeEnv,
|
|
468
|
+
requestBodyLimitBytes: normalizePositiveInt(options.requestBodyLimitBytes, DEFAULT_REQUEST_BODY_LIMIT),
|
|
469
|
+
createAiSession: deps.createAiSession || createAiSession,
|
|
470
|
+
listAdvertisedBackends: deps.listAdvertisedBackends || listAdvertisedBackends,
|
|
471
|
+
normalizeRuntimeBackendAlias: deps.normalizeRuntimeBackendAlias || normalizeRuntimeBackendAlias,
|
|
472
|
+
resolveConfiguredRuntimeBackend: deps.resolveConfiguredRuntimeBackend || resolveConfiguredRuntimeBackend,
|
|
473
|
+
isRuntimeSupportedBackend: deps.isRuntimeSupportedBackend || isRuntimeSupportedBackend,
|
|
474
|
+
resolveAiSessionCommandLine: deps.resolveAiSessionCommandLine || resolveAiSessionCommandLine,
|
|
475
|
+
resolveAiSessionOptions: deps.resolveAiSessionOptions || resolveAiSessionOptions,
|
|
476
|
+
materializeImageInputs: deps.materializeImageInputs || materializeImageInputs,
|
|
477
|
+
fetchImpl: deps.fetchImpl || fetch,
|
|
478
|
+
logger: deps.logger || console,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const server = http.createServer((req, res) => {
|
|
482
|
+
void handleRequest(req, res, state).catch((error) => {
|
|
483
|
+
state.logger.error?.(`[serve-ai] request failed: ${error?.message || error}`);
|
|
484
|
+
sendOpenAiError(
|
|
485
|
+
res,
|
|
486
|
+
Number(error?.statusCode) || 500,
|
|
487
|
+
error?.message || "internal server error",
|
|
488
|
+
{
|
|
489
|
+
type: error?.openAiType || "server_error",
|
|
490
|
+
code: error?.openAiCode || "internal_error",
|
|
491
|
+
param: error?.openAiParam || null,
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await new Promise((resolve, reject) => {
|
|
498
|
+
server.once("error", reject);
|
|
499
|
+
server.listen(port, host, () => {
|
|
500
|
+
server.off("error", reject);
|
|
501
|
+
resolve();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const address = server.address();
|
|
506
|
+
const resolvedPort =
|
|
507
|
+
address && typeof address === "object" && typeof address.port === "number" ? address.port : port;
|
|
508
|
+
return {
|
|
509
|
+
server,
|
|
510
|
+
host,
|
|
511
|
+
port: resolvedPort,
|
|
512
|
+
url: `http://${host}:${resolvedPort}`,
|
|
513
|
+
configPath: runtimeConfig.activeConfigPath,
|
|
514
|
+
configSource: runtimeConfig.source,
|
|
515
|
+
conductorConfigPath: runtimeConfig.conductorConfigPath,
|
|
516
|
+
serveAiConfigPath: runtimeConfig.serveAiConfigPath,
|
|
517
|
+
close: async () => {
|
|
518
|
+
await new Promise((resolve, reject) => {
|
|
519
|
+
server.close((error) => {
|
|
520
|
+
if (error) {
|
|
521
|
+
reject(error);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
resolve();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|