@hilbras/omninode 2.0.0-alpha.5 → 2.0.0-alpha.7
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 +17 -3
- package/dist/cli/index.js +333 -71
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +156 -17
- package/dist/index.js +343 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ existing intelligence; it does not try to become another model.
|
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
21
|
-
**v2.0.0-alpha.
|
|
21
|
+
**v2.0.0-alpha.7 — v2 Phase 7: OmniHilbras Integration v2**
|
|
22
22
|
(the reliability & interoperability line: architecture → execution →
|
|
23
|
-
protocol → adapters → pipelines) (the v2
|
|
23
|
+
protocol → adapters → pipelines → providers → OmniHilbras) (the v2
|
|
24
24
|
roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
|
|
25
25
|
the
|
|
26
26
|
full loop from §28 runs in one command —
|
|
@@ -31,6 +31,20 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
|
|
|
31
31
|
|
|
32
32
|
### What's new in v2 (so far)
|
|
33
33
|
|
|
34
|
+
- **Phase 7 — OmniHilbras Integration v2**: the dedicated adapter now
|
|
35
|
+
captures gateway-level metadata (version, tier, region) and attaches it
|
|
36
|
+
to discovered models, honors provider-level `timeout_ms`, and supports
|
|
37
|
+
streaming completions (`stream()` over SSE) — while a guard test proves
|
|
38
|
+
the core references OmniHilbras *only* through the provider layer, and a
|
|
39
|
+
full pipeline runs with no OmniHilbras configured at all.
|
|
40
|
+
- **Phase 6 — Provider Infrastructure v2**: standardized provider contract
|
|
41
|
+
(`providerId`, secret-free `authentication`, declared `capabilities`,
|
|
42
|
+
`connect()` / `getModel()` discovery), richer model metadata (context
|
|
43
|
+
window, input/output modalities, tool/vision/structured-output/streaming
|
|
44
|
+
support), and **unified error normalization** — every provider failure now
|
|
45
|
+
carries a stable `kind` (auth, rate limit, invalid request, model not
|
|
46
|
+
found, timeout, network, server, unknown) plus `retryable` and
|
|
47
|
+
`retryAfterMs`. Details: [PROVIDERS.md](docs/PROVIDERS.md).
|
|
34
48
|
- **Phase 5 — Pipeline Lifecycle**: first-class pipeline cancellation
|
|
35
49
|
(`omninode pipeline cancel <run-id>`: stop scheduling → cancel running
|
|
36
50
|
tasks → terminate agent processes → persist a `cancelled` run), `pipeline
|
|
@@ -319,7 +333,7 @@ required for the core engine.
|
|
|
319
333
|
- [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
|
|
320
334
|
- [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
|
|
321
335
|
- [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
|
|
322
|
-
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
336
|
+
- [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
|
|
323
337
|
- [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
|
|
324
338
|
|
|
325
339
|
Prefer a container? `docker build -t omninode .` then
|
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
9
|
+
var OMNINODE_VERSION = "2.0.0-alpha.7";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -68,6 +68,7 @@ var providerConfigSchema = z.object({
|
|
|
68
68
|
type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
|
|
69
69
|
base_url: z.string().url(),
|
|
70
70
|
api_key_env_var: z.string().min(1).optional(),
|
|
71
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
71
72
|
headers: z.record(z.string()).optional(),
|
|
72
73
|
enabled: z.boolean().optional(),
|
|
73
74
|
metadata: z.record(z.unknown()).optional()
|
|
@@ -132,7 +133,8 @@ var projectConfigSchema = z.object({
|
|
|
132
133
|
memory: z.object({
|
|
133
134
|
provider: z.string().min(1).default("local"),
|
|
134
135
|
base_url: z.string().url().optional(),
|
|
135
|
-
api_key_env_var: z.string().min(1).optional()
|
|
136
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
137
|
+
timeout_ms: z.number().int().positive().optional()
|
|
136
138
|
}).strict().optional()
|
|
137
139
|
}).strict();
|
|
138
140
|
var appConfigSchema = z.object({
|
|
@@ -245,6 +247,7 @@ function toProviderConfig(raw) {
|
|
|
245
247
|
type: raw.type,
|
|
246
248
|
baseUrl: raw.base_url,
|
|
247
249
|
apiKeyEnvVar: raw.api_key_env_var,
|
|
250
|
+
timeoutMs: raw.timeout_ms,
|
|
248
251
|
headers: raw.headers,
|
|
249
252
|
enabled: raw.enabled,
|
|
250
253
|
metadata: raw.metadata
|
|
@@ -363,35 +366,32 @@ function authHeaders(config, env = process.env) {
|
|
|
363
366
|
}
|
|
364
367
|
|
|
365
368
|
// src/providers/http.ts
|
|
369
|
+
var HttpResponseError = class extends Error {
|
|
370
|
+
constructor(message, status, cause) {
|
|
371
|
+
super(message, { cause });
|
|
372
|
+
this.status = status;
|
|
373
|
+
this.name = "HttpResponseError";
|
|
374
|
+
}
|
|
375
|
+
status;
|
|
376
|
+
};
|
|
366
377
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
367
378
|
async function requestJson(url, options = {}) {
|
|
368
379
|
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
380
|
-
});
|
|
381
|
-
} catch (error) {
|
|
382
|
-
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
383
|
-
throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
|
|
384
|
-
cause: error
|
|
385
|
-
});
|
|
386
|
-
}
|
|
380
|
+
const response = await fetch(url, {
|
|
381
|
+
method,
|
|
382
|
+
headers: {
|
|
383
|
+
accept: "application/json",
|
|
384
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
385
|
+
...headers
|
|
386
|
+
},
|
|
387
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
388
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
389
|
+
});
|
|
387
390
|
let parsed;
|
|
388
391
|
try {
|
|
389
392
|
parsed = await response.json();
|
|
390
393
|
} catch (error) {
|
|
391
|
-
throw new
|
|
392
|
-
cause: error,
|
|
393
|
-
details: { status: response.status }
|
|
394
|
-
});
|
|
394
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
395
395
|
}
|
|
396
396
|
return { status: response.status, body: parsed };
|
|
397
397
|
}
|
|
@@ -452,24 +452,167 @@ var Logger = class _Logger {
|
|
|
452
452
|
};
|
|
453
453
|
var logger = new Logger();
|
|
454
454
|
|
|
455
|
+
// src/providers/errors.ts
|
|
456
|
+
function kindToCode(kind) {
|
|
457
|
+
switch (kind) {
|
|
458
|
+
case "AUTHENTICATION_ERROR":
|
|
459
|
+
return "PROVIDER_AUTH_FAILED";
|
|
460
|
+
case "MODEL_NOT_FOUND":
|
|
461
|
+
return "MODEL_NOT_FOUND";
|
|
462
|
+
default:
|
|
463
|
+
return "PROVIDER_UNAVAILABLE";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function isRetryable(kind) {
|
|
467
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
468
|
+
}
|
|
469
|
+
function kindFromStatus(status) {
|
|
470
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
471
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
472
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
473
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
474
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
475
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
476
|
+
return "UNKNOWN_ERROR";
|
|
477
|
+
}
|
|
478
|
+
function extractMessage(body) {
|
|
479
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
480
|
+
const root = body;
|
|
481
|
+
const error = root.error ?? root;
|
|
482
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
483
|
+
return typeof message === "string" ? message : void 0;
|
|
484
|
+
}
|
|
485
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
486
|
+
if (!value) return void 0;
|
|
487
|
+
const seconds = Number(value);
|
|
488
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
489
|
+
const date = Date.parse(value);
|
|
490
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
491
|
+
}
|
|
492
|
+
function isTimeoutError(error) {
|
|
493
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
494
|
+
}
|
|
495
|
+
function fallbackMessage(kind, context, status) {
|
|
496
|
+
const { provider, operation } = context;
|
|
497
|
+
switch (kind) {
|
|
498
|
+
case "AUTHENTICATION_ERROR":
|
|
499
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
500
|
+
case "RATE_LIMIT_ERROR":
|
|
501
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
502
|
+
case "MODEL_NOT_FOUND":
|
|
503
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
504
|
+
case "TIMEOUT":
|
|
505
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
506
|
+
case "SERVER_ERROR":
|
|
507
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
508
|
+
case "INVALID_REQUEST":
|
|
509
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
510
|
+
default:
|
|
511
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function providerHttpError(context) {
|
|
515
|
+
const status = context.status ?? 0;
|
|
516
|
+
const kind = kindFromStatus(status);
|
|
517
|
+
const detail = extractMessage(context.body);
|
|
518
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
519
|
+
return new ProviderError(
|
|
520
|
+
context.code ?? kindToCode(kind),
|
|
521
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
522
|
+
{
|
|
523
|
+
cause: context.cause,
|
|
524
|
+
details: {
|
|
525
|
+
kind,
|
|
526
|
+
provider: context.provider,
|
|
527
|
+
operation: context.operation,
|
|
528
|
+
status,
|
|
529
|
+
retryable: isRetryable(kind),
|
|
530
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
531
|
+
...detail ? { providerMessage: detail } : {}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
function providerTransportError(context) {
|
|
537
|
+
const cause = context.cause;
|
|
538
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
539
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
540
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
541
|
+
cause: context.cause,
|
|
542
|
+
details: {
|
|
543
|
+
kind,
|
|
544
|
+
provider: context.provider,
|
|
545
|
+
operation: context.operation,
|
|
546
|
+
retryable: isRetryable(kind)
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
455
551
|
// src/providers/openai-compatible/index.ts
|
|
552
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
456
553
|
var OpenAICompatibleProvider = class {
|
|
457
554
|
config;
|
|
555
|
+
providerId;
|
|
556
|
+
authentication;
|
|
458
557
|
log;
|
|
459
|
-
|
|
558
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
559
|
+
discovered;
|
|
560
|
+
env;
|
|
561
|
+
constructor(config, log = logger, env = process.env) {
|
|
460
562
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
563
|
+
this.env = env;
|
|
564
|
+
this.providerId = config.providerId ?? config.name;
|
|
565
|
+
this.authentication = {
|
|
566
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
567
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
568
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
569
|
+
};
|
|
461
570
|
this.log = log.child({ provider: config.name });
|
|
462
571
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
572
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
573
|
+
get requestTimeoutMs() {
|
|
574
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
575
|
+
}
|
|
576
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
577
|
+
async connect() {
|
|
578
|
+
authHeaders(this.config, this.env);
|
|
579
|
+
const models = await this.listModels();
|
|
580
|
+
this.discovered = models;
|
|
581
|
+
return models;
|
|
582
|
+
}
|
|
583
|
+
async getModel(modelId) {
|
|
584
|
+
const models = this.discovered ?? await this.connect();
|
|
585
|
+
return models.find((m) => m.id === modelId);
|
|
586
|
+
}
|
|
587
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
588
|
+
async rawModelsResponse() {
|
|
589
|
+
const response = await this.modelsRequest();
|
|
590
|
+
return response.body;
|
|
591
|
+
}
|
|
592
|
+
async modelsRequest() {
|
|
593
|
+
let response;
|
|
594
|
+
try {
|
|
595
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
596
|
+
headers: authHeaders(this.config, this.env),
|
|
597
|
+
timeoutMs: this.requestTimeoutMs
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (error instanceof ProviderError) throw error;
|
|
601
|
+
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
469
602
|
}
|
|
603
|
+
const { status, body } = response;
|
|
470
604
|
if (status !== 200) {
|
|
471
|
-
throw
|
|
605
|
+
throw providerHttpError({
|
|
606
|
+
provider: this.config.name,
|
|
607
|
+
operation: "listModels",
|
|
608
|
+
status,
|
|
609
|
+
body
|
|
610
|
+
});
|
|
472
611
|
}
|
|
612
|
+
return response;
|
|
613
|
+
}
|
|
614
|
+
async listModels() {
|
|
615
|
+
const body = await this.rawModelsResponse();
|
|
473
616
|
const entries = extractModelEntries(body, this.config);
|
|
474
617
|
const models = [];
|
|
475
618
|
let skipped = 0;
|
|
@@ -478,22 +621,44 @@ var OpenAICompatibleProvider = class {
|
|
|
478
621
|
skipped += 1;
|
|
479
622
|
continue;
|
|
480
623
|
}
|
|
481
|
-
models.push(
|
|
482
|
-
provider: this.config.name,
|
|
483
|
-
id: entry2.id,
|
|
484
|
-
displayName: entry2.id,
|
|
485
|
-
status: "available",
|
|
486
|
-
metadata: {
|
|
487
|
-
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
488
|
-
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {}
|
|
489
|
-
}
|
|
490
|
-
});
|
|
624
|
+
models.push(this.toModelInfo(entry2));
|
|
491
625
|
}
|
|
492
626
|
if (skipped > 0) {
|
|
493
627
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
494
628
|
}
|
|
495
629
|
return models;
|
|
496
630
|
}
|
|
631
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
632
|
+
toModelInfo(entry2) {
|
|
633
|
+
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
634
|
+
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
635
|
+
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
636
|
+
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
637
|
+
return {
|
|
638
|
+
provider: this.config.name,
|
|
639
|
+
id: entry2.id,
|
|
640
|
+
displayName: entry2.id,
|
|
641
|
+
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
642
|
+
status: "available",
|
|
643
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
644
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
645
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
646
|
+
...parameters !== void 0 ? {
|
|
647
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
648
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
649
|
+
supportsStreaming: parameters.includes("stream")
|
|
650
|
+
} : {},
|
|
651
|
+
metadata: {
|
|
652
|
+
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
653
|
+
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
654
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
/** Streaming where the gateway supports it (§11). */
|
|
659
|
+
stream(request, onChunk) {
|
|
660
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
661
|
+
}
|
|
497
662
|
async healthCheck() {
|
|
498
663
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
499
664
|
try {
|
|
@@ -523,27 +688,35 @@ var OpenAICompatibleProvider = class {
|
|
|
523
688
|
}
|
|
524
689
|
}
|
|
525
690
|
async chat(request) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
);
|
|
691
|
+
let response;
|
|
692
|
+
try {
|
|
693
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
694
|
+
method: "POST",
|
|
695
|
+
headers: authHeaders(this.config, this.env),
|
|
696
|
+
timeoutMs: this.requestTimeoutMs,
|
|
697
|
+
body: {
|
|
698
|
+
model: request.model,
|
|
699
|
+
messages: request.messages,
|
|
700
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
701
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
702
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (error instanceof ProviderError) throw error;
|
|
707
|
+
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
544
708
|
}
|
|
709
|
+
const { status, body } = response;
|
|
545
710
|
if (status !== 200) {
|
|
546
|
-
throw
|
|
711
|
+
throw providerHttpError({
|
|
712
|
+
provider: this.config.name,
|
|
713
|
+
operation: "chat",
|
|
714
|
+
status,
|
|
715
|
+
body,
|
|
716
|
+
...status === 404 ? {
|
|
717
|
+
code: "MODEL_NOT_FOUND"
|
|
718
|
+
} : {}
|
|
719
|
+
});
|
|
547
720
|
}
|
|
548
721
|
const payload = body;
|
|
549
722
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -565,6 +738,81 @@ var OpenAICompatibleProvider = class {
|
|
|
565
738
|
};
|
|
566
739
|
}
|
|
567
740
|
};
|
|
741
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
742
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
743
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
744
|
+
let response;
|
|
745
|
+
try {
|
|
746
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
747
|
+
method: "POST",
|
|
748
|
+
headers: {
|
|
749
|
+
accept: "text/event-stream",
|
|
750
|
+
"content-type": "application/json",
|
|
751
|
+
...authHeaders(config, env)
|
|
752
|
+
},
|
|
753
|
+
body: JSON.stringify({
|
|
754
|
+
model: request.model,
|
|
755
|
+
messages: request.messages,
|
|
756
|
+
stream: true,
|
|
757
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
758
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
759
|
+
}),
|
|
760
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
761
|
+
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
764
|
+
}
|
|
765
|
+
if (!response.ok) {
|
|
766
|
+
const body = await response.json().catch(() => void 0);
|
|
767
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
768
|
+
}
|
|
769
|
+
if (!response.body) {
|
|
770
|
+
throw providerHttpError({
|
|
771
|
+
provider: config.name,
|
|
772
|
+
operation: "stream",
|
|
773
|
+
status: 500,
|
|
774
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
let content = "";
|
|
778
|
+
let model = request.model;
|
|
779
|
+
let finishReason;
|
|
780
|
+
const decoder = new TextDecoder();
|
|
781
|
+
let buffer = "";
|
|
782
|
+
for await (const piece of response.body) {
|
|
783
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
784
|
+
let newline = buffer.indexOf("\n");
|
|
785
|
+
while (newline >= 0) {
|
|
786
|
+
const line = buffer.slice(0, newline).trim();
|
|
787
|
+
buffer = buffer.slice(newline + 1);
|
|
788
|
+
newline = buffer.indexOf("\n");
|
|
789
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
790
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
791
|
+
if (data === "[DONE]") continue;
|
|
792
|
+
let event;
|
|
793
|
+
try {
|
|
794
|
+
event = JSON.parse(data);
|
|
795
|
+
} catch {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (typeof event.model === "string") model = event.model;
|
|
799
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
800
|
+
const first = choices[0];
|
|
801
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
802
|
+
if (delta.length > 0) content += delta;
|
|
803
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
804
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
808
|
+
}
|
|
809
|
+
function normalizeModalities(value) {
|
|
810
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
811
|
+
if (!list) return void 0;
|
|
812
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
813
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
814
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
815
|
+
}
|
|
568
816
|
function normalizeBaseUrl(baseUrl) {
|
|
569
817
|
return baseUrl.replace(/\/+$/, "");
|
|
570
818
|
}
|
|
@@ -579,14 +827,6 @@ function extractModelEntries(body, config) {
|
|
|
579
827
|
{ details: { endpoint: "models" } }
|
|
580
828
|
);
|
|
581
829
|
}
|
|
582
|
-
function authFailed(config, status) {
|
|
583
|
-
const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
|
|
584
|
-
return new ProviderError(
|
|
585
|
-
"PROVIDER_AUTH_FAILED",
|
|
586
|
-
`Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
|
|
587
|
-
{ details: { status } }
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
830
|
function unavailable(config, message, status) {
|
|
591
831
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
592
832
|
details: { status }
|
|
@@ -599,11 +839,33 @@ function inferHealth(error) {
|
|
|
599
839
|
|
|
600
840
|
// src/providers/omnihilbras/index.ts
|
|
601
841
|
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
842
|
+
gateway;
|
|
843
|
+
async listModels() {
|
|
844
|
+
const raw = await this.rawModelsResponse();
|
|
845
|
+
await this.extractGatewayMetadata(raw);
|
|
846
|
+
const models = await super.listModels();
|
|
847
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
848
|
+
}
|
|
602
849
|
/**
|
|
603
850
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
604
851
|
* (use healthCheck() for a non-throwing probe).
|
|
605
852
|
*/
|
|
606
|
-
|
|
853
|
+
/**
|
|
854
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
855
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
856
|
+
*/
|
|
857
|
+
gatewayMetadata() {
|
|
858
|
+
return this.gateway;
|
|
859
|
+
}
|
|
860
|
+
async extractGatewayMetadata(body) {
|
|
861
|
+
if (body !== null && typeof body === "object") {
|
|
862
|
+
const gateway = body.gateway;
|
|
863
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
864
|
+
this.gateway = gateway;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async connectAndRegister(registry) {
|
|
607
869
|
const apiKey = resolveApiKey(this.config);
|
|
608
870
|
const discovered = await this.listModels();
|
|
609
871
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -827,7 +1089,7 @@ async function testProvider(name, options = {}) {
|
|
|
827
1089
|
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
828
1090
|
const registry = new ModelRegistry();
|
|
829
1091
|
try {
|
|
830
|
-
const result = await provider.
|
|
1092
|
+
const result = await provider.connectAndRegister(registry);
|
|
831
1093
|
printProviderStatus(result.status);
|
|
832
1094
|
console.log(` registered: ${result.registered}`);
|
|
833
1095
|
} catch (error) {
|