@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/dist/index.js CHANGED
@@ -125,6 +125,7 @@ var providerConfigSchema = z.object({
125
125
  type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
126
126
  base_url: z.string().url(),
127
127
  api_key_env_var: z.string().min(1).optional(),
128
+ timeout_ms: z.number().int().positive().optional(),
128
129
  headers: z.record(z.string()).optional(),
129
130
  enabled: z.boolean().optional(),
130
131
  metadata: z.record(z.unknown()).optional()
@@ -189,7 +190,8 @@ var projectConfigSchema = z.object({
189
190
  memory: z.object({
190
191
  provider: z.string().min(1).default("local"),
191
192
  base_url: z.string().url().optional(),
192
- api_key_env_var: z.string().min(1).optional()
193
+ api_key_env_var: z.string().min(1).optional(),
194
+ timeout_ms: z.number().int().positive().optional()
193
195
  }).strict().optional()
194
196
  }).strict();
195
197
  var appConfigSchema = z.object({
@@ -302,6 +304,7 @@ function toProviderConfig(raw) {
302
304
  type: raw.type,
303
305
  baseUrl: raw.base_url,
304
306
  apiKeyEnvVar: raw.api_key_env_var,
307
+ timeoutMs: raw.timeout_ms,
305
308
  headers: raw.headers,
306
309
  enabled: raw.enabled,
307
310
  metadata: raw.metadata
@@ -394,57 +397,200 @@ function authHeaders(config, env = process.env) {
394
397
  }
395
398
 
396
399
  // src/providers/http.ts
400
+ var HttpResponseError = class extends Error {
401
+ constructor(message, status, cause) {
402
+ super(message, { cause });
403
+ this.status = status;
404
+ this.name = "HttpResponseError";
405
+ }
406
+ status;
407
+ };
397
408
  var DEFAULT_TIMEOUT_MS = 3e4;
398
409
  async function requestJson(url, options = {}) {
399
410
  const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
400
- let response;
401
- try {
402
- response = await fetch(url, {
403
- method,
404
- headers: {
405
- accept: "application/json",
406
- ...body !== void 0 ? { "content-type": "application/json" } : {},
407
- ...headers
408
- },
409
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
410
- signal: AbortSignal.timeout(timeoutMs)
411
- });
412
- } catch (error) {
413
- const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
414
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
415
- cause: error
416
- });
417
- }
411
+ const response = await fetch(url, {
412
+ method,
413
+ headers: {
414
+ accept: "application/json",
415
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
416
+ ...headers
417
+ },
418
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
419
+ signal: AbortSignal.timeout(timeoutMs)
420
+ });
418
421
  let parsed;
419
422
  try {
420
423
  parsed = await response.json();
421
424
  } catch (error) {
422
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
423
- cause: error,
424
- details: { status: response.status }
425
- });
425
+ throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
426
426
  }
427
427
  return { status: response.status, body: parsed };
428
428
  }
429
429
 
430
+ // src/providers/errors.ts
431
+ function kindToCode(kind) {
432
+ switch (kind) {
433
+ case "AUTHENTICATION_ERROR":
434
+ return "PROVIDER_AUTH_FAILED";
435
+ case "MODEL_NOT_FOUND":
436
+ return "MODEL_NOT_FOUND";
437
+ default:
438
+ return "PROVIDER_UNAVAILABLE";
439
+ }
440
+ }
441
+ function isRetryable(kind) {
442
+ return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
443
+ }
444
+ function kindFromStatus(status) {
445
+ if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
446
+ if (status === 429) return "RATE_LIMIT_ERROR";
447
+ if (status === 404) return "MODEL_NOT_FOUND";
448
+ if (status === 408 || status === 504) return "TIMEOUT";
449
+ if (status >= 500) return "SERVER_ERROR";
450
+ if (status >= 400) return "INVALID_REQUEST";
451
+ return "UNKNOWN_ERROR";
452
+ }
453
+ function extractMessage(body) {
454
+ if (body === null || typeof body !== "object") return void 0;
455
+ const root = body;
456
+ const error = root.error ?? root;
457
+ const message = error.message ?? error.detail ?? root.message;
458
+ return typeof message === "string" ? message : void 0;
459
+ }
460
+ function parseRetryAfter(value, now = Date.now()) {
461
+ if (!value) return void 0;
462
+ const seconds = Number(value);
463
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
464
+ const date = Date.parse(value);
465
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
466
+ }
467
+ function isTimeoutError(error) {
468
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
469
+ }
470
+ function isNetworkError(error) {
471
+ return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
472
+ }
473
+ function fallbackMessage(kind, context, status) {
474
+ const { provider, operation } = context;
475
+ switch (kind) {
476
+ case "AUTHENTICATION_ERROR":
477
+ return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
478
+ case "RATE_LIMIT_ERROR":
479
+ return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
480
+ case "MODEL_NOT_FOUND":
481
+ return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
482
+ case "TIMEOUT":
483
+ return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
484
+ case "SERVER_ERROR":
485
+ return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
486
+ case "INVALID_REQUEST":
487
+ return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
488
+ default:
489
+ return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
490
+ }
491
+ }
492
+ function providerHttpError(context) {
493
+ const status = context.status ?? 0;
494
+ const kind = kindFromStatus(status);
495
+ const detail = extractMessage(context.body);
496
+ const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
497
+ return new ProviderError(
498
+ context.code ?? kindToCode(kind),
499
+ detail ?? fallbackMessage(kind, context, status),
500
+ {
501
+ cause: context.cause,
502
+ details: {
503
+ kind,
504
+ provider: context.provider,
505
+ operation: context.operation,
506
+ status,
507
+ retryable: isRetryable(kind),
508
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
509
+ ...detail ? { providerMessage: detail } : {}
510
+ }
511
+ }
512
+ );
513
+ }
514
+ function providerTransportError(context) {
515
+ const cause = context.cause;
516
+ const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
517
+ const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
518
+ return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
519
+ cause: context.cause,
520
+ details: {
521
+ kind,
522
+ provider: context.provider,
523
+ operation: context.operation,
524
+ retryable: isRetryable(kind)
525
+ }
526
+ });
527
+ }
528
+
430
529
  // src/providers/openai-compatible/index.ts
530
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
431
531
  var OpenAICompatibleProvider = class {
432
532
  config;
533
+ providerId;
534
+ authentication;
433
535
  log;
434
- constructor(config, log = logger) {
536
+ /** Discovered models, so getModel() answers without another round-trip. */
537
+ discovered;
538
+ env;
539
+ constructor(config, log = logger, env = process.env) {
435
540
  this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
541
+ this.env = env;
542
+ this.providerId = config.providerId ?? config.name;
543
+ this.authentication = {
544
+ method: config.apiKeyEnvVar ? "bearer" : "none",
545
+ ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
546
+ configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
547
+ };
436
548
  this.log = log.child({ provider: config.name });
437
549
  }
438
- async listModels() {
439
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
440
- headers: authHeaders(this.config)
441
- });
442
- if (status === 401 || status === 403) {
443
- throw authFailed(this.config, status);
550
+ /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
551
+ get requestTimeoutMs() {
552
+ return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
553
+ }
554
+ /** Authenticate + discover (roadmap §10, Model Discovery). */
555
+ async connect() {
556
+ authHeaders(this.config, this.env);
557
+ const models = await this.listModels();
558
+ this.discovered = models;
559
+ return models;
560
+ }
561
+ async getModel(modelId) {
562
+ const models = this.discovered ?? await this.connect();
563
+ return models.find((m) => m.id === modelId);
564
+ }
565
+ /** Raw /models body (protected so adapters can capture gateway-level metadata). */
566
+ async rawModelsResponse() {
567
+ const response = await this.modelsRequest();
568
+ return response.body;
569
+ }
570
+ async modelsRequest() {
571
+ let response;
572
+ try {
573
+ response = await requestJson(`${this.config.baseUrl}/models`, {
574
+ headers: authHeaders(this.config, this.env),
575
+ timeoutMs: this.requestTimeoutMs
576
+ });
577
+ } catch (error) {
578
+ if (error instanceof ProviderError) throw error;
579
+ throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
444
580
  }
581
+ const { status, body } = response;
445
582
  if (status !== 200) {
446
- throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
583
+ throw providerHttpError({
584
+ provider: this.config.name,
585
+ operation: "listModels",
586
+ status,
587
+ body
588
+ });
447
589
  }
590
+ return response;
591
+ }
592
+ async listModels() {
593
+ const body = await this.rawModelsResponse();
448
594
  const entries = extractModelEntries(body, this.config);
449
595
  const models = [];
450
596
  let skipped = 0;
@@ -453,22 +599,44 @@ var OpenAICompatibleProvider = class {
453
599
  skipped += 1;
454
600
  continue;
455
601
  }
456
- models.push({
457
- provider: this.config.name,
458
- id: entry.id,
459
- displayName: entry.id,
460
- status: "available",
461
- metadata: {
462
- ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
463
- ...typeof entry.created === "number" ? { createdAt: entry.created } : {}
464
- }
465
- });
602
+ models.push(this.toModelInfo(entry));
466
603
  }
467
604
  if (skipped > 0) {
468
605
  this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
469
606
  }
470
607
  return models;
471
608
  }
609
+ /** Maps a raw model entry onto the normalized metadata shape (§10). */
610
+ toModelInfo(entry) {
611
+ const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
612
+ const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
613
+ const inputTypes = normalizeModalities(entry.input_modalities);
614
+ const outputTypes = normalizeModalities(entry.output_modalities);
615
+ return {
616
+ provider: this.config.name,
617
+ id: entry.id,
618
+ displayName: entry.id,
619
+ ...typeof entry.name === "string" ? { name: entry.name } : {},
620
+ status: "available",
621
+ ...contextWindow !== void 0 ? { contextWindow } : {},
622
+ ...inputTypes !== void 0 ? { inputTypes } : {},
623
+ ...outputTypes !== void 0 ? { outputTypes } : {},
624
+ ...parameters !== void 0 ? {
625
+ supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
626
+ supportsStructuredOutput: parameters.includes("response_format"),
627
+ supportsStreaming: parameters.includes("stream")
628
+ } : {},
629
+ metadata: {
630
+ ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
631
+ ...typeof entry.created === "number" ? { createdAt: entry.created } : {},
632
+ ...parameters !== void 0 ? { supportedParameters: parameters } : {}
633
+ }
634
+ };
635
+ }
636
+ /** Streaming where the gateway supports it (§11). */
637
+ stream(request, onChunk) {
638
+ return streamChatCompletion(this.config, request, onChunk, this.env);
639
+ }
472
640
  async healthCheck() {
473
641
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
474
642
  try {
@@ -498,27 +666,35 @@ var OpenAICompatibleProvider = class {
498
666
  }
499
667
  }
500
668
  async chat(request) {
501
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
502
- method: "POST",
503
- headers: authHeaders(this.config),
504
- body: {
505
- model: request.model,
506
- messages: request.messages,
507
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
508
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
509
- ...request.stop !== void 0 ? { stop: request.stop } : {}
510
- }
511
- });
512
- if (status === 401 || status === 403) throw authFailed(this.config, status);
513
- if (status === 404) {
514
- throw new ProviderError(
515
- "MODEL_NOT_FOUND",
516
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
517
- { details: { status } }
518
- );
669
+ let response;
670
+ try {
671
+ response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
672
+ method: "POST",
673
+ headers: authHeaders(this.config, this.env),
674
+ timeoutMs: this.requestTimeoutMs,
675
+ body: {
676
+ model: request.model,
677
+ messages: request.messages,
678
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
679
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
680
+ ...request.stop !== void 0 ? { stop: request.stop } : {}
681
+ }
682
+ });
683
+ } catch (error) {
684
+ if (error instanceof ProviderError) throw error;
685
+ throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
519
686
  }
687
+ const { status, body } = response;
520
688
  if (status !== 200) {
521
- throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
689
+ throw providerHttpError({
690
+ provider: this.config.name,
691
+ operation: "chat",
692
+ status,
693
+ body,
694
+ ...status === 404 ? {
695
+ code: "MODEL_NOT_FOUND"
696
+ } : {}
697
+ });
522
698
  }
523
699
  const payload = body;
524
700
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
@@ -540,6 +716,81 @@ var OpenAICompatibleProvider = class {
540
716
  };
541
717
  }
542
718
  };
719
+ async function streamChatCompletion(config, request, onChunk, env = process.env) {
720
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
721
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
722
+ let response;
723
+ try {
724
+ response = await fetch(`${baseUrl}/chat/completions`, {
725
+ method: "POST",
726
+ headers: {
727
+ accept: "text/event-stream",
728
+ "content-type": "application/json",
729
+ ...authHeaders(config, env)
730
+ },
731
+ body: JSON.stringify({
732
+ model: request.model,
733
+ messages: request.messages,
734
+ stream: true,
735
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
736
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
737
+ }),
738
+ signal: AbortSignal.timeout(timeoutMs)
739
+ });
740
+ } catch (error) {
741
+ throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
742
+ }
743
+ if (!response.ok) {
744
+ const body = await response.json().catch(() => void 0);
745
+ throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
746
+ }
747
+ if (!response.body) {
748
+ throw providerHttpError({
749
+ provider: config.name,
750
+ operation: "stream",
751
+ status: 500,
752
+ body: { error: { message: "streaming is not supported by this endpoint" } }
753
+ });
754
+ }
755
+ let content = "";
756
+ let model = request.model;
757
+ let finishReason;
758
+ const decoder = new TextDecoder();
759
+ let buffer = "";
760
+ for await (const piece of response.body) {
761
+ buffer += decoder.decode(piece, { stream: true });
762
+ let newline = buffer.indexOf("\n");
763
+ while (newline >= 0) {
764
+ const line = buffer.slice(0, newline).trim();
765
+ buffer = buffer.slice(newline + 1);
766
+ newline = buffer.indexOf("\n");
767
+ if (line.length === 0 || line.startsWith(":")) continue;
768
+ const data = line.startsWith("data:") ? line.slice(5).trim() : line;
769
+ if (data === "[DONE]") continue;
770
+ let event;
771
+ try {
772
+ event = JSON.parse(data);
773
+ } catch {
774
+ continue;
775
+ }
776
+ if (typeof event.model === "string") model = event.model;
777
+ const choices = Array.isArray(event.choices) ? event.choices : [];
778
+ const first = choices[0];
779
+ const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
780
+ if (delta.length > 0) content += delta;
781
+ if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
782
+ onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
783
+ }
784
+ }
785
+ return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
786
+ }
787
+ function normalizeModalities(value) {
788
+ const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
789
+ if (!list) return void 0;
790
+ const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
791
+ const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
792
+ return modalities.length > 0 ? modalities : void 0;
793
+ }
543
794
  function normalizeBaseUrl(baseUrl) {
544
795
  return baseUrl.replace(/\/+$/, "");
545
796
  }
@@ -554,14 +805,6 @@ function extractModelEntries(body, config) {
554
805
  { details: { endpoint: "models" } }
555
806
  );
556
807
  }
557
- function authFailed(config, status) {
558
- const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
559
- return new ProviderError(
560
- "PROVIDER_AUTH_FAILED",
561
- `Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
562
- { details: { status } }
563
- );
564
- }
565
808
  function unavailable(config, message, status) {
566
809
  return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
567
810
  details: { status }
@@ -574,11 +817,33 @@ function inferHealth(error) {
574
817
 
575
818
  // src/providers/omnihilbras/index.ts
576
819
  var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
820
+ gateway;
821
+ async listModels() {
822
+ const raw = await this.rawModelsResponse();
823
+ await this.extractGatewayMetadata(raw);
824
+ const models = await super.listModels();
825
+ return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
826
+ }
577
827
  /**
578
828
  * Run the full connect flow. Throws on authentication or transport failure
579
829
  * (use healthCheck() for a non-throwing probe).
580
830
  */
581
- async connect(registry) {
831
+ /**
832
+ * Gateway metadata reported alongside the model list (gateway version, tier,
833
+ * region…), kept separate from per-model metadata (§11 — provider metadata).
834
+ */
835
+ gatewayMetadata() {
836
+ return this.gateway;
837
+ }
838
+ async extractGatewayMetadata(body) {
839
+ if (body !== null && typeof body === "object") {
840
+ const gateway = body.gateway;
841
+ if (gateway !== null && typeof gateway === "object") {
842
+ this.gateway = gateway;
843
+ }
844
+ }
845
+ }
846
+ async connectAndRegister(registry) {
582
847
  const apiKey = resolveApiKey(this.config);
583
848
  const discovered = await this.listModels();
584
849
  const seen = /* @__PURE__ */ new Set();
@@ -3325,7 +3590,7 @@ var ProjectStores = class {
3325
3590
  };
3326
3591
 
3327
3592
  // src/version.ts
3328
- var OMNINODE_VERSION = "2.0.0-alpha.5";
3593
+ var OMNINODE_VERSION = "2.0.0-alpha.7";
3329
3594
  export {
3330
3595
  AgentAdapterRegistry,
3331
3596
  AgentError,
@@ -3395,22 +3660,30 @@ export {
3395
3660
  encodeMessage,
3396
3661
  envelope,
3397
3662
  expandEnvRefs,
3663
+ extractMessage,
3398
3664
  extractReportFromText,
3399
3665
  findConfigFile,
3666
+ isNetworkError,
3400
3667
  isOmniNodeError,
3668
+ isRetryable,
3401
3669
  isSupportedProtocol,
3670
+ isTimeoutError,
3402
3671
  keywords,
3403
3672
  killTree,
3673
+ kindFromStatus,
3404
3674
  loadConfig,
3405
3675
  logger,
3406
3676
  newMessageId,
3407
3677
  parseHello,
3408
3678
  parsePlanJson,
3679
+ parseRetryAfter,
3409
3680
  pipelineConfigSchema,
3410
3681
  pipelineStepConfigSchema,
3411
3682
  plannerConfigSchema,
3412
3683
  projectConfigSchema,
3413
3684
  providerConfigSchema,
3685
+ providerHttpError,
3686
+ providerTransportError,
3414
3687
  registryKey,
3415
3688
  requestJson,
3416
3689
  requireCompletion,