@jmanuelcorral/openteam 0.16.3 → 0.17.1

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/cli.js CHANGED
@@ -19,7 +19,7 @@ import { randomBytes } from "node:crypto";
19
19
 
20
20
  // src/config/resolve.ts
21
21
  function resolveGraphConfig(config, source) {
22
- const configuredMode = config.graph?.mode ?? "shadow";
22
+ const configuredMode = config.graph?.mode ?? "active";
23
23
  const killSwitch = config.graph?.killSwitch ?? false;
24
24
  const effectiveMode = killSwitch ? "off" : configuredMode;
25
25
  return {
@@ -129,7 +129,8 @@ var AgentRoleProfileSchema = z.object({
129
129
  localFirst: z.boolean(),
130
130
  requiresLocalRuntime: z.boolean().optional(),
131
131
  localRuntimes: z.array(z.string().min(1)).optional(),
132
- localModels: z.record(z.string().min(1), z.string().min(1)).optional()
132
+ localModels: z.record(z.string().min(1), z.string().min(1)).optional(),
133
+ localModelClass: z.enum(["thinking", "coder"]).optional()
133
134
  }).strict();
134
135
  var TEAM_ROLES = {
135
136
  architect: {
@@ -140,7 +141,8 @@ var TEAM_ROLES = {
140
141
  minCodeQualityTier: 5,
141
142
  needsToolCallingByDefault: true,
142
143
  preferredFrontierModels: ["auto"],
143
- localFirst: false
144
+ localFirst: false,
145
+ localModelClass: "thinking"
144
146
  },
145
147
  integration: {
146
148
  roleID: "integration",
@@ -150,7 +152,8 @@ var TEAM_ROLES = {
150
152
  minCodeQualityTier: 4,
151
153
  needsToolCallingByDefault: true,
152
154
  preferredFrontierModels: ["auto"],
153
- localFirst: false
155
+ localFirst: false,
156
+ localModelClass: "coder"
154
157
  },
155
158
  "local-runtime": {
156
159
  roleID: "local-runtime",
@@ -160,7 +163,8 @@ var TEAM_ROLES = {
160
163
  minCodeQualityTier: 4,
161
164
  needsToolCallingByDefault: true,
162
165
  preferredFrontierModels: ["auto"],
163
- localFirst: true
166
+ localFirst: true,
167
+ localModelClass: "coder"
164
168
  },
165
169
  "routing-cost": {
166
170
  roleID: "routing-cost",
@@ -170,7 +174,8 @@ var TEAM_ROLES = {
170
174
  minCodeQualityTier: 4,
171
175
  needsToolCallingByDefault: false,
172
176
  preferredFrontierModels: ["auto"],
173
- localFirst: true
177
+ localFirst: true,
178
+ localModelClass: "thinking"
174
179
  },
175
180
  scribe: {
176
181
  roleID: "scribe",
@@ -203,7 +208,8 @@ var TEAM_ROLES = {
203
208
  needsToolCallingByDefault: false,
204
209
  preferredFrontierModels: [],
205
210
  localFirst: true,
206
- requiresLocalRuntime: true
211
+ requiresLocalRuntime: true,
212
+ localModelClass: "thinking"
207
213
  },
208
214
  tester: {
209
215
  roleID: "tester",
@@ -213,7 +219,8 @@ var TEAM_ROLES = {
213
219
  minCodeQualityTier: 3,
214
220
  needsToolCallingByDefault: true,
215
221
  preferredFrontierModels: ["auto"],
216
- localFirst: true
222
+ localFirst: true,
223
+ localModelClass: "coder"
217
224
  },
218
225
  reviewer: {
219
226
  roleID: "reviewer",
@@ -223,7 +230,8 @@ var TEAM_ROLES = {
223
230
  minCodeQualityTier: 5,
224
231
  needsToolCallingByDefault: false,
225
232
  preferredFrontierModels: ["auto"],
226
- localFirst: true
233
+ localFirst: true,
234
+ localModelClass: "coder"
227
235
  }
228
236
  };
229
237
  var LEGACY_TEAM_ROLE_ALIASES = {
@@ -326,11 +334,11 @@ var GraphEngineSchema = z2.enum(["builtin", "langgraph"]);
326
334
  var DEFAULT_GRAPH_JOURNAL_ROOT = ".opencode/openteam-local/graph/runs";
327
335
  var DEFAULT_GRAPH_WORKTREES_DIR = ".opencode/openteam-local/graph/worktrees";
328
336
  var GraphConfigSchema = z2.object({
329
- mode: GraphModeSchema.default("shadow"),
337
+ mode: GraphModeSchema.default("active"),
330
338
  engine: GraphEngineSchema.optional(),
331
339
  killSwitch: z2.boolean().default(false),
332
340
  journalRoot: z2.string().min(1).default(DEFAULT_GRAPH_JOURNAL_ROOT),
333
- operatorApproval: z2.boolean().default(false),
341
+ operatorApproval: z2.boolean().default(true),
334
342
  worktrees: z2.object({
335
343
  enabled: z2.boolean().default(false)
336
344
  }).strict().default({ enabled: false })
@@ -424,6 +432,9 @@ var OrchestratorRolesSchema = z2.record(z2.string().min(1), z2.unknown()).defaul
424
432
  if (parsed.data.localModels !== undefined) {
425
433
  profile.localModels = parsed.data.localModels;
426
434
  }
435
+ if (parsed.data.localModelClass !== undefined) {
436
+ profile.localModelClass = parsed.data.localModelClass;
437
+ }
427
438
  parsedRoles[roleID] = profile;
428
439
  }
429
440
  return parsedRoles;
@@ -450,7 +461,10 @@ var LocalRuntimeSchema = z2.object({
450
461
  discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
451
462
  defaultModel: ModelRefSchema,
452
463
  maxConcurrency: z2.number().int().positive().optional(),
453
- reservedInteractiveSlots: z2.number().int().nonnegative().optional()
464
+ modelConcurrency: z2.record(z2.string().min(1), z2.number().int().positive()).optional(),
465
+ reservedInteractiveSlots: z2.number().int().nonnegative().optional(),
466
+ thinkingModel: z2.string().min(1).optional(),
467
+ coderModel: z2.string().min(1).optional()
454
468
  }).strict().superRefine((runtime, ctx) => {
455
469
  const resolvedKind = runtime.kind ?? runtime.id;
456
470
  if (!LocalRuntimeKindSchema.safeParse(resolvedKind).success) {
@@ -576,10 +590,10 @@ var OpenTeamConfigObjectSchema = z2.object({
576
590
  storage: StorageConfigSchema,
577
591
  memory: MemoryConfigSchema.optional(),
578
592
  graph: GraphConfigSchema.default({
579
- mode: "shadow",
593
+ mode: "active",
580
594
  killSwitch: false,
581
595
  journalRoot: DEFAULT_GRAPH_JOURNAL_ROOT,
582
- operatorApproval: false,
596
+ operatorApproval: true,
583
597
  worktrees: { enabled: false }
584
598
  }),
585
599
  orchestrator: OrchestratorConfigSchema.optional()
@@ -669,6 +683,16 @@ function classifyHeuristic(task) {
669
683
  };
670
684
  }
671
685
 
686
+ // src/local/slotPool.ts
687
+ var POOL_SEPARATOR = "::";
688
+ function slotPoolKey(runtime, modelID) {
689
+ if (modelID === undefined) {
690
+ return runtime.id;
691
+ }
692
+ const declared = runtime.modelConcurrency?.[modelID];
693
+ return declared === undefined ? runtime.id : `${runtime.id}${POOL_SEPARATOR}${modelID}`;
694
+ }
695
+
672
696
  // src/capabilities/budget.ts
673
697
  var WARNING_UTILIZATION = 0.8;
674
698
  function projectedSpend(current, estimatedCostUSD) {
@@ -1604,9 +1628,19 @@ function configuredRuntime(runtimeId, config) {
1604
1628
  function runtimeIdOfModel(model, config) {
1605
1629
  return config.local.runtimes.find((runtime) => sameModel(runtime.defaultModel, model))?.id;
1606
1630
  }
1607
- function roleLocalModel(role, runtimeId, fallback) {
1631
+ function roleLocalModel(role, runtimeId, fallback, runtime) {
1608
1632
  const pinned = role.localModels?.[runtimeId];
1609
- return pinned === undefined ? fallback : { ...fallback, modelID: pinned };
1633
+ if (pinned !== undefined) {
1634
+ return { ...fallback, modelID: pinned };
1635
+ }
1636
+ const classModel = runtimeClassModel(role.localModelClass, runtime);
1637
+ return classModel === undefined ? fallback : { ...fallback, modelID: classModel };
1638
+ }
1639
+ function runtimeClassModel(modelClass, runtime) {
1640
+ if (modelClass === undefined || runtime === undefined) {
1641
+ return;
1642
+ }
1643
+ return modelClass === "thinking" ? runtime.thinkingModel : runtime.coderModel;
1610
1644
  }
1611
1645
  function selectDispatchRuntime(role, decision2, reachable, config) {
1612
1646
  if (decision2.routeKind !== "local") {
@@ -1622,7 +1656,7 @@ function selectDispatchRuntime(role, decision2, reachable, config) {
1622
1656
  if (runtime === undefined) {
1623
1657
  continue;
1624
1658
  }
1625
- const model = roleLocalModel(role, runtimeId, runtime.defaultModel);
1659
+ const model = roleLocalModel(role, runtimeId, runtime.defaultModel, runtime);
1626
1660
  return {
1627
1661
  runtimeId,
1628
1662
  decision: { ...decision2, selected: model }
@@ -1631,7 +1665,7 @@ function selectDispatchRuntime(role, decision2, reachable, config) {
1631
1665
  }
1632
1666
  const resolvedRuntimeId = runtimeIdOfModel(decision2.selected, config);
1633
1667
  if (resolvedRuntimeId !== undefined) {
1634
- const pinned = roleLocalModel(role, resolvedRuntimeId, decision2.selected);
1668
+ const pinned = roleLocalModel(role, resolvedRuntimeId, decision2.selected, configuredRuntime(resolvedRuntimeId, config));
1635
1669
  if (pinned !== decision2.selected) {
1636
1670
  return {
1637
1671
  runtimeId: resolvedRuntimeId,
@@ -2267,18 +2301,19 @@ async function runRoleTaskWithCorrelation(input, deps, correlation, batch) {
2267
2301
  if (runtimeId !== undefined && deps.runtimeLimiter !== undefined) {
2268
2302
  const limiter = deps.runtimeLimiter;
2269
2303
  const priority = priorityForRole(role);
2270
- release = limiter.tryAcquire(runtimeId, priority);
2304
+ const poolKey = slotPoolKey(configuredRuntime(runtimeId, input.config) ?? { id: runtimeId }, decision2.selected.modelID);
2305
+ release = limiter.tryAcquire(poolKey, priority);
2271
2306
  let waitedMs = 0;
2272
2307
  if (release === undefined) {
2273
- const acquisition = await limiter.acquire(runtimeId, priority);
2308
+ const acquisition = await limiter.acquire(poolKey, priority);
2274
2309
  release = acquisition.release;
2275
2310
  waitedMs = acquisition.waitedMs;
2276
2311
  }
2277
2312
  queueObservation = {
2278
2313
  runtimeId,
2279
2314
  waitedMs,
2280
- activeCount: limiter.activeCount(runtimeId),
2281
- queuedCount: limiter.queuedCount(runtimeId)
2315
+ activeCount: limiter.activeCount(poolKey),
2316
+ queuedCount: limiter.queuedCount(poolKey)
2282
2317
  };
2283
2318
  }
2284
2319
  try {
@@ -3079,7 +3114,10 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
3079
3114
  "discovery",
3080
3115
  "defaultModel",
3081
3116
  "maxConcurrency",
3082
- "reservedInteractiveSlots"
3117
+ "modelConcurrency",
3118
+ "reservedInteractiveSlots",
3119
+ "thinkingModel",
3120
+ "coderModel"
3083
3121
  ]
3084
3122
  ],
3085
3123
  ["local.runtimes[].defaultModel", MODEL_REF_KEYS],
@@ -9999,24 +10037,141 @@ function ensureFoundryV1BaseURL(endpoint) {
9999
10037
  }
10000
10038
 
10001
10039
  // src/local/lemonade.ts
10002
- var LEMONADE_DEFAULT_BASE_URL = "http://localhost:8000/api/v1";
10040
+ var LEMONADE_DEFAULT_BASE_URL = "http://localhost:13305/api/v1";
10041
+ var NON_TEXT_LABELS = new Set([
10042
+ "image",
10043
+ "tts",
10044
+ "transcription",
10045
+ "embedding",
10046
+ "reranking"
10047
+ ]);
10048
+ var PARALLEL_ARG = /(?:^|\s)(?:--parallel|-np)[\s=]+(\d+)(?!\d)/;
10003
10049
  function createLemonadeAdapter() {
10050
+ const listModels = async (options) => {
10051
+ const baseURL = normalizeBaseURL(options.baseURL ?? LEMONADE_DEFAULT_BASE_URL);
10052
+ const downloaded = await listLemonadeModels(baseURL, options.fetch);
10053
+ const loaded = await readLoadedModels(baseURL, options.fetch);
10054
+ return enrichWithLoadState(downloaded, loaded);
10055
+ };
10004
10056
  return {
10005
10057
  id: "lemonade",
10006
10058
  defaultBaseURL: LEMONADE_DEFAULT_BASE_URL,
10007
- listModels(options) {
10008
- return listOpenAICompatibleModels(normalizeBaseURL(options.baseURL ?? LEMONADE_DEFAULT_BASE_URL), options.fetch);
10009
- },
10010
- probe(options) {
10011
- return probeOpenAICompatibleRuntime({
10012
- id: "lemonade",
10013
- baseURL: options.baseURL ?? LEMONADE_DEFAULT_BASE_URL,
10014
- fetch: options.fetch,
10015
- probedAt: options.probedAt
10016
- });
10059
+ listModels,
10060
+ async probe(options) {
10061
+ const baseURL = normalizeBaseURL(options.baseURL ?? LEMONADE_DEFAULT_BASE_URL);
10062
+ try {
10063
+ return {
10064
+ id: "lemonade",
10065
+ baseURL,
10066
+ reachable: true,
10067
+ models: await listModels({ ...options, baseURL }),
10068
+ probedAt: options.probedAt
10069
+ };
10070
+ } catch (error) {
10071
+ return unavailableSnapshot({
10072
+ id: "lemonade",
10073
+ baseURL,
10074
+ probedAt: options.probedAt,
10075
+ error
10076
+ });
10077
+ }
10017
10078
  }
10018
10079
  };
10019
10080
  }
10081
+ async function listLemonadeModels(baseURL, fetch2) {
10082
+ const response = await fetch2(`${baseURL}/models`, {
10083
+ method: "GET",
10084
+ headers: { accept: "application/json" }
10085
+ });
10086
+ if (!response.ok) {
10087
+ throw new Error(`GET /models failed with HTTP ${response.status}`);
10088
+ }
10089
+ let payload;
10090
+ try {
10091
+ payload = await response.json();
10092
+ } catch (error) {
10093
+ throw new Error(`Malformed JSON from /models: ${errorMessage(error)}`);
10094
+ }
10095
+ const nonText = readNonTextModelIDs(payload);
10096
+ return parseOpenAIModels(payload).filter((model) => !nonText.has(model.modelID));
10097
+ }
10098
+ function readNonTextModelIDs(payload) {
10099
+ const ids = new Set;
10100
+ if (!isObject3(payload) || !Array.isArray(payload.data)) {
10101
+ return ids;
10102
+ }
10103
+ for (const entry of payload.data) {
10104
+ if (!isObject3(entry) || typeof entry.id !== "string") {
10105
+ continue;
10106
+ }
10107
+ if (isNonTextModel(entry)) {
10108
+ ids.add(entry.id);
10109
+ }
10110
+ }
10111
+ return ids;
10112
+ }
10113
+ function isNonTextModel(entry) {
10114
+ if (!Array.isArray(entry.labels)) {
10115
+ return false;
10116
+ }
10117
+ return entry.labels.some((label) => typeof label === "string" && NON_TEXT_LABELS.has(label));
10118
+ }
10119
+ async function readLoadedModels(baseURL, fetch2) {
10120
+ let payload;
10121
+ try {
10122
+ const response = await fetch2(`${baseURL}/health`, {
10123
+ method: "GET",
10124
+ headers: { accept: "application/json" }
10125
+ });
10126
+ if (!response.ok) {
10127
+ return;
10128
+ }
10129
+ payload = await response.json();
10130
+ } catch {
10131
+ return;
10132
+ }
10133
+ if (!isObject3(payload) || !Array.isArray(payload.all_models_loaded)) {
10134
+ return;
10135
+ }
10136
+ const loaded = new Map;
10137
+ for (const entry of payload.all_models_loaded) {
10138
+ if (!isObject3(entry) || typeof entry.model_name !== "string") {
10139
+ continue;
10140
+ }
10141
+ if (entry.loaded === false) {
10142
+ continue;
10143
+ }
10144
+ loaded.set(entry.model_name, { slots: readSlotCount(entry) });
10145
+ }
10146
+ return loaded;
10147
+ }
10148
+ function readSlotCount(entry) {
10149
+ const options = entry.recipe_options;
10150
+ if (!isObject3(options) || typeof options.llamacpp_args !== "string") {
10151
+ return;
10152
+ }
10153
+ const match = PARALLEL_ARG.exec(options.llamacpp_args);
10154
+ if (match === null) {
10155
+ return;
10156
+ }
10157
+ const parsed = Number.parseInt(match[1], 10);
10158
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
10159
+ }
10160
+ function enrichWithLoadState(models, loaded) {
10161
+ if (loaded === undefined) {
10162
+ return [...models];
10163
+ }
10164
+ return models.map((model) => {
10165
+ const hit = loaded.get(model.modelID);
10166
+ if (hit === undefined) {
10167
+ return { ...model, loaded: false };
10168
+ }
10169
+ return hit.slots === undefined ? { ...model, loaded: true } : { ...model, loaded: true, slots: hit.slots };
10170
+ });
10171
+ }
10172
+ function isObject3(value) {
10173
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10174
+ }
10020
10175
 
10021
10176
  // src/local/lmstudio.ts
10022
10177
  var LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
@@ -10245,6 +10400,9 @@ function createDetect(exec) {
10245
10400
  reachable: snapshot.reachable,
10246
10401
  models: snapshot.models.map((model) => model.modelID)
10247
10402
  };
10403
+ if (snapshot.models.some((model) => model.loaded !== undefined)) {
10404
+ detected.modelInfo = snapshot.models;
10405
+ }
10248
10406
  if (snapshot.error !== undefined) {
10249
10407
  detected.error = snapshot.error;
10250
10408
  }
@@ -10272,6 +10430,9 @@ function createProbeRuntime(exec) {
10272
10430
  reachable: snapshot?.reachable ?? false,
10273
10431
  models: snapshot?.models.map((model) => model.modelID) ?? []
10274
10432
  };
10433
+ if (snapshot?.models.some((model) => model.loaded !== undefined) === true) {
10434
+ detected.modelInfo = snapshot.models;
10435
+ }
10275
10436
  if (snapshot?.error !== undefined) {
10276
10437
  detected.error = snapshot.error;
10277
10438
  }
@@ -11001,6 +11162,22 @@ function renderConsoleStatus(console_) {
11001
11162
  }
11002
11163
 
11003
11164
  // src/commands/doctor.ts
11165
+ function runtimeClassLine(runtime) {
11166
+ if (runtime === undefined) {
11167
+ return;
11168
+ }
11169
+ const thinking = runtime.thinkingModel;
11170
+ const coder = runtime.coderModel;
11171
+ if (thinking === undefined && coder === undefined) {
11172
+ return;
11173
+ }
11174
+ const parts = [
11175
+ `thinking ${thinking ?? "(default)"}`,
11176
+ `coder ${coder ?? "(default)"}`,
11177
+ `default ${runtime.defaultModel.modelID}`
11178
+ ];
11179
+ return ` ↳ ${parts.join(" · ")}`;
11180
+ }
11004
11181
  function runtimeLine(snapshot, runtime) {
11005
11182
  const mark = snapshot.reachable ? "✓" : "✗";
11006
11183
  const detail = snapshot.reachable ? `${snapshot.models.length} model(s)` : snapshot.error ?? "unreachable";
@@ -11008,6 +11185,14 @@ function runtimeLine(snapshot, runtime) {
11008
11185
  const slots = declared === undefined ? ` · ${DEFAULT_LOCAL_MAX_CONCURRENCY} slot(s) (default)` : ` · ${declared} slot(s) (declared)`;
11009
11186
  return ` ${mark} ${snapshot.id.padEnd(14)} ${snapshot.baseURL || "(no baseURL)"} — ${detail}${slots}`;
11010
11187
  }
11188
+ function runtimeModelSlotsLine(runtime) {
11189
+ const entries = Object.entries(runtime?.modelConcurrency ?? {});
11190
+ if (entries.length === 0) {
11191
+ return;
11192
+ }
11193
+ const detail = entries.map(([modelID, slots]) => `${modelID} ${slots} slot(s)`).join(" · ");
11194
+ return ` ↳ per-model slots: ${detail}`;
11195
+ }
11011
11196
  function isNetworkRuntime(baseURL) {
11012
11197
  if (baseURL.length === 0) {
11013
11198
  return false;
@@ -11257,7 +11442,16 @@ function renderDoctor(input) {
11257
11442
  lines.push(" (no runtime enabled)");
11258
11443
  } else {
11259
11444
  for (const snapshot of input.snapshots) {
11260
- lines.push(runtimeLine(snapshot, enabledRuntimes.find((r) => r.id === snapshot.id)));
11445
+ const runtime = enabledRuntimes.find((r) => r.id === snapshot.id);
11446
+ lines.push(runtimeLine(snapshot, runtime));
11447
+ const classLine = runtimeClassLine(runtime);
11448
+ if (classLine !== undefined) {
11449
+ lines.push(classLine);
11450
+ }
11451
+ const modelSlots = runtimeModelSlotsLine(runtime);
11452
+ if (modelSlots !== undefined) {
11453
+ lines.push(modelSlots);
11454
+ }
11261
11455
  }
11262
11456
  }
11263
11457
  for (const snapshot of input.snapshots) {
@@ -11993,6 +12187,15 @@ function buildOpenTeamConfig(answers) {
11993
12187
  },
11994
12188
  maxConcurrency: choice.maxConcurrency ?? DEFAULT_LOCAL_MAX_CONCURRENCY
11995
12189
  };
12190
+ if (choice.modelConcurrency !== undefined && Object.keys(choice.modelConcurrency).length > 0) {
12191
+ runtime.modelConcurrency = { ...choice.modelConcurrency };
12192
+ }
12193
+ if (choice.thinkingModelID !== undefined && choice.thinkingModelID !== choice.defaultModelID) {
12194
+ runtime.thinkingModel = choice.thinkingModelID;
12195
+ }
12196
+ if (choice.coderModelID !== undefined && choice.coderModelID !== choice.defaultModelID) {
12197
+ runtime.coderModel = choice.coderModelID;
12198
+ }
11996
12199
  if (choice.kind !== undefined && choice.kind !== choice.id) {
11997
12200
  runtime.kind = choice.kind;
11998
12201
  }
@@ -12023,7 +12226,7 @@ function buildOpenTeamConfig(answers) {
12023
12226
  },
12024
12227
  local: { runtimes },
12025
12228
  privacyMode: answers.privacyMode,
12026
- graph: { mode: "shadow" }
12229
+ graph: { mode: "active" }
12027
12230
  });
12028
12231
  }
12029
12232
  function buildOpencodeConfig(answers) {
@@ -12183,7 +12386,40 @@ async function chooseFrontier(prompt, profiles) {
12183
12386
  }
12184
12387
  return parseFrontierRef(selected);
12185
12388
  }
12186
- async function chooseRuntimeModel(prompt, models, meta) {
12389
+ function orderModelChoices(models, modelInfo) {
12390
+ const byID = new Map((modelInfo ?? []).map((model) => [model.modelID, model]));
12391
+ const anyLoadState = (modelInfo ?? []).some((model) => model.loaded !== undefined);
12392
+ const decorated = models.map((modelID, index) => {
12393
+ const info = byID.get(modelID);
12394
+ return {
12395
+ modelID,
12396
+ index,
12397
+ loaded: info?.loaded === true,
12398
+ slots: info?.slots
12399
+ };
12400
+ });
12401
+ if (!anyLoadState) {
12402
+ return decorated.map((entry) => ({
12403
+ value: entry.modelID,
12404
+ label: entry.modelID
12405
+ }));
12406
+ }
12407
+ return [...decorated].sort((a, b) => a.loaded === b.loaded ? a.index - b.index : a.loaded ? -1 : 1).map((entry) => ({
12408
+ value: entry.modelID,
12409
+ label: `${entry.modelID} ${modelStateSuffix(entry.loaded, entry.slots)}`
12410
+ }));
12411
+ }
12412
+ function modelStateSuffix(loaded, slots) {
12413
+ if (!loaded) {
12414
+ return "(not loaded)";
12415
+ }
12416
+ return slots === undefined ? "(loaded)" : `(loaded · ${slots} slot${slots === 1 ? "" : "s"})`;
12417
+ }
12418
+ function preferredModelID(models, modelInfo) {
12419
+ const loaded = (modelInfo ?? []).find((model) => model.loaded === true && models.includes(model.modelID));
12420
+ return loaded?.modelID ?? models[0];
12421
+ }
12422
+ async function chooseRuntimeModel(prompt, models, meta, modelInfo) {
12187
12423
  if (models.length === 0) {
12188
12424
  return prompt.text({
12189
12425
  message: `Default model for ${meta.label} (no models detected)`,
@@ -12191,13 +12427,10 @@ async function chooseRuntimeModel(prompt, models, meta) {
12191
12427
  initial: meta.fallbackModelID
12192
12428
  });
12193
12429
  }
12194
- const first = models[0];
12430
+ const first = preferredModelID(models, modelInfo);
12195
12431
  const opts = {
12196
12432
  message: `Default local model for ${meta.label}`,
12197
- choices: models.map((modelID) => ({
12198
- value: modelID,
12199
- label: modelID
12200
- }))
12433
+ choices: orderModelChoices(models, modelInfo)
12201
12434
  };
12202
12435
  if (first !== undefined) {
12203
12436
  opts.initial = first;
@@ -12233,9 +12466,13 @@ function normalizeRemoteBaseURL(raw, defaultBaseURL) {
12233
12466
  }
12234
12467
  return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
12235
12468
  }
12469
+ function connectionOf(info) {
12470
+ const models = info?.models ?? [];
12471
+ return info?.modelInfo === undefined ? { models } : { models, modelInfo: info.modelInfo };
12472
+ }
12236
12473
  async function resolveRuntimeConnection(deps, meta, info) {
12237
12474
  if (meta.defaultBaseURL === undefined) {
12238
- return { models: info?.models ?? [] };
12475
+ return connectionOf(info);
12239
12476
  }
12240
12477
  const location = await deps.prompt.select({
12241
12478
  message: `Where does ${meta.label} run?`,
@@ -12254,40 +12491,88 @@ async function resolveRuntimeConnection(deps, meta, info) {
12254
12491
  });
12255
12492
  if (location === RUNTIME_LOCATION_LOCAL) {
12256
12493
  const baseURL2 = info?.baseURL !== undefined && info.baseURL.length > 0 ? info.baseURL : meta.defaultBaseURL;
12257
- return { baseURL: baseURL2, models: info?.models ?? [] };
12494
+ return { baseURL: baseURL2, ...connectionOf(info) };
12258
12495
  }
12259
12496
  const entered = await deps.prompt.text({
12260
12497
  message: `${meta.label} URL on the network (OpenAI-compatible endpoint)`,
12261
12498
  placeholder: remoteBaseURLExample(meta)
12262
12499
  });
12263
12500
  const baseURL = normalizeRemoteBaseURL(entered, meta.defaultBaseURL);
12264
- let models = [];
12501
+ let probedRuntime;
12265
12502
  if (deps.probeRuntime !== undefined) {
12266
12503
  try {
12267
- const probed = await deps.probeRuntime(meta.id, baseURL);
12268
- models = probed.models;
12504
+ probedRuntime = await deps.probeRuntime(meta.id, baseURL);
12269
12505
  } catch {}
12270
12506
  }
12271
- return { baseURL, models };
12507
+ return { baseURL, ...connectionOf(probedRuntime) };
12508
+ }
12509
+ async function chooseClassModel(prompt, models, meta, defaultModelID, modelClass, modelInfo) {
12510
+ const label = modelClass === "thinking" ? `Thinking model for ${meta.label} (design, analysis, review reasoning)` : `Coder model for ${meta.label} (writing and editing code)`;
12511
+ if (models.length === 0) {
12512
+ const raw = await prompt.text({
12513
+ message: `${label} — enter to reuse the default`,
12514
+ placeholder: defaultModelID,
12515
+ initial: defaultModelID
12516
+ });
12517
+ return raw.trim().length > 0 ? raw.trim() : defaultModelID;
12518
+ }
12519
+ return prompt.select({
12520
+ message: label,
12521
+ choices: orderModelChoices(models, modelInfo).map((choice) => ({
12522
+ value: choice.value,
12523
+ label: choice.value === defaultModelID ? `${choice.label} — default` : choice.label
12524
+ })),
12525
+ initial: defaultModelID
12526
+ });
12272
12527
  }
12273
12528
  async function resolveEnabledRuntime(deps, meta, info) {
12274
12529
  const connection = await resolveRuntimeConnection(deps, meta, info);
12275
- const defaultModelID = await chooseRuntimeModel(deps.prompt, connection.models, meta);
12530
+ const defaultModelID = await chooseRuntimeModel(deps.prompt, connection.models, meta, connection.modelInfo);
12531
+ const thinkingModelID = await chooseClassModel(deps.prompt, connection.models, meta, defaultModelID, "thinking", connection.modelInfo);
12532
+ const coderModelID = await chooseClassModel(deps.prompt, connection.models, meta, defaultModelID, "coder", connection.modelInfo);
12276
12533
  const choice = {
12277
12534
  id: meta.id,
12278
12535
  enabled: true,
12279
12536
  defaultModelID,
12537
+ thinkingModelID,
12538
+ coderModelID,
12280
12539
  models: connection.models
12281
12540
  };
12282
12541
  if (connection.baseURL !== undefined) {
12283
12542
  choice.baseURL = connection.baseURL;
12284
12543
  }
12544
+ const modelConcurrency = reportedModelSlots(connection.modelInfo, [
12545
+ defaultModelID,
12546
+ thinkingModelID,
12547
+ coderModelID
12548
+ ]);
12549
+ if (modelConcurrency !== undefined) {
12550
+ choice.modelConcurrency = modelConcurrency;
12551
+ }
12285
12552
  return choice;
12286
12553
  }
12554
+ function reportedModelSlots(modelInfo, usedModelIDs) {
12555
+ if (modelInfo === undefined) {
12556
+ return;
12557
+ }
12558
+ const used = new Set(usedModelIDs);
12559
+ const slots = {};
12560
+ for (const model of modelInfo) {
12561
+ if (model.slots !== undefined && used.has(model.modelID)) {
12562
+ slots[model.modelID] = model.slots;
12563
+ }
12564
+ }
12565
+ return Object.keys(slots).length > 0 ? slots : undefined;
12566
+ }
12287
12567
  function instanceSuffix(index) {
12288
12568
  return index < 26 ? String.fromCharCode(97 + index) : String(index + 1);
12289
12569
  }
12290
- async function promptSlotCount(prompt, meta) {
12570
+ async function promptSlotCount(prompt, meta, reported) {
12571
+ const detected = reportedSlotSummary(reported);
12572
+ if (detected !== undefined) {
12573
+ prompt.note(`${meta.label} reported its slots per model: ${detected.summary}. ` + "Slots belong to the loaded model, not the server, so openteam queues " + "each model against its own capacity instead of one shared number.", "Detected concurrency");
12574
+ return detected.max;
12575
+ }
12291
12576
  const raw = await prompt.text({
12292
12577
  message: `How many concurrent generation slots does ${meta.label} have? (match its real capacity; leaving one free keeps interactive work responsive)`,
12293
12578
  placeholder: String(DEFAULT_LOCAL_MAX_CONCURRENCY),
@@ -12296,6 +12581,16 @@ async function promptSlotCount(prompt, meta) {
12296
12581
  const parsed = Number.parseInt(raw.trim(), 10);
12297
12582
  return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LOCAL_MAX_CONCURRENCY;
12298
12583
  }
12584
+ function reportedSlotSummary(reported) {
12585
+ const entries = Object.entries(reported ?? {});
12586
+ if (entries.length === 0) {
12587
+ return;
12588
+ }
12589
+ return {
12590
+ summary: entries.map(([modelID, slots]) => `${modelID} = ${slots}`).join(", "),
12591
+ max: Math.max(...entries.map(([, slots]) => slots))
12592
+ };
12593
+ }
12299
12594
  async function promptInstanceId(prompt, meta, defaultId, usedIds) {
12300
12595
  for (;; ) {
12301
12596
  const raw = await prompt.text({
@@ -12317,7 +12612,7 @@ async function resolveRuntimeInstances(deps, meta, info, usedIds) {
12317
12612
  });
12318
12613
  if (!fleet) {
12319
12614
  const choice = await resolveEnabledRuntime(deps, meta, info);
12320
- choice.maxConcurrency = await promptSlotCount(deps.prompt, meta);
12615
+ choice.maxConcurrency = await promptSlotCount(deps.prompt, meta, choice.modelConcurrency);
12321
12616
  usedIds.add(choice.id);
12322
12617
  return [choice];
12323
12618
  }
@@ -12328,7 +12623,7 @@ async function resolveRuntimeInstances(deps, meta, info, usedIds) {
12328
12623
  const choice = await resolveEnabledRuntime(deps, meta, info);
12329
12624
  choice.id = id;
12330
12625
  choice.kind = meta.id;
12331
- choice.maxConcurrency = await promptSlotCount(deps.prompt, meta);
12626
+ choice.maxConcurrency = await promptSlotCount(deps.prompt, meta, choice.modelConcurrency);
12332
12627
  usedIds.add(id);
12333
12628
  instances.push(choice);
12334
12629
  const another = await deps.prompt.confirm({
@@ -16164,7 +16459,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
16164
16459
  // package.json
16165
16460
  var package_default = {
16166
16461
  name: "@jmanuelcorral/openteam",
16167
- version: "0.16.3",
16462
+ version: "0.17.1",
16168
16463
  packageManager: "bun@1.3.14",
16169
16464
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
16170
16465
  license: "MIT",
@@ -16320,7 +16615,7 @@ async function buildGateInput(config, storage, opencodeVersion, warn, packagedRe
16320
16615
  } catch {}
16321
16616
  const soak = await readAndParseCertificate(storage, SOAK_CERT_PATH, (raw) => parseSoakCertificate(raw, DEFAULT_SOAK_POLICY, soakLedgerDigest), (cert) => cert.digest);
16322
16617
  const migration = "complete";
16323
- const operatorApproval = config.graph?.operatorApproval ?? false;
16618
+ const operatorApproval = config.graph?.operatorApproval ?? true;
16324
16619
  return { shadow, release, soak, migration, operatorApproval };
16325
16620
  }
16326
16621
  var OPENCODE_VERSION_UNRESOLVED_REASON = "the opencode version is not determinable outside a running opencode session, " + "so the cutover gate cannot be verified here — the running plugin logs the " + "authoritative gate result and warns loudly on a real denial";