@hilbras/omninode 2.0.0-alpha.5 → 2.0.0-alpha.6
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 +11 -3
- package/dist/cli/index.js +218 -69
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +87 -5
- package/dist/index.js +228 -68
- 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.6 — v2 Phase 6: Provider Infrastructure v2**
|
|
22
22
|
(the reliability & interoperability line: architecture → execution →
|
|
23
|
-
protocol → adapters → pipelines) (the v2
|
|
23
|
+
protocol → adapters → pipelines → providers) (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,14 @@ 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 6 — Provider Infrastructure v2**: standardized provider contract
|
|
35
|
+
(`providerId`, secret-free `authentication`, declared `capabilities`,
|
|
36
|
+
`connect()` / `getModel()` discovery), richer model metadata (context
|
|
37
|
+
window, input/output modalities, tool/vision/structured-output/streaming
|
|
38
|
+
support), and **unified error normalization** — every provider failure now
|
|
39
|
+
carries a stable `kind` (auth, rate limit, invalid request, model not
|
|
40
|
+
found, timeout, network, server, unknown) plus `retryable` and
|
|
41
|
+
`retryAfterMs`. Details: [PROVIDERS.md](docs/PROVIDERS.md).
|
|
34
42
|
- **Phase 5 — Pipeline Lifecycle**: first-class pipeline cancellation
|
|
35
43
|
(`omninode pipeline cancel <run-id>`: stop scheduling → cancel running
|
|
36
44
|
tasks → terminate agent processes → persist a `cancelled` run), `pipeline
|
|
@@ -319,7 +327,7 @@ required for the core engine.
|
|
|
319
327
|
- [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
|
|
320
328
|
- [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
|
|
321
329
|
- [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)
|
|
330
|
+
- [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
331
|
- [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
|
|
324
332
|
|
|
325
333
|
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.6";
|
|
10
10
|
|
|
11
11
|
// src/errors/index.ts
|
|
12
12
|
var OmniNodeError = class extends Error {
|
|
@@ -363,35 +363,32 @@ function authHeaders(config, env = process.env) {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
// src/providers/http.ts
|
|
366
|
+
var HttpResponseError = class extends Error {
|
|
367
|
+
constructor(message, status, cause) {
|
|
368
|
+
super(message, { cause });
|
|
369
|
+
this.status = status;
|
|
370
|
+
this.name = "HttpResponseError";
|
|
371
|
+
}
|
|
372
|
+
status;
|
|
373
|
+
};
|
|
366
374
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
367
375
|
async function requestJson(url, options = {}) {
|
|
368
376
|
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
|
-
}
|
|
377
|
+
const response = await fetch(url, {
|
|
378
|
+
method,
|
|
379
|
+
headers: {
|
|
380
|
+
accept: "application/json",
|
|
381
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
382
|
+
...headers
|
|
383
|
+
},
|
|
384
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
385
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
386
|
+
});
|
|
387
387
|
let parsed;
|
|
388
388
|
try {
|
|
389
389
|
parsed = await response.json();
|
|
390
390
|
} catch (error) {
|
|
391
|
-
throw new
|
|
392
|
-
cause: error,
|
|
393
|
-
details: { status: response.status }
|
|
394
|
-
});
|
|
391
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
395
392
|
}
|
|
396
393
|
return { status: response.status, body: parsed };
|
|
397
394
|
}
|
|
@@ -452,23 +449,151 @@ var Logger = class _Logger {
|
|
|
452
449
|
};
|
|
453
450
|
var logger = new Logger();
|
|
454
451
|
|
|
452
|
+
// src/providers/errors.ts
|
|
453
|
+
function kindToCode(kind) {
|
|
454
|
+
switch (kind) {
|
|
455
|
+
case "AUTHENTICATION_ERROR":
|
|
456
|
+
return "PROVIDER_AUTH_FAILED";
|
|
457
|
+
case "MODEL_NOT_FOUND":
|
|
458
|
+
return "MODEL_NOT_FOUND";
|
|
459
|
+
default:
|
|
460
|
+
return "PROVIDER_UNAVAILABLE";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function isRetryable(kind) {
|
|
464
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
465
|
+
}
|
|
466
|
+
function kindFromStatus(status) {
|
|
467
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
468
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
469
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
470
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
471
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
472
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
473
|
+
return "UNKNOWN_ERROR";
|
|
474
|
+
}
|
|
475
|
+
function extractMessage(body) {
|
|
476
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
477
|
+
const root = body;
|
|
478
|
+
const error = root.error ?? root;
|
|
479
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
480
|
+
return typeof message === "string" ? message : void 0;
|
|
481
|
+
}
|
|
482
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
483
|
+
if (!value) return void 0;
|
|
484
|
+
const seconds = Number(value);
|
|
485
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
486
|
+
const date = Date.parse(value);
|
|
487
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
488
|
+
}
|
|
489
|
+
function isTimeoutError(error) {
|
|
490
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
491
|
+
}
|
|
492
|
+
function fallbackMessage(kind, context, status) {
|
|
493
|
+
const { provider, operation } = context;
|
|
494
|
+
switch (kind) {
|
|
495
|
+
case "AUTHENTICATION_ERROR":
|
|
496
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
497
|
+
case "RATE_LIMIT_ERROR":
|
|
498
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
499
|
+
case "MODEL_NOT_FOUND":
|
|
500
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
501
|
+
case "TIMEOUT":
|
|
502
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
503
|
+
case "SERVER_ERROR":
|
|
504
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
505
|
+
case "INVALID_REQUEST":
|
|
506
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
507
|
+
default:
|
|
508
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function providerHttpError(context) {
|
|
512
|
+
const status = context.status ?? 0;
|
|
513
|
+
const kind = kindFromStatus(status);
|
|
514
|
+
const detail = extractMessage(context.body);
|
|
515
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
516
|
+
return new ProviderError(
|
|
517
|
+
context.code ?? kindToCode(kind),
|
|
518
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
519
|
+
{
|
|
520
|
+
cause: context.cause,
|
|
521
|
+
details: {
|
|
522
|
+
kind,
|
|
523
|
+
provider: context.provider,
|
|
524
|
+
operation: context.operation,
|
|
525
|
+
status,
|
|
526
|
+
retryable: isRetryable(kind),
|
|
527
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
528
|
+
...detail ? { providerMessage: detail } : {}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
function providerTransportError(context) {
|
|
534
|
+
const cause = context.cause;
|
|
535
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
536
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
537
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
538
|
+
cause: context.cause,
|
|
539
|
+
details: {
|
|
540
|
+
kind,
|
|
541
|
+
provider: context.provider,
|
|
542
|
+
operation: context.operation,
|
|
543
|
+
retryable: isRetryable(kind)
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
455
548
|
// src/providers/openai-compatible/index.ts
|
|
456
549
|
var OpenAICompatibleProvider = class {
|
|
457
550
|
config;
|
|
551
|
+
providerId;
|
|
552
|
+
authentication;
|
|
458
553
|
log;
|
|
459
|
-
|
|
554
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
555
|
+
discovered;
|
|
556
|
+
env;
|
|
557
|
+
constructor(config, log = logger, env = process.env) {
|
|
460
558
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
559
|
+
this.env = env;
|
|
560
|
+
this.providerId = config.providerId ?? config.name;
|
|
561
|
+
this.authentication = {
|
|
562
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
563
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
564
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
565
|
+
};
|
|
461
566
|
this.log = log.child({ provider: config.name });
|
|
462
567
|
}
|
|
568
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
569
|
+
async connect() {
|
|
570
|
+
authHeaders(this.config, this.env);
|
|
571
|
+
const models = await this.listModels();
|
|
572
|
+
this.discovered = models;
|
|
573
|
+
return models;
|
|
574
|
+
}
|
|
575
|
+
async getModel(modelId) {
|
|
576
|
+
const models = this.discovered ?? await this.connect();
|
|
577
|
+
return models.find((m) => m.id === modelId);
|
|
578
|
+
}
|
|
463
579
|
async listModels() {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
580
|
+
let response;
|
|
581
|
+
try {
|
|
582
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
583
|
+
headers: authHeaders(this.config, this.env)
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error instanceof ProviderError) throw error;
|
|
587
|
+
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
469
588
|
}
|
|
589
|
+
const { status, body } = response;
|
|
470
590
|
if (status !== 200) {
|
|
471
|
-
throw
|
|
591
|
+
throw providerHttpError({
|
|
592
|
+
provider: this.config.name,
|
|
593
|
+
operation: "listModels",
|
|
594
|
+
status,
|
|
595
|
+
body
|
|
596
|
+
});
|
|
472
597
|
}
|
|
473
598
|
const entries = extractModelEntries(body, this.config);
|
|
474
599
|
const models = [];
|
|
@@ -478,22 +603,40 @@ var OpenAICompatibleProvider = class {
|
|
|
478
603
|
skipped += 1;
|
|
479
604
|
continue;
|
|
480
605
|
}
|
|
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
|
-
});
|
|
606
|
+
models.push(this.toModelInfo(entry2));
|
|
491
607
|
}
|
|
492
608
|
if (skipped > 0) {
|
|
493
609
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
494
610
|
}
|
|
495
611
|
return models;
|
|
496
612
|
}
|
|
613
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
614
|
+
toModelInfo(entry2) {
|
|
615
|
+
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
616
|
+
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
617
|
+
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
618
|
+
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
619
|
+
return {
|
|
620
|
+
provider: this.config.name,
|
|
621
|
+
id: entry2.id,
|
|
622
|
+
displayName: entry2.id,
|
|
623
|
+
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
624
|
+
status: "available",
|
|
625
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
626
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
627
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
628
|
+
...parameters !== void 0 ? {
|
|
629
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
630
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
631
|
+
supportsStreaming: parameters.includes("stream")
|
|
632
|
+
} : {},
|
|
633
|
+
metadata: {
|
|
634
|
+
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
635
|
+
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
636
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
497
640
|
async healthCheck() {
|
|
498
641
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
499
642
|
try {
|
|
@@ -523,27 +666,34 @@ var OpenAICompatibleProvider = class {
|
|
|
523
666
|
}
|
|
524
667
|
}
|
|
525
668
|
async chat(request) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{ details: { status } }
|
|
543
|
-
);
|
|
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
|
+
body: {
|
|
675
|
+
model: request.model,
|
|
676
|
+
messages: request.messages,
|
|
677
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
678
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
679
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (error instanceof ProviderError) throw error;
|
|
684
|
+
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
544
685
|
}
|
|
686
|
+
const { status, body } = response;
|
|
545
687
|
if (status !== 200) {
|
|
546
|
-
throw
|
|
688
|
+
throw providerHttpError({
|
|
689
|
+
provider: this.config.name,
|
|
690
|
+
operation: "chat",
|
|
691
|
+
status,
|
|
692
|
+
body,
|
|
693
|
+
...status === 404 ? {
|
|
694
|
+
code: "MODEL_NOT_FOUND"
|
|
695
|
+
} : {}
|
|
696
|
+
});
|
|
547
697
|
}
|
|
548
698
|
const payload = body;
|
|
549
699
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -565,6 +715,13 @@ var OpenAICompatibleProvider = class {
|
|
|
565
715
|
};
|
|
566
716
|
}
|
|
567
717
|
};
|
|
718
|
+
function normalizeModalities(value) {
|
|
719
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
720
|
+
if (!list) return void 0;
|
|
721
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
722
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
723
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
724
|
+
}
|
|
568
725
|
function normalizeBaseUrl(baseUrl) {
|
|
569
726
|
return baseUrl.replace(/\/+$/, "");
|
|
570
727
|
}
|
|
@@ -579,14 +736,6 @@ function extractModelEntries(body, config) {
|
|
|
579
736
|
{ details: { endpoint: "models" } }
|
|
580
737
|
);
|
|
581
738
|
}
|
|
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
739
|
function unavailable(config, message, status) {
|
|
591
740
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
592
741
|
details: { status }
|
|
@@ -603,7 +752,7 @@ var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
|
603
752
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
604
753
|
* (use healthCheck() for a non-throwing probe).
|
|
605
754
|
*/
|
|
606
|
-
async
|
|
755
|
+
async connectAndRegister(registry) {
|
|
607
756
|
const apiKey = resolveApiKey(this.config);
|
|
608
757
|
const discovered = await this.listModels();
|
|
609
758
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -827,7 +976,7 @@ async function testProvider(name, options = {}) {
|
|
|
827
976
|
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
828
977
|
const registry = new ModelRegistry();
|
|
829
978
|
try {
|
|
830
|
-
const result = await provider.
|
|
979
|
+
const result = await provider.connectAndRegister(registry);
|
|
831
980
|
printProviderStatus(result.status);
|
|
832
981
|
console.log(` registered: ${result.registered}`);
|
|
833
982
|
} catch (error) {
|