@vizuh/sabi 0.1.2 → 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/README.md +14 -8
- package/mod/sabi.mjs +248 -6
- package/package.json +1 -1
- package/sabi.config.json +14 -1
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
# @vizuh/sabi
|
|
1
|
+
# @vizuh/sabi: Command Code adapter
|
|
2
|
+
|
|
3
|
+
This npm artifact is one Sabi adapter, not the whole Sabi product. Sabi also supports a local
|
|
4
|
+
OpenAI-compatible proxy for OpenCode, Hermes, Prime Agent, Kilo and other clients, plus an
|
|
5
|
+
experimental controller surface for Claude Code, Codex and Orca. See the
|
|
6
|
+
[adapter directory](../../../docs/adapters/README.md) for the product map and evidence boundaries.
|
|
2
7
|
|
|
3
8
|
Adaptive inference scheduling for [Command Code](https://commandcode.ai): a mod that plans each
|
|
4
|
-
continuing round
|
|
9
|
+
continuing round, including model and reasoning effort, from the trajectory's own state (tool calls and
|
|
5
10
|
their results, failure evidence, context size). Round 1 always runs on your session model; from
|
|
6
11
|
round 2 on, a read round goes cheap, edits and tests go mid, and a failing tool escalates.
|
|
7
12
|
|
|
@@ -10,15 +15,16 @@ lives in the [repository](https://github.com/vizuh/sabi).
|
|
|
10
15
|
|
|
11
16
|
The two paths are independent:
|
|
12
17
|
|
|
13
|
-
-
|
|
18
|
+
- The Command Code mod needs no Sabi provider key or proxy. It routes the subscription already
|
|
14
19
|
available to Command Code.
|
|
15
|
-
-
|
|
16
|
-
OpenRouter, Ollama or another configured upstream.
|
|
20
|
+
- The local proxy works with OpenCode, Hermes, Kilo, and other OpenAI-compatible clients. It uses
|
|
21
|
+
OpenRouter, Ollama, or another configured upstream.
|
|
17
22
|
|
|
18
23
|
For the proxy, Sabi loads only the credential names referenced by `sabi.config.json`. Existing
|
|
19
24
|
environment variables win, followed by `SABI_SECRETS_FILE`, the nearest workspace `secrets/.env`,
|
|
20
|
-
and `~/.config/sabi/secrets.env` or `~/.config/sabi/.env`. It
|
|
21
|
-
|
|
25
|
+
and `~/.config/sabi/secrets.env` or `~/.config/sabi/.env`. It does not copy loaded values into generated harness configuration or logs. If you use a workspace
|
|
26
|
+
secrets/.env, that source file is already in the worktree: keep it out of version control, add it to
|
|
27
|
+
.gitignore, and protect its file permissions. Users who do not use Jev can set `judge.enabled` to
|
|
22
28
|
`false`; users without a central secrets file can keep exporting provider variables normally.
|
|
23
29
|
|
|
24
30
|
## Install
|
|
@@ -43,7 +49,7 @@ cmd mods remove sabi
|
|
|
43
49
|
|
|
44
50
|
The package ships a default `sabi.config.json` next to the bundle, so it works with no setup.
|
|
45
51
|
To change tiers, policy or telemetry, put your own `sabi.config.json` in the project you run in,
|
|
46
|
-
or at `~/.config/sabi/sabi.config.json
|
|
52
|
+
or at `~/.config/sabi/sabi.config.json`; both take precedence over the shipped default (or set
|
|
47
53
|
`SABI_CONFIG` to point at one explicitly).
|
|
48
54
|
|
|
49
55
|
`harness.tiers` defaults to the strongest Command Code ids available from the Go plan up. A model
|
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) {
|
|
@@ -589,6 +619,31 @@ function validateConfig(value, source = "<inline>") {
|
|
|
589
619
|
throw new Error(`Sabi config ${source}: telemetry.captureChars must be a positive number`);
|
|
590
620
|
}
|
|
591
621
|
}
|
|
622
|
+
const controller = config.controller;
|
|
623
|
+
if (controller !== void 0) {
|
|
624
|
+
if (!isObject(controller)) throw new Error(`Sabi config ${source}: controller must be an object`);
|
|
625
|
+
const controllerFields = /* @__PURE__ */ new Set(["preferredHarnesses", "harnesses"]);
|
|
626
|
+
for (const field of Object.keys(controller)) {
|
|
627
|
+
if (!controllerFields.has(field)) throw new Error(`Sabi config ${source}: controller.${field} is not a supported field`);
|
|
628
|
+
}
|
|
629
|
+
const validateStrings = (value2, label) => {
|
|
630
|
+
if (!Array.isArray(value2) || value2.some((item) => typeof item !== "string" || !item.trim())) {
|
|
631
|
+
throw new Error(`Sabi config ${source}: ${label} must be an array of nonempty strings`);
|
|
632
|
+
}
|
|
633
|
+
if (new Set(value2).size !== value2.length) throw new Error(`Sabi config ${source}: ${label} must not contain duplicates`);
|
|
634
|
+
};
|
|
635
|
+
if (controller.preferredHarnesses !== void 0) validateStrings(controller.preferredHarnesses, "controller.preferredHarnesses");
|
|
636
|
+
if (controller.harnesses !== void 0) {
|
|
637
|
+
if (!isObject(controller.harnesses)) throw new Error(`Sabi config ${source}: controller.harnesses must be an object`);
|
|
638
|
+
for (const [harnessName, settings] of Object.entries(controller.harnesses)) {
|
|
639
|
+
if (!isObject(settings)) throw new Error(`Sabi config ${source}: controller.harnesses.${harnessName} must be an object`);
|
|
640
|
+
for (const field of Object.keys(settings)) {
|
|
641
|
+
if (field !== "preferredModels") throw new Error(`Sabi config ${source}: controller.harnesses.${harnessName}.${field} is not a supported field`);
|
|
642
|
+
}
|
|
643
|
+
if (settings.preferredModels !== void 0) validateStrings(settings.preferredModels, `controller.harnesses.${harnessName}.preferredModels`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
592
647
|
const harness = config.harness;
|
|
593
648
|
if (harness !== void 0) {
|
|
594
649
|
if (typeof harness !== "object" || harness === null) {
|
|
@@ -607,11 +662,131 @@ function validateConfig(value, source = "<inline>") {
|
|
|
607
662
|
if (tier.minPlan !== void 0 && typeof tier.minPlan !== "string") {
|
|
608
663
|
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.minPlan must be a string`);
|
|
609
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
|
+
}
|
|
610
676
|
}
|
|
611
677
|
}
|
|
612
678
|
return { ...config, upstreams, models, aliases, policy };
|
|
613
679
|
}
|
|
614
680
|
|
|
681
|
+
// packages/core/src/log.ts
|
|
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";
|
|
685
|
+
import path2 from "node:path";
|
|
686
|
+
function defaultLogPath() {
|
|
687
|
+
return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
|
|
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
|
+
}
|
|
775
|
+
function appendDecision(record, logFile = defaultLogPath()) {
|
|
776
|
+
try {
|
|
777
|
+
appendPrivateLine(logFile, `${JSON.stringify(record)}
|
|
778
|
+
`);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
recordLogWriteFailure(logFile, error);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function hashIdentity(kind, ...parts) {
|
|
784
|
+
return createHmac("sha256", getIdentitySalt()).update(JSON.stringify([kind, ...parts])).digest("hex");
|
|
785
|
+
}
|
|
786
|
+
function sessionIdFor(session, client = "unknown") {
|
|
787
|
+
return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
|
|
788
|
+
}
|
|
789
|
+
|
|
615
790
|
// packages/core/src/harness.ts
|
|
616
791
|
function roundKindOf(round, calls) {
|
|
617
792
|
if (round.assistantTurns === 0 || round.lastRole === "user") return "first-turn";
|
|
@@ -733,8 +908,10 @@ function sanitizeReason(reason, policy) {
|
|
|
733
908
|
import * as readline from "node:readline/promises";
|
|
734
909
|
|
|
735
910
|
// packages/adapters/command-code/mod/sabi.ts
|
|
911
|
+
import path3 from "node:path";
|
|
736
912
|
var MOD_ID = "sabi";
|
|
737
913
|
var DECISION_TYPE = "sabi/decision";
|
|
914
|
+
var CLIENT_ID = "command-code";
|
|
738
915
|
function readLedger(state) {
|
|
739
916
|
const raw = state.modState?.[MOD_ID] ?? {};
|
|
740
917
|
return {
|
|
@@ -746,7 +923,8 @@ function readLedger(state) {
|
|
|
746
923
|
toolNames: Array.isArray(raw.toolNames) ? raw.toolNames : [],
|
|
747
924
|
hasTools: raw.hasTools === true,
|
|
748
925
|
lastModel: raw.lastModel,
|
|
749
|
-
lastUsage: raw.lastUsage
|
|
926
|
+
lastUsage: raw.lastUsage,
|
|
927
|
+
sessionId: typeof raw.sessionId === "string" ? raw.sessionId : void 0
|
|
750
928
|
};
|
|
751
929
|
}
|
|
752
930
|
function writeLedger(state, ledger) {
|
|
@@ -755,6 +933,29 @@ function writeLedger(state, ledger) {
|
|
|
755
933
|
function compactedSince(ledger, stats) {
|
|
756
934
|
return ledger.messageCount > 0 && stats.messageCount > 0 && stats.messageCount < ledger.messageCount;
|
|
757
935
|
}
|
|
936
|
+
function hashedToolNames(names) {
|
|
937
|
+
return names.map((name) => hashIdentity("tool", name));
|
|
938
|
+
}
|
|
939
|
+
function toUsageTotals(usage) {
|
|
940
|
+
if (!usage) return void 0;
|
|
941
|
+
const validTokens = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
942
|
+
if (!validTokens(usage.inputTokens) || !validTokens(usage.outputTokens)) return void 0;
|
|
943
|
+
const cachedTokens = usage.cachedInputTokens === void 0 ? 0 : usage.cachedInputTokens;
|
|
944
|
+
if (!validTokens(cachedTokens) || cachedTokens > usage.inputTokens) return void 0;
|
|
945
|
+
const promptTokens = usage.inputTokens;
|
|
946
|
+
const completionTokens = usage.outputTokens;
|
|
947
|
+
const totalTokens = promptTokens + completionTokens;
|
|
948
|
+
if (!Number.isSafeInteger(totalTokens)) return void 0;
|
|
949
|
+
return {
|
|
950
|
+
promptTokens,
|
|
951
|
+
completionTokens,
|
|
952
|
+
cachedTokens,
|
|
953
|
+
totalTokens
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function logPathFor(ctx) {
|
|
957
|
+
return ctx?.cwd?.trim() ? path3.join(ctx.cwd, ".sabi", "decisions.jsonl") : void 0;
|
|
958
|
+
}
|
|
758
959
|
function sabi(cmd) {
|
|
759
960
|
let config;
|
|
760
961
|
try {
|
|
@@ -785,6 +986,9 @@ function sabi(cmd) {
|
|
|
785
986
|
nextPlan = void 0;
|
|
786
987
|
servedBy = void 0;
|
|
787
988
|
const ledger = readLedger(state);
|
|
989
|
+
if (!ledger.sessionId) {
|
|
990
|
+
ledger.sessionId = sessionIdFor(void 0, CLIENT_ID);
|
|
991
|
+
}
|
|
788
992
|
return writeLedger(state, { ...ledger, rounds: turnNumber });
|
|
789
993
|
},
|
|
790
994
|
// Sabi observes tool outcomes and never rewrites what the model sees.
|
|
@@ -847,7 +1051,7 @@ function sabi(cmd) {
|
|
|
847
1051
|
lastUsage: usedThisTurn ? usage : void 0
|
|
848
1052
|
};
|
|
849
1053
|
previousFailure = adopted ? { failure: adopted.state.failure, failureEvidence: adopted.state.failureEvidence } : void 0;
|
|
850
|
-
|
|
1054
|
+
recordCustomEntry(ctx, {
|
|
851
1055
|
turn: turnNumber,
|
|
852
1056
|
planned: servingPlan ? {
|
|
853
1057
|
tier: servingPlan.tier,
|
|
@@ -869,11 +1073,49 @@ function sabi(cmd) {
|
|
|
869
1073
|
// Decision records never embed raw tool output by default; snippet capture is opt-in.
|
|
870
1074
|
captureSnippets: telemetry.captureSnippets
|
|
871
1075
|
});
|
|
1076
|
+
if (servingPlan) {
|
|
1077
|
+
const sessionId = ledger.sessionId ?? sessionIdFor(void 0, CLIENT_ID);
|
|
1078
|
+
const usageTotals = toUsageTotals(usage);
|
|
1079
|
+
const decision = {
|
|
1080
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1081
|
+
sessionId,
|
|
1082
|
+
// sessionKnown stays false: the host does not expose a real session ID to the mod.
|
|
1083
|
+
client: CLIENT_ID,
|
|
1084
|
+
turnId: hashIdentity("turn", sessionId, String(turnNumber)),
|
|
1085
|
+
servedModel: servedBy ?? void 0,
|
|
1086
|
+
alias: "sabi-code",
|
|
1087
|
+
mode: "auto",
|
|
1088
|
+
rule: servingPlan.rule,
|
|
1089
|
+
tier: servingPlan.tier,
|
|
1090
|
+
reason: sanitizeReason(String(servingPlan.reason ?? ""), telemetry),
|
|
1091
|
+
// This is the host adapter, not a provider entitlement claim.
|
|
1092
|
+
upstream: CLIENT_ID,
|
|
1093
|
+
upstreamModel: servingPlan.model,
|
|
1094
|
+
stream: false,
|
|
1095
|
+
state: {
|
|
1096
|
+
...servingPlan.state,
|
|
1097
|
+
// Hash tool names; never persist raw tool outputs or args.
|
|
1098
|
+
toolNames: hashedToolNames(servingPlan.state.toolNames),
|
|
1099
|
+
lastToolNames: hashedToolNames(servingPlan.state.lastToolNames)
|
|
1100
|
+
},
|
|
1101
|
+
sessionKnown: false,
|
|
1102
|
+
...usageTotals ? { usage: usageTotals } : {},
|
|
1103
|
+
// outcome means the model request round completed, not that the whole task succeeded.
|
|
1104
|
+
outcome: "ok"
|
|
1105
|
+
};
|
|
1106
|
+
const logFile = logPathFor(ctx);
|
|
1107
|
+
if (logFile) {
|
|
1108
|
+
try {
|
|
1109
|
+
appendDecision(decision, logFile);
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
872
1114
|
return writeLedger(state, next);
|
|
873
1115
|
}
|
|
874
1116
|
});
|
|
875
1117
|
}
|
|
876
|
-
function
|
|
1118
|
+
function recordCustomEntry(ctx, data) {
|
|
877
1119
|
ctx?.session?.appendCustomEntry({ customType: DECISION_TYPE, data });
|
|
878
1120
|
}
|
|
879
1121
|
export {
|
package/package.json
CHANGED
package/sabi.config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"provenance": "Model ids, context windows and prices verified live from https://openrouter.ai/api/v1/models on 2026-09-
|
|
2
|
+
"provenance": "Model ids, context windows, output limits and prices verified live from https://openrouter.ai/api/v1/models on 2026-09-20; input modalities for the same ids verified from the same endpoint. Prices are USD per 1M tokens. Declared modalities are enforced: an undeclared capability is unknown, a declared one is binding.",
|
|
3
3
|
"server": { "host": "127.0.0.1", "port": 8787 },
|
|
4
4
|
"upstreams": {
|
|
5
5
|
"openrouter": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"upstream": "openrouter",
|
|
23
23
|
"model": "deepseek/deepseek-v4-flash-0731",
|
|
24
24
|
"contextWindow": 1310720,
|
|
25
|
+
"maxOutputTokens": 943718,
|
|
25
26
|
"capabilities": { "inputModalities": ["text"] },
|
|
26
27
|
"cost": { "input": 0.06, "output": 0.12, "cacheRead": 0.012 }
|
|
27
28
|
},
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
"upstream": "openrouter",
|
|
30
31
|
"model": "openai/gpt-5.6-luna",
|
|
31
32
|
"contextWindow": 1050000,
|
|
33
|
+
"maxOutputTokens": 128000,
|
|
32
34
|
"capabilities": { "inputModalities": ["text", "image", "file"] },
|
|
33
35
|
"cost": { "input": 0.2, "output": 1.2, "cacheRead": 0.02 }
|
|
34
36
|
},
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"upstream": "openrouter",
|
|
37
39
|
"model": "anthropic/claude-sonnet-5",
|
|
38
40
|
"contextWindow": 1000000,
|
|
41
|
+
"maxOutputTokens": 128000,
|
|
39
42
|
"capabilities": { "inputModalities": ["text", "image", "file"] },
|
|
40
43
|
"cost": { "input": 2, "output": 10, "cacheRead": 0.2 }
|
|
41
44
|
},
|
|
@@ -65,11 +68,21 @@
|
|
|
65
68
|
"exploration": "cheap",
|
|
66
69
|
"unclassified": "cheap"
|
|
67
70
|
},
|
|
71
|
+
"transportFallback": {
|
|
72
|
+
"enabled": false
|
|
73
|
+
},
|
|
68
74
|
"telemetry": {
|
|
69
75
|
"allowlistOnly": true,
|
|
70
76
|
"captureSnippets": false,
|
|
71
77
|
"captureChars": 800
|
|
72
78
|
},
|
|
79
|
+
"controller": {
|
|
80
|
+
"preferredHarnesses": ["opencode", "command-code", "claude", "codex", "hermes"],
|
|
81
|
+
"harnesses": {
|
|
82
|
+
"command-code": { "preferredModels": ["moonshotai/kimi-k3"] },
|
|
83
|
+
"opencode": { "preferredModels": ["opencode-go/kimi-k3"] }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
73
86
|
"judge": {
|
|
74
87
|
"enabled": true,
|
|
75
88
|
"baseURL": "https://api.typesafe.ai/v1",
|