@vizuh/sabi 0.1.2 → 0.1.3

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
@@ -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 model and reasoning effort from the trajectory's own state (tool calls and
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
- - **Command Code mod:** no Sabi provider key and no proxy; it routes the subscription already
18
+ - The Command Code mod needs no Sabi provider key or proxy. It routes the subscription already
14
19
  available to Command Code.
15
- - **Local proxy:** works with OpenCode, Hermes, Kilo and other OpenAI-compatible clients, using
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 never copies secret values into a
21
- harness config, terminal, worktree, log or Git. Users who do not use Jev can set `judge.enabled` to
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` both take precedence over the shipped default (or set
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.2 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
1
+ // @vizuh/sabi 0.1.3 — 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
@@ -589,6 +589,31 @@ function validateConfig(value, source = "<inline>") {
589
589
  throw new Error(`Sabi config ${source}: telemetry.captureChars must be a positive number`);
590
590
  }
591
591
  }
592
+ const controller = config.controller;
593
+ if (controller !== void 0) {
594
+ if (!isObject(controller)) throw new Error(`Sabi config ${source}: controller must be an object`);
595
+ const controllerFields = /* @__PURE__ */ new Set(["preferredHarnesses", "harnesses"]);
596
+ for (const field of Object.keys(controller)) {
597
+ if (!controllerFields.has(field)) throw new Error(`Sabi config ${source}: controller.${field} is not a supported field`);
598
+ }
599
+ const validateStrings = (value2, label) => {
600
+ if (!Array.isArray(value2) || value2.some((item) => typeof item !== "string" || !item.trim())) {
601
+ throw new Error(`Sabi config ${source}: ${label} must be an array of nonempty strings`);
602
+ }
603
+ if (new Set(value2).size !== value2.length) throw new Error(`Sabi config ${source}: ${label} must not contain duplicates`);
604
+ };
605
+ if (controller.preferredHarnesses !== void 0) validateStrings(controller.preferredHarnesses, "controller.preferredHarnesses");
606
+ if (controller.harnesses !== void 0) {
607
+ if (!isObject(controller.harnesses)) throw new Error(`Sabi config ${source}: controller.harnesses must be an object`);
608
+ for (const [harnessName, settings] of Object.entries(controller.harnesses)) {
609
+ if (!isObject(settings)) throw new Error(`Sabi config ${source}: controller.harnesses.${harnessName} must be an object`);
610
+ for (const field of Object.keys(settings)) {
611
+ if (field !== "preferredModels") throw new Error(`Sabi config ${source}: controller.harnesses.${harnessName}.${field} is not a supported field`);
612
+ }
613
+ if (settings.preferredModels !== void 0) validateStrings(settings.preferredModels, `controller.harnesses.${harnessName}.preferredModels`);
614
+ }
615
+ }
616
+ }
592
617
  const harness = config.harness;
593
618
  if (harness !== void 0) {
594
619
  if (typeof harness !== "object" || harness === null) {
@@ -612,6 +637,25 @@ function validateConfig(value, source = "<inline>") {
612
637
  return { ...config, upstreams, models, aliases, policy };
613
638
  }
614
639
 
640
+ // packages/core/src/log.ts
641
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
642
+ import { createHash, randomUUID } from "node:crypto";
643
+ import path2 from "node:path";
644
+ function defaultLogPath() {
645
+ return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
646
+ }
647
+ function appendDecision(record, logFile = defaultLogPath()) {
648
+ mkdirSync(path2.dirname(logFile), { recursive: true });
649
+ appendFileSync(logFile, `${JSON.stringify(record)}
650
+ `);
651
+ }
652
+ function hashIdentity(kind, ...parts) {
653
+ return createHash("sha256").update(JSON.stringify([kind, ...parts])).digest("hex");
654
+ }
655
+ function sessionIdFor(session, client = "unknown") {
656
+ return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
657
+ }
658
+
615
659
  // packages/core/src/harness.ts
616
660
  function roundKindOf(round, calls) {
617
661
  if (round.assistantTurns === 0 || round.lastRole === "user") return "first-turn";
@@ -733,8 +777,10 @@ function sanitizeReason(reason, policy) {
733
777
  import * as readline from "node:readline/promises";
734
778
 
735
779
  // packages/adapters/command-code/mod/sabi.ts
780
+ import path3 from "node:path";
736
781
  var MOD_ID = "sabi";
737
782
  var DECISION_TYPE = "sabi/decision";
783
+ var CLIENT_ID = "command-code";
738
784
  function readLedger(state) {
739
785
  const raw = state.modState?.[MOD_ID] ?? {};
740
786
  return {
@@ -746,7 +792,8 @@ function readLedger(state) {
746
792
  toolNames: Array.isArray(raw.toolNames) ? raw.toolNames : [],
747
793
  hasTools: raw.hasTools === true,
748
794
  lastModel: raw.lastModel,
749
- lastUsage: raw.lastUsage
795
+ lastUsage: raw.lastUsage,
796
+ sessionId: typeof raw.sessionId === "string" ? raw.sessionId : void 0
750
797
  };
751
798
  }
752
799
  function writeLedger(state, ledger) {
@@ -755,6 +802,29 @@ function writeLedger(state, ledger) {
755
802
  function compactedSince(ledger, stats) {
756
803
  return ledger.messageCount > 0 && stats.messageCount > 0 && stats.messageCount < ledger.messageCount;
757
804
  }
805
+ function hashedToolNames(names) {
806
+ return names.map((name) => hashIdentity("tool", name));
807
+ }
808
+ function toUsageTotals(usage) {
809
+ if (!usage) return void 0;
810
+ const validTokens = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
811
+ if (!validTokens(usage.inputTokens) || !validTokens(usage.outputTokens)) return void 0;
812
+ const cachedTokens = usage.cachedInputTokens === void 0 ? 0 : usage.cachedInputTokens;
813
+ if (!validTokens(cachedTokens) || cachedTokens > usage.inputTokens) return void 0;
814
+ const promptTokens = usage.inputTokens;
815
+ const completionTokens = usage.outputTokens;
816
+ const totalTokens = promptTokens + completionTokens;
817
+ if (!Number.isSafeInteger(totalTokens)) return void 0;
818
+ return {
819
+ promptTokens,
820
+ completionTokens,
821
+ cachedTokens,
822
+ totalTokens
823
+ };
824
+ }
825
+ function logPathFor(ctx) {
826
+ return ctx?.cwd?.trim() ? path3.join(ctx.cwd, ".sabi", "decisions.jsonl") : void 0;
827
+ }
758
828
  function sabi(cmd) {
759
829
  let config;
760
830
  try {
@@ -785,6 +855,9 @@ function sabi(cmd) {
785
855
  nextPlan = void 0;
786
856
  servedBy = void 0;
787
857
  const ledger = readLedger(state);
858
+ if (!ledger.sessionId) {
859
+ ledger.sessionId = sessionIdFor(void 0, CLIENT_ID);
860
+ }
788
861
  return writeLedger(state, { ...ledger, rounds: turnNumber });
789
862
  },
790
863
  // Sabi observes tool outcomes and never rewrites what the model sees.
@@ -847,7 +920,7 @@ function sabi(cmd) {
847
920
  lastUsage: usedThisTurn ? usage : void 0
848
921
  };
849
922
  previousFailure = adopted ? { failure: adopted.state.failure, failureEvidence: adopted.state.failureEvidence } : void 0;
850
- recordDecision(ctx, {
923
+ recordCustomEntry(ctx, {
851
924
  turn: turnNumber,
852
925
  planned: servingPlan ? {
853
926
  tier: servingPlan.tier,
@@ -869,11 +942,49 @@ function sabi(cmd) {
869
942
  // Decision records never embed raw tool output by default; snippet capture is opt-in.
870
943
  captureSnippets: telemetry.captureSnippets
871
944
  });
945
+ if (servingPlan) {
946
+ const sessionId = ledger.sessionId ?? sessionIdFor(void 0, CLIENT_ID);
947
+ const usageTotals = toUsageTotals(usage);
948
+ const decision = {
949
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
950
+ sessionId,
951
+ // sessionKnown stays false: the host does not expose a real session ID to the mod.
952
+ client: CLIENT_ID,
953
+ turnId: hashIdentity("turn", sessionId, String(turnNumber)),
954
+ servedModel: servedBy ?? void 0,
955
+ alias: "sabi-code",
956
+ mode: "auto",
957
+ rule: servingPlan.rule,
958
+ tier: servingPlan.tier,
959
+ reason: sanitizeReason(String(servingPlan.reason ?? ""), telemetry),
960
+ // This is the host adapter, not a provider entitlement claim.
961
+ upstream: CLIENT_ID,
962
+ upstreamModel: servingPlan.model,
963
+ stream: false,
964
+ state: {
965
+ ...servingPlan.state,
966
+ // Hash tool names; never persist raw tool outputs or args.
967
+ toolNames: hashedToolNames(servingPlan.state.toolNames),
968
+ lastToolNames: hashedToolNames(servingPlan.state.lastToolNames)
969
+ },
970
+ sessionKnown: false,
971
+ ...usageTotals ? { usage: usageTotals } : {},
972
+ // outcome means the model request round completed, not that the whole task succeeded.
973
+ outcome: "ok"
974
+ };
975
+ const logFile = logPathFor(ctx);
976
+ if (logFile) {
977
+ try {
978
+ appendDecision(decision, logFile);
979
+ } catch {
980
+ }
981
+ }
982
+ }
872
983
  return writeLedger(state, next);
873
984
  }
874
985
  });
875
986
  }
876
- function recordDecision(ctx, data) {
987
+ function recordCustomEntry(ctx, data) {
877
988
  ctx?.session?.appendCustomEntry({ customType: DECISION_TYPE, data });
878
989
  }
879
990
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizuh/sabi",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Adaptive inference scheduling for Command Code: one bundled mod that routes each continuing round by model, effort and trajectory state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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-18; input modalities for the same ids verified from the same endpoint the same day (`architecture.input_modalities`). Prices are USD per 1M tokens. Declared modalities are enforced: an undeclared capability is unknown, a declared one is binding.",
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
  },
@@ -70,6 +73,13 @@
70
73
  "captureSnippets": false,
71
74
  "captureChars": 800
72
75
  },
76
+ "controller": {
77
+ "preferredHarnesses": ["opencode", "command-code", "claude", "codex", "hermes"],
78
+ "harnesses": {
79
+ "command-code": { "preferredModels": ["moonshotai/kimi-k3"] },
80
+ "opencode": { "preferredModels": ["opencode-go/kimi-k3"] }
81
+ }
82
+ },
73
83
  "judge": {
74
84
  "enabled": true,
75
85
  "baseURL": "https://api.typesafe.ai/v1",