@open-cr-agent/runtime-opencode 0.4.0 → 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/dist/runtime.d.ts CHANGED
@@ -24,6 +24,7 @@ export declare class OpenCodeRuntime implements AgentRuntime {
24
24
  complete(request: CompletionRequest, signal: AbortSignal): Promise<CompletionResult>;
25
25
  dispose(): Promise<void>;
26
26
  private prompt;
27
+ private unprepared;
27
28
  private start;
28
29
  private launch;
29
30
  }
@@ -117,4 +118,5 @@ export declare function openCodeConfig(tools: Pick<ToolServer, "url" | "headers"
117
118
  };
118
119
  };
119
120
  };
121
+ export declare function providersOf(options: Pick<RuntimeOptions, "models" | "agentModels">): string[];
120
122
  //# sourceMappingURL=runtime.d.ts.map
package/dist/runtime.js CHANGED
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { OcraError, } from "@open-cr-agent/core";
6
- import { completeWithFailback, MAX_AGENT_STEPS, ModelHealth, parseModel, RESUME_MESSAGE, REVIEW_TOOLS, reviewTools, withFailback, withoutSecrets, } from "@open-cr-agent/core/internal";
6
+ import { callChain, completeWithFailback, MAX_AGENT_STEPS, ModelHealth, parseModel, RESUME_MESSAGE, REVIEW_TOOLS, reviewTools, withFailback, withoutSecrets, } from "@open-cr-agent/core/internal";
7
7
  import { createOpencodeClient } from "@opencode-ai/sdk/v2";
8
8
  import { resolveOpencodeBinary } from "./binary.js";
9
9
  import { startOpencodeServer } from "./opencode-server.js";
@@ -68,20 +68,17 @@ export class OpenCodeRuntime {
68
68
  throw new OcraError("INTERNAL", "An OpenCodeRuntime instance serves a single review run");
69
69
  }
70
70
  this.context = spec.context;
71
- const chain = this.options.models[spec.modelTier] ?? [];
72
- if (chain.length === 0) {
73
- yield {
74
- type: "error",
75
- taskId: spec.taskId,
76
- retryable: false,
77
- error: noModel(spec.modelTier),
78
- };
71
+ const chain = callChain(this.options.models, spec.modelTier, spec.models);
72
+ const refused = chain.length === 0 ? noModel(spec.modelTier) : this.unprepared(spec.models);
73
+ if (refused) {
74
+ yield { type: "error", taskId: spec.taskId, retryable: false, error: refused };
79
75
  return;
80
76
  }
81
77
  const infra = await this.start();
82
78
  yield* withFailback({
83
79
  taskId: spec.taskId,
84
80
  tier: spec.modelTier,
81
+ ...(spec.models?.length ? { agent: spec.reviewer } : {}),
85
82
  chain,
86
83
  health: this.health,
87
84
  signal,
@@ -98,12 +95,14 @@ export class OpenCodeRuntime {
98
95
  });
99
96
  }
100
97
  async complete(request, signal) {
101
- const chain = this.options.models[request.tier] ?? [];
102
- if (chain.length === 0)
103
- throw new OcraError("CONFIG_INVALID", noModel(request.tier));
98
+ const chain = callChain(this.options.models, request.tier, request.models);
99
+ const refused = chain.length === 0 ? noModel(request.tier) : this.unprepared(request.models);
100
+ if (refused)
101
+ throw new OcraError("CONFIG_INVALID", refused);
104
102
  const infra = await this.start();
105
103
  return completeWithFailback({
106
104
  tier: request.tier,
105
+ ...(request.models?.length ? { agent: request.agent ?? request.tier } : {}),
107
106
  chain,
108
107
  health: this.health,
109
108
  signal,
@@ -138,13 +137,25 @@ export class OpenCodeRuntime {
138
137
  }
139
138
  : outcome);
140
139
  }
140
+ // The server holds the credentials of the providers it started with; an
141
+ // agent's own chain naming another would fail at OpenCode as "model not
142
+ // found". The tier chains are always among them.
143
+ unprepared(own) {
144
+ if (!own?.length)
145
+ return undefined;
146
+ const prepared = new Set(providersOf(this.options));
147
+ const model = own.find((m) => !prepared.has(parseModel(m).providerID));
148
+ return model === undefined
149
+ ? undefined
150
+ : `"${model}" is not among the models the runtime was started with (RuntimeOptions.models or agentModels)`;
151
+ }
141
152
  start() {
142
153
  this.infra ??= this.launch();
143
154
  return this.infra;
144
155
  }
145
156
  async launch() {
146
157
  const custom = this.options.providers ?? {};
147
- const missing = missingCredentials(this.options.env, providersOf(this.options.models), custom);
158
+ const missing = missingCredentials(this.options.env, providersOf(this.options), custom);
148
159
  if (missing.length > 0)
149
160
  throw new OcraError("CONFIG_CREDENTIALS_MISSING", missing.join("; "));
150
161
  const root = await mkdtemp(join(tmpdir(), "ocra-opencode-"));
@@ -161,7 +172,7 @@ export class OpenCodeRuntime {
161
172
  const server = await startOpencodeServer({
162
173
  binary: this.options.binary ?? resolveOpencodeBinary(this.options.env),
163
174
  cwd: workspace,
164
- env: serverEnv(this.options.env, dirs, providersOf(this.options.models), custom),
175
+ env: serverEnv(this.options.env, dirs, providersOf(this.options), custom),
165
176
  config: openCodeConfig(tools, this.helperTools, custom, this.options.sampling),
166
177
  });
167
178
  started = server;
@@ -179,7 +190,7 @@ export class OpenCodeRuntime {
179
190
  rmSync(root, { recursive: true, force: true });
180
191
  };
181
192
  process.on("exit", onExit);
182
- const secrets = credentialValues(this.options.env, providersOf(this.options.models), custom);
193
+ const secrets = credentialValues(this.options.env, providersOf(this.options), custom);
183
194
  return { root, tools, server, client, dispatcher, onExit, secrets };
184
195
  }
185
196
  catch (error) {
@@ -270,8 +281,10 @@ function providerConfig(custom, temperature) {
270
281
  },
271
282
  ]));
272
283
  }
273
- function providersOf(models) {
274
- return Object.values(models).flatMap((chain) => (chain ?? []).map((m) => parseModel(m).providerID));
284
+ // The providers of every chain a call may run on: the tiers' and the
285
+ // agents' own (ADR-0025), so each gets its credentials like a tier's.
286
+ export function providersOf(options) {
287
+ return [...Object.values(options.models), ...Object.values(options.agentModels ?? {})].flatMap((chain) => (chain ?? []).map((m) => parseModel(m).providerID));
275
288
  }
276
289
  function noModel(tier) {
277
290
  return `No model configured for the "${tier}" tier; set models.${tier} in .ocra/config.json or OCRA_MODEL_${tier.toUpperCase()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-cr-agent/runtime-opencode",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenCode-based agent runtime for Open-CR-Agent",
5
5
  "keywords": [
6
6
  "code-review",
@@ -34,7 +34,7 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.30.1",
37
- "@open-cr-agent/core": "0.4.0",
37
+ "@open-cr-agent/core": "0.5.0",
38
38
  "@opencode-ai/sdk": "1.18.32",
39
39
  "opencode-ai": "1.18.32",
40
40
  "undici": "^8.11.2",