opencode-codeops 1.10.3 → 2.0.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.
@@ -303,19 +303,89 @@ export function modelSupportsReasoning(model) {
303
303
  return capabilities.reasoning === true
304
304
  }
305
305
 
306
+ /**
307
+ * Order of provider reasoning levels, weakest to strongest.
308
+ *
309
+ * The scale is used only to pick the nearest exposed variant when a model
310
+ * does not offer the exact requested level.
311
+ */
312
+ const EFFORT_RANK = Object.freeze({
313
+ none: 0,
314
+ minimal: 1,
315
+ low: 2,
316
+ medium: 3,
317
+ high: 4,
318
+ xhigh: 5,
319
+ max: 6,
320
+ })
321
+
322
+ /**
323
+ * Choose the model variant key to apply for a requested level.
324
+ *
325
+ * The exact level wins when the model exposes it. Otherwise the nearest
326
+ * exposed level on the provider scale is chosen, and ties resolve to the
327
+ * higher level so ordinary work is never under-powered. `none` is
328
+ * exact-match only: mapping it to a reasoning level would enable reasoning
329
+ * the caller explicitly disabled. Keys whose value is not a plain object are
330
+ * ignored, so a malformed variant can never reach the request options.
331
+ *
332
+ * @param level - Requested level (any routing enum value)
333
+ * @param model - Model object from the hook input
334
+ * @returns The variant key to apply, or `undefined` when none applies
335
+ *
336
+ * @example
337
+ * selectEffortVariant("medium", { variants: { low: {}, high: {} } }) // "high"
338
+ */
339
+ export function selectEffortVariant(level, model) {
340
+ if (!isRoutingReasoning(level)) return undefined
341
+ const variants = extractModelVariants(model)
342
+ if (variants === undefined) return undefined
343
+
344
+ if (level === "none") {
345
+ return isPlainObject(variants.none) ? "none" : undefined
346
+ }
347
+
348
+ const candidates = Object.keys(variants).filter(
349
+ (key) =>
350
+ key !== "none" &&
351
+ Object.prototype.hasOwnProperty.call(EFFORT_RANK, key) &&
352
+ isPlainObject(variants[key])
353
+ )
354
+ if (candidates.includes(level)) return level
355
+
356
+ let best
357
+ for (const key of candidates) {
358
+ if (best === undefined) {
359
+ best = key
360
+ continue
361
+ }
362
+ const distance = Math.abs(EFFORT_RANK[key] - EFFORT_RANK[level])
363
+ const bestDistance = Math.abs(EFFORT_RANK[best] - EFFORT_RANK[level])
364
+ if (
365
+ distance < bestDistance ||
366
+ (distance === bestDistance && EFFORT_RANK[key] > EFFORT_RANK[best])
367
+ ) {
368
+ best = key
369
+ }
370
+ }
371
+ return best
372
+ }
373
+
306
374
  /**
307
375
  * Merge the model's variant options for a level into the request options.
308
376
  *
309
377
  * The runtime model carries a `variants` record whose entries are the exact
310
378
  * provider options for each level (for example `reasoningEffort`, or a nested
311
379
  * `reasoning.effort`). This function is the only place that mapping is
312
- * consumed, so the plugin never hardcodes a provider key. A matching variant
313
- * is applied even when the model's reasoning capability flag is absent or
314
- * false: the variant itself is the provider-known option set, and the host
315
- * applies inherited variants the same way. Only when no variants record
316
- * exists does the documented `reasoningEffort` passthrough require an
317
- * explicit reasoning capability. The function is pure: it returns a new
318
- * object when a change applies and the original object reference otherwise.
380
+ * consumed, so the plugin never hardcodes a provider key. The applied key is
381
+ * chosen by {@link selectEffortVariant}: exact match first, then the nearest
382
+ * exposed level. A matching variant applies even when the model's reasoning
383
+ * capability flag is absent or false, because the variant itself is the
384
+ * provider-known option set and the host applies inherited variants the same
385
+ * way. Only when no variants record exists does the documented
386
+ * `reasoningEffort` passthrough require an explicit reasoning capability. The
387
+ * function is pure: it returns a new object when a change applies and the
388
+ * original object reference otherwise.
319
389
  *
320
390
  * @param options - Current provider options
321
391
  * @param level - Candidate level from {@link resolveEffort}
@@ -327,16 +397,53 @@ export function applyEffort(options, level, model) {
327
397
 
328
398
  const variants = extractModelVariants(model)
329
399
  if (variants !== undefined) {
330
- if (!Object.prototype.hasOwnProperty.call(variants, level)) return options
331
- const variantOptions = variants[level]
332
- if (!isPlainObject(variantOptions)) return options
333
- return deepMergePlain(options, variantOptions)
400
+ const variantKey = selectEffortVariant(level, model)
401
+ if (variantKey === undefined) return options
402
+ return deepMergePlain(options, variants[variantKey])
334
403
  }
335
404
 
336
405
  if (!modelSupportsReasoning(model)) return options
337
406
  return { ...options, reasoningEffort: level }
338
407
  }
339
408
 
409
+ /**
410
+ * Normalize a host model's variant collection into a keyed record.
411
+ *
412
+ * OpenCode 2 exposes model variants as an array of entries shaped
413
+ * `{ id, settings }`, while {@link applyEffort} consumes a record keyed by
414
+ * variant id. This adapter bridges the two without changing the core logic.
415
+ * A record input is passed through unchanged, so v1-style callers and tests
416
+ * keep working. Malformed entries — non-objects, missing ids, non-object
417
+ * settings — are skipped; anything that is not an array or plain object yields
418
+ * an empty record.
419
+ *
420
+ * @param variants - Variant collection from the host (any shape)
421
+ * @returns A record mapping variant id to its settings object
422
+ *
423
+ * @example
424
+ * normalizeModelVariants([{ id: "high", settings: { reasoningEffort: "high" } }])
425
+ * // { high: { reasoningEffort: "high" } }
426
+ */
427
+ export function normalizeModelVariants(variants) {
428
+ if (isPlainObject(variants)) return { ...variants }
429
+ if (!Array.isArray(variants)) return {}
430
+
431
+ const record = {}
432
+ for (const entry of variants) {
433
+ if (!isPlainObject(entry)) continue
434
+ const id = entry.id
435
+ if (typeof id !== "string" || id.length === 0) continue
436
+ const settings = isPlainObject(entry.settings) ? entry.settings : {}
437
+ Object.defineProperty(record, id, {
438
+ value: settings,
439
+ writable: true,
440
+ enumerable: true,
441
+ configurable: true,
442
+ })
443
+ }
444
+ return record
445
+ }
446
+
340
447
  /**
341
448
  * Recursively merge plain objects into a new object.
342
449
  *
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "1.10.3",
3
+ "version": "2.0.0",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=18"
7
+ "node": ">=18",
8
+ "opencode": ">=2 <3"
8
9
  },
9
10
  "main": "./plugin/index.ts",
10
11
  "exports": {
11
- ".": "./plugin/index.ts"
12
+ ".": "./plugin/index.ts",
13
+ "./server": "./plugin/index.ts"
12
14
  },
13
15
  "bin": {
14
16
  "opencode-codeops": "bin/index.mjs"
@@ -62,11 +64,10 @@
62
64
  "bugs": {
63
65
  "url": "https://github.com/blendsdk/opencode-codeops/issues"
64
66
  },
65
- "peerDependencies": {
66
- "@opencode-ai/plugin": "^1.18.30"
67
+ "dependencies": {
68
+ "@opencode/plugin": "^2.0.24"
67
69
  },
68
70
  "devDependencies": {
69
- "@opencode-ai/plugin": "1.18.30",
70
71
  "@types/node": "^22.20.2",
71
72
  "typescript": "^5.0.0"
72
73
  }