@kal-elsam/kairo-runtime 0.4.2 → 0.5.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 +124 -17
- package/global-template/components/catalog.json +63 -6
- package/global-template/components/sdd-core/personas/teaching.md +42 -0
- package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
- package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
- package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
- package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
- package/package.json +1 -1
- package/scripts/cockpit-smoke.mjs +29 -8
- package/scripts/smoke-test.sh +9 -0
- package/src/cli.js +54 -5
- package/src/global/action-planner.js +17 -4
- package/src/global/component-authoring.js +1 -2
- package/src/global/component-ecosystem-checks.js +74 -15
- package/src/global/component-health.js +56 -0
- package/src/global/component-integration-cli.js +302 -0
- package/src/global/component-manifest.js +214 -0
- package/src/global/component-registry.js +19 -2
- package/src/global/component-resolve.js +51 -0
- package/src/global/components/sdd-core.js +6 -0
- package/src/global/control-plane-proposals.js +229 -0
- package/src/global/control-plane-snapshot.js +212 -0
- package/src/global/dashboard-guidance.js +27 -3
- package/src/global/diff.js +2 -5
- package/src/global/global-cli.js +16 -1
- package/src/global/global-doctor.js +75 -2
- package/src/global/global-installer.js +43 -6
- package/src/global/governance-actions.js +212 -0
- package/src/global/governance-repair.js +13 -0
- package/src/global/ink/cockpit-changes.js +124 -0
- package/src/global/ink/cockpit-control-center.js +106 -0
- package/src/global/ink/cockpit-controller.js +17 -0
- package/src/global/ink/cockpit-enter.js +27 -0
- package/src/global/ink/cockpit-focus.js +7 -1
- package/src/global/ink/cockpit-models.js +98 -46
- package/src/global/ink/cockpit-proposals.js +82 -0
- package/src/global/ink/cockpit-recovery.js +120 -0
- package/src/global/ink/cockpit-runs.js +47 -0
- package/src/global/ink/cockpit-scan.js +60 -0
- package/src/global/ink/cockpit-views.js +144 -74
- package/src/global/ink/orchestrator-app.js +156 -42
- package/src/global/ink/orchestrator-state.js +22 -1
- package/src/global/ink/use-orchestrator-data.js +239 -17
- package/src/global/integrations/engram-apply.js +105 -0
- package/src/global/integrations/engram-evidence.js +218 -0
- package/src/global/integrations/engram-exec.js +107 -0
- package/src/global/integrations/engram-plan.js +60 -0
- package/src/global/integrations/engram-provider.js +22 -0
- package/src/global/integrations/engram-receipts.js +116 -0
- package/src/global/integrations/engram-rollback.js +131 -0
- package/src/global/integrations/index.js +12 -0
- package/src/global/integrations/provider-contract.js +83 -0
- package/src/global/integrations/provider-registry.js +47 -0
- package/src/global/integrations/sdd-apply.js +190 -0
- package/src/global/integrations/sdd-destinations.js +95 -0
- package/src/global/integrations/sdd-evidence.js +89 -0
- package/src/global/integrations/sdd-fs-guard.js +183 -0
- package/src/global/integrations/sdd-lifecycle.js +84 -0
- package/src/global/integrations/sdd-persona.js +71 -0
- package/src/global/integrations/sdd-plan.js +87 -0
- package/src/global/integrations/sdd-provider.js +21 -0
- package/src/global/integrations/sdd-receipts.js +44 -0
- package/src/global/integrations/sdd-rollback.js +269 -0
- package/src/global/integrations/sdd-skill-files.js +61 -0
- package/src/global/integrations/sdd-state.js +175 -0
- package/src/global/integrations/sdd-verify.js +122 -0
- package/src/global/intelligence/backends/opencode-catalog.js +212 -0
- package/src/global/intelligence/backends/opencode-cli.js +247 -0
- package/src/global/intelligence/backends/opencode-evidence.js +129 -0
- package/src/global/intelligence/backends/opencode-http.js +152 -0
- package/src/global/intelligence/backends/opencode-invoke.js +83 -0
- package/src/global/intelligence/backends/opencode-providers.js +67 -0
- package/src/global/intelligence/backends/opencode-runtime.js +242 -0
- package/src/global/intelligence/context-compiler.js +95 -50
- package/src/global/intelligence/index.js +38 -1
- package/src/global/intelligence/orchestrate.js +4 -2
- package/src/global/intelligence/registry.js +62 -6
- package/src/global/intelligence/router.js +188 -70
- package/src/global/intelligence/transport-registry.js +147 -0
- package/src/global/intelligence/types.js +21 -2
- package/src/global/intelligence-cli.js +48 -17
- package/src/global/intelligence-session.js +37 -0
- package/src/global/json-output.js +1 -0
- package/src/global/load-component-catalog.js +10 -28
- package/src/global/load-workspace-component-catalog.js +22 -63
- package/src/global/state-migration.js +21 -1
- package/src/global/state.js +13 -8
- package/src/global/status.js +4 -17
- package/src/global/sync.js +2 -5
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const EXECUTABLE = "opencode";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
5
|
+
const DEFAULT_TERMINATION_GRACE_MS = 1000;
|
|
6
|
+
const DEFAULT_KILL_GRACE_MS = 1000;
|
|
7
|
+
const STDERR_LIMIT = 480;
|
|
8
|
+
const STDOUT_BUFFER_LIMIT = 1_048_576;
|
|
9
|
+
const STDERR_BUFFER_LIMIT = STDERR_LIMIT;
|
|
10
|
+
|
|
11
|
+
function isSensitiveEnvName(name) {
|
|
12
|
+
return /(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i.test(String(name ?? ""));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function appendLimited(current, chunk, limit) {
|
|
16
|
+
if (current.length >= limit) return current;
|
|
17
|
+
const next = current + String(chunk);
|
|
18
|
+
return next.length > limit ? next.slice(0, limit) : next;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Supervised, non-mutating OpenCode CLI invocation.
|
|
23
|
+
* Always uses `opencode run --format json --model` — never `--auto`.
|
|
24
|
+
* Timeout: SIGTERM → grace → SIGKILL → grace → unref + terminationFailed.
|
|
25
|
+
*/
|
|
26
|
+
export function runOpencodeJson({
|
|
27
|
+
modelRef,
|
|
28
|
+
prompt,
|
|
29
|
+
cwd,
|
|
30
|
+
env,
|
|
31
|
+
spawnImpl = spawn,
|
|
32
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
33
|
+
terminationGraceMs = DEFAULT_TERMINATION_GRACE_MS,
|
|
34
|
+
killGraceMs = DEFAULT_KILL_GRACE_MS
|
|
35
|
+
} = {}) {
|
|
36
|
+
if (!modelRef || typeof modelRef !== "string") {
|
|
37
|
+
return Promise.reject(new Error("runOpencodeJson requires modelRef."));
|
|
38
|
+
}
|
|
39
|
+
if (prompt == null) {
|
|
40
|
+
return Promise.reject(new Error("runOpencodeJson requires prompt."));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const args = ["run", "--format", "json", "--model", modelRef, String(prompt)];
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const child = spawnImpl(EXECUTABLE, args, {
|
|
47
|
+
cwd,
|
|
48
|
+
env,
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let stdout = "";
|
|
53
|
+
let stderr = "";
|
|
54
|
+
let settled = false;
|
|
55
|
+
let timedOut = false;
|
|
56
|
+
let timers = [];
|
|
57
|
+
|
|
58
|
+
const clearTimers = () => {
|
|
59
|
+
for (const timer of timers) clearTimeout(timer);
|
|
60
|
+
timers = [];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const settle = (result) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
clearTimers();
|
|
67
|
+
detachIo();
|
|
68
|
+
resolve(result);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const schedule = (fn, ms) => {
|
|
72
|
+
const timer = setTimeout(fn, ms);
|
|
73
|
+
timers.push(timer);
|
|
74
|
+
return timer;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const finish = ({ status = null, signal = null, terminationFailed = false } = {}) => {
|
|
78
|
+
settle({
|
|
79
|
+
stdout,
|
|
80
|
+
stderr,
|
|
81
|
+
status,
|
|
82
|
+
signal,
|
|
83
|
+
timedOut,
|
|
84
|
+
terminationFailed
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const forceUnref = () => {
|
|
89
|
+
try {
|
|
90
|
+
child.unref?.();
|
|
91
|
+
} catch {
|
|
92
|
+
/* ignore unref failures */
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onStdout = (chunk) => {
|
|
97
|
+
stdout = appendLimited(stdout, chunk, STDOUT_BUFFER_LIMIT);
|
|
98
|
+
};
|
|
99
|
+
const onStderr = (chunk) => {
|
|
100
|
+
stderr = appendLimited(stderr, chunk, STDERR_BUFFER_LIMIT);
|
|
101
|
+
};
|
|
102
|
+
// Keep listening after settle so late errors never crash Node.
|
|
103
|
+
const onError = (error) => {
|
|
104
|
+
if (settled) return;
|
|
105
|
+
settled = true;
|
|
106
|
+
clearTimers();
|
|
107
|
+
detachIo();
|
|
108
|
+
reject(error);
|
|
109
|
+
};
|
|
110
|
+
const onClose = (status, signal) => {
|
|
111
|
+
finish({
|
|
112
|
+
status: status ?? null,
|
|
113
|
+
signal: signal ?? null,
|
|
114
|
+
terminationFailed: false
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const detachIo = () => {
|
|
119
|
+
child.stdout?.off?.("data", onStdout);
|
|
120
|
+
child.stderr?.off?.("data", onStderr);
|
|
121
|
+
child.off?.("close", onClose);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const onTimeout = () => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
timedOut = true;
|
|
127
|
+
safeKill(child, "SIGTERM");
|
|
128
|
+
schedule(() => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
safeKill(child, "SIGKILL");
|
|
131
|
+
schedule(() => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
forceUnref();
|
|
134
|
+
finish({ terminationFailed: true });
|
|
135
|
+
}, killGraceMs);
|
|
136
|
+
}, terminationGraceMs);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
schedule(onTimeout, timeoutMs);
|
|
140
|
+
|
|
141
|
+
child.stdout?.on("data", onStdout);
|
|
142
|
+
child.stderr?.on("data", onStderr);
|
|
143
|
+
child.on("error", onError);
|
|
144
|
+
child.on("close", onClose);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function sanitizeCliStderr(text, { limit = STDERR_LIMIT } = {}) {
|
|
149
|
+
let out = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
150
|
+
out = out.replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]");
|
|
151
|
+
out = out.replace(
|
|
152
|
+
/\b([A-Z][A-Z0-9_]*)\s*[:=]\s*(["'])(?:\\.|(?!\2).)*\2/g,
|
|
153
|
+
(match, name) => (isSensitiveEnvName(name) ? `${name}=[REDACTED]` : match)
|
|
154
|
+
);
|
|
155
|
+
out = out.replace(/\b([A-Z][A-Z0-9_]*)\s*[:=]\s*\S+/g, (match, name) => (
|
|
156
|
+
isSensitiveEnvName(name) ? `${name}=[REDACTED]` : match
|
|
157
|
+
));
|
|
158
|
+
if (out.length > limit) {
|
|
159
|
+
if (limit <= 0) return "";
|
|
160
|
+
if (limit === 1) return "…";
|
|
161
|
+
out = `${out.slice(0, limit - 1)}…`;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function safeKill(child, signal) {
|
|
167
|
+
try {
|
|
168
|
+
if (typeof child.kill !== "function") return false;
|
|
169
|
+
return child.kill(signal) !== false;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse NDJSON OpenCode `--format json` events into text, usage, and errors.
|
|
177
|
+
* Rejects malformed stdout explicitly (any non-empty line that is not JSON).
|
|
178
|
+
*/
|
|
179
|
+
export function parseOpencodeJsonEvents(stdout) {
|
|
180
|
+
const events = [];
|
|
181
|
+
const texts = [];
|
|
182
|
+
let error = null;
|
|
183
|
+
const usage = {
|
|
184
|
+
inputTokens: null,
|
|
185
|
+
outputTokens: null,
|
|
186
|
+
cachedTokens: null,
|
|
187
|
+
estimatedCost: null
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const lines = String(stdout ?? "")
|
|
191
|
+
.split(/\r?\n/)
|
|
192
|
+
.map((line) => line.trim())
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
let event;
|
|
197
|
+
try {
|
|
198
|
+
event = JSON.parse(line);
|
|
199
|
+
} catch {
|
|
200
|
+
return {
|
|
201
|
+
content: null,
|
|
202
|
+
error: "OpenCode CLI returned malformed JSON events.",
|
|
203
|
+
events,
|
|
204
|
+
usage
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
events.push(event);
|
|
209
|
+
|
|
210
|
+
if (event.type === "error" || event.error) {
|
|
211
|
+
error =
|
|
212
|
+
event.error?.message ??
|
|
213
|
+
event.message ??
|
|
214
|
+
"OpenCode CLI error event";
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (event.type === "text") {
|
|
219
|
+
if (typeof event.part?.text === "string") texts.push(event.part.text);
|
|
220
|
+
else if (typeof event.text === "string") texts.push(event.text);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (event.type === "step_finish" && event.part) {
|
|
224
|
+
applyStepFinishUsage(usage, event.part);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
content: texts.length > 0 ? texts.join("") : null,
|
|
230
|
+
error,
|
|
231
|
+
events,
|
|
232
|
+
usage
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyStepFinishUsage(usage, part) {
|
|
237
|
+
const tokens = part.tokens ?? part.usage ?? {};
|
|
238
|
+
if (tokens.input != null || tokens.prompt != null) {
|
|
239
|
+
usage.inputTokens = tokens.input ?? tokens.prompt;
|
|
240
|
+
}
|
|
241
|
+
if (tokens.output != null || tokens.completion != null) {
|
|
242
|
+
usage.outputTokens = tokens.output ?? tokens.completion;
|
|
243
|
+
}
|
|
244
|
+
const cached = tokens.cache?.read ?? tokens.cached;
|
|
245
|
+
if (cached != null) usage.cachedTokens = cached;
|
|
246
|
+
if (typeof part.cost === "number") usage.estimatedCost = part.cost;
|
|
247
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { probeCommand, isExecutableAvailable } from "../../cli-probe.js";
|
|
2
|
+
|
|
3
|
+
export const ENTITLEMENT_STATES = Object.freeze({
|
|
4
|
+
UNKNOWN: "unknown",
|
|
5
|
+
UNVERIFIED: "entitlement_unverified",
|
|
6
|
+
LIMIT_REACHED: "limit_reached"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const BILLING_MODELS = Object.freeze({
|
|
10
|
+
GO_PLAN: "go_plan",
|
|
11
|
+
ZEN_CREDITS: "zen_credits",
|
|
12
|
+
CLI_RUNTIME: "cli_runtime"
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Safe OpenCode evidence: CLI presence + `opencode auth list` + env key.
|
|
17
|
+
* Never reads auth.json. Never claims subscription/balance without HTTP evidence.
|
|
18
|
+
*/
|
|
19
|
+
export function collectOpencodeCliEvidence({
|
|
20
|
+
env = process.env,
|
|
21
|
+
whichImpl = isExecutableAvailable,
|
|
22
|
+
probeImpl = probeCommand
|
|
23
|
+
} = {}) {
|
|
24
|
+
const cliInstalled = whichImpl("opencode", { env });
|
|
25
|
+
if (!cliInstalled) {
|
|
26
|
+
return {
|
|
27
|
+
cliInstalled: false,
|
|
28
|
+
authListOk: false,
|
|
29
|
+
authProviders: [],
|
|
30
|
+
error: null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = probeImpl("opencode", ["auth", "list"], {
|
|
35
|
+
env,
|
|
36
|
+
timeoutMs: 8000
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
return {
|
|
41
|
+
cliInstalled: true,
|
|
42
|
+
authListOk: false,
|
|
43
|
+
authProviders: [],
|
|
44
|
+
error: result.error ?? result.stderr ?? "opencode auth list failed"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
cliInstalled: true,
|
|
50
|
+
authListOk: true,
|
|
51
|
+
authProviders: parseAuthListProviders(result.stdout),
|
|
52
|
+
error: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function parseAuthListProviders(rawOutput) {
|
|
57
|
+
const plain = String(rawOutput ?? "").replace(/\u001b\[[0-9;]*m/g, "");
|
|
58
|
+
const found = new Set();
|
|
59
|
+
|
|
60
|
+
for (const line of plain.split(/\r?\n/)) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed || /credentials|environment|outro|intro/i.test(trimmed)) continue;
|
|
63
|
+
|
|
64
|
+
const match = trimmed.match(/^(?:[●*•]\s*)?(.+?)\s+(oauth|api)\s*$/i);
|
|
65
|
+
if (match) {
|
|
66
|
+
found.add(normalizeProviderLabel(match[1]));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (/opencode\s+go/i.test(trimmed)) found.add("OpenCode Go");
|
|
71
|
+
if (/opencode\s+zen/i.test(trimmed)) found.add("OpenCode Zen");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [...found].sort((a, b) => a.localeCompare(b));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hasAuthProvider(providers, product) {
|
|
78
|
+
const list = Array.isArray(providers) ? providers : [];
|
|
79
|
+
if (product === "go") {
|
|
80
|
+
return list.some((entry) => /opencode\s*go/i.test(entry) || /^go$/i.test(entry));
|
|
81
|
+
}
|
|
82
|
+
if (product === "zen") {
|
|
83
|
+
return list.some((entry) => /opencode\s*zen/i.test(entry) || /^zen$/i.test(entry));
|
|
84
|
+
}
|
|
85
|
+
return list.length > 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function classifyModelsProbeStatus(status) {
|
|
89
|
+
if (status === 429) return ENTITLEMENT_STATES.LIMIT_REACHED;
|
|
90
|
+
if (status === 401 || status === 403) return ENTITLEMENT_STATES.UNKNOWN;
|
|
91
|
+
if (status >= 200 && status < 300) return ENTITLEMENT_STATES.UNVERIFIED;
|
|
92
|
+
return ENTITLEMENT_STATES.UNKNOWN;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildDirectEvidenceBase({
|
|
96
|
+
product,
|
|
97
|
+
hasApiKey,
|
|
98
|
+
cliEvidence = null,
|
|
99
|
+
modelsHttpStatus = null,
|
|
100
|
+
modelsOk = false
|
|
101
|
+
}) {
|
|
102
|
+
const authProviders = cliEvidence?.authProviders ?? [];
|
|
103
|
+
const configuredViaCli = hasAuthProvider(authProviders, product);
|
|
104
|
+
const configured = Boolean(hasApiKey) || configuredViaCli;
|
|
105
|
+
const authenticated = Boolean(hasApiKey) && modelsOk;
|
|
106
|
+
const entitlement = modelsHttpStatus === 429
|
|
107
|
+
? ENTITLEMENT_STATES.LIMIT_REACHED
|
|
108
|
+
: (authenticated || configured
|
|
109
|
+
? ENTITLEMENT_STATES.UNVERIFIED
|
|
110
|
+
: ENTITLEMENT_STATES.UNKNOWN);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
configured,
|
|
114
|
+
authenticated,
|
|
115
|
+
entitlement,
|
|
116
|
+
billingModel: product === "go" ? BILLING_MODELS.GO_PLAN : BILLING_MODELS.ZEN_CREDITS,
|
|
117
|
+
evidence: {
|
|
118
|
+
hasApiKey: Boolean(hasApiKey),
|
|
119
|
+
cliInstalled: Boolean(cliEvidence?.cliInstalled),
|
|
120
|
+
authListOk: Boolean(cliEvidence?.authListOk),
|
|
121
|
+
authProviders,
|
|
122
|
+
modelsHttpStatus
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeProviderLabel(value) {
|
|
128
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
129
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { TRANSPORT_KINDS } from "../types.js";
|
|
2
|
+
import { fetchJson } from "../http.js";
|
|
3
|
+
|
|
4
|
+
export async function invokeChatCompletions({
|
|
5
|
+
baseUrl,
|
|
6
|
+
apiKey,
|
|
7
|
+
modelId,
|
|
8
|
+
contextPack,
|
|
9
|
+
request,
|
|
10
|
+
fetchImpl
|
|
11
|
+
}) {
|
|
12
|
+
const result = await fetchJson(`${baseUrl}/chat/completions`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: bearerHeaders(apiKey),
|
|
15
|
+
body: {
|
|
16
|
+
model: modelId,
|
|
17
|
+
messages: buildChatMessages(contextPack, request),
|
|
18
|
+
stream: false
|
|
19
|
+
},
|
|
20
|
+
timeoutMs: request.timeoutMs ?? 120000,
|
|
21
|
+
fetchImpl
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!result.ok) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: redactSecret(result.error ?? "chat completions failed", apiKey)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const usage = result.data?.usage ?? {};
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
model: result.data?.model ?? modelId,
|
|
35
|
+
content: result.data?.choices?.[0]?.message?.content ?? "",
|
|
36
|
+
usage: {
|
|
37
|
+
inputTokens: usage.prompt_tokens ?? null,
|
|
38
|
+
outputTokens: usage.completion_tokens ?? null,
|
|
39
|
+
cachedTokens: usage.prompt_tokens_details?.cached_tokens ?? null,
|
|
40
|
+
estimatedCost: null
|
|
41
|
+
},
|
|
42
|
+
raw: result.data
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function invokeResponses({
|
|
47
|
+
baseUrl,
|
|
48
|
+
apiKey,
|
|
49
|
+
modelId,
|
|
50
|
+
contextPack,
|
|
51
|
+
request,
|
|
52
|
+
fetchImpl
|
|
53
|
+
}) {
|
|
54
|
+
const result = await fetchJson(`${baseUrl}/responses`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: bearerHeaders(apiKey),
|
|
57
|
+
body: {
|
|
58
|
+
model: modelId,
|
|
59
|
+
input: buildResponsesInput(contextPack, request)
|
|
60
|
+
},
|
|
61
|
+
timeoutMs: request.timeoutMs ?? 120000,
|
|
62
|
+
fetchImpl
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: redactSecret(result.error ?? "responses request failed", apiKey)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const usage = result.data?.usage ?? {};
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
model: result.data?.model ?? modelId,
|
|
76
|
+
content: extractResponsesContent(result.data),
|
|
77
|
+
usage: {
|
|
78
|
+
inputTokens: usage.input_tokens ?? usage.prompt_tokens ?? null,
|
|
79
|
+
outputTokens: usage.output_tokens ?? usage.completion_tokens ?? null,
|
|
80
|
+
cachedTokens: usage.input_tokens_details?.cached_tokens ?? null,
|
|
81
|
+
estimatedCost: null
|
|
82
|
+
},
|
|
83
|
+
raw: result.data
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function invokeViaTransport(transport, options) {
|
|
88
|
+
if (transport === TRANSPORT_KINDS.RESPONSES) {
|
|
89
|
+
return invokeResponses(options);
|
|
90
|
+
}
|
|
91
|
+
return invokeChatCompletions(options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bearerHeaders(apiKey) {
|
|
95
|
+
return {
|
|
96
|
+
Authorization: `Bearer ${apiKey}`,
|
|
97
|
+
"Content-Type": "application/json"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function redactSecret(message, secret) {
|
|
102
|
+
if (!message || !secret) return message ?? null;
|
|
103
|
+
return String(message).split(String(secret)).join("[REDACTED]");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildChatMessages(contextPack, request) {
|
|
107
|
+
const messages = [];
|
|
108
|
+
if (contextPack?.systemPrompt) {
|
|
109
|
+
messages.push({ role: "system", content: contextPack.systemPrompt });
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(request.messages) && request.messages.length > 0) {
|
|
112
|
+
messages.push(...request.messages);
|
|
113
|
+
} else if (request.prompt) {
|
|
114
|
+
messages.push({ role: "user", content: request.prompt });
|
|
115
|
+
}
|
|
116
|
+
return messages;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildResponsesInput(contextPack, request) {
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (contextPack?.systemPrompt) {
|
|
122
|
+
parts.push({ role: "system", content: contextPack.systemPrompt });
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(request.messages) && request.messages.length > 0) {
|
|
125
|
+
parts.push(...request.messages);
|
|
126
|
+
} else if (request.prompt) {
|
|
127
|
+
parts.push({ role: "user", content: request.prompt });
|
|
128
|
+
}
|
|
129
|
+
return parts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractResponsesContent(data) {
|
|
133
|
+
if (!data) return "";
|
|
134
|
+
if (typeof data.output_text === "string") return data.output_text;
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(data.output)) {
|
|
137
|
+
const texts = [];
|
|
138
|
+
for (const item of data.output) {
|
|
139
|
+
if (item?.type === "message" && Array.isArray(item.content)) {
|
|
140
|
+
for (const part of item.content) {
|
|
141
|
+
if (part?.type === "output_text" && part.text) texts.push(part.text);
|
|
142
|
+
else if (typeof part?.text === "string") texts.push(part.text);
|
|
143
|
+
}
|
|
144
|
+
} else if (typeof item?.text === "string") {
|
|
145
|
+
texts.push(item.text);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (texts.length > 0) return texts.join("");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return data?.choices?.[0]?.message?.content ?? "";
|
|
152
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { OPENCODE_API_KEY_ENV } from "../types.js";
|
|
2
|
+
import {
|
|
3
|
+
isDirectTransport,
|
|
4
|
+
normalizeModelId,
|
|
5
|
+
resolveOpencodeTransport,
|
|
6
|
+
toRuntimeModelRef
|
|
7
|
+
} from "../transport-registry.js";
|
|
8
|
+
import { invokeViaTransport } from "./opencode-http.js";
|
|
9
|
+
import { resolveOpencodeApiKey, redactSecret } from "./opencode-catalog.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compose direct HTTP invoke onto an OpenCode catalog backend.
|
|
13
|
+
* No Go→Zen fallback here — routing owns that policy.
|
|
14
|
+
*/
|
|
15
|
+
export function withOpencodeDirectInvoke(catalog, {
|
|
16
|
+
env = process.env,
|
|
17
|
+
apiKey = null,
|
|
18
|
+
fetchImpl = globalThis.fetch,
|
|
19
|
+
defaultModelId = null
|
|
20
|
+
} = {}) {
|
|
21
|
+
const resolvedKey = resolveOpencodeApiKey({ env, apiKey });
|
|
22
|
+
const { id, label, product, baseUrl } = catalog;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
...catalog,
|
|
26
|
+
|
|
27
|
+
async invoke(contextPack, request = {}) {
|
|
28
|
+
if (!resolvedKey) {
|
|
29
|
+
return invokeError(id, `${OPENCODE_API_KEY_ENV} is not set. Kairo never stores credentials.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const modelId = normalizeModelId(request.modelId) ?? defaultModelId;
|
|
33
|
+
const transport = resolveOpencodeTransport(product, modelId);
|
|
34
|
+
|
|
35
|
+
if (!isDirectTransport(transport)) {
|
|
36
|
+
const runtimeRef = toRuntimeModelRef(product, modelId) ?? modelId;
|
|
37
|
+
return invokeError(
|
|
38
|
+
id,
|
|
39
|
+
`Model ${modelId} has no direct ${label} transport. Use --backend opencode --model ${runtimeRef} with OpenCode CLI installed and authenticated.`,
|
|
40
|
+
modelId
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await invokeViaTransport(transport, {
|
|
45
|
+
baseUrl,
|
|
46
|
+
apiKey: resolvedKey,
|
|
47
|
+
modelId,
|
|
48
|
+
contextPack,
|
|
49
|
+
request,
|
|
50
|
+
fetchImpl
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
return invokeError(id, redactSecret(result.error, resolvedKey), modelId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
backendId: id,
|
|
60
|
+
model: result.model,
|
|
61
|
+
content: result.content,
|
|
62
|
+
usage: {
|
|
63
|
+
...result.usage,
|
|
64
|
+
model: result.model,
|
|
65
|
+
backendId: id,
|
|
66
|
+
fallbackUsed: false
|
|
67
|
+
},
|
|
68
|
+
raw: result.raw
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function invokeError(backendId, message, model = null) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
backendId,
|
|
78
|
+
model,
|
|
79
|
+
content: null,
|
|
80
|
+
error: message,
|
|
81
|
+
usage: null
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_IDS,
|
|
3
|
+
OPENCODE_GO_BASE_URL,
|
|
4
|
+
OPENCODE_GO_DEFAULT_MODEL,
|
|
5
|
+
OPENCODE_ZEN_BASE_URL,
|
|
6
|
+
OPENCODE_ZEN_DEFAULT_FREE_MODEL
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
createOpencodeCatalogBackend,
|
|
10
|
+
goCostClassForModel,
|
|
11
|
+
zenCostClassForModel
|
|
12
|
+
} from "./opencode-catalog.js";
|
|
13
|
+
import { withOpencodeDirectInvoke } from "./opencode-invoke.js";
|
|
14
|
+
|
|
15
|
+
export function createOpencodeGoBackend({
|
|
16
|
+
env = process.env,
|
|
17
|
+
fetchImpl = globalThis.fetch,
|
|
18
|
+
apiKey = null,
|
|
19
|
+
baseUrl = OPENCODE_GO_BASE_URL,
|
|
20
|
+
collectCliEvidence
|
|
21
|
+
} = {}) {
|
|
22
|
+
const catalog = createOpencodeCatalogBackend({
|
|
23
|
+
id: BACKEND_IDS.OPENCODE_GO,
|
|
24
|
+
label: "OpenCode Go",
|
|
25
|
+
product: "go",
|
|
26
|
+
baseUrl,
|
|
27
|
+
defaultModelId: OPENCODE_GO_DEFAULT_MODEL,
|
|
28
|
+
costClassForModel: goCostClassForModel,
|
|
29
|
+
env,
|
|
30
|
+
fetchImpl,
|
|
31
|
+
apiKey,
|
|
32
|
+
collectCliEvidence
|
|
33
|
+
});
|
|
34
|
+
return withOpencodeDirectInvoke(catalog, {
|
|
35
|
+
env,
|
|
36
|
+
apiKey,
|
|
37
|
+
fetchImpl,
|
|
38
|
+
defaultModelId: OPENCODE_GO_DEFAULT_MODEL
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createOpencodeZenBackend({
|
|
43
|
+
env = process.env,
|
|
44
|
+
fetchImpl = globalThis.fetch,
|
|
45
|
+
apiKey = null,
|
|
46
|
+
baseUrl = OPENCODE_ZEN_BASE_URL,
|
|
47
|
+
collectCliEvidence
|
|
48
|
+
} = {}) {
|
|
49
|
+
const catalog = createOpencodeCatalogBackend({
|
|
50
|
+
id: BACKEND_IDS.OPENCODE_ZEN,
|
|
51
|
+
label: "OpenCode Zen",
|
|
52
|
+
product: "zen",
|
|
53
|
+
baseUrl,
|
|
54
|
+
defaultModelId: OPENCODE_ZEN_DEFAULT_FREE_MODEL,
|
|
55
|
+
costClassForModel: zenCostClassForModel,
|
|
56
|
+
env,
|
|
57
|
+
fetchImpl,
|
|
58
|
+
apiKey,
|
|
59
|
+
collectCliEvidence
|
|
60
|
+
});
|
|
61
|
+
return withOpencodeDirectInvoke(catalog, {
|
|
62
|
+
env,
|
|
63
|
+
apiKey,
|
|
64
|
+
fetchImpl,
|
|
65
|
+
defaultModelId: OPENCODE_ZEN_DEFAULT_FREE_MODEL
|
|
66
|
+
});
|
|
67
|
+
}
|