@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 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.5 — v2 Phase 5: Pipeline Lifecycle v2**
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.5";
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
- let response;
370
- try {
371
- response = await fetch(url, {
372
- method,
373
- headers: {
374
- accept: "application/json",
375
- ...body !== void 0 ? { "content-type": "application/json" } : {},
376
- ...headers
377
- },
378
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
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 ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
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
- constructor(config, log = logger) {
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
- async listModels() {
464
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
465
- headers: authHeaders(this.config)
466
- });
467
- if (status === 401 || status === 403) {
468
- throw authFailed(this.config, status);
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 unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
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
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
527
- method: "POST",
528
- headers: authHeaders(this.config),
529
- body: {
530
- model: request.model,
531
- messages: request.messages,
532
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
533
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
534
- ...request.stop !== void 0 ? { stop: request.stop } : {}
535
- }
536
- });
537
- if (status === 401 || status === 403) throw authFailed(this.config, status);
538
- if (status === 404) {
539
- throw new ProviderError(
540
- "MODEL_NOT_FOUND",
541
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
542
- { details: { status } }
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 unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
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
- async connect(registry) {
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.connect(registry);
1092
+ const result = await provider.connectAndRegister(registry);
831
1093
  printProviderStatus(result.status);
832
1094
  console.log(` registered: ${result.registered}`);
833
1095
  } catch (error) {