@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.
Files changed (103) hide show
  1. package/README.md +124 -17
  2. package/global-template/components/catalog.json +63 -6
  3. package/global-template/components/sdd-core/personas/teaching.md +42 -0
  4. package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
  5. package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
  6. package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
  7. package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
  8. package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
  9. package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
  10. package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
  11. package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
  12. package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
  13. package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
  14. package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
  15. package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
  16. package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
  17. package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
  18. package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
  19. package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
  20. package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
  21. package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
  22. package/package.json +1 -1
  23. package/scripts/cockpit-smoke.mjs +29 -8
  24. package/scripts/smoke-test.sh +9 -0
  25. package/src/cli.js +54 -5
  26. package/src/global/action-planner.js +17 -4
  27. package/src/global/component-authoring.js +1 -2
  28. package/src/global/component-ecosystem-checks.js +74 -15
  29. package/src/global/component-health.js +56 -0
  30. package/src/global/component-integration-cli.js +302 -0
  31. package/src/global/component-manifest.js +214 -0
  32. package/src/global/component-registry.js +19 -2
  33. package/src/global/component-resolve.js +51 -0
  34. package/src/global/components/sdd-core.js +6 -0
  35. package/src/global/control-plane-proposals.js +229 -0
  36. package/src/global/control-plane-snapshot.js +212 -0
  37. package/src/global/dashboard-guidance.js +27 -3
  38. package/src/global/diff.js +2 -5
  39. package/src/global/global-cli.js +16 -1
  40. package/src/global/global-doctor.js +75 -2
  41. package/src/global/global-installer.js +43 -6
  42. package/src/global/governance-actions.js +212 -0
  43. package/src/global/governance-repair.js +13 -0
  44. package/src/global/ink/cockpit-changes.js +124 -0
  45. package/src/global/ink/cockpit-control-center.js +106 -0
  46. package/src/global/ink/cockpit-controller.js +17 -0
  47. package/src/global/ink/cockpit-enter.js +27 -0
  48. package/src/global/ink/cockpit-focus.js +7 -1
  49. package/src/global/ink/cockpit-models.js +98 -46
  50. package/src/global/ink/cockpit-proposals.js +82 -0
  51. package/src/global/ink/cockpit-recovery.js +120 -0
  52. package/src/global/ink/cockpit-runs.js +47 -0
  53. package/src/global/ink/cockpit-scan.js +60 -0
  54. package/src/global/ink/cockpit-views.js +144 -74
  55. package/src/global/ink/orchestrator-app.js +156 -42
  56. package/src/global/ink/orchestrator-state.js +22 -1
  57. package/src/global/ink/use-orchestrator-data.js +239 -17
  58. package/src/global/integrations/engram-apply.js +105 -0
  59. package/src/global/integrations/engram-evidence.js +218 -0
  60. package/src/global/integrations/engram-exec.js +107 -0
  61. package/src/global/integrations/engram-plan.js +60 -0
  62. package/src/global/integrations/engram-provider.js +22 -0
  63. package/src/global/integrations/engram-receipts.js +116 -0
  64. package/src/global/integrations/engram-rollback.js +131 -0
  65. package/src/global/integrations/index.js +12 -0
  66. package/src/global/integrations/provider-contract.js +83 -0
  67. package/src/global/integrations/provider-registry.js +47 -0
  68. package/src/global/integrations/sdd-apply.js +190 -0
  69. package/src/global/integrations/sdd-destinations.js +95 -0
  70. package/src/global/integrations/sdd-evidence.js +89 -0
  71. package/src/global/integrations/sdd-fs-guard.js +183 -0
  72. package/src/global/integrations/sdd-lifecycle.js +84 -0
  73. package/src/global/integrations/sdd-persona.js +71 -0
  74. package/src/global/integrations/sdd-plan.js +87 -0
  75. package/src/global/integrations/sdd-provider.js +21 -0
  76. package/src/global/integrations/sdd-receipts.js +44 -0
  77. package/src/global/integrations/sdd-rollback.js +269 -0
  78. package/src/global/integrations/sdd-skill-files.js +61 -0
  79. package/src/global/integrations/sdd-state.js +175 -0
  80. package/src/global/integrations/sdd-verify.js +122 -0
  81. package/src/global/intelligence/backends/opencode-catalog.js +212 -0
  82. package/src/global/intelligence/backends/opencode-cli.js +247 -0
  83. package/src/global/intelligence/backends/opencode-evidence.js +129 -0
  84. package/src/global/intelligence/backends/opencode-http.js +152 -0
  85. package/src/global/intelligence/backends/opencode-invoke.js +83 -0
  86. package/src/global/intelligence/backends/opencode-providers.js +67 -0
  87. package/src/global/intelligence/backends/opencode-runtime.js +242 -0
  88. package/src/global/intelligence/context-compiler.js +95 -50
  89. package/src/global/intelligence/index.js +38 -1
  90. package/src/global/intelligence/orchestrate.js +4 -2
  91. package/src/global/intelligence/registry.js +62 -6
  92. package/src/global/intelligence/router.js +188 -70
  93. package/src/global/intelligence/transport-registry.js +147 -0
  94. package/src/global/intelligence/types.js +21 -2
  95. package/src/global/intelligence-cli.js +48 -17
  96. package/src/global/intelligence-session.js +37 -0
  97. package/src/global/json-output.js +1 -0
  98. package/src/global/load-component-catalog.js +10 -28
  99. package/src/global/load-workspace-component-catalog.js +22 -63
  100. package/src/global/state-migration.js +21 -1
  101. package/src/global/state.js +13 -8
  102. package/src/global/status.js +4 -17
  103. package/src/global/sync.js +2 -5
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  BACKEND_IDS,
3
+ OPENCODE_GO_DEFAULT_MODEL,
4
+ OPENCODE_ZEN_DEFAULT_FREE_MODEL,
3
5
  OPENROUTER_FREE_MODEL,
4
6
  PRIVACY_CLASSES,
5
7
  ROUTING_MODES,
@@ -7,36 +9,42 @@ import {
7
9
  } from "./types.js";
8
10
  import { estimateTokens } from "./context-compiler.js";
9
11
 
10
- const TASK_WEIGHTS = {
11
- architecture: "heavy",
12
- security: "heavy",
13
- review: "heavy",
14
- diagnose: "light",
15
- explain: "light",
16
- scaffold: "light",
17
- test: "light",
18
- default: "light"
19
- };
20
-
21
12
  export function classifyTaskWeight(task = "") {
22
13
  const text = String(task).toLowerCase();
23
14
  if (/architect|adr|design system|security|threat/.test(text)) return "heavy";
24
15
  if (/review|refactor complex|debug complex/.test(text)) return "heavy";
25
16
  if (/test|scaffold|explain|status|diagnose|lint|format/.test(text)) return "light";
26
- return TASK_WEIGHTS.default;
17
+ return "light";
18
+ }
19
+
20
+ /** Direct Go/Zen HTTP is eligible only with key + proven auth + available. */
21
+ export function isDirectProviderEligible(backend) {
22
+ return Boolean(backend?.hasApiKey && backend?.authenticated && backend?.available);
23
+ }
24
+
25
+ function diagnosticsDecision(reason, estimatedTokens) {
26
+ return createRoutingDecision({
27
+ backendId: null,
28
+ model: null,
29
+ reason,
30
+ estimatedTokens,
31
+ privacyImpact: PRIVACY_CLASSES.UNKNOWN,
32
+ mode: ROUTING_MODES.DIAGNOSTICS,
33
+ requiresCloudConsent: false,
34
+ canInvoke: false,
35
+ fallback: null
36
+ });
27
37
  }
28
38
 
29
- /**
30
- * Resolve which backend/model to use.
31
- * Precedence: user override > Ollama local > OpenRouter free (consent) > diagnostics.
32
- */
39
+ /** Precedence: CLI → profile → Ollama → Go → Zen → OpenRouter → diagnostics. Go has no Zen invoke fallback. */
33
40
  export function resolveRoutingDecision({
34
41
  backends = [],
35
42
  profile = {},
36
43
  contextPack = null,
37
44
  task = null,
38
45
  cloudConsent = false,
39
- tokenBudget = null
46
+ tokenBudget = null,
47
+ sessionOverride = null
40
48
  } = {}) {
41
49
  const estimatedTokens = contextPack?.estimatedTokens
42
50
  ?? estimateTokens(contextPack?.systemPrompt ?? "")
@@ -44,24 +52,24 @@ export function resolveRoutingDecision({
44
52
 
45
53
  const budget = tokenBudget ?? profile.tokenBudget ?? null;
46
54
  if (budget != null && estimatedTokens > budget) {
47
- return createRoutingDecision({
48
- backendId: null,
49
- model: null,
50
- reason: `Estimated tokens (${estimatedTokens}) exceed budget (${budget}). Compact context or raise tokenBudget.`,
51
- estimatedTokens,
52
- privacyImpact: PRIVACY_CLASSES.UNKNOWN,
53
- mode: ROUTING_MODES.DIAGNOSTICS,
54
- requiresCloudConsent: false,
55
- canInvoke: false
56
- });
55
+ return diagnosticsDecision(
56
+ `Estimated tokens (${estimatedTokens}) exceed budget (${budget}). Compact context or raise tokenBudget.`,
57
+ estimatedTokens
58
+ );
57
59
  }
58
60
 
59
- const override = resolveUserOverride(profile, backends);
61
+ const effectiveProfile = applySessionOverride(profile, sessionOverride);
62
+ const override = resolveUserOverride(effectiveProfile, backends, sessionOverride);
63
+ if (override?.ineligible) {
64
+ return diagnosticsDecision(override.reason, estimatedTokens);
65
+ }
60
66
  if (override) {
61
67
  return createRoutingDecision({
62
68
  backendId: override.backend.id,
63
69
  model: override.model,
64
- reason: `User override: ${override.backend.id}/${override.model.modelId}`,
70
+ reason: override.source === "cli"
71
+ ? `CLI override: ${override.backend.id}/${override.model.modelId}`
72
+ : `User override: ${override.backend.id}/${override.model.modelId}`,
65
73
  estimatedTokens,
66
74
  privacyImpact: override.model.privacyClass,
67
75
  mode: ROUTING_MODES.USER_OVERRIDE,
@@ -87,18 +95,60 @@ export function resolveRoutingDecision({
87
95
  });
88
96
  }
89
97
 
98
+ const go = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE_GO);
99
+ if (isDirectProviderEligible(go)) {
100
+ const model = selectOpencodeModel(go.models, OPENCODE_GO_DEFAULT_MODEL, BACKEND_IDS.OPENCODE_GO);
101
+ return createRoutingDecision({
102
+ backendId: BACKEND_IDS.OPENCODE_GO,
103
+ model,
104
+ reason: cloudConsent
105
+ ? `OpenCode Go approved: ${model.modelId}`
106
+ : `OpenCode Go available (${model.modelId}) but cloud consent required before invoke`,
107
+ estimatedTokens,
108
+ privacyImpact: PRIVACY_CLASSES.CLOUD,
109
+ mode: ROUTING_MODES.CLOUD_CONSENT,
110
+ requiresCloudConsent: true,
111
+ canInvoke: cloudConsent,
112
+ fallback: null
113
+ });
114
+ }
115
+ // CLI-configured or broken-key Go must not silently spend Zen/OpenRouter.
116
+ if (go?.configured || go?.hasApiKey) {
117
+ return diagnosticsDecision(
118
+ describeIneligibleCloudStop(go, "OpenCode Go", "Zen/OpenRouter"),
119
+ estimatedTokens
120
+ );
121
+ }
122
+
123
+ const zen = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE_ZEN);
124
+ if (isDirectProviderEligible(zen)) {
125
+ const model = selectOpencodeModel(zen.models, OPENCODE_ZEN_DEFAULT_FREE_MODEL, BACKEND_IDS.OPENCODE_ZEN);
126
+ return createRoutingDecision({
127
+ backendId: BACKEND_IDS.OPENCODE_ZEN,
128
+ model,
129
+ reason: cloudConsent
130
+ ? `OpenCode Zen approved: ${model.modelId}`
131
+ : `OpenCode Zen available (${model.modelId}) but cloud consent required before invoke`,
132
+ estimatedTokens,
133
+ privacyImpact: PRIVACY_CLASSES.CLOUD,
134
+ mode: ROUTING_MODES.CLOUD_CONSENT,
135
+ requiresCloudConsent: true,
136
+ canInvoke: cloudConsent,
137
+ fallback: buildOpenRouterFallback(backends, cloudConsent)
138
+ });
139
+ }
140
+ if (zen?.configured || zen?.hasApiKey) {
141
+ return diagnosticsDecision(
142
+ describeIneligibleCloudStop(zen, "OpenCode Zen", "OpenRouter"),
143
+ estimatedTokens
144
+ );
145
+ }
146
+
90
147
  const openrouter = backends.find((entry) => entry.id === BACKEND_IDS.OPENROUTER);
91
148
  if (openrouter?.hasApiKey) {
92
149
  const model = openrouter.models?.find((entry) => entry.modelId === OPENROUTER_FREE_MODEL)
93
150
  ?? openrouter.models?.[0]
94
- ?? {
95
- provider: BACKEND_IDS.OPENROUTER,
96
- modelId: OPENROUTER_FREE_MODEL,
97
- local: false,
98
- privacyClass: PRIVACY_CLASSES.CLOUD,
99
- costClass: "free",
100
- opaque: true
101
- };
151
+ ?? opaqueCloudModel(BACKEND_IDS.OPENROUTER, OPENROUTER_FREE_MODEL, "free");
102
152
 
103
153
  return createRoutingDecision({
104
154
  backendId: BACKEND_IDS.OPENROUTER,
@@ -115,52 +165,95 @@ export function resolveRoutingDecision({
115
165
  });
116
166
  }
117
167
 
118
- return createRoutingDecision({
119
- backendId: null,
120
- model: null,
121
- reason: "No intelligence backend available. Remaining in diagnostics/configuration mode.",
122
- estimatedTokens,
123
- privacyImpact: PRIVACY_CLASSES.UNKNOWN,
124
- mode: ROUTING_MODES.DIAGNOSTICS,
125
- requiresCloudConsent: false,
126
- canInvoke: false
127
- });
168
+ return diagnosticsDecision(
169
+ "No intelligence backend available. Remaining in diagnostics/configuration mode.",
170
+ estimatedTokens
171
+ );
172
+ }
173
+
174
+ function applySessionOverride(profile, sessionOverride) {
175
+ if (!sessionOverride) return profile;
176
+ return {
177
+ ...profile,
178
+ preferredBackend: sessionOverride.preferredBackend ?? profile.preferredBackend,
179
+ preferredModel: sessionOverride.preferredModel ?? profile.preferredModel
180
+ };
128
181
  }
129
182
 
130
- function resolveUserOverride(profile, backends) {
183
+ function resolveUserOverride(profile, backends, sessionOverride) {
131
184
  const preferredBackend = profile.preferredBackend ?? null;
132
185
  const preferredModel = profile.preferredModel ?? null;
133
186
  if (!preferredBackend && !preferredModel) return null;
134
187
 
188
+ const source = sessionOverride?.preferredBackend || sessionOverride?.preferredModel
189
+ ? "cli"
190
+ : "profile";
191
+ const label = source === "cli" ? "CLI" : "User";
192
+ const reject = (reason) => ({
193
+ ineligible: true,
194
+ reason: `${reason} Remaining in diagnostics; automatic routing was not applied.`
195
+ });
196
+
135
197
  if (preferredBackend) {
136
- const backend = backends.find((entry) => entry.id === preferredBackend);
137
- if (!backend || (!backend.available && !backend.hasApiKey && !backend.detected)) {
138
- return null;
198
+ const backend = backends.find((entry) => entry.id === preferredBackend) ?? null;
199
+ if (!isOverrideEligible(backend)) {
200
+ return reject(`${label} override ${preferredBackend} is not eligible for invoke.`);
139
201
  }
140
- const model = (backend.models ?? []).find((entry) => entry.modelId === preferredModel)
141
- ?? backend.models?.[0]
142
- ?? (preferredModel
143
- ? {
144
- provider: preferredBackend,
145
- modelId: preferredModel,
146
- local: backend.id === BACKEND_IDS.OLLAMA,
147
- privacyClass: backend.id === BACKEND_IDS.OLLAMA ? PRIVACY_CLASSES.LOCAL : PRIVACY_CLASSES.CLOUD,
148
- costClass: "unknown",
149
- opaque: true
150
- }
151
- : null);
152
- if (!model) return null;
153
- return { backend, model };
202
+ if (preferredModel) {
203
+ const exact = (backend.models ?? []).find((entry) => entry.modelId === preferredModel);
204
+ if (!exact) {
205
+ return reject(
206
+ `${label} override model ${preferredModel} was not found on ${preferredBackend}.`
207
+ );
208
+ }
209
+ return { backend, model: exact, source };
210
+ }
211
+ const model = backend.models?.[0] ?? null;
212
+ if (!model) return reject(`${label} override ${preferredBackend} has no usable model.`);
213
+ return { backend, model, source };
154
214
  }
155
215
 
156
- if (preferredModel) {
157
- for (const backend of backends) {
158
- const model = (backend.models ?? []).find((entry) => entry.modelId === preferredModel);
159
- if (model) return { backend, model };
216
+ for (const backend of backends) {
217
+ const model = (backend.models ?? []).find((entry) => entry.modelId === preferredModel);
218
+ if (!model) continue;
219
+ if (!isOverrideEligible(backend)) {
220
+ return reject(
221
+ `${label} override model ${preferredModel} resolved to ineligible backend ${backend.id}.`
222
+ );
160
223
  }
224
+ return { backend, model, source };
161
225
  }
226
+ return reject(`${label} override model ${preferredModel} was not found.`);
227
+ }
228
+
229
+ /** Runtime is exempt from /models auth; Go/Zen require proven authentication. */
230
+ function isOverrideEligible(backend) {
231
+ if (!backend) return false;
232
+ if (backend.id === BACKEND_IDS.OPENCODE) return Boolean(backend.available || backend.detected);
233
+ if (backend.id === BACKEND_IDS.OLLAMA) return Boolean(backend.available);
234
+ if (backend.id === BACKEND_IDS.OPENCODE_GO || backend.id === BACKEND_IDS.OPENCODE_ZEN) {
235
+ return isDirectProviderEligible(backend);
236
+ }
237
+ if (backend.id === BACKEND_IDS.OPENROUTER) return Boolean(backend.hasApiKey);
238
+ return Boolean(backend.available || backend.hasApiKey || backend.detected);
239
+ }
240
+
241
+ function describeIneligibleCloudStop(backend, label, blockedFallback) {
242
+ const detail = backend?.error
243
+ ?? backend?.recommendation
244
+ ?? "configured but not authenticated/available";
245
+ return `${label} ${detail}. Invoke blocked (canInvoke=false); ${blockedFallback} fallback suppressed.`;
246
+ }
162
247
 
163
- return null;
248
+ function opaqueCloudModel(provider, modelId, costClass) {
249
+ return {
250
+ provider,
251
+ modelId,
252
+ local: false,
253
+ privacyClass: PRIVACY_CLASSES.CLOUD,
254
+ costClass,
255
+ opaque: true
256
+ };
164
257
  }
165
258
 
166
259
  function selectLocalModel(models, task) {
@@ -171,6 +264,14 @@ function selectLocalModel(models, task) {
171
264
  return models[0];
172
265
  }
173
266
 
267
+ function selectOpencodeModel(models, preferredId, provider) {
268
+ const list = Array.isArray(models) ? models : [];
269
+ return list.find((entry) => entry.modelId === preferredId)
270
+ ?? list.find((entry) => entry.costClass === "free")
271
+ ?? list[0]
272
+ ?? opaqueCloudModel(provider, preferredId, provider === BACKEND_IDS.OPENCODE_GO ? "paid" : "unknown");
273
+ }
274
+
174
275
  function buildLocalFallback(backends) {
175
276
  const ollama = backends.find((entry) => entry.id === BACKEND_IDS.OLLAMA && entry.available);
176
277
  if (!ollama?.models?.length) return null;
@@ -181,6 +282,23 @@ function buildLocalFallback(backends) {
181
282
  }
182
283
 
183
284
  function buildCloudFallback(backends, cloudConsent) {
285
+ const go = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE_GO);
286
+ if (isDirectProviderEligible(go)) {
287
+ return {
288
+ backendId: BACKEND_IDS.OPENCODE_GO,
289
+ modelId: go.models?.[0]?.modelId ?? OPENCODE_GO_DEFAULT_MODEL,
290
+ requiresConsent: !cloudConsent
291
+ };
292
+ }
293
+ if (go?.configured || go?.hasApiKey) return null;
294
+ const zen = backends.find((entry) => entry.id === BACKEND_IDS.OPENCODE_ZEN);
295
+ if (zen?.configured || zen?.hasApiKey) {
296
+ if (!isDirectProviderEligible(zen)) return null;
297
+ }
298
+ return buildOpenRouterFallback(backends, cloudConsent);
299
+ }
300
+
301
+ function buildOpenRouterFallback(backends, cloudConsent) {
184
302
  const openrouter = backends.find((entry) => entry.id === BACKEND_IDS.OPENROUTER && entry.hasApiKey);
185
303
  if (!openrouter) return null;
186
304
  return {
@@ -0,0 +1,147 @@
1
+ import { TRANSPORT_KINDS } from "./types.js";
2
+
3
+ /**
4
+ * Explicit transport compatibility for OpenCode Go / Zen.
5
+ * Never infer protocol from model name — only this registry decides.
6
+ */
7
+
8
+ const GO_DIRECT = {
9
+ "glm-5.2": TRANSPORT_KINDS.CHAT_COMPLETIONS,
10
+ "glm-5.1": TRANSPORT_KINDS.CHAT_COMPLETIONS,
11
+ "kimi-k2.7-code": TRANSPORT_KINDS.CHAT_COMPLETIONS,
12
+ "kimi-k2.6": TRANSPORT_KINDS.CHAT_COMPLETIONS,
13
+ "deepseek-v4-pro": TRANSPORT_KINDS.CHAT_COMPLETIONS,
14
+ "deepseek-v4-flash": TRANSPORT_KINDS.CHAT_COMPLETIONS,
15
+ "mimo-v2.5": TRANSPORT_KINDS.CHAT_COMPLETIONS,
16
+ "mimo-v2.5-pro": TRANSPORT_KINDS.CHAT_COMPLETIONS
17
+ };
18
+
19
+ const GO_RUNTIME = {
20
+ "minimax-m3": TRANSPORT_KINDS.RUNTIME,
21
+ "minimax-m2.7": TRANSPORT_KINDS.RUNTIME,
22
+ "minimax-m2.5": TRANSPORT_KINDS.RUNTIME,
23
+ "qwen3.7-max": TRANSPORT_KINDS.RUNTIME,
24
+ "qwen3.7-plus": TRANSPORT_KINDS.RUNTIME,
25
+ "qwen3.6-plus": TRANSPORT_KINDS.RUNTIME
26
+ };
27
+
28
+ const ZEN_RESPONSES = {
29
+ "gpt-5.6-sol": TRANSPORT_KINDS.RESPONSES,
30
+ "gpt-5.6-terra": TRANSPORT_KINDS.RESPONSES,
31
+ "gpt-5.6-luna": TRANSPORT_KINDS.RESPONSES,
32
+ "gpt-5.5": TRANSPORT_KINDS.RESPONSES,
33
+ "gpt-5.5-pro": TRANSPORT_KINDS.RESPONSES,
34
+ "gpt-5.4": TRANSPORT_KINDS.RESPONSES,
35
+ "gpt-5.4-pro": TRANSPORT_KINDS.RESPONSES,
36
+ "gpt-5.4-mini": TRANSPORT_KINDS.RESPONSES,
37
+ "gpt-5.4-nano": TRANSPORT_KINDS.RESPONSES,
38
+ "gpt-5.3-codex": TRANSPORT_KINDS.RESPONSES,
39
+ "gpt-5.3-codex-spark": TRANSPORT_KINDS.RESPONSES,
40
+ "gpt-5.2": TRANSPORT_KINDS.RESPONSES,
41
+ "gpt-5.2-codex": TRANSPORT_KINDS.RESPONSES,
42
+ "gpt-5.1": TRANSPORT_KINDS.RESPONSES,
43
+ "gpt-5.1-codex": TRANSPORT_KINDS.RESPONSES,
44
+ "gpt-5.1-codex-max": TRANSPORT_KINDS.RESPONSES,
45
+ "gpt-5.1-codex-mini": TRANSPORT_KINDS.RESPONSES,
46
+ "gpt-5": TRANSPORT_KINDS.RESPONSES,
47
+ "gpt-5-codex": TRANSPORT_KINDS.RESPONSES,
48
+ "gpt-5-nano": TRANSPORT_KINDS.RESPONSES
49
+ };
50
+
51
+ const ZEN_CHAT = {
52
+ "deepseek-v4-pro": TRANSPORT_KINDS.CHAT_COMPLETIONS,
53
+ "deepseek-v4-flash": TRANSPORT_KINDS.CHAT_COMPLETIONS,
54
+ "minimax-m3": TRANSPORT_KINDS.CHAT_COMPLETIONS,
55
+ "minimax-m2.7": TRANSPORT_KINDS.CHAT_COMPLETIONS,
56
+ "minimax-m2.5": TRANSPORT_KINDS.CHAT_COMPLETIONS,
57
+ "glm-5.2": TRANSPORT_KINDS.CHAT_COMPLETIONS,
58
+ "glm-5.1": TRANSPORT_KINDS.CHAT_COMPLETIONS,
59
+ "glm-5": TRANSPORT_KINDS.CHAT_COMPLETIONS,
60
+ "kimi-k2.5": TRANSPORT_KINDS.CHAT_COMPLETIONS,
61
+ "kimi-k2.6": TRANSPORT_KINDS.CHAT_COMPLETIONS,
62
+ "kimi-k2.7-code": TRANSPORT_KINDS.CHAT_COMPLETIONS,
63
+ "grok-4.5": TRANSPORT_KINDS.CHAT_COMPLETIONS,
64
+ "grok-build-0.1": TRANSPORT_KINDS.CHAT_COMPLETIONS,
65
+ "big-pickle": TRANSPORT_KINDS.CHAT_COMPLETIONS,
66
+ "mimo-v2.5-free": TRANSPORT_KINDS.CHAT_COMPLETIONS,
67
+ "north-mini-code-free": TRANSPORT_KINDS.CHAT_COMPLETIONS,
68
+ "nemotron-3-ultra-free": TRANSPORT_KINDS.CHAT_COMPLETIONS,
69
+ "deepseek-v4-flash-free": TRANSPORT_KINDS.CHAT_COMPLETIONS
70
+ };
71
+
72
+ const ZEN_RUNTIME = {
73
+ "claude-fable-5": TRANSPORT_KINDS.RUNTIME,
74
+ "claude-opus-4-8": TRANSPORT_KINDS.RUNTIME,
75
+ "claude-opus-4-7": TRANSPORT_KINDS.RUNTIME,
76
+ "claude-opus-4-6": TRANSPORT_KINDS.RUNTIME,
77
+ "claude-opus-4-5": TRANSPORT_KINDS.RUNTIME,
78
+ "claude-sonnet-5": TRANSPORT_KINDS.RUNTIME,
79
+ "claude-sonnet-4-6": TRANSPORT_KINDS.RUNTIME,
80
+ "claude-sonnet-4-5": TRANSPORT_KINDS.RUNTIME,
81
+ "claude-haiku-4-5": TRANSPORT_KINDS.RUNTIME,
82
+ "gemini-3.5-flash": TRANSPORT_KINDS.RUNTIME,
83
+ "gemini-3.1-pro": TRANSPORT_KINDS.RUNTIME,
84
+ "gemini-3-flash": TRANSPORT_KINDS.RUNTIME,
85
+ "qwen3.7-max": TRANSPORT_KINDS.RUNTIME,
86
+ "qwen3.7-plus": TRANSPORT_KINDS.RUNTIME,
87
+ "qwen3.6-plus": TRANSPORT_KINDS.RUNTIME,
88
+ "qwen3.5-plus": TRANSPORT_KINDS.RUNTIME
89
+ };
90
+
91
+ export const OPENCODE_GO_TRANSPORTS = Object.freeze({ ...GO_DIRECT, ...GO_RUNTIME });
92
+ export const OPENCODE_ZEN_TRANSPORTS = Object.freeze({
93
+ ...ZEN_RESPONSES,
94
+ ...ZEN_CHAT,
95
+ ...ZEN_RUNTIME
96
+ });
97
+
98
+ export function resolveOpencodeTransport(product, modelId) {
99
+ const id = normalizeModelId(modelId);
100
+ if (!id) return null;
101
+ if (product === "go") return OPENCODE_GO_TRANSPORTS[id] ?? null;
102
+ if (product === "zen") return OPENCODE_ZEN_TRANSPORTS[id] ?? null;
103
+ return null;
104
+ }
105
+
106
+ export function isDirectTransport(transport) {
107
+ return transport === TRANSPORT_KINDS.CHAT_COMPLETIONS
108
+ || transport === TRANSPORT_KINDS.RESPONSES;
109
+ }
110
+
111
+ export function listRegisteredModelIds(product, { directOnly = false, runtimeOnly = false } = {}) {
112
+ const table = product === "go" ? OPENCODE_GO_TRANSPORTS : OPENCODE_ZEN_TRANSPORTS;
113
+ return Object.entries(table)
114
+ .filter(([, transport]) => {
115
+ if (directOnly) return isDirectTransport(transport);
116
+ if (runtimeOnly) return transport === TRANSPORT_KINDS.RUNTIME;
117
+ return true;
118
+ })
119
+ .map(([modelId]) => modelId);
120
+ }
121
+
122
+ export function normalizeModelId(modelId) {
123
+ if (!modelId) return null;
124
+ const raw = String(modelId).trim();
125
+ if (!raw) return null;
126
+ const slash = raw.lastIndexOf("/");
127
+ return slash >= 0 ? raw.slice(slash + 1) : raw;
128
+ }
129
+
130
+ export function toRuntimeModelRef(product, modelId) {
131
+ const id = normalizeModelId(modelId);
132
+ if (!id) return null;
133
+ return product === "go" ? `opencode-go/${id}` : `opencode/${id}`;
134
+ }
135
+
136
+ export function resolveRuntimeProduct(modelId) {
137
+ const raw = String(modelId ?? "");
138
+ if (raw.startsWith("opencode-go/")) return "go";
139
+ if (raw.startsWith("opencode/")) return "zen";
140
+ if (OPENCODE_GO_TRANSPORTS[normalizeModelId(raw)] === TRANSPORT_KINDS.RUNTIME) {
141
+ return OPENCODE_ZEN_TRANSPORTS[normalizeModelId(raw)] === TRANSPORT_KINDS.RUNTIME
142
+ ? "zen"
143
+ : "go";
144
+ }
145
+ if (OPENCODE_ZEN_TRANSPORTS[normalizeModelId(raw)] === TRANSPORT_KINDS.RUNTIME) return "zen";
146
+ return null;
147
+ }
@@ -1,9 +1,26 @@
1
1
  export const BACKEND_IDS = {
2
2
  OLLAMA: "ollama",
3
3
  OPENROUTER: "openrouter",
4
+ OPENCODE_GO: "opencode-go",
5
+ OPENCODE_ZEN: "opencode-zen",
6
+ OPENCODE: "opencode",
4
7
  CUSTOM: "custom"
5
8
  };
6
9
 
10
+ export const TRANSPORT_KINDS = {
11
+ CHAT_COMPLETIONS: "chat_completions",
12
+ RESPONSES: "responses",
13
+ RUNTIME: "runtime"
14
+ };
15
+
16
+ export const OPENCODE_API_KEY_ENV = "OPENCODE_API_KEY";
17
+
18
+ export const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
19
+ export const OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
20
+
21
+ export const OPENCODE_GO_DEFAULT_MODEL = "kimi-k2.7-code";
22
+ export const OPENCODE_ZEN_DEFAULT_FREE_MODEL = "big-pickle";
23
+
7
24
  export const COST_CLASSES = {
8
25
  FREE: "free",
9
26
  LOCAL: "local",
@@ -38,7 +55,8 @@ export function createModelDescriptor({
38
55
  tools = false,
39
56
  reasoning = false,
40
57
  rateLimits = null,
41
- opaque = false
58
+ opaque = false,
59
+ transport = null
42
60
  }) {
43
61
  return {
44
62
  provider,
@@ -50,7 +68,8 @@ export function createModelDescriptor({
50
68
  tools,
51
69
  reasoning,
52
70
  rateLimits,
53
- opaque
71
+ opaque,
72
+ transport
54
73
  };
55
74
  }
56
75