@vizuh/sabi 0.1.3 → 0.1.4
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/mod/sabi.mjs +139 -8
- package/package.json +1 -1
- package/sabi.config.json +3 -0
package/mod/sabi.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @vizuh/sabi 0.1.
|
|
1
|
+
// @vizuh/sabi 0.1.4 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
|
|
2
2
|
// Source and docs: https://github.com/vizuh/sabi
|
|
3
3
|
|
|
4
4
|
// packages/core/src/state.ts
|
|
@@ -331,8 +331,8 @@ function servesInputModalities(declared, required) {
|
|
|
331
331
|
return required.every((modality) => declared.includes(modality));
|
|
332
332
|
}
|
|
333
333
|
function firstServingTier(tiers, required, declared) {
|
|
334
|
-
for (const
|
|
335
|
-
if (servesInputModalities(declared(
|
|
334
|
+
for (const name of Object.keys(tiers).sort()) {
|
|
335
|
+
if (servesInputModalities(declared(tiers[name]), required)) return name;
|
|
336
336
|
}
|
|
337
337
|
return void 0;
|
|
338
338
|
}
|
|
@@ -441,6 +441,9 @@ function validateModelMetadata(value, label) {
|
|
|
441
441
|
if (!Array.isArray(items) || items.some((item) => typeof item !== "string" || !item.trim())) {
|
|
442
442
|
fail(`capabilities.${field}`, "must be an array of nonempty strings");
|
|
443
443
|
}
|
|
444
|
+
if ((field === "inputModalities" || field === "outputModalities") && items.length === 0) {
|
|
445
|
+
fail(`capabilities.${field}`, "must not be empty");
|
|
446
|
+
}
|
|
444
447
|
const strings = items;
|
|
445
448
|
if (new Set(strings).size !== strings.length) fail(`capabilities.${field}`, "must not contain duplicates");
|
|
446
449
|
const allowed = field.endsWith("Modalities") ? MODALITIES : field === "structuredOutput" ? ["json_object", "json_schema"] : void 0;
|
|
@@ -530,11 +533,30 @@ function validateConfig(value, source = "<inline>") {
|
|
|
530
533
|
throw new Error(`Sabi config ${source}: alias '${alias}' targets unknown tier '${target}'`);
|
|
531
534
|
}
|
|
532
535
|
}
|
|
536
|
+
const knownRules = new Set(POLICY_ORDER);
|
|
533
537
|
for (const [condition, tier] of Object.entries(policy)) {
|
|
538
|
+
if (!knownRules.has(condition)) {
|
|
539
|
+
throw new Error(`Sabi config ${source}: policy rule '${condition}' is not a known rule (${POLICY_ORDER.join(", ")})`);
|
|
540
|
+
}
|
|
534
541
|
if (typeof tier !== "string" || tier !== "off" && !Object.hasOwn(models, tier)) {
|
|
535
542
|
throw new Error(`Sabi config ${source}: policy rule '${condition}' targets unknown tier '${tier}'`);
|
|
536
543
|
}
|
|
537
544
|
}
|
|
545
|
+
if (Object.values(aliases).includes("auto")) {
|
|
546
|
+
const fallbackTier = typeof policy.unclassified === "string" && policy.unclassified !== "off" ? policy.unclassified : "cheap";
|
|
547
|
+
if (!Object.hasOwn(models, fallbackTier)) {
|
|
548
|
+
throw new Error(`Sabi config ${source}: policy.unclassified must resolve to a declared tier (got '${String(policy.unclassified)}')`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const transportFallback = config.transportFallback;
|
|
552
|
+
if (transportFallback !== void 0) {
|
|
553
|
+
if (!isObject(transportFallback)) {
|
|
554
|
+
throw new Error(`Sabi config ${source}: transportFallback must be an object`);
|
|
555
|
+
}
|
|
556
|
+
if (transportFallback.enabled !== void 0 && typeof transportFallback.enabled !== "boolean") {
|
|
557
|
+
throw new Error(`Sabi config ${source}: transportFallback.enabled must be a boolean`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
538
560
|
const judge = config.judge;
|
|
539
561
|
if (judge !== void 0) {
|
|
540
562
|
if (typeof judge !== "object" || judge === null || typeof judge.enabled !== "boolean") {
|
|
@@ -556,6 +578,14 @@ function validateConfig(value, source = "<inline>") {
|
|
|
556
578
|
if (!Array.isArray(judge.callOn) || judge.callOn.some((rule) => typeof rule !== "string")) {
|
|
557
579
|
throw new Error(`Sabi config ${source}: judge.callOn must be an array of policy rule names`);
|
|
558
580
|
}
|
|
581
|
+
for (const rule of judge.callOn) {
|
|
582
|
+
if (!knownRules.has(rule)) {
|
|
583
|
+
throw new Error(`Sabi config ${source}: judge.callOn rule '${rule}' is not a known rule (${POLICY_ORDER.join(", ")})`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (judge.includeSnippets !== void 0 && typeof judge.includeSnippets !== "boolean") {
|
|
588
|
+
throw new Error(`Sabi config ${source}: judge.includeSnippets must be a boolean`);
|
|
559
589
|
}
|
|
560
590
|
for (const [name, value2] of Object.entries(judge.thresholds ?? {})) {
|
|
561
591
|
if (typeof value2 !== "number" || value2 < 0 || value2 > 1) {
|
|
@@ -632,25 +662,126 @@ function validateConfig(value, source = "<inline>") {
|
|
|
632
662
|
if (tier.minPlan !== void 0 && typeof tier.minPlan !== "string") {
|
|
633
663
|
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.minPlan must be a string`);
|
|
634
664
|
}
|
|
665
|
+
if (tier.inputModalities !== void 0) {
|
|
666
|
+
if (!Array.isArray(tier.inputModalities) || tier.inputModalities.length === 0 || tier.inputModalities.some((modality) => typeof modality !== "string" || !MODALITIES.includes(modality))) {
|
|
667
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must be a nonempty array of: ${MODALITIES.join(", ")}`);
|
|
668
|
+
}
|
|
669
|
+
if (new Set(tier.inputModalities).size !== tier.inputModalities.length) {
|
|
670
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must not contain duplicates`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (tier.contextWindow !== void 0 && (typeof tier.contextWindow !== "number" || !Number.isSafeInteger(tier.contextWindow) || tier.contextWindow < 1)) {
|
|
674
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.contextWindow must be a safe integer >= 1`);
|
|
675
|
+
}
|
|
635
676
|
}
|
|
636
677
|
}
|
|
637
678
|
return { ...config, upstreams, models, aliases, policy };
|
|
638
679
|
}
|
|
639
680
|
|
|
640
681
|
// packages/core/src/log.ts
|
|
641
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
642
|
-
import {
|
|
682
|
+
import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
683
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
684
|
+
import os2 from "node:os";
|
|
643
685
|
import path2 from "node:path";
|
|
644
686
|
function defaultLogPath() {
|
|
645
687
|
return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
|
|
646
688
|
}
|
|
689
|
+
function appendPrivateLine(logFile, line) {
|
|
690
|
+
const dir = path2.dirname(logFile);
|
|
691
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
692
|
+
appendFileSync(logFile, line, { mode: 384 });
|
|
693
|
+
if (process.platform !== "win32") {
|
|
694
|
+
chmodSync(dir, 448);
|
|
695
|
+
chmodSync(logFile, 384);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function identitySaltPath(env = process.env) {
|
|
699
|
+
const override = env.SABI_ID_SALT_FILE?.trim();
|
|
700
|
+
if (override) return override;
|
|
701
|
+
const base = env.XDG_CONFIG_HOME?.trim() || path2.join(os2.homedir(), ".config");
|
|
702
|
+
return path2.join(base, "sabi", ".identity-salt");
|
|
703
|
+
}
|
|
704
|
+
var cachedSalt;
|
|
705
|
+
var cachedSaltSource;
|
|
706
|
+
var ephemeralSaltWarned = false;
|
|
707
|
+
function loadOrCreateSalt(source) {
|
|
708
|
+
try {
|
|
709
|
+
if (existsSync2(source)) {
|
|
710
|
+
const stored = readFileSync2(source, "utf8").trim();
|
|
711
|
+
if (stored.length >= 16) return stored;
|
|
712
|
+
}
|
|
713
|
+
const fresh = randomBytes(32).toString("hex");
|
|
714
|
+
mkdirSync(path2.dirname(source), { recursive: true });
|
|
715
|
+
writeFileSync(source, `${fresh}
|
|
716
|
+
`, { mode: 384 });
|
|
717
|
+
try {
|
|
718
|
+
chmodSync(source, 384);
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
return fresh;
|
|
722
|
+
} catch {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function getIdentitySalt(env = process.env) {
|
|
727
|
+
const override = env.SABI_ID_SALT?.trim();
|
|
728
|
+
if (override) return override;
|
|
729
|
+
const source = identitySaltPath(env);
|
|
730
|
+
if (cachedSalt !== void 0 && cachedSaltSource === source) return cachedSalt;
|
|
731
|
+
const salt = loadOrCreateSalt(source);
|
|
732
|
+
if (salt === void 0) {
|
|
733
|
+
if (!ephemeralSaltWarned) {
|
|
734
|
+
ephemeralSaltWarned = true;
|
|
735
|
+
console.warn(`Sabi: identity salt at ${source} is unreadable \u2014 using an ephemeral per-process salt`);
|
|
736
|
+
}
|
|
737
|
+
cachedSalt = randomBytes(32).toString("hex");
|
|
738
|
+
cachedSaltSource = source;
|
|
739
|
+
return cachedSalt;
|
|
740
|
+
}
|
|
741
|
+
cachedSalt = salt;
|
|
742
|
+
cachedSaltSource = source;
|
|
743
|
+
return salt;
|
|
744
|
+
}
|
|
745
|
+
var logWriteFailures = 0;
|
|
746
|
+
var warnedLogFiles = /* @__PURE__ */ new Set();
|
|
747
|
+
function logWriteFailurePath(logFile) {
|
|
748
|
+
return `${logFile}.write-failures.json`;
|
|
749
|
+
}
|
|
750
|
+
function recordLogWriteFailure(logFile, error) {
|
|
751
|
+
logWriteFailures += 1;
|
|
752
|
+
if (!warnedLogFiles.has(logFile)) {
|
|
753
|
+
warnedLogFiles.add(logFile);
|
|
754
|
+
console.warn(`Sabi: could not append to the decision log at ${logFile}: ${error?.message ?? error} (telemetry degraded, will retry next round)`);
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
const sidecar = logWriteFailurePath(logFile);
|
|
758
|
+
let failures = 0;
|
|
759
|
+
try {
|
|
760
|
+
const prior = JSON.parse(readFileSync2(sidecar, "utf8"));
|
|
761
|
+
if (typeof prior.failures === "number" && Number.isFinite(prior.failures)) failures = Math.floor(prior.failures);
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
const state = {
|
|
765
|
+
failures: failures + 1,
|
|
766
|
+
lastTs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
767
|
+
lastError: String(error?.message ?? error).slice(0, 120)
|
|
768
|
+
};
|
|
769
|
+
mkdirSync(path2.dirname(sidecar), { recursive: true });
|
|
770
|
+
writeFileSync(sidecar, `${JSON.stringify(state)}
|
|
771
|
+
`);
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
}
|
|
647
775
|
function appendDecision(record, logFile = defaultLogPath()) {
|
|
648
|
-
|
|
649
|
-
|
|
776
|
+
try {
|
|
777
|
+
appendPrivateLine(logFile, `${JSON.stringify(record)}
|
|
650
778
|
`);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
recordLogWriteFailure(logFile, error);
|
|
781
|
+
}
|
|
651
782
|
}
|
|
652
783
|
function hashIdentity(kind, ...parts) {
|
|
653
|
-
return
|
|
784
|
+
return createHmac("sha256", getIdentitySalt()).update(JSON.stringify([kind, ...parts])).digest("hex");
|
|
654
785
|
}
|
|
655
786
|
function sessionIdFor(session, client = "unknown") {
|
|
656
787
|
return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
|
package/package.json
CHANGED