@vizuh/sabi 0.1.4 → 0.2.0
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 +3 -1
- package/mod/sabi.mjs +519 -56
- package/package.json +4 -2
- package/sabi.config.json +16 -15
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@ The two paths are independent:
|
|
|
18
18
|
- The Command Code mod needs no Sabi provider key or proxy. It routes the subscription already
|
|
19
19
|
available to Command Code.
|
|
20
20
|
- The local proxy works with OpenCode, Hermes, Kilo, and other OpenAI-compatible clients. It uses
|
|
21
|
-
OpenRouter, Ollama, or another configured upstream.
|
|
21
|
+
OpenRouter, Ollama, or another configured upstream. In the shipped default the OpenRouter
|
|
22
|
+
upstream is **free-models-only** (`paidModelsAllowed: false`): a priced model id is refused
|
|
23
|
+
before the request leaves the process, so the proxy cannot spend on its own.
|
|
22
24
|
|
|
23
25
|
For the proxy, Sabi loads only the credential names referenced by `sabi.config.json`. Existing
|
|
24
26
|
environment variables win, followed by `SABI_SECRETS_FILE`, the nearest workspace `secrets/.env`,
|
package/mod/sabi.mjs
CHANGED
|
@@ -1,6 +1,260 @@
|
|
|
1
|
-
// @vizuh/sabi 0.
|
|
1
|
+
// @vizuh/sabi 0.2.0 — 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
|
+
// packages/core/src/telemetry.ts
|
|
5
|
+
var ALLOWLIST = /* @__PURE__ */ new Set([
|
|
6
|
+
"error-line",
|
|
7
|
+
"python-traceback",
|
|
8
|
+
"panic",
|
|
9
|
+
"exception",
|
|
10
|
+
"typescript-error",
|
|
11
|
+
"fail-marker",
|
|
12
|
+
"command-failed",
|
|
13
|
+
"nonzero-exit",
|
|
14
|
+
"failure-count",
|
|
15
|
+
"command-not-found",
|
|
16
|
+
"permission-denied",
|
|
17
|
+
"missing-file",
|
|
18
|
+
"soft-warning",
|
|
19
|
+
"soft-deprecated",
|
|
20
|
+
"soft-retrying",
|
|
21
|
+
"soft-timeout",
|
|
22
|
+
"tool-error",
|
|
23
|
+
"permission-denial",
|
|
24
|
+
"rate-limited",
|
|
25
|
+
"quota-exceeded",
|
|
26
|
+
"timeout",
|
|
27
|
+
"mutation",
|
|
28
|
+
"verification-receipt",
|
|
29
|
+
"summary-claim",
|
|
30
|
+
"scope-observed",
|
|
31
|
+
"constraint",
|
|
32
|
+
"prior-failure",
|
|
33
|
+
"context-boundary",
|
|
34
|
+
"observation"
|
|
35
|
+
]);
|
|
36
|
+
function allowlisted(value) {
|
|
37
|
+
const code = value.split(":")[0]?.trim();
|
|
38
|
+
return ALLOWLIST.has(code);
|
|
39
|
+
}
|
|
40
|
+
function telemetryPolicy(config) {
|
|
41
|
+
const captureSnippets = config?.captureSnippets === true;
|
|
42
|
+
const captureChars = config?.captureChars ?? 800;
|
|
43
|
+
return {
|
|
44
|
+
captureSnippets,
|
|
45
|
+
allowlisted: (value) => allowlisted(value),
|
|
46
|
+
snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function sanitizeReason(reason, policy) {
|
|
50
|
+
if (!policy.captureSnippets) {
|
|
51
|
+
if (!reason || allowlisted(reason)) return reason;
|
|
52
|
+
const segments = reason.split(":").map((segment) => segment.trim());
|
|
53
|
+
const last = segments[segments.length - 1] ?? "";
|
|
54
|
+
if (allowlisted(last)) return reason;
|
|
55
|
+
return "reason withheld (telemetry.allowlistOnly)";
|
|
56
|
+
}
|
|
57
|
+
return policy.snippet(reason);
|
|
58
|
+
}
|
|
59
|
+
var KEYWORD_REDACT = /\b(bearer|authorization|api[-_]?\s*key|token|secret|password|passwd)\b["']?\s*[:=\s]\s*["']?[^\s"'{},\]]+/gi;
|
|
60
|
+
var BARE_TOKEN_REDACT = [
|
|
61
|
+
[/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"],
|
|
62
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "AKIA[REDACTED]"],
|
|
63
|
+
[/\bAIza[0-9A-Za-z_-]{30,}\b/g, "AIza[REDACTED]"],
|
|
64
|
+
[/\bghp_[A-Za-z0-9]{8,}\b/g, "ghp_[REDACTED]"],
|
|
65
|
+
[/\bghu_[A-Za-z0-9]{8,}\b/g, "ghu_[REDACTED]"],
|
|
66
|
+
[/\bghs_[A-Za-z0-9]{8,}\b/g, "ghs_[REDACTED]"],
|
|
67
|
+
[/\bgithub_pat_[A-Za-z0-9_]{8,}\b/g, "github_pat_[REDACTED]"]
|
|
68
|
+
];
|
|
69
|
+
var CREDENTIAL_URL_REDACT = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^:@\s/]+:[^@\s/]+@/g;
|
|
70
|
+
function sanitizeError(error) {
|
|
71
|
+
const first = String(error ?? "").split("\n")[0]?.slice(0, 200) ?? "";
|
|
72
|
+
if (!first.trim()) return "unknown error";
|
|
73
|
+
let redacted = first.replace(KEYWORD_REDACT, "$1=REDACTED");
|
|
74
|
+
for (const [pattern, replacement] of BARE_TOKEN_REDACT) redacted = redacted.replace(pattern, replacement);
|
|
75
|
+
redacted = redacted.replace(CREDENTIAL_URL_REDACT, "$1REDACTED@");
|
|
76
|
+
if (looksLikeCanary(redacted)) return "upstream error redacted (possible secret)";
|
|
77
|
+
return redacted || "unknown error";
|
|
78
|
+
}
|
|
79
|
+
var CANARY_PATTERNS = [
|
|
80
|
+
/\b(BEGIN|END)\s+(RSA|OPENSSH|EC|DSA)\s+PRIVATE\s+KEY/i,
|
|
81
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/,
|
|
82
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
83
|
+
/\bAIza[0-9A-Za-z_-]{30,}\b/,
|
|
84
|
+
/\bghp_[A-Za-z0-9]{8,}\b/,
|
|
85
|
+
/\bghu_[A-Za-z0-9]{8,}\b/,
|
|
86
|
+
/\bghs_[A-Za-z0-9]{8,}\b/,
|
|
87
|
+
/\bgithub_pat_[A-Za-z0-9_]{8,}\b/,
|
|
88
|
+
/[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]+:[^@\s/]+@/,
|
|
89
|
+
/(?:password|passwd|pwd|secret)\s*[:=]\s*\S+/i
|
|
90
|
+
];
|
|
91
|
+
function looksLikeCanary(text) {
|
|
92
|
+
return CANARY_PATTERNS.some((re) => re.test(text));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// packages/core/src/evidence.ts
|
|
96
|
+
var MAX_EVIDENCE_ITEMS = 16;
|
|
97
|
+
var MAX_EVIDENCE_DETAIL_CHARS = 240;
|
|
98
|
+
var MAX_SCOPE_ITEMS = 32;
|
|
99
|
+
var MAX_SCOPE_LABEL_CHARS = 120;
|
|
100
|
+
var MAX_RECEIPT_ID_CHARS = 128;
|
|
101
|
+
var EVIDENCE_CODES = /* @__PURE__ */ new Set([
|
|
102
|
+
"error-line",
|
|
103
|
+
"python-traceback",
|
|
104
|
+
"panic",
|
|
105
|
+
"exception",
|
|
106
|
+
"typescript-error",
|
|
107
|
+
"fail-marker",
|
|
108
|
+
"command-failed",
|
|
109
|
+
"nonzero-exit",
|
|
110
|
+
"failure-count",
|
|
111
|
+
"command-not-found",
|
|
112
|
+
"permission-denied",
|
|
113
|
+
"missing-file",
|
|
114
|
+
"soft-warning",
|
|
115
|
+
"soft-deprecated",
|
|
116
|
+
"soft-retrying",
|
|
117
|
+
"soft-timeout",
|
|
118
|
+
"tool-error",
|
|
119
|
+
"permission-denial",
|
|
120
|
+
"rate-limited",
|
|
121
|
+
"quota-exceeded",
|
|
122
|
+
"timeout",
|
|
123
|
+
"mutation",
|
|
124
|
+
"verification-receipt",
|
|
125
|
+
"summary-claim",
|
|
126
|
+
"scope-observed",
|
|
127
|
+
"constraint",
|
|
128
|
+
"prior-failure",
|
|
129
|
+
"context-boundary",
|
|
130
|
+
"observation"
|
|
131
|
+
]);
|
|
132
|
+
var EVIDENCE_SOURCES = /* @__PURE__ */ new Set(["tool", "user", "harness", "judge", "summary"]);
|
|
133
|
+
var EVIDENCE_STATUSES = /* @__PURE__ */ new Set(["observed", "verified", "unverified", "contradicted"]);
|
|
134
|
+
function boundedText(value, maxChars) {
|
|
135
|
+
if (typeof value !== "string") return void 0;
|
|
136
|
+
const text = value.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
137
|
+
if (!text || looksLikeCanary(text)) return void 0;
|
|
138
|
+
return text.slice(0, maxChars);
|
|
139
|
+
}
|
|
140
|
+
function generationOf(value) {
|
|
141
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
142
|
+
}
|
|
143
|
+
function isEvidenceCode(value) {
|
|
144
|
+
return typeof value === "string" && EVIDENCE_CODES.has(value);
|
|
145
|
+
}
|
|
146
|
+
function isEvidenceSource(value) {
|
|
147
|
+
return typeof value === "string" && EVIDENCE_SOURCES.has(value);
|
|
148
|
+
}
|
|
149
|
+
function isEvidenceStatus(value) {
|
|
150
|
+
return typeof value === "string" && EVIDENCE_STATUSES.has(value);
|
|
151
|
+
}
|
|
152
|
+
function normalizeTrajectoryEvidence(value, fallbackGeneration = 0) {
|
|
153
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
154
|
+
const item = value;
|
|
155
|
+
if (!isEvidenceCode(item.code) || !isEvidenceSource(item.source) || !isEvidenceStatus(item.status)) return void 0;
|
|
156
|
+
const detail = boundedText(item.detail, MAX_EVIDENCE_DETAIL_CHARS);
|
|
157
|
+
return {
|
|
158
|
+
code: item.code,
|
|
159
|
+
source: item.source,
|
|
160
|
+
status: item.status,
|
|
161
|
+
contextGeneration: generationOf(item.contextGeneration ?? fallbackGeneration),
|
|
162
|
+
...detail ? { detail } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function boundTrajectoryEvidence(values, maxItems = MAX_EVIDENCE_ITEMS) {
|
|
166
|
+
const result2 = [];
|
|
167
|
+
if (maxItems <= 0) return result2;
|
|
168
|
+
const seen = /* @__PURE__ */ new Set();
|
|
169
|
+
for (const value of values ?? []) {
|
|
170
|
+
const item = normalizeTrajectoryEvidence(value);
|
|
171
|
+
if (!item) continue;
|
|
172
|
+
const key = JSON.stringify(item);
|
|
173
|
+
if (seen.has(key)) continue;
|
|
174
|
+
seen.add(key);
|
|
175
|
+
result2.push(item);
|
|
176
|
+
if (result2.length >= Math.max(0, Math.min(maxItems, MAX_EVIDENCE_ITEMS))) break;
|
|
177
|
+
}
|
|
178
|
+
return result2;
|
|
179
|
+
}
|
|
180
|
+
function countScope(value) {
|
|
181
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? { count: value } : {};
|
|
182
|
+
if (!Array.isArray(value)) return {};
|
|
183
|
+
const labels = [...new Set(value.map((item) => boundedText(item, MAX_SCOPE_LABEL_CHARS)).filter((item) => Boolean(item)))].slice(0, MAX_SCOPE_ITEMS);
|
|
184
|
+
return { count: labels.length, labels };
|
|
185
|
+
}
|
|
186
|
+
function buildScopeCoverage(input = {}) {
|
|
187
|
+
const expected = countScope(input.expected);
|
|
188
|
+
const observed = countScope(input.observed);
|
|
189
|
+
const explicit = input.source === "explicit" || input.expected !== void 0;
|
|
190
|
+
const source = input.source ?? (explicit ? "explicit" : input.observed !== void 0 ? "inferred" : "unknown");
|
|
191
|
+
const expectedCount = expected.count;
|
|
192
|
+
const observedCount = observed.count;
|
|
193
|
+
const ratio = expectedCount !== void 0 && expectedCount > 0 && observedCount !== void 0 ? Math.min(1, observedCount / expectedCount) : void 0;
|
|
194
|
+
const missing = input.missing ? [...new Set(input.missing.map((item) => boundedText(item, MAX_SCOPE_LABEL_CHARS)).filter((item) => Boolean(item)))].slice(0, MAX_SCOPE_ITEMS) : expected.labels && observed.labels ? expected.labels.filter((item) => !observed.labels.includes(item)).slice(0, MAX_SCOPE_ITEMS) : void 0;
|
|
195
|
+
return {
|
|
196
|
+
...expectedCount !== void 0 ? { expected: expectedCount } : {},
|
|
197
|
+
...observedCount !== void 0 ? { observed: observedCount } : {},
|
|
198
|
+
...ratio !== void 0 ? { ratio } : {},
|
|
199
|
+
source,
|
|
200
|
+
...missing && missing.length > 0 ? { missing } : {}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function validReceipt(value, generation) {
|
|
204
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
205
|
+
const item = value;
|
|
206
|
+
const id = boundedText(item.id, MAX_RECEIPT_ID_CHARS);
|
|
207
|
+
const status = item.status === "passed" || item.status === "failed" ? item.status : void 0;
|
|
208
|
+
const source = item.source === "tool" || item.source === "user" || item.source === "harness" ? item.source : void 0;
|
|
209
|
+
const receiptGeneration = generationOf(item.generation);
|
|
210
|
+
if (!id || !status || !source || item.valid === false || receiptGeneration !== generation) return void 0;
|
|
211
|
+
return { id, status, source, generation: receiptGeneration, ...item.valid === true ? { valid: true } : {} };
|
|
212
|
+
}
|
|
213
|
+
function deriveVerificationState(input = {}) {
|
|
214
|
+
const generation = generationOf(input.generation);
|
|
215
|
+
const receiptCandidate = input.receipt;
|
|
216
|
+
if (receiptCandidate !== void 0) {
|
|
217
|
+
const receipt = validReceipt(receiptCandidate, generation);
|
|
218
|
+
if (receipt) return { status: receipt.status, receiptId: receipt.id, generation };
|
|
219
|
+
return { status: "unknown", reason: "invalid-receipt", generation };
|
|
220
|
+
}
|
|
221
|
+
if (input.summaryClaim) return { status: "unknown", reason: "summary-without-receipt", generation };
|
|
222
|
+
if (input.mutation) return { status: "needed", reason: "mutation-without-receipt", generation };
|
|
223
|
+
if (input.verificationAttempted) return { status: "attempted", reason: "missing-receipt", generation };
|
|
224
|
+
return { status: "not-required", generation };
|
|
225
|
+
}
|
|
226
|
+
function evidenceForTrajectory(state, options = {}) {
|
|
227
|
+
const generation = generationOf(state.contextGeneration);
|
|
228
|
+
const evidence = [];
|
|
229
|
+
if (state.roundKind === "implementation") evidence.push({ code: "mutation", source: "tool", status: "observed", contextGeneration: generation });
|
|
230
|
+
for (const code of state.failureEvidence) {
|
|
231
|
+
if (isEvidenceCode(code)) evidence.push({ code, source: "tool", status: "observed", contextGeneration: generation });
|
|
232
|
+
}
|
|
233
|
+
if (options.verificationReceipt) evidence.push({ code: "verification-receipt", source: "harness", status: "verified", contextGeneration: generation });
|
|
234
|
+
if (options.summaryClaim) evidence.push({ code: "summary-claim", source: "summary", status: "unverified", contextGeneration: generation });
|
|
235
|
+
if (generation > 0) evidence.push({ code: "context-boundary", source: "harness", status: "observed", contextGeneration: generation });
|
|
236
|
+
return boundTrajectoryEvidence(evidence);
|
|
237
|
+
}
|
|
238
|
+
function decorateTrajectoryState(state, metadata = {}) {
|
|
239
|
+
const generation = generationOf(metadata.generation ?? state.contextGeneration);
|
|
240
|
+
const receipt = metadata.verificationReceipt;
|
|
241
|
+
const verification = deriveVerificationState({
|
|
242
|
+
mutation: state.roundKind === "implementation",
|
|
243
|
+
verificationAttempted: state.roundKind === "verification",
|
|
244
|
+
summaryClaim: metadata.summaryClaim,
|
|
245
|
+
generation,
|
|
246
|
+
receipt
|
|
247
|
+
});
|
|
248
|
+
const scopeCoverage = metadata.scope ? buildScopeCoverage(metadata.scope) : void 0;
|
|
249
|
+
const evidence = evidenceForTrajectory(state, { summaryClaim: metadata.summaryClaim, verificationReceipt: receipt !== void 0 });
|
|
250
|
+
return {
|
|
251
|
+
...state,
|
|
252
|
+
...evidence.length > 0 ? { evidence } : {},
|
|
253
|
+
verification,
|
|
254
|
+
...scopeCoverage ? { scopeCoverage } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
4
258
|
// packages/core/src/state.ts
|
|
5
259
|
var EXPLORE_TOOLS = /* @__PURE__ */ new Set([
|
|
6
260
|
"read",
|
|
@@ -337,6 +591,96 @@ function firstServingTier(tiers, required, declared) {
|
|
|
337
591
|
return void 0;
|
|
338
592
|
}
|
|
339
593
|
|
|
594
|
+
// packages/core/src/cache-routing.ts
|
|
595
|
+
var SWITCH_ACTIONS = /* @__PURE__ */ new Set([
|
|
596
|
+
"escalate-model",
|
|
597
|
+
"fresh-context",
|
|
598
|
+
"rollback-with-reflection",
|
|
599
|
+
"retry-with-feedback"
|
|
600
|
+
]);
|
|
601
|
+
function finiteNonNegative(value) {
|
|
602
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
603
|
+
}
|
|
604
|
+
function boundedTokens(value) {
|
|
605
|
+
return Number.isSafeInteger(value) && finiteNonNegative(value) ? value : void 0;
|
|
606
|
+
}
|
|
607
|
+
function cacheObservationFromUsage(usage) {
|
|
608
|
+
if (!usage || !Number.isSafeInteger(usage.promptTokens) || usage.promptTokens < 0 || !Number.isSafeInteger(usage.cachedTokens) || usage.cachedTokens < 0) {
|
|
609
|
+
return { status: "unknown" };
|
|
610
|
+
}
|
|
611
|
+
const cachedTokens = Math.min(usage.promptTokens, usage.cachedTokens);
|
|
612
|
+
return {
|
|
613
|
+
status: cachedTokens > 0 ? "hit" : "miss",
|
|
614
|
+
promptTokens: usage.promptTokens,
|
|
615
|
+
cachedTokens
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function phaseOf(input, sameToolCycle) {
|
|
619
|
+
if (input.state.failure === "hard") return "failure";
|
|
620
|
+
if (input.recoveryAction && SWITCH_ACTIONS.has(input.recoveryAction)) return "escalation";
|
|
621
|
+
if (sameToolCycle) return "same-tool-cycle";
|
|
622
|
+
if (input.previousTier !== void 0) return "new-phase";
|
|
623
|
+
return "unknown";
|
|
624
|
+
}
|
|
625
|
+
function routeCost(rate, tokens) {
|
|
626
|
+
return finiteNonNegative(rate) && boundedTokens(tokens) !== void 0 ? rate * tokens / 1e6 : void 0;
|
|
627
|
+
}
|
|
628
|
+
function switchEconomics(input, cachedTokens, contextTokens) {
|
|
629
|
+
const expectedGain = input.previousCost && input.plannedCost && contextTokens !== void 0 ? routeCost(Math.max(0, input.previousCost.input - input.plannedCost.input), contextTokens) : void 0;
|
|
630
|
+
const cachePenalty = input.previousCost && input.plannedCost && cachedTokens !== void 0 ? routeCost(Math.max(0, input.plannedCost.input - (input.previousCost.cacheRead ?? input.previousCost.input)), cachedTokens) : void 0;
|
|
631
|
+
return {
|
|
632
|
+
...expectedGain !== void 0 ? { expectedGain } : {},
|
|
633
|
+
...cachePenalty !== void 0 ? { cachePenalty } : {}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function result(input, action, phase, selectedTier, reason, extra = {}) {
|
|
637
|
+
const cache = input.previousCache;
|
|
638
|
+
const contextTokens = boundedTokens(input.state.contextTokens) ?? boundedTokens(input.state.estimatedTokens);
|
|
639
|
+
const cachedTokens = boundedTokens(cache?.cachedTokens);
|
|
640
|
+
const reprocessTokens = cache?.status === "hit" && cachedTokens !== void 0 ? cachedTokens : void 0;
|
|
641
|
+
return {
|
|
642
|
+
action,
|
|
643
|
+
phase,
|
|
644
|
+
cacheStatus: cache?.status ?? "unknown",
|
|
645
|
+
plannedTier: input.plannedTier,
|
|
646
|
+
selectedTier,
|
|
647
|
+
...input.previousTier !== void 0 ? { previousTier: input.previousTier } : {},
|
|
648
|
+
...contextTokens !== void 0 ? { estimatedContextTokens: contextTokens } : {},
|
|
649
|
+
...cachedTokens !== void 0 ? { cachedTokens } : {},
|
|
650
|
+
...reprocessTokens !== void 0 ? { reprocessTokens } : {},
|
|
651
|
+
...extra,
|
|
652
|
+
reason
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function cacheAwareRoute(input) {
|
|
656
|
+
const sameToolCycle = input.state.lastRole === "tool" && input.previousTier !== void 0 && (input.state.contextGeneration ?? 0) === (input.previousGeneration ?? 0);
|
|
657
|
+
const phase = phaseOf(input, sameToolCycle);
|
|
658
|
+
const canKeep = input.previousTier !== void 0 && input.canKeepPrevious !== false;
|
|
659
|
+
const cache = input.previousCache;
|
|
660
|
+
const cachedTokens = boundedTokens(cache?.cachedTokens);
|
|
661
|
+
const contextTokens = boundedTokens(input.state.contextTokens) ?? boundedTokens(input.state.estimatedTokens);
|
|
662
|
+
const economics = switchEconomics(input, cachedTokens, contextTokens);
|
|
663
|
+
if (!input.previousTier || !canKeep) {
|
|
664
|
+
return result(input, "evaluate", phase, input.plannedTier, "no usable previous route affinity; policy decision evaluated", economics);
|
|
665
|
+
}
|
|
666
|
+
if (input.plannedTier === input.previousTier) {
|
|
667
|
+
return result(input, "evaluate", phase, input.previousTier, "policy selected the current model; route unchanged", economics);
|
|
668
|
+
}
|
|
669
|
+
if (phase === "same-tool-cycle") {
|
|
670
|
+
return result(input, "keep", phase, input.previousTier, "same tool cycle; keep the current model", economics);
|
|
671
|
+
}
|
|
672
|
+
if (phase === "failure" || phase === "escalation") {
|
|
673
|
+
return result(input, "switch", phase, input.plannedTier, "failure or escalation requires evaluating a different model", economics);
|
|
674
|
+
}
|
|
675
|
+
if (cache?.status === "hit" && cachedTokens !== void 0 && economics.expectedGain !== void 0 && economics.cachePenalty !== void 0 && economics.expectedGain > economics.cachePenalty) {
|
|
676
|
+
return result(input, "switch", phase, input.plannedTier, "expected cost gain exceeds the measured cache penalty", economics);
|
|
677
|
+
}
|
|
678
|
+
if (cache?.status === "hit" && cachedTokens !== void 0) {
|
|
679
|
+
return result(input, "keep", phase, input.previousTier, "cache hit retained; unpriced policy gain does not exceed cache loss", economics);
|
|
680
|
+
}
|
|
681
|
+
return result(input, "switch", phase, input.plannedTier, "policy changed phase without a measured cache hit to preserve", economics);
|
|
682
|
+
}
|
|
683
|
+
|
|
340
684
|
// packages/core/src/config.ts
|
|
341
685
|
import { existsSync, readFileSync } from "node:fs";
|
|
342
686
|
import os from "node:os";
|
|
@@ -516,6 +860,9 @@ function validateConfig(value, source = "<inline>") {
|
|
|
516
860
|
if (upstream.enabled !== void 0 && typeof upstream.enabled !== "boolean") {
|
|
517
861
|
throw new Error(`Sabi config ${source}: upstream '${name}'.enabled must be a boolean`);
|
|
518
862
|
}
|
|
863
|
+
if (upstream.paidModelsAllowed !== void 0 && typeof upstream.paidModelsAllowed !== "boolean") {
|
|
864
|
+
throw new Error(`Sabi config ${source}: upstream '${name}'.paidModelsAllowed must be a boolean`);
|
|
865
|
+
}
|
|
519
866
|
}
|
|
520
867
|
if (!Object.keys(models).length) throw new Error(`Sabi config ${source}: no models declared`);
|
|
521
868
|
for (const [name, model] of Object.entries(models)) {
|
|
@@ -678,6 +1025,24 @@ function validateConfig(value, source = "<inline>") {
|
|
|
678
1025
|
return { ...config, upstreams, models, aliases, policy };
|
|
679
1026
|
}
|
|
680
1027
|
|
|
1028
|
+
// packages/core/src/recovery-actions.ts
|
|
1029
|
+
var RECOVERY_ACTIONS = [
|
|
1030
|
+
"continue",
|
|
1031
|
+
"retry-same",
|
|
1032
|
+
"retry-with-feedback",
|
|
1033
|
+
"gather-evidence",
|
|
1034
|
+
"escalate-model",
|
|
1035
|
+
"fresh-context",
|
|
1036
|
+
"rollback-with-reflection",
|
|
1037
|
+
"ask-user"
|
|
1038
|
+
];
|
|
1039
|
+
function isRecoveryAction(value) {
|
|
1040
|
+
return typeof value === "string" && RECOVERY_ACTIONS.includes(value);
|
|
1041
|
+
}
|
|
1042
|
+
function normalizeRecoveryAction(value, fallback = "ask-user") {
|
|
1043
|
+
return isRecoveryAction(value) ? value : fallback;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
681
1046
|
// packages/core/src/log.ts
|
|
682
1047
|
import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
683
1048
|
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -774,12 +1139,102 @@ function recordLogWriteFailure(logFile, error) {
|
|
|
774
1139
|
}
|
|
775
1140
|
function appendDecision(record, logFile = defaultLogPath()) {
|
|
776
1141
|
try {
|
|
777
|
-
appendPrivateLine(logFile, `${
|
|
1142
|
+
appendPrivateLine(logFile, `${serializeDecisionRecord(record)}
|
|
778
1143
|
`);
|
|
779
1144
|
} catch (error) {
|
|
780
1145
|
recordLogWriteFailure(logFile, error);
|
|
781
1146
|
}
|
|
782
1147
|
}
|
|
1148
|
+
function boundedStringList(values, maxItems, maxChars) {
|
|
1149
|
+
if (!Array.isArray(values)) return [];
|
|
1150
|
+
return values.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, maxChars)).slice(0, maxItems);
|
|
1151
|
+
}
|
|
1152
|
+
function isPlainObject(value) {
|
|
1153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1154
|
+
}
|
|
1155
|
+
function sanitizeState(state, config) {
|
|
1156
|
+
const policy = telemetryPolicy(config);
|
|
1157
|
+
const failureEvidence = boundedStringList(state.failureEvidence, 8, 64).filter((value) => policy.allowlisted(value));
|
|
1158
|
+
const evidence = boundTrajectoryEvidence(state.evidence);
|
|
1159
|
+
const clean = {
|
|
1160
|
+
messageCount: state.messageCount,
|
|
1161
|
+
assistantTurns: state.assistantTurns,
|
|
1162
|
+
toolMessages: state.toolMessages,
|
|
1163
|
+
lastRole: String(state.lastRole ?? "").slice(0, 32),
|
|
1164
|
+
contextChars: state.contextChars,
|
|
1165
|
+
estimatedTokens: state.estimatedTokens,
|
|
1166
|
+
hasTools: state.hasTools,
|
|
1167
|
+
toolNames: boundedStringList(state.toolNames, 40, 120),
|
|
1168
|
+
lastToolNames: boundedStringList(state.lastToolNames, 12, 120),
|
|
1169
|
+
roundKind: state.roundKind,
|
|
1170
|
+
failure: state.failure,
|
|
1171
|
+
failureEvidence,
|
|
1172
|
+
...state.contextTokens !== void 0 ? { contextTokens: state.contextTokens } : {},
|
|
1173
|
+
...state.contextKnown !== void 0 ? { contextKnown: state.contextKnown } : {},
|
|
1174
|
+
...state.contextGeneration !== void 0 ? { contextGeneration: state.contextGeneration } : {},
|
|
1175
|
+
...state.repeatedFailure !== void 0 ? { repeatedFailure: state.repeatedFailure } : {},
|
|
1176
|
+
...state.failureStreak !== void 0 ? { failureStreak: state.failureStreak } : {},
|
|
1177
|
+
...state.contextWindow !== void 0 ? { contextWindow: state.contextWindow } : {},
|
|
1178
|
+
...state.inputModalities ? { inputModalities: state.inputModalities.slice(0, 5) } : {},
|
|
1179
|
+
...state.mediaCounts ? { mediaCounts: state.mediaCounts } : {},
|
|
1180
|
+
...evidence.length > 0 ? { evidence } : {},
|
|
1181
|
+
...state.verification ? { verification: { ...state.verification, receiptId: state.verification.receiptId?.slice(0, 128) } } : {},
|
|
1182
|
+
...state.scopeCoverage ? {
|
|
1183
|
+
scopeCoverage: {
|
|
1184
|
+
...state.scopeCoverage,
|
|
1185
|
+
...state.scopeCoverage.missing ? { missing: boundedStringList(state.scopeCoverage.missing, 32, 120) } : {}
|
|
1186
|
+
}
|
|
1187
|
+
} : {}
|
|
1188
|
+
};
|
|
1189
|
+
return clean;
|
|
1190
|
+
}
|
|
1191
|
+
function sanitizeDecisionRecord(record, config) {
|
|
1192
|
+
const policy = telemetryPolicy(config);
|
|
1193
|
+
const state = isPlainObject(record.state) ? sanitizeState(record.state, config) : void 0;
|
|
1194
|
+
const sanitized = {
|
|
1195
|
+
ts: record.ts,
|
|
1196
|
+
sessionId: record.sessionId,
|
|
1197
|
+
sessionKnown: record.sessionKnown,
|
|
1198
|
+
requestId: record.requestId,
|
|
1199
|
+
client: record.client,
|
|
1200
|
+
turnId: record.turnId,
|
|
1201
|
+
servedModel: record.servedModel,
|
|
1202
|
+
alias: record.alias,
|
|
1203
|
+
mode: record.mode,
|
|
1204
|
+
rule: record.rule,
|
|
1205
|
+
tier: record.tier,
|
|
1206
|
+
reason: sanitizeReason(String(record.reason ?? ""), policy),
|
|
1207
|
+
upstream: record.upstream,
|
|
1208
|
+
upstreamModel: record.upstreamModel,
|
|
1209
|
+
stream: record.stream,
|
|
1210
|
+
state,
|
|
1211
|
+
cache: record.cache,
|
|
1212
|
+
judge: record.judge,
|
|
1213
|
+
usage: record.usage,
|
|
1214
|
+
cost: record.cost,
|
|
1215
|
+
latencyMs: record.latencyMs,
|
|
1216
|
+
ttftMs: record.ttftMs,
|
|
1217
|
+
outcome: record.outcome,
|
|
1218
|
+
...record.error ? { error: sanitizeError(record.error) } : {},
|
|
1219
|
+
transport: record.transport,
|
|
1220
|
+
fallback: record.fallback,
|
|
1221
|
+
...record.recovery ? {
|
|
1222
|
+
recovery: {
|
|
1223
|
+
...record.recovery,
|
|
1224
|
+
action: normalizeRecoveryAction(record.recovery.action),
|
|
1225
|
+
evidenceGrade: record.recovery.evidenceGrade === "matched" || record.recovery.evidenceGrade === "replayed" ? record.recovery.evidenceGrade : "observed",
|
|
1226
|
+
failureSignature: String(record.recovery.failureSignature).slice(0, 64),
|
|
1227
|
+
stateFingerprint: String(record.recovery.stateFingerprint).slice(0, 64),
|
|
1228
|
+
...record.recovery.route ? { route: String(record.recovery.route).slice(0, 160) } : {},
|
|
1229
|
+
...record.recovery.receiptId ? { receiptId: String(record.recovery.receiptId).slice(0, 128) } : {}
|
|
1230
|
+
}
|
|
1231
|
+
} : {}
|
|
1232
|
+
};
|
|
1233
|
+
return sanitized;
|
|
1234
|
+
}
|
|
1235
|
+
function serializeDecisionRecord(record, config) {
|
|
1236
|
+
return JSON.stringify(sanitizeDecisionRecord(record, config));
|
|
1237
|
+
}
|
|
783
1238
|
function hashIdentity(kind, ...parts) {
|
|
784
1239
|
return createHmac("sha256", getIdentitySalt()).update(JSON.stringify([kind, ...parts])).digest("hex");
|
|
785
1240
|
}
|
|
@@ -804,7 +1259,7 @@ function trajectoryFromRound(round, previous) {
|
|
|
804
1259
|
const sameFailure = previous?.failure === "hard" && failure === "hard";
|
|
805
1260
|
const repeatedFailure = sameFailure === true;
|
|
806
1261
|
const failureStreak = repeatedFailure ? 2 : failure === "hard" ? 1 : 0;
|
|
807
|
-
|
|
1262
|
+
const state = {
|
|
808
1263
|
messageCount: round.messageCount,
|
|
809
1264
|
assistantTurns: round.assistantTurns,
|
|
810
1265
|
toolMessages: round.calls.length,
|
|
@@ -825,6 +1280,12 @@ function trajectoryFromRound(round, previous) {
|
|
|
825
1280
|
...round.inputModalities ? { inputModalities: round.inputModalities } : {},
|
|
826
1281
|
...round.mediaCounts && Object.keys(round.mediaCounts).length ? { inputModalities: round.inputModalities ?? modalitiesOf(round.mediaCounts), mediaCounts: round.mediaCounts } : {}
|
|
827
1282
|
};
|
|
1283
|
+
return decorateTrajectoryState(state, {
|
|
1284
|
+
generation: round.contextGeneration,
|
|
1285
|
+
verificationReceipt: round.verificationReceipt,
|
|
1286
|
+
summaryClaim: round.summaryClaim,
|
|
1287
|
+
scope: round.scope
|
|
1288
|
+
});
|
|
828
1289
|
}
|
|
829
1290
|
function roundMedia(round) {
|
|
830
1291
|
return { counts: round.mediaCounts ?? {}, payloadChars: 0 };
|
|
@@ -844,6 +1305,20 @@ function planRound(state, policy, tiers, options = {}) {
|
|
|
844
1305
|
rule = "capability";
|
|
845
1306
|
tier = alternate;
|
|
846
1307
|
}
|
|
1308
|
+
const cache = cacheAwareRoute({
|
|
1309
|
+
state: withWindow,
|
|
1310
|
+
plannedTier: tier,
|
|
1311
|
+
previousTier: options.previous?.tier,
|
|
1312
|
+
previousLastRole: options.previous?.lastRole,
|
|
1313
|
+
previousGeneration: options.previous?.generation,
|
|
1314
|
+
previousCache: options.previous?.cache,
|
|
1315
|
+
canKeepPrevious: Boolean(options.previous?.tier && tiers[options.previous.tier] && servesInputModalities(tiers[options.previous.tier]?.inputModalities, required))
|
|
1316
|
+
});
|
|
1317
|
+
if (cache.selectedTier !== tier) {
|
|
1318
|
+
tier = cache.selectedTier;
|
|
1319
|
+
rule = "cache-affinity";
|
|
1320
|
+
reason = cache.reason;
|
|
1321
|
+
}
|
|
847
1322
|
const chosen = tiers[tier];
|
|
848
1323
|
if (!chosen || !chosen.model) return void 0;
|
|
849
1324
|
return {
|
|
@@ -852,61 +1327,31 @@ function planRound(state, policy, tiers, options = {}) {
|
|
|
852
1327
|
effort: chosen.effort,
|
|
853
1328
|
rule,
|
|
854
1329
|
reason,
|
|
855
|
-
state: withWindow
|
|
1330
|
+
state: withWindow,
|
|
1331
|
+
cache
|
|
856
1332
|
};
|
|
857
1333
|
}
|
|
858
1334
|
|
|
859
|
-
// packages/core/src/telemetry.ts
|
|
860
|
-
var ALLOWLIST = /* @__PURE__ */ new Set([
|
|
861
|
-
"error-line",
|
|
862
|
-
"python-traceback",
|
|
863
|
-
"panic",
|
|
864
|
-
"exception",
|
|
865
|
-
"typescript-error",
|
|
866
|
-
"fail-marker",
|
|
867
|
-
"command-failed",
|
|
868
|
-
"nonzero-exit",
|
|
869
|
-
"failure-count",
|
|
870
|
-
"command-not-found",
|
|
871
|
-
"permission-denied",
|
|
872
|
-
"missing-file",
|
|
873
|
-
"soft-warning",
|
|
874
|
-
"soft-deprecated",
|
|
875
|
-
"soft-retrying",
|
|
876
|
-
"soft-timeout",
|
|
877
|
-
"tool-error",
|
|
878
|
-
"permission-denial",
|
|
879
|
-
"rate-limited",
|
|
880
|
-
"quota-exceeded",
|
|
881
|
-
"timeout"
|
|
882
|
-
]);
|
|
883
|
-
function allowlisted(value) {
|
|
884
|
-
const code = value.split(":")[0]?.trim();
|
|
885
|
-
return ALLOWLIST.has(code);
|
|
886
|
-
}
|
|
887
|
-
function telemetryPolicy(config) {
|
|
888
|
-
const captureSnippets = config?.captureSnippets === true;
|
|
889
|
-
const captureChars = config?.captureChars ?? 800;
|
|
890
|
-
return {
|
|
891
|
-
captureSnippets,
|
|
892
|
-
allowlisted: (value) => allowlisted(value),
|
|
893
|
-
snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
function sanitizeReason(reason, policy) {
|
|
897
|
-
if (!policy.captureSnippets) {
|
|
898
|
-
if (!reason || allowlisted(reason)) return reason;
|
|
899
|
-
const segments = reason.split(":").map((segment) => segment.trim());
|
|
900
|
-
const last = segments[segments.length - 1] ?? "";
|
|
901
|
-
if (allowlisted(last)) return reason;
|
|
902
|
-
return "reason withheld (telemetry.allowlistOnly)";
|
|
903
|
-
}
|
|
904
|
-
return policy.snippet(reason);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
1335
|
// packages/core/src/prompt.ts
|
|
908
1336
|
import * as readline from "node:readline/promises";
|
|
909
1337
|
|
|
1338
|
+
// packages/core/src/signals.ts
|
|
1339
|
+
var KNOWN_SIGNAL_KINDS = [
|
|
1340
|
+
"failure.real",
|
|
1341
|
+
"failure.transport",
|
|
1342
|
+
"progress.stalled",
|
|
1343
|
+
"verification.complete",
|
|
1344
|
+
"coverage",
|
|
1345
|
+
"context.pressure",
|
|
1346
|
+
"context.staleness",
|
|
1347
|
+
"task.ambiguity",
|
|
1348
|
+
"mutation.risk",
|
|
1349
|
+
"retry.value",
|
|
1350
|
+
"evidence.nextValue",
|
|
1351
|
+
"model.requiredStrength"
|
|
1352
|
+
];
|
|
1353
|
+
var KNOWN_KINDS = new Set(KNOWN_SIGNAL_KINDS);
|
|
1354
|
+
|
|
910
1355
|
// packages/adapters/command-code/mod/sabi.ts
|
|
911
1356
|
import path3 from "node:path";
|
|
912
1357
|
var MOD_ID = "sabi";
|
|
@@ -924,6 +1369,9 @@ function readLedger(state) {
|
|
|
924
1369
|
hasTools: raw.hasTools === true,
|
|
925
1370
|
lastModel: raw.lastModel,
|
|
926
1371
|
lastUsage: raw.lastUsage,
|
|
1372
|
+
lastTier: raw.lastTier,
|
|
1373
|
+
lastLastRole: raw.lastLastRole,
|
|
1374
|
+
lastCache: raw.lastCache,
|
|
927
1375
|
sessionId: typeof raw.sessionId === "string" ? raw.sessionId : void 0
|
|
928
1376
|
};
|
|
929
1377
|
}
|
|
@@ -992,12 +1440,12 @@ function sabi(cmd) {
|
|
|
992
1440
|
return writeLedger(state, { ...ledger, rounds: turnNumber });
|
|
993
1441
|
},
|
|
994
1442
|
// Sabi observes tool outcomes and never rewrites what the model sees.
|
|
995
|
-
afterToolCall: ({ toolName, input, isError, result }) => {
|
|
1443
|
+
afterToolCall: ({ toolName, input, isError, result: result2 }) => {
|
|
996
1444
|
calls.push({
|
|
997
1445
|
name: toolName,
|
|
998
1446
|
args: JSON.stringify(input ?? {}),
|
|
999
1447
|
failed: isError === true,
|
|
1000
|
-
output: typeof
|
|
1448
|
+
output: typeof result2 === "string" ? result2 : void 0
|
|
1001
1449
|
});
|
|
1002
1450
|
return void 0;
|
|
1003
1451
|
},
|
|
@@ -1008,6 +1456,9 @@ function sabi(cmd) {
|
|
|
1008
1456
|
previousFailure = void 0;
|
|
1009
1457
|
ledger.generation = (ledger.generation ?? 0) + 1;
|
|
1010
1458
|
ledger.contextTokens = void 0;
|
|
1459
|
+
ledger.lastTier = void 0;
|
|
1460
|
+
ledger.lastLastRole = void 0;
|
|
1461
|
+
ledger.lastCache = { status: "unknown" };
|
|
1011
1462
|
}
|
|
1012
1463
|
const round = {
|
|
1013
1464
|
messageCount: stats.messageCount,
|
|
@@ -1022,7 +1473,15 @@ function sabi(cmd) {
|
|
|
1022
1473
|
...Object.keys(stats.media.counts).length ? { inputModalities: modalitiesOf(stats.media.counts), mediaCounts: stats.media.counts } : {}
|
|
1023
1474
|
};
|
|
1024
1475
|
const trajectory = trajectoryFromRound(round, previousFailure);
|
|
1025
|
-
const plan = planRound(trajectory, policy, tiers, {
|
|
1476
|
+
const plan = planRound(trajectory, policy, tiers, {
|
|
1477
|
+
contextWindow: config.harness?.contextWindow,
|
|
1478
|
+
previous: {
|
|
1479
|
+
tier: ledger.lastTier,
|
|
1480
|
+
lastRole: ledger.lastLastRole,
|
|
1481
|
+
generation: ledger.generation,
|
|
1482
|
+
cache: ledger.lastCache
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1026
1485
|
if (!plan) return void 0;
|
|
1027
1486
|
nextPlan = plan;
|
|
1028
1487
|
return plan.effort ? { model: plan.model, effort: plan.effort } : { model: plan.model };
|
|
@@ -1048,7 +1507,10 @@ function sabi(cmd) {
|
|
|
1048
1507
|
// Only advance attribution when a fresh value actually arrived this turn. A missing
|
|
1049
1508
|
// usage or model event stays unknown rather than re-serializing an old round's value.
|
|
1050
1509
|
lastModel: servedBy,
|
|
1051
|
-
lastUsage: usedThisTurn ? usage : void 0
|
|
1510
|
+
lastUsage: usedThisTurn ? usage : void 0,
|
|
1511
|
+
lastTier: adopted?.tier ?? (compacted ? void 0 : ledger.lastTier),
|
|
1512
|
+
lastLastRole: adopted?.state.lastRole ?? (compacted ? void 0 : ledger.lastLastRole),
|
|
1513
|
+
lastCache: usedThisTurn ? cacheObservationFromUsage(toUsageTotals(usage)) : { status: "unknown" }
|
|
1052
1514
|
};
|
|
1053
1515
|
previousFailure = adopted ? { failure: adopted.state.failure, failureEvidence: adopted.state.failureEvidence } : void 0;
|
|
1054
1516
|
recordCustomEntry(ctx, {
|
|
@@ -1091,6 +1553,7 @@ function sabi(cmd) {
|
|
|
1091
1553
|
// This is the host adapter, not a provider entitlement claim.
|
|
1092
1554
|
upstream: CLIENT_ID,
|
|
1093
1555
|
upstreamModel: servingPlan.model,
|
|
1556
|
+
cache: servingPlan.cache,
|
|
1094
1557
|
stream: false,
|
|
1095
1558
|
state: {
|
|
1096
1559
|
...servingPlan.state,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizuh/sabi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"command-code",
|
|
16
16
|
"mod",
|
|
17
17
|
"llm",
|
|
18
|
-
"routing"
|
|
18
|
+
"routing",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"inference-scheduling"
|
|
19
21
|
],
|
|
20
22
|
"publishConfig": {
|
|
21
23
|
"access": "public"
|
package/sabi.config.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"provenance": "
|
|
2
|
+
"provenance": "Operator rule (2026-09-22): OpenRouter may serve FREE models only. The openrouter upstream declares paidModelsAllowed:false, so any model whose price is unknown or non-zero is refused at dispatch (packages/core/src/compatibility.ts) — a config mistake cannot spend money. Every openrouter model id below carries the :free variant and cost 0. Ids, context windows, output ceilings, modalities and zero pricing verified live from https://openrouter.ai/api/v1/models and a real completion per id on 2026-09-22. Jev (TypeSafe judge) is a separate upstream and unaffected by this rule.",
|
|
3
3
|
"server": { "host": "127.0.0.1", "port": 8787 },
|
|
4
4
|
"upstreams": {
|
|
5
5
|
"openrouter": {
|
|
6
6
|
"baseURL": "https://openrouter.ai/api/v1",
|
|
7
7
|
"apiKey": "$OPENROUTER_API_KEY",
|
|
8
8
|
"streamUsage": true,
|
|
9
|
+
"paidModelsAllowed": false,
|
|
9
10
|
"headers": {
|
|
10
11
|
"HTTP-Referer": "https://github.com/vizuh/sabi",
|
|
11
12
|
"X-Title": "Sabi"
|
|
@@ -20,27 +21,27 @@
|
|
|
20
21
|
"models": {
|
|
21
22
|
"cheap": {
|
|
22
23
|
"upstream": "openrouter",
|
|
23
|
-
"model": "
|
|
24
|
-
"contextWindow":
|
|
25
|
-
"maxOutputTokens":
|
|
24
|
+
"model": "poolside/laguna-s-2.1:free",
|
|
25
|
+
"contextWindow": 262144,
|
|
26
|
+
"maxOutputTokens": 32768,
|
|
26
27
|
"capabilities": { "inputModalities": ["text"] },
|
|
27
|
-
"cost": { "input": 0
|
|
28
|
+
"cost": { "input": 0, "output": 0 }
|
|
28
29
|
},
|
|
29
30
|
"mid": {
|
|
30
31
|
"upstream": "openrouter",
|
|
31
|
-
"model": "
|
|
32
|
-
"contextWindow":
|
|
33
|
-
"maxOutputTokens":
|
|
34
|
-
"capabilities": { "inputModalities": ["text", "image"
|
|
35
|
-
"cost": { "input": 0
|
|
32
|
+
"model": "dots-studio/dots-3-note-preview:free",
|
|
33
|
+
"contextWindow": 512000,
|
|
34
|
+
"maxOutputTokens": 460800,
|
|
35
|
+
"capabilities": { "inputModalities": ["text", "image"] },
|
|
36
|
+
"cost": { "input": 0, "output": 0 }
|
|
36
37
|
},
|
|
37
38
|
"strong": {
|
|
38
39
|
"upstream": "openrouter",
|
|
39
|
-
"model": "
|
|
40
|
+
"model": "nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
40
41
|
"contextWindow": 1000000,
|
|
41
|
-
"maxOutputTokens":
|
|
42
|
-
"capabilities": { "inputModalities": ["text"
|
|
43
|
-
"cost": { "input":
|
|
42
|
+
"maxOutputTokens": 65536,
|
|
43
|
+
"capabilities": { "inputModalities": ["text"] },
|
|
44
|
+
"cost": { "input": 0, "output": 0 }
|
|
44
45
|
},
|
|
45
46
|
"local": {
|
|
46
47
|
"upstream": "ollama",
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
"costPerMTokInput": 0.042
|
|
97
98
|
},
|
|
98
99
|
"harness": {
|
|
99
|
-
"provenance": "Command Code catalog ids, efforts and min plans verified 2026-09-18 against `cmd --list-models` and the bundled reference models.md. Used by the in-process mod adapter (harness keeps its own loop, no proxy, no key).
|
|
100
|
+
"provenance": "Command Code catalog ids, efforts and min plans verified 2026-09-18 against `cmd --list-models` and the bundled reference models.md. Used by the in-process mod adapter (harness keeps its own loop, no proxy, no key). This is the Command Code subscription catalog, not OpenRouter — the free-models-only rule above does not apply to it.",
|
|
100
101
|
"tiers": {
|
|
101
102
|
"cheap": {
|
|
102
103
|
"model": "deepseek/deepseek-v4-flash",
|