@hilbras/omninode 2.0.0-alpha.13 → 2.0.0-alpha.14
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 +8 -1
- package/dist/cli/chunk-6Z6O63SS.js +708 -0
- package/dist/cli/chunk-6Z6O63SS.js.map +1 -0
- package/dist/cli/index.js +1163 -1572
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/providers-OYXBAQTX.js +36 -0
- package/dist/cli/providers-OYXBAQTX.js.map +1 -0
- package/dist/index.d.ts +80 -31
- package/dist/index.js +149 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/errors/index.ts
|
|
4
|
+
var OmniNodeError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
constructor(code, message, options = {}) {
|
|
8
|
+
super(message, { cause: options.cause });
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = options.details;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var ConfigError = class extends OmniNodeError {
|
|
15
|
+
constructor(code, message, options = {}) {
|
|
16
|
+
super(code, message, options);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ProviderError = class extends OmniNodeError {
|
|
20
|
+
constructor(code, message, options = {}) {
|
|
21
|
+
super(code, message, options);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var AgentError = class extends OmniNodeError {
|
|
25
|
+
constructor(code, message, options = {}) {
|
|
26
|
+
super(code, message, options);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var TaskError = class extends OmniNodeError {
|
|
30
|
+
constructor(code, message, options = {}) {
|
|
31
|
+
super(code, message, options);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var PipelineError = class extends OmniNodeError {
|
|
35
|
+
constructor(code, message, options = {}) {
|
|
36
|
+
super(code, message, options);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var PlannerError = class extends OmniNodeError {
|
|
40
|
+
constructor(code, message, options = {}) {
|
|
41
|
+
super(code, message, options);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var MemoryError = class extends OmniNodeError {
|
|
45
|
+
constructor(message, options = {}) {
|
|
46
|
+
super("MEMORY_UNAVAILABLE", message, options);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function isOmniNodeError(value) {
|
|
50
|
+
return value instanceof OmniNodeError;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/providers/auth.ts
|
|
54
|
+
function resolveApiKey(config, env = process.env) {
|
|
55
|
+
if (!config.apiKeyEnvVar) return void 0;
|
|
56
|
+
const value = env[config.apiKeyEnvVar];
|
|
57
|
+
if (!value) {
|
|
58
|
+
throw new ProviderError(
|
|
59
|
+
"PROVIDER_AUTH_FAILED",
|
|
60
|
+
`Environment variable "${config.apiKeyEnvVar}" (referenced by provider "${config.name}") is not set.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function authHeaders(config, env = process.env) {
|
|
66
|
+
const key = resolveApiKey(config, env);
|
|
67
|
+
const base = { ...config.headers ?? {} };
|
|
68
|
+
return key ? { ...base, authorization: `Bearer ${key}` } : base;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/providers/http.ts
|
|
72
|
+
var HttpResponseError = class extends Error {
|
|
73
|
+
constructor(message, status, cause) {
|
|
74
|
+
super(message, { cause });
|
|
75
|
+
this.status = status;
|
|
76
|
+
this.name = "HttpResponseError";
|
|
77
|
+
}
|
|
78
|
+
status;
|
|
79
|
+
};
|
|
80
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
81
|
+
async function requestJson(url, options = {}) {
|
|
82
|
+
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
method,
|
|
85
|
+
headers: {
|
|
86
|
+
accept: "application/json",
|
|
87
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
88
|
+
...headers
|
|
89
|
+
},
|
|
90
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
91
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
92
|
+
});
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = await response.json();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
98
|
+
}
|
|
99
|
+
return { status: response.status, body: parsed };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/logger/index.ts
|
|
103
|
+
var LEVEL_ORDER = {
|
|
104
|
+
debug: 10,
|
|
105
|
+
info: 20,
|
|
106
|
+
warn: 30,
|
|
107
|
+
error: 40
|
|
108
|
+
};
|
|
109
|
+
var ConsoleLogSink = class {
|
|
110
|
+
constructor(stream = process.stderr) {
|
|
111
|
+
this.stream = stream;
|
|
112
|
+
}
|
|
113
|
+
stream;
|
|
114
|
+
write(_level, line) {
|
|
115
|
+
this.stream.write(line + "\n");
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var Logger = class _Logger {
|
|
119
|
+
level;
|
|
120
|
+
format;
|
|
121
|
+
sink;
|
|
122
|
+
context;
|
|
123
|
+
constructor(options = {}) {
|
|
124
|
+
this.level = options.level ?? "info";
|
|
125
|
+
this.format = options.format ?? "text";
|
|
126
|
+
this.sink = options.sink ?? new ConsoleLogSink();
|
|
127
|
+
this.context = options.context ?? {};
|
|
128
|
+
}
|
|
129
|
+
child(context) {
|
|
130
|
+
return new _Logger({
|
|
131
|
+
level: this.level,
|
|
132
|
+
format: this.format,
|
|
133
|
+
sink: this.sink,
|
|
134
|
+
context: { ...this.context, ...context }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
debug(message, data) {
|
|
138
|
+
this.log("debug", message, data);
|
|
139
|
+
}
|
|
140
|
+
info(message, data) {
|
|
141
|
+
this.log("info", message, data);
|
|
142
|
+
}
|
|
143
|
+
warn(message, data) {
|
|
144
|
+
this.log("warn", message, data);
|
|
145
|
+
}
|
|
146
|
+
error(message, data) {
|
|
147
|
+
this.log("error", message, data);
|
|
148
|
+
}
|
|
149
|
+
log(level, message, data) {
|
|
150
|
+
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
|
|
151
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
152
|
+
const merged = { ...this.context, ...data };
|
|
153
|
+
if (this.format === "json") {
|
|
154
|
+
this.sink.write(level, JSON.stringify({ at, level, message, ...merged }));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const parts = [at, level.toUpperCase(), message];
|
|
158
|
+
if (Object.keys(merged).length > 0) {
|
|
159
|
+
parts.push(JSON.stringify(merged));
|
|
160
|
+
}
|
|
161
|
+
this.sink.write(level, parts.join(" "));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var logger = new Logger();
|
|
165
|
+
function configureLogger(options) {
|
|
166
|
+
if (options.level !== void 0) logger.level = options.level;
|
|
167
|
+
if (options.format !== void 0) logger.format = options.format;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/providers/errors.ts
|
|
171
|
+
function kindToCode(kind) {
|
|
172
|
+
switch (kind) {
|
|
173
|
+
case "AUTHENTICATION_ERROR":
|
|
174
|
+
return "PROVIDER_AUTH_FAILED";
|
|
175
|
+
case "MODEL_NOT_FOUND":
|
|
176
|
+
return "MODEL_NOT_FOUND";
|
|
177
|
+
default:
|
|
178
|
+
return "PROVIDER_UNAVAILABLE";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isRetryable(kind) {
|
|
182
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
183
|
+
}
|
|
184
|
+
function kindFromStatus(status) {
|
|
185
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
186
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
187
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
188
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
189
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
190
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
191
|
+
return "UNKNOWN_ERROR";
|
|
192
|
+
}
|
|
193
|
+
function extractMessage(body) {
|
|
194
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
195
|
+
const root = body;
|
|
196
|
+
const error = root.error ?? root;
|
|
197
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
198
|
+
return typeof message === "string" ? message : void 0;
|
|
199
|
+
}
|
|
200
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
201
|
+
if (!value) return void 0;
|
|
202
|
+
const seconds = Number(value);
|
|
203
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
204
|
+
const date = Date.parse(value);
|
|
205
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
206
|
+
}
|
|
207
|
+
function isTimeoutError(error) {
|
|
208
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
209
|
+
}
|
|
210
|
+
function isNetworkError(error) {
|
|
211
|
+
return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
|
|
212
|
+
}
|
|
213
|
+
function fallbackMessage(kind, context, status) {
|
|
214
|
+
const { provider, operation } = context;
|
|
215
|
+
switch (kind) {
|
|
216
|
+
case "AUTHENTICATION_ERROR":
|
|
217
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
218
|
+
case "RATE_LIMIT_ERROR":
|
|
219
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
220
|
+
case "MODEL_NOT_FOUND":
|
|
221
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
222
|
+
case "TIMEOUT":
|
|
223
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
224
|
+
case "SERVER_ERROR":
|
|
225
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
226
|
+
case "INVALID_REQUEST":
|
|
227
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
228
|
+
default:
|
|
229
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function providerHttpError(context) {
|
|
233
|
+
const status = context.status ?? 0;
|
|
234
|
+
const kind = kindFromStatus(status);
|
|
235
|
+
const detail = extractMessage(context.body);
|
|
236
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
237
|
+
return new ProviderError(
|
|
238
|
+
context.code ?? kindToCode(kind),
|
|
239
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
240
|
+
{
|
|
241
|
+
cause: context.cause,
|
|
242
|
+
details: {
|
|
243
|
+
kind,
|
|
244
|
+
provider: context.provider,
|
|
245
|
+
operation: context.operation,
|
|
246
|
+
status,
|
|
247
|
+
retryable: isRetryable(kind),
|
|
248
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
249
|
+
...detail ? { providerMessage: detail } : {}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
function providerTransportError(context) {
|
|
255
|
+
const cause = context.cause;
|
|
256
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
257
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
258
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
259
|
+
cause: context.cause,
|
|
260
|
+
details: {
|
|
261
|
+
kind,
|
|
262
|
+
provider: context.provider,
|
|
263
|
+
operation: context.operation,
|
|
264
|
+
retryable: isRetryable(kind)
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/providers/openai-compatible/index.ts
|
|
270
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
271
|
+
var OpenAICompatibleProvider = class {
|
|
272
|
+
config;
|
|
273
|
+
providerId;
|
|
274
|
+
authentication;
|
|
275
|
+
log;
|
|
276
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
277
|
+
discovered;
|
|
278
|
+
env;
|
|
279
|
+
audit;
|
|
280
|
+
constructor(config, log = logger, env = process.env, audit) {
|
|
281
|
+
this.audit = audit;
|
|
282
|
+
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
283
|
+
this.env = env;
|
|
284
|
+
this.providerId = config.providerId ?? config.name;
|
|
285
|
+
this.authentication = {
|
|
286
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
287
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
288
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
289
|
+
};
|
|
290
|
+
this.log = log.child({ provider: config.name });
|
|
291
|
+
}
|
|
292
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
293
|
+
get requestTimeoutMs() {
|
|
294
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
295
|
+
}
|
|
296
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
297
|
+
async connect() {
|
|
298
|
+
authHeaders(this.config, this.env);
|
|
299
|
+
const models = await this.listModels();
|
|
300
|
+
this.discovered = models;
|
|
301
|
+
return models;
|
|
302
|
+
}
|
|
303
|
+
async getModel(modelId) {
|
|
304
|
+
const models = this.discovered ?? await this.connect();
|
|
305
|
+
return models.find((m) => m.id === modelId);
|
|
306
|
+
}
|
|
307
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
308
|
+
async rawModelsResponse() {
|
|
309
|
+
const response = await this.modelsRequest();
|
|
310
|
+
return response.body;
|
|
311
|
+
}
|
|
312
|
+
async modelsRequest() {
|
|
313
|
+
let response;
|
|
314
|
+
try {
|
|
315
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
316
|
+
headers: authHeaders(this.config, this.env),
|
|
317
|
+
timeoutMs: this.requestTimeoutMs
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error instanceof ProviderError) {
|
|
321
|
+
await this.recordProviderError("listModels", error);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
const normalized = providerTransportError({
|
|
325
|
+
provider: this.config.name,
|
|
326
|
+
operation: "listModels",
|
|
327
|
+
cause: error
|
|
328
|
+
});
|
|
329
|
+
await this.recordProviderError("listModels", normalized);
|
|
330
|
+
throw normalized;
|
|
331
|
+
}
|
|
332
|
+
const { status, body } = response;
|
|
333
|
+
if (status !== 200) {
|
|
334
|
+
const httpError = providerHttpError({
|
|
335
|
+
provider: this.config.name,
|
|
336
|
+
operation: "listModels",
|
|
337
|
+
status,
|
|
338
|
+
body
|
|
339
|
+
});
|
|
340
|
+
await this.recordProviderError("listModels", httpError);
|
|
341
|
+
throw httpError;
|
|
342
|
+
}
|
|
343
|
+
return response;
|
|
344
|
+
}
|
|
345
|
+
async listModels() {
|
|
346
|
+
const body = await this.rawModelsResponse();
|
|
347
|
+
const entries = extractModelEntries(body, this.config);
|
|
348
|
+
const models = [];
|
|
349
|
+
let skipped = 0;
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (typeof entry.id !== "string" || entry.id.length === 0) {
|
|
352
|
+
skipped += 1;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
models.push(this.toModelInfo(entry));
|
|
356
|
+
}
|
|
357
|
+
if (skipped > 0) {
|
|
358
|
+
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
359
|
+
}
|
|
360
|
+
return models;
|
|
361
|
+
}
|
|
362
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
363
|
+
toModelInfo(entry) {
|
|
364
|
+
const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
365
|
+
const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
|
|
366
|
+
const inputTypes = normalizeModalities(entry.input_modalities);
|
|
367
|
+
const outputTypes = normalizeModalities(entry.output_modalities);
|
|
368
|
+
return {
|
|
369
|
+
provider: this.config.name,
|
|
370
|
+
id: entry.id,
|
|
371
|
+
displayName: entry.id,
|
|
372
|
+
...typeof entry.name === "string" ? { name: entry.name } : {},
|
|
373
|
+
status: "available",
|
|
374
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
375
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
376
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
377
|
+
...parameters !== void 0 ? {
|
|
378
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
379
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
380
|
+
supportsStreaming: parameters.includes("stream")
|
|
381
|
+
} : {},
|
|
382
|
+
metadata: {
|
|
383
|
+
...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
|
|
384
|
+
...typeof entry.created === "number" ? { createdAt: entry.created } : {},
|
|
385
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** Streaming where the gateway supports it (§11). */
|
|
390
|
+
stream(request, onChunk) {
|
|
391
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
392
|
+
}
|
|
393
|
+
/** Provider errors are part of the audit trail (§14 correlation, §18 observability). */
|
|
394
|
+
async recordProviderError(operation, error) {
|
|
395
|
+
await this.audit?.record({
|
|
396
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
397
|
+
action: "provider.error",
|
|
398
|
+
id: this.providerId,
|
|
399
|
+
providerId: this.providerId,
|
|
400
|
+
detail: {
|
|
401
|
+
operation,
|
|
402
|
+
code: error.code,
|
|
403
|
+
kind: error.details?.kind,
|
|
404
|
+
message: error.message
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
async healthCheck() {
|
|
409
|
+
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
410
|
+
try {
|
|
411
|
+
const started = Date.now();
|
|
412
|
+
const models = await this.listModels();
|
|
413
|
+
return {
|
|
414
|
+
name: this.config.name,
|
|
415
|
+
type: this.config.type,
|
|
416
|
+
baseUrl: this.config.baseUrl,
|
|
417
|
+
health: "healthy",
|
|
418
|
+
connected: true,
|
|
419
|
+
modelCount: models.length,
|
|
420
|
+
lastCheckedAt,
|
|
421
|
+
message: `Discovered ${models.length} model(s) in ${Date.now() - started}ms.`
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return {
|
|
425
|
+
name: this.config.name,
|
|
426
|
+
type: this.config.type,
|
|
427
|
+
baseUrl: this.config.baseUrl,
|
|
428
|
+
health: inferHealth(error),
|
|
429
|
+
connected: false,
|
|
430
|
+
modelCount: 0,
|
|
431
|
+
lastCheckedAt,
|
|
432
|
+
message: error instanceof Error ? error.message : String(error)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async chat(request) {
|
|
437
|
+
let response;
|
|
438
|
+
try {
|
|
439
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
440
|
+
method: "POST",
|
|
441
|
+
headers: authHeaders(this.config, this.env),
|
|
442
|
+
timeoutMs: this.requestTimeoutMs,
|
|
443
|
+
body: {
|
|
444
|
+
model: request.model,
|
|
445
|
+
messages: request.messages,
|
|
446
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
447
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
448
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof ProviderError) {
|
|
453
|
+
await this.recordProviderError("chat", error);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
const normalized = providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
457
|
+
await this.recordProviderError("chat", normalized);
|
|
458
|
+
throw normalized;
|
|
459
|
+
}
|
|
460
|
+
const { status, body } = response;
|
|
461
|
+
if (status !== 200) {
|
|
462
|
+
const httpError = providerHttpError({
|
|
463
|
+
provider: this.config.name,
|
|
464
|
+
operation: "chat",
|
|
465
|
+
status,
|
|
466
|
+
body,
|
|
467
|
+
...status === 404 ? {
|
|
468
|
+
code: "MODEL_NOT_FOUND"
|
|
469
|
+
} : {}
|
|
470
|
+
});
|
|
471
|
+
await this.recordProviderError("chat", httpError);
|
|
472
|
+
throw httpError;
|
|
473
|
+
}
|
|
474
|
+
const payload = body;
|
|
475
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
476
|
+
const first = choices[0];
|
|
477
|
+
if (!first || typeof first.message?.content !== "string") {
|
|
478
|
+
throw unavailable(this.config, "Chat completion response had no message content.", status);
|
|
479
|
+
}
|
|
480
|
+
const usage = payload.usage ?? {};
|
|
481
|
+
return {
|
|
482
|
+
model: typeof payload.model === "string" ? payload.model : request.model,
|
|
483
|
+
content: first.message.content,
|
|
484
|
+
finishReason: typeof first.finish_reason === "string" ? first.finish_reason : void 0,
|
|
485
|
+
usage: {
|
|
486
|
+
promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : void 0,
|
|
487
|
+
completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : void 0,
|
|
488
|
+
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : void 0
|
|
489
|
+
},
|
|
490
|
+
raw: body
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
495
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
496
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
497
|
+
let response;
|
|
498
|
+
try {
|
|
499
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: {
|
|
502
|
+
accept: "text/event-stream",
|
|
503
|
+
"content-type": "application/json",
|
|
504
|
+
...authHeaders(config, env)
|
|
505
|
+
},
|
|
506
|
+
body: JSON.stringify({
|
|
507
|
+
model: request.model,
|
|
508
|
+
messages: request.messages,
|
|
509
|
+
stream: true,
|
|
510
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
511
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
512
|
+
}),
|
|
513
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
517
|
+
}
|
|
518
|
+
if (!response.ok) {
|
|
519
|
+
const body = await response.json().catch(() => void 0);
|
|
520
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
521
|
+
}
|
|
522
|
+
if (!response.body) {
|
|
523
|
+
throw providerHttpError({
|
|
524
|
+
provider: config.name,
|
|
525
|
+
operation: "stream",
|
|
526
|
+
status: 500,
|
|
527
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
let content = "";
|
|
531
|
+
let model = request.model;
|
|
532
|
+
let finishReason;
|
|
533
|
+
const decoder = new TextDecoder();
|
|
534
|
+
let buffer = "";
|
|
535
|
+
for await (const piece of response.body) {
|
|
536
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
537
|
+
let newline = buffer.indexOf("\n");
|
|
538
|
+
while (newline >= 0) {
|
|
539
|
+
const line = buffer.slice(0, newline).trim();
|
|
540
|
+
buffer = buffer.slice(newline + 1);
|
|
541
|
+
newline = buffer.indexOf("\n");
|
|
542
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
543
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
544
|
+
if (data === "[DONE]") continue;
|
|
545
|
+
let event;
|
|
546
|
+
try {
|
|
547
|
+
event = JSON.parse(data);
|
|
548
|
+
} catch {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (typeof event.model === "string") model = event.model;
|
|
552
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
553
|
+
const first = choices[0];
|
|
554
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
555
|
+
if (delta.length > 0) content += delta;
|
|
556
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
557
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
561
|
+
}
|
|
562
|
+
function normalizeModalities(value) {
|
|
563
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
564
|
+
if (!list) return void 0;
|
|
565
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
566
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
567
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
568
|
+
}
|
|
569
|
+
function normalizeBaseUrl(baseUrl) {
|
|
570
|
+
return baseUrl.replace(/\/+$/, "");
|
|
571
|
+
}
|
|
572
|
+
function extractModelEntries(body, config) {
|
|
573
|
+
if (Array.isArray(body)) return body;
|
|
574
|
+
if (body !== null && typeof body === "object" && Array.isArray(body.data)) {
|
|
575
|
+
return body.data;
|
|
576
|
+
}
|
|
577
|
+
throw new ProviderError(
|
|
578
|
+
"PROVIDER_UNAVAILABLE",
|
|
579
|
+
`Provider "${config.name}" returned an unrecognized model list shape.`,
|
|
580
|
+
{ details: { endpoint: "models" } }
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
function unavailable(config, message, status) {
|
|
584
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
585
|
+
details: { status }
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function inferHealth(error) {
|
|
589
|
+
const status = error instanceof ProviderError && typeof error.details?.status === "number" ? error.details.status : void 0;
|
|
590
|
+
return status !== void 0 && status >= 500 ? "degraded" : "unreachable";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/providers/omnihilbras/index.ts
|
|
594
|
+
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
595
|
+
gateway;
|
|
596
|
+
async listModels() {
|
|
597
|
+
const raw = await this.rawModelsResponse();
|
|
598
|
+
await this.extractGatewayMetadata(raw);
|
|
599
|
+
const models = await super.listModels();
|
|
600
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Run the full connect flow. Throws on authentication or transport failure
|
|
604
|
+
* (use healthCheck() for a non-throwing probe).
|
|
605
|
+
*/
|
|
606
|
+
/**
|
|
607
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
608
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
609
|
+
*/
|
|
610
|
+
gatewayMetadata() {
|
|
611
|
+
return this.gateway;
|
|
612
|
+
}
|
|
613
|
+
async extractGatewayMetadata(body) {
|
|
614
|
+
if (body !== null && typeof body === "object") {
|
|
615
|
+
const gateway = body.gateway;
|
|
616
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
617
|
+
this.gateway = gateway;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async connectAndRegister(registry) {
|
|
622
|
+
const apiKey = resolveApiKey(this.config);
|
|
623
|
+
const discovered = await this.listModels();
|
|
624
|
+
const seen = /* @__PURE__ */ new Set();
|
|
625
|
+
const models = [];
|
|
626
|
+
const duplicates = [];
|
|
627
|
+
for (const model of discovered) {
|
|
628
|
+
if (seen.has(model.id)) {
|
|
629
|
+
duplicates.push(model.id);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
seen.add(model.id);
|
|
633
|
+
models.push({
|
|
634
|
+
...model,
|
|
635
|
+
provider: this.config.name,
|
|
636
|
+
status: "available",
|
|
637
|
+
metadata: { ...model.metadata, gateway: "omnihilbras" }
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (registry) {
|
|
641
|
+
for (const model of models) registry.register(model);
|
|
642
|
+
}
|
|
643
|
+
const message = `Authenticated${apiKey ? " with API key" : " (no API key configured)"}; validated ${models.length} unique model(s)` + (duplicates.length > 0 ? `, skipped ${duplicates.length} duplicate(s)` : "") + ".";
|
|
644
|
+
const status = {
|
|
645
|
+
name: this.config.name,
|
|
646
|
+
type: this.config.type,
|
|
647
|
+
baseUrl: this.config.baseUrl,
|
|
648
|
+
health: "healthy",
|
|
649
|
+
connected: true,
|
|
650
|
+
modelCount: models.length,
|
|
651
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
652
|
+
message
|
|
653
|
+
};
|
|
654
|
+
return { status, models, duplicates, registered: registry ? models.length : 0 };
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/providers/factory.ts
|
|
659
|
+
var OPENAI_COMPATIBLE_TYPES = [
|
|
660
|
+
"openai-compatible",
|
|
661
|
+
"openrouter",
|
|
662
|
+
"local",
|
|
663
|
+
"custom"
|
|
664
|
+
];
|
|
665
|
+
function createProvider(config, options = {}) {
|
|
666
|
+
const shared = {
|
|
667
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
668
|
+
...options.audit !== void 0 ? { audit: options.audit } : {}
|
|
669
|
+
};
|
|
670
|
+
if (config.type === "omnihilbras") {
|
|
671
|
+
return new OmniHilbrasProvider(config, void 0, options.env, options.audit);
|
|
672
|
+
}
|
|
673
|
+
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
674
|
+
return new OpenAICompatibleProvider(config, void 0, options.env, options.audit);
|
|
675
|
+
}
|
|
676
|
+
void shared;
|
|
677
|
+
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export {
|
|
681
|
+
OmniNodeError,
|
|
682
|
+
ConfigError,
|
|
683
|
+
ProviderError,
|
|
684
|
+
AgentError,
|
|
685
|
+
TaskError,
|
|
686
|
+
PipelineError,
|
|
687
|
+
PlannerError,
|
|
688
|
+
MemoryError,
|
|
689
|
+
isOmniNodeError,
|
|
690
|
+
logger,
|
|
691
|
+
configureLogger,
|
|
692
|
+
resolveApiKey,
|
|
693
|
+
authHeaders,
|
|
694
|
+
requestJson,
|
|
695
|
+
isRetryable,
|
|
696
|
+
kindFromStatus,
|
|
697
|
+
extractMessage,
|
|
698
|
+
parseRetryAfter,
|
|
699
|
+
isTimeoutError,
|
|
700
|
+
isNetworkError,
|
|
701
|
+
providerHttpError,
|
|
702
|
+
providerTransportError,
|
|
703
|
+
OpenAICompatibleProvider,
|
|
704
|
+
OmniHilbrasProvider,
|
|
705
|
+
OPENAI_COMPATIBLE_TYPES,
|
|
706
|
+
createProvider
|
|
707
|
+
};
|
|
708
|
+
//# sourceMappingURL=chunk-6Z6O63SS.js.map
|