@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 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.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 [name, tier] of Object.entries(tiers)) {
335
- if (servesInputModalities(declared(tier), required)) return name;
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
- recordDecision(ctx, {
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 recordDecision(ctx, data) {
1118
+ function recordCustomEntry(ctx, data) {
877
1119
  ctx?.session?.appendCustomEntry({ customType: DECISION_TYPE, data });
878
1120
  }
879
1121
  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.4",
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
  },
@@ -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",