@jmanuelcorral/openteam 0.17.0 → 0.17.2

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.
@@ -36,13 +36,18 @@
36
36
  {
37
37
  "id": "lemonade",
38
38
  "enabled": false,
39
- "baseURL": "http://localhost:8000/api/v1",
39
+ "baseURL": "http://localhost:13305/api/v1",
40
40
  "defaultModel": {
41
41
  "providerID": "lemonade",
42
42
  "modelID": "Qwen3-Coder-30B-A3B-Instruct-GGUF"
43
43
  },
44
44
  "thinkingModel": "Qwen3-30B-A3B-Thinking-GGUF",
45
- "coderModel": "Qwen3-Coder-30B-A3B-Instruct-GGUF"
45
+ "coderModel": "Qwen3-Coder-30B-A3B-Instruct-GGUF",
46
+ "maxConcurrency": 3,
47
+ "modelConcurrency": {
48
+ "Qwen3-Coder-30B-A3B-Instruct-GGUF": 3,
49
+ "Qwen3-30B-A3B-Thinking-GGUF": 1
50
+ }
46
51
  },
47
52
  {
48
53
  "id": "foundry-local",
package/README.es.md CHANGED
@@ -52,7 +52,7 @@ El router mantiene una garantía de privacidad: con `privacyMode: "forceLocalOnS
52
52
  - Runtimes locales opcionales:
53
53
  - Ollama en `http://localhost:11434/v1`.
54
54
  - LM Studio en `http://localhost:1234/v1`.
55
- - Lemonade Server en `http://localhost:8000/api/v1`.
55
+ - Lemonade Server en `http://localhost:13305/api/v1`.
56
56
  - Foundry Local con puerto dinámico descubierto por CLI en Windows.
57
57
 
58
58
  ## Instalación
@@ -373,6 +373,7 @@ ejecutas desde un subdirectorio verás "provider not configured" y debes hacer
373
373
  | `local.runtimes[].discovery` | `"cli" \| "sdk" \| "manual"` opcional | sin default por item | Discovery de Foundry Local; el registry usa CLI si está configurado y no es `manual`. |
374
374
  | `local.runtimes[].defaultModel` | `ModelRef` | requerido | Modelo por defecto de ese runtime. |
375
375
  | `local.runtimes[].maxConcurrency` | entero positivo opcional | `4` | Límite de generaciones concurrentes de esta instancia (su número real de slots). Se aplica por instancia. |
376
+ | `local.runtimes[].modelConcurrency` | `Record<modelID, entero positivo>` opcional | ninguno | Slots por modelo, porque los slots pertenecen al modelo cargado y no al servidor: un mismo host Lemonade puede servir un modelo con `--parallel 3` y otro con `--parallel 1` a la vez. Cada modelo declarado obtiene su propia cola; el resto comparte el pool de `maxConcurrency` de la instancia. `openteam setup` lo rellena solo cuando el runtime informa de sus slots. |
376
377
  | `budgets.sessionUSD` | `number` positivo opcional | sin límite | Presupuesto frontier por sesión. |
377
378
  | `budgets.monthlyUSD` | `number` positivo opcional | sin límite | Presupuesto frontier mensual. |
378
379
  | `budgets.frontierTokensPerSession` | `number` entero positivo opcional | sin límite | Límite de tokens frontier por sesión. |
@@ -729,7 +730,7 @@ Invoke-RestMethod -Uri "http://localhost:1234/v1/models" -Method Get
729
730
  ### Lemonade Server
730
731
 
731
732
  ```powershell
732
- Invoke-RestMethod -Uri "http://localhost:8000/api/v1/models" -Method Get
733
+ Invoke-RestMethod -Uri "http://localhost:13305/api/v1/models" -Method Get
733
734
  ```
734
735
 
735
736
  ### Foundry Local
@@ -824,7 +825,7 @@ Consulta [docs/compatibility.md](docs/compatibility.md). Resumen verificado el `
824
825
 
825
826
  | Superficie | Versión / contrato |
826
827
  | --- | --- |
827
- | openteam | `0.17.0`; versión actual del paquete |
828
+ | openteam | `0.17.2`; versión actual del paquete |
828
829
  | `@opencode-ai/plugin` | `1.18.19` |
829
830
  | `@opencode-ai/sdk` | `1.18.19` |
830
831
  | Hook de routing | `chat.message`; no `chat.params` para cambiar modelo |
package/README.md CHANGED
@@ -61,7 +61,7 @@ The router keeps a privacy guarantee: with `privacyMode: "forceLocalOnSensitive"
61
61
  - Optional local runtimes:
62
62
  - Ollama at `http://localhost:11434/v1`.
63
63
  - LM Studio at `http://localhost:1234/v1`.
64
- - Lemonade Server at `http://localhost:8000/api/v1`.
64
+ - Lemonade Server at `http://localhost:13305/api/v1`.
65
65
  - Foundry Local with a dynamic port discovered via CLI on Windows.
66
66
 
67
67
  > Local runtimes are **optional**. You can run openteam **frontier-only**, or point it at an Ollama / LM Studio / Lemonade Server running on **another machine on your LAN**.
@@ -325,6 +325,7 @@ configured" and should `cd` to the project root first.
325
325
  | `local.runtimes[].discovery` | `"cli" \| "sdk" \| "manual"` optional | none per item | Foundry Local discovery; the registry uses CLI when configured and not `manual`. |
326
326
  | `local.runtimes[].defaultModel` | `ModelRef` | required | Default model for that runtime. |
327
327
  | `local.runtimes[].maxConcurrency` | positive integer, optional | `4` | Concurrent-generation cap for this instance (its real slot count). Applied per instance. |
328
+ | `local.runtimes[].modelConcurrency` | `Record<modelID, positive integer>`, optional | none | Per-model slot counts, because slots belong to the loaded model rather than to the server: a single Lemonade host can serve one model at `--parallel 3` and another at `--parallel 1` simultaneously. Each declared model gets its own queue; every other model shares the instance-wide `maxConcurrency` pool. Populated automatically by `openteam setup` when the runtime reports its slots. |
328
329
  | `budgets.sessionUSD` | positive number, optional | unlimited | Frontier budget per session. |
329
330
  | `budgets.monthlyUSD` | positive number, optional | unlimited | Monthly frontier budget. |
330
331
  | `budgets.frontierTokensPerSession` | positive integer, optional | unlimited | Frontier token limit per session. |
@@ -622,7 +623,7 @@ Invoke-RestMethod -Uri "http://localhost:1234/v1/models" -Method Get
622
623
  ### Lemonade Server
623
624
 
624
625
  ```powershell
625
- Invoke-RestMethod -Uri "http://localhost:8000/api/v1/models" -Method Get
626
+ Invoke-RestMethod -Uri "http://localhost:13305/api/v1/models" -Method Get
626
627
  ```
627
628
 
628
629
  ### Foundry Local
@@ -713,7 +714,7 @@ See [docs/compatibility.md](docs/compatibility.md). Verified summary as of `2026
713
714
 
714
715
  | Surface | Version / contract |
715
716
  | --- | --- |
716
- | openteam | `0.17.0`; current package version |
717
+ | openteam | `0.17.2`; current package version |
717
718
  | `@opencode-ai/plugin` | `1.18.19` |
718
719
  | `@opencode-ai/sdk` | `1.18.19` |
719
720
  | Routing hook | `chat.message`; not `chat.params` for model changes |
@@ -34,12 +34,12 @@
34
34
  "idempotent": true
35
35
  },
36
36
  "performance": {
37
- "p95Millis": 0.4712040000000002,
37
+ "p95Millis": 0.462524999999971,
38
38
  "budgetMillis": 50,
39
39
  "samples": 100
40
40
  }
41
41
  },
42
42
  "verdict": "pass",
43
43
  "failedGates": [],
44
- "digest": "c2bae4d5fdfb94e26393cce05e69eacb8829647b091f82d64ce7c6d1a956c891"
44
+ "digest": "1f51bbf60529fba584b0d050615a88ae6456b85bd19b91aca60c80b650161333"
45
45
  }
@@ -20,7 +20,7 @@
20
20
  "rawFindings": 0
21
21
  },
22
22
  "overhead": {
23
- "p95Millis": 0.34261300000000006,
23
+ "p95Millis": 0.35418200000000866,
24
24
  "budgetMillis": 25,
25
25
  "samples": 1000
26
26
  },
@@ -35,5 +35,5 @@
35
35
  },
36
36
  "verdict": "pass",
37
37
  "failedGates": [],
38
- "digest": "2ceb71a2556e57ea51007d88b1e94bf0081ae975eb756b1fad05641bb4515332"
38
+ "digest": "8c2d475944be4285bdb807ad34543e43291724d3e2f695febf20b0704b255b42"
39
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"setupAdapters.d.ts","sourceRoot":"","sources":["../../src/cli/setupAdapters.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EAEf,QAAQ,EAET,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,KAAK,QAAQ,EAEd,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAyBrD,qDAAqD;AACrD,eAAO,MAAM,aAAa,EAAE,QA8E3B,CAAC;AA6BF;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,OAAO,CACzD,sBAAsB,EAAE,CACzB,CAcA;AACD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAuB7E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,GACb,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CA2BnE"}
1
+ {"version":3,"file":"setupAdapters.d.ts","sourceRoot":"","sources":["../../src/cli/setupAdapters.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EAEf,QAAQ,EAET,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,KAAK,QAAQ,EAEd,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAyBrD,qDAAqD;AACrD,eAAO,MAAM,aAAa,EAAE,QA8E3B,CAAC;AA6BF;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,OAAO,CACzD,sBAAsB,EAAE,CACzB,CAcA;AACD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CA6B7E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,GACb,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CA8BnE"}
package/dist/cli.js CHANGED
@@ -461,6 +461,7 @@ var LocalRuntimeSchema = z2.object({
461
461
  discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
462
462
  defaultModel: ModelRefSchema,
463
463
  maxConcurrency: z2.number().int().positive().optional(),
464
+ modelConcurrency: z2.record(z2.string().min(1), z2.number().int().positive()).optional(),
464
465
  reservedInteractiveSlots: z2.number().int().nonnegative().optional(),
465
466
  thinkingModel: z2.string().min(1).optional(),
466
467
  coderModel: z2.string().min(1).optional()
@@ -682,6 +683,16 @@ function classifyHeuristic(task) {
682
683
  };
683
684
  }
684
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
+
685
696
  // src/capabilities/budget.ts
686
697
  var WARNING_UTILIZATION = 0.8;
687
698
  function projectedSpend(current, estimatedCostUSD) {
@@ -965,9 +976,6 @@ function chooseModel(input) {
965
976
  noLocalRationale: "forced-local-unavailable"
966
977
  }));
967
978
  }
968
- if (override === "alwaysFrontier") {
969
- return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["explicit-always-frontier"], []));
970
- }
971
979
  if (input.config.router.localOnly) {
972
980
  if (local === null) {
973
981
  return finalizeDecision(input, tier, frontier, budgetAction, blockedDecision(frontier.model, ["router-local-only", "forced-local-none-configured"], "forced-local-none-configured"));
@@ -978,6 +986,9 @@ function chooseModel(input) {
978
986
  noLocalRationale: "forced-local-unavailable"
979
987
  }));
980
988
  }
989
+ if (override === "alwaysFrontier") {
990
+ return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["explicit-always-frontier"], []));
991
+ }
981
992
  if (input.config.router.frontierOnly) {
982
993
  return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["router-frontier-only"], []));
983
994
  }
@@ -1969,8 +1980,8 @@ function roleLocalReachable(role, batch) {
1969
1980
  function permissionsFor(roleID) {
1970
1981
  return ROLE_PERMISSIONS[roleID] ?? [];
1971
1982
  }
1972
- function roleRequiresFrontier(role, task) {
1973
- return task.explicitOverride === undefined && role.localFirst === false && role.defaultTier === "hard" && role.preferredFrontierModels.length > 0;
1983
+ function roleRequiresFrontier(role, task, config) {
1984
+ return task.explicitOverride === undefined && config.router.localOnly !== true && role.localFirst === false && role.defaultTier === "hard" && role.preferredFrontierModels.length > 0;
1974
1985
  }
1975
1986
  function roleAdjustedTask(role, input, requirement) {
1976
1987
  const task = {
@@ -1980,11 +1991,19 @@ function roleAdjustedTask(role, input, requirement) {
1980
1991
  if (requirement.needsTools) {
1981
1992
  task.requiresTools = true;
1982
1993
  }
1983
- if (roleRequiresFrontier(role, task)) {
1994
+ if (roleRequiresFrontier(role, task, input.config)) {
1984
1995
  task.explicitOverride = "alwaysFrontier";
1985
1996
  }
1986
1997
  return task;
1987
1998
  }
1999
+ function roleAdjustedTaskForRole(input) {
2000
+ const role = roleFor(input);
2001
+ const tier = elevateTierForPermissions(classifyHeuristic({
2002
+ ...input.task,
2003
+ prompt: input.task.prompt ?? input.prompt
2004
+ }).tier, permissionsFor(role.roleID));
2005
+ return roleAdjustedTask(role, input, roleToRequirement(role, input.task, tier));
2006
+ }
1988
2007
  function satisfiesRoleRequirement(requirement, profile) {
1989
2008
  return profile.kind === "frontier" && profile.availability !== "unavailable" && profile.contextWindow >= requirement.minContextWindow && profile.reasoningTier >= requirement.minReasoningTier && profile.codeQualityTier >= requirement.minCodeQualityTier && (!requirement.needsTools || profile.supportsToolCalling) && (!requirement.needsVision || profile.supportsVision);
1990
2009
  }
@@ -2002,7 +2021,7 @@ function routingInput(input, role) {
2002
2021
  const permissionTier = elevateTierForPermissions(classifiedTier, permissionsFor(role.roleID));
2003
2022
  const tier = permissionTier;
2004
2023
  const requirement = roleToRequirement(role, input.task, tier);
2005
- const task = roleAdjustedTask(role, input, requirement);
2024
+ const task = roleAdjustedTaskForRole(input);
2006
2025
  const baseInput = {
2007
2026
  config: input.config,
2008
2027
  task,
@@ -2290,18 +2309,19 @@ async function runRoleTaskWithCorrelation(input, deps, correlation, batch) {
2290
2309
  if (runtimeId !== undefined && deps.runtimeLimiter !== undefined) {
2291
2310
  const limiter = deps.runtimeLimiter;
2292
2311
  const priority = priorityForRole(role);
2293
- release = limiter.tryAcquire(runtimeId, priority);
2312
+ const poolKey = slotPoolKey(configuredRuntime(runtimeId, input.config) ?? { id: runtimeId }, decision2.selected.modelID);
2313
+ release = limiter.tryAcquire(poolKey, priority);
2294
2314
  let waitedMs = 0;
2295
2315
  if (release === undefined) {
2296
- const acquisition = await limiter.acquire(runtimeId, priority);
2316
+ const acquisition = await limiter.acquire(poolKey, priority);
2297
2317
  release = acquisition.release;
2298
2318
  waitedMs = acquisition.waitedMs;
2299
2319
  }
2300
2320
  queueObservation = {
2301
2321
  runtimeId,
2302
2322
  waitedMs,
2303
- activeCount: limiter.activeCount(runtimeId),
2304
- queuedCount: limiter.queuedCount(runtimeId)
2323
+ activeCount: limiter.activeCount(poolKey),
2324
+ queuedCount: limiter.queuedCount(poolKey)
2305
2325
  };
2306
2326
  }
2307
2327
  try {
@@ -3102,6 +3122,7 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
3102
3122
  "discovery",
3103
3123
  "defaultModel",
3104
3124
  "maxConcurrency",
3125
+ "modelConcurrency",
3105
3126
  "reservedInteractiveSlots",
3106
3127
  "thinkingModel",
3107
3128
  "coderModel"
@@ -10024,24 +10045,141 @@ function ensureFoundryV1BaseURL(endpoint) {
10024
10045
  }
10025
10046
 
10026
10047
  // src/local/lemonade.ts
10027
- var LEMONADE_DEFAULT_BASE_URL = "http://localhost:8000/api/v1";
10048
+ var LEMONADE_DEFAULT_BASE_URL = "http://localhost:13305/api/v1";
10049
+ var NON_TEXT_LABELS = new Set([
10050
+ "image",
10051
+ "tts",
10052
+ "transcription",
10053
+ "embedding",
10054
+ "reranking"
10055
+ ]);
10056
+ var PARALLEL_ARG = /(?:^|\s)(?:--parallel|-np)[\s=]+(\d+)(?!\d)/;
10028
10057
  function createLemonadeAdapter() {
10058
+ const listModels = async (options) => {
10059
+ const baseURL = normalizeBaseURL(options.baseURL ?? LEMONADE_DEFAULT_BASE_URL);
10060
+ const downloaded = await listLemonadeModels(baseURL, options.fetch);
10061
+ const loaded = await readLoadedModels(baseURL, options.fetch);
10062
+ return enrichWithLoadState(downloaded, loaded);
10063
+ };
10029
10064
  return {
10030
10065
  id: "lemonade",
10031
10066
  defaultBaseURL: LEMONADE_DEFAULT_BASE_URL,
10032
- listModels(options) {
10033
- return listOpenAICompatibleModels(normalizeBaseURL(options.baseURL ?? LEMONADE_DEFAULT_BASE_URL), options.fetch);
10034
- },
10035
- probe(options) {
10036
- return probeOpenAICompatibleRuntime({
10037
- id: "lemonade",
10038
- baseURL: options.baseURL ?? LEMONADE_DEFAULT_BASE_URL,
10039
- fetch: options.fetch,
10040
- probedAt: options.probedAt
10041
- });
10067
+ listModels,
10068
+ async probe(options) {
10069
+ const baseURL = normalizeBaseURL(options.baseURL ?? LEMONADE_DEFAULT_BASE_URL);
10070
+ try {
10071
+ return {
10072
+ id: "lemonade",
10073
+ baseURL,
10074
+ reachable: true,
10075
+ models: await listModels({ ...options, baseURL }),
10076
+ probedAt: options.probedAt
10077
+ };
10078
+ } catch (error) {
10079
+ return unavailableSnapshot({
10080
+ id: "lemonade",
10081
+ baseURL,
10082
+ probedAt: options.probedAt,
10083
+ error
10084
+ });
10085
+ }
10042
10086
  }
10043
10087
  };
10044
10088
  }
10089
+ async function listLemonadeModels(baseURL, fetch2) {
10090
+ const response = await fetch2(`${baseURL}/models`, {
10091
+ method: "GET",
10092
+ headers: { accept: "application/json" }
10093
+ });
10094
+ if (!response.ok) {
10095
+ throw new Error(`GET /models failed with HTTP ${response.status}`);
10096
+ }
10097
+ let payload;
10098
+ try {
10099
+ payload = await response.json();
10100
+ } catch (error) {
10101
+ throw new Error(`Malformed JSON from /models: ${errorMessage(error)}`);
10102
+ }
10103
+ const nonText = readNonTextModelIDs(payload);
10104
+ return parseOpenAIModels(payload).filter((model) => !nonText.has(model.modelID));
10105
+ }
10106
+ function readNonTextModelIDs(payload) {
10107
+ const ids = new Set;
10108
+ if (!isObject3(payload) || !Array.isArray(payload.data)) {
10109
+ return ids;
10110
+ }
10111
+ for (const entry of payload.data) {
10112
+ if (!isObject3(entry) || typeof entry.id !== "string") {
10113
+ continue;
10114
+ }
10115
+ if (isNonTextModel(entry)) {
10116
+ ids.add(entry.id);
10117
+ }
10118
+ }
10119
+ return ids;
10120
+ }
10121
+ function isNonTextModel(entry) {
10122
+ if (!Array.isArray(entry.labels)) {
10123
+ return false;
10124
+ }
10125
+ return entry.labels.some((label) => typeof label === "string" && NON_TEXT_LABELS.has(label));
10126
+ }
10127
+ async function readLoadedModels(baseURL, fetch2) {
10128
+ let payload;
10129
+ try {
10130
+ const response = await fetch2(`${baseURL}/health`, {
10131
+ method: "GET",
10132
+ headers: { accept: "application/json" }
10133
+ });
10134
+ if (!response.ok) {
10135
+ return;
10136
+ }
10137
+ payload = await response.json();
10138
+ } catch {
10139
+ return;
10140
+ }
10141
+ if (!isObject3(payload) || !Array.isArray(payload.all_models_loaded)) {
10142
+ return;
10143
+ }
10144
+ const loaded = new Map;
10145
+ for (const entry of payload.all_models_loaded) {
10146
+ if (!isObject3(entry) || typeof entry.model_name !== "string") {
10147
+ continue;
10148
+ }
10149
+ if (entry.loaded === false) {
10150
+ continue;
10151
+ }
10152
+ loaded.set(entry.model_name, { slots: readSlotCount(entry) });
10153
+ }
10154
+ return loaded;
10155
+ }
10156
+ function readSlotCount(entry) {
10157
+ const options = entry.recipe_options;
10158
+ if (!isObject3(options) || typeof options.llamacpp_args !== "string") {
10159
+ return;
10160
+ }
10161
+ const match = PARALLEL_ARG.exec(options.llamacpp_args);
10162
+ if (match === null) {
10163
+ return;
10164
+ }
10165
+ const parsed = Number.parseInt(match[1], 10);
10166
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
10167
+ }
10168
+ function enrichWithLoadState(models, loaded) {
10169
+ if (loaded === undefined) {
10170
+ return [...models];
10171
+ }
10172
+ return models.map((model) => {
10173
+ const hit = loaded.get(model.modelID);
10174
+ if (hit === undefined) {
10175
+ return { ...model, loaded: false };
10176
+ }
10177
+ return hit.slots === undefined ? { ...model, loaded: true } : { ...model, loaded: true, slots: hit.slots };
10178
+ });
10179
+ }
10180
+ function isObject3(value) {
10181
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10182
+ }
10045
10183
 
10046
10184
  // src/local/lmstudio.ts
10047
10185
  var LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
@@ -10270,6 +10408,9 @@ function createDetect(exec) {
10270
10408
  reachable: snapshot.reachable,
10271
10409
  models: snapshot.models.map((model) => model.modelID)
10272
10410
  };
10411
+ if (snapshot.models.some((model) => model.loaded !== undefined)) {
10412
+ detected.modelInfo = snapshot.models;
10413
+ }
10273
10414
  if (snapshot.error !== undefined) {
10274
10415
  detected.error = snapshot.error;
10275
10416
  }
@@ -10297,6 +10438,9 @@ function createProbeRuntime(exec) {
10297
10438
  reachable: snapshot?.reachable ?? false,
10298
10439
  models: snapshot?.models.map((model) => model.modelID) ?? []
10299
10440
  };
10441
+ if (snapshot?.models.some((model) => model.loaded !== undefined) === true) {
10442
+ detected.modelInfo = snapshot.models;
10443
+ }
10300
10444
  if (snapshot?.error !== undefined) {
10301
10445
  detected.error = snapshot.error;
10302
10446
  }
@@ -11049,6 +11193,14 @@ function runtimeLine(snapshot, runtime) {
11049
11193
  const slots = declared === undefined ? ` · ${DEFAULT_LOCAL_MAX_CONCURRENCY} slot(s) (default)` : ` · ${declared} slot(s) (declared)`;
11050
11194
  return ` ${mark} ${snapshot.id.padEnd(14)} ${snapshot.baseURL || "(no baseURL)"} — ${detail}${slots}`;
11051
11195
  }
11196
+ function runtimeModelSlotsLine(runtime) {
11197
+ const entries = Object.entries(runtime?.modelConcurrency ?? {});
11198
+ if (entries.length === 0) {
11199
+ return;
11200
+ }
11201
+ const detail = entries.map(([modelID, slots]) => `${modelID} ${slots} slot(s)`).join(" · ");
11202
+ return ` ↳ per-model slots: ${detail}`;
11203
+ }
11052
11204
  function isNetworkRuntime(baseURL) {
11053
11205
  if (baseURL.length === 0) {
11054
11206
  return false;
@@ -11304,6 +11456,10 @@ function renderDoctor(input) {
11304
11456
  if (classLine !== undefined) {
11305
11457
  lines.push(classLine);
11306
11458
  }
11459
+ const modelSlots = runtimeModelSlotsLine(runtime);
11460
+ if (modelSlots !== undefined) {
11461
+ lines.push(modelSlots);
11462
+ }
11307
11463
  }
11308
11464
  }
11309
11465
  for (const snapshot of input.snapshots) {
@@ -12039,6 +12195,9 @@ function buildOpenTeamConfig(answers) {
12039
12195
  },
12040
12196
  maxConcurrency: choice.maxConcurrency ?? DEFAULT_LOCAL_MAX_CONCURRENCY
12041
12197
  };
12198
+ if (choice.modelConcurrency !== undefined && Object.keys(choice.modelConcurrency).length > 0) {
12199
+ runtime.modelConcurrency = { ...choice.modelConcurrency };
12200
+ }
12042
12201
  if (choice.thinkingModelID !== undefined && choice.thinkingModelID !== choice.defaultModelID) {
12043
12202
  runtime.thinkingModel = choice.thinkingModelID;
12044
12203
  }
@@ -12235,7 +12394,40 @@ async function chooseFrontier(prompt, profiles) {
12235
12394
  }
12236
12395
  return parseFrontierRef(selected);
12237
12396
  }
12238
- async function chooseRuntimeModel(prompt, models, meta) {
12397
+ function orderModelChoices(models, modelInfo) {
12398
+ const byID = new Map((modelInfo ?? []).map((model) => [model.modelID, model]));
12399
+ const anyLoadState = (modelInfo ?? []).some((model) => model.loaded !== undefined);
12400
+ const decorated = models.map((modelID, index) => {
12401
+ const info = byID.get(modelID);
12402
+ return {
12403
+ modelID,
12404
+ index,
12405
+ loaded: info?.loaded === true,
12406
+ slots: info?.slots
12407
+ };
12408
+ });
12409
+ if (!anyLoadState) {
12410
+ return decorated.map((entry) => ({
12411
+ value: entry.modelID,
12412
+ label: entry.modelID
12413
+ }));
12414
+ }
12415
+ return [...decorated].sort((a, b) => a.loaded === b.loaded ? a.index - b.index : a.loaded ? -1 : 1).map((entry) => ({
12416
+ value: entry.modelID,
12417
+ label: `${entry.modelID} ${modelStateSuffix(entry.loaded, entry.slots)}`
12418
+ }));
12419
+ }
12420
+ function modelStateSuffix(loaded, slots) {
12421
+ if (!loaded) {
12422
+ return "(not loaded)";
12423
+ }
12424
+ return slots === undefined ? "(loaded)" : `(loaded · ${slots} slot${slots === 1 ? "" : "s"})`;
12425
+ }
12426
+ function preferredModelID(models, modelInfo) {
12427
+ const loaded = (modelInfo ?? []).find((model) => model.loaded === true && models.includes(model.modelID));
12428
+ return loaded?.modelID ?? models[0];
12429
+ }
12430
+ async function chooseRuntimeModel(prompt, models, meta, modelInfo) {
12239
12431
  if (models.length === 0) {
12240
12432
  return prompt.text({
12241
12433
  message: `Default model for ${meta.label} (no models detected)`,
@@ -12243,13 +12435,10 @@ async function chooseRuntimeModel(prompt, models, meta) {
12243
12435
  initial: meta.fallbackModelID
12244
12436
  });
12245
12437
  }
12246
- const first = models[0];
12438
+ const first = preferredModelID(models, modelInfo);
12247
12439
  const opts = {
12248
12440
  message: `Default local model for ${meta.label}`,
12249
- choices: models.map((modelID) => ({
12250
- value: modelID,
12251
- label: modelID
12252
- }))
12441
+ choices: orderModelChoices(models, modelInfo)
12253
12442
  };
12254
12443
  if (first !== undefined) {
12255
12444
  opts.initial = first;
@@ -12285,9 +12474,13 @@ function normalizeRemoteBaseURL(raw, defaultBaseURL) {
12285
12474
  }
12286
12475
  return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
12287
12476
  }
12477
+ function connectionOf(info) {
12478
+ const models = info?.models ?? [];
12479
+ return info?.modelInfo === undefined ? { models } : { models, modelInfo: info.modelInfo };
12480
+ }
12288
12481
  async function resolveRuntimeConnection(deps, meta, info) {
12289
12482
  if (meta.defaultBaseURL === undefined) {
12290
- return { models: info?.models ?? [] };
12483
+ return connectionOf(info);
12291
12484
  }
12292
12485
  const location = await deps.prompt.select({
12293
12486
  message: `Where does ${meta.label} run?`,
@@ -12306,23 +12499,22 @@ async function resolveRuntimeConnection(deps, meta, info) {
12306
12499
  });
12307
12500
  if (location === RUNTIME_LOCATION_LOCAL) {
12308
12501
  const baseURL2 = info?.baseURL !== undefined && info.baseURL.length > 0 ? info.baseURL : meta.defaultBaseURL;
12309
- return { baseURL: baseURL2, models: info?.models ?? [] };
12502
+ return { baseURL: baseURL2, ...connectionOf(info) };
12310
12503
  }
12311
12504
  const entered = await deps.prompt.text({
12312
12505
  message: `${meta.label} URL on the network (OpenAI-compatible endpoint)`,
12313
12506
  placeholder: remoteBaseURLExample(meta)
12314
12507
  });
12315
12508
  const baseURL = normalizeRemoteBaseURL(entered, meta.defaultBaseURL);
12316
- let models = [];
12509
+ let probedRuntime;
12317
12510
  if (deps.probeRuntime !== undefined) {
12318
12511
  try {
12319
- const probed = await deps.probeRuntime(meta.id, baseURL);
12320
- models = probed.models;
12512
+ probedRuntime = await deps.probeRuntime(meta.id, baseURL);
12321
12513
  } catch {}
12322
12514
  }
12323
- return { baseURL, models };
12515
+ return { baseURL, ...connectionOf(probedRuntime) };
12324
12516
  }
12325
- async function chooseClassModel(prompt, models, meta, defaultModelID, modelClass) {
12517
+ async function chooseClassModel(prompt, models, meta, defaultModelID, modelClass, modelInfo) {
12326
12518
  const label = modelClass === "thinking" ? `Thinking model for ${meta.label} (design, analysis, review reasoning)` : `Coder model for ${meta.label} (writing and editing code)`;
12327
12519
  if (models.length === 0) {
12328
12520
  const raw = await prompt.text({
@@ -12334,18 +12526,18 @@ async function chooseClassModel(prompt, models, meta, defaultModelID, modelClass
12334
12526
  }
12335
12527
  return prompt.select({
12336
12528
  message: label,
12337
- choices: models.map((modelID) => ({
12338
- value: modelID,
12339
- label: modelID === defaultModelID ? `${modelID} (default)` : modelID
12529
+ choices: orderModelChoices(models, modelInfo).map((choice) => ({
12530
+ value: choice.value,
12531
+ label: choice.value === defaultModelID ? `${choice.label} — default` : choice.label
12340
12532
  })),
12341
12533
  initial: defaultModelID
12342
12534
  });
12343
12535
  }
12344
12536
  async function resolveEnabledRuntime(deps, meta, info) {
12345
12537
  const connection = await resolveRuntimeConnection(deps, meta, info);
12346
- const defaultModelID = await chooseRuntimeModel(deps.prompt, connection.models, meta);
12347
- const thinkingModelID = await chooseClassModel(deps.prompt, connection.models, meta, defaultModelID, "thinking");
12348
- const coderModelID = await chooseClassModel(deps.prompt, connection.models, meta, defaultModelID, "coder");
12538
+ const defaultModelID = await chooseRuntimeModel(deps.prompt, connection.models, meta, connection.modelInfo);
12539
+ const thinkingModelID = await chooseClassModel(deps.prompt, connection.models, meta, defaultModelID, "thinking", connection.modelInfo);
12540
+ const coderModelID = await chooseClassModel(deps.prompt, connection.models, meta, defaultModelID, "coder", connection.modelInfo);
12349
12541
  const choice = {
12350
12542
  id: meta.id,
12351
12543
  enabled: true,
@@ -12357,12 +12549,38 @@ async function resolveEnabledRuntime(deps, meta, info) {
12357
12549
  if (connection.baseURL !== undefined) {
12358
12550
  choice.baseURL = connection.baseURL;
12359
12551
  }
12552
+ const modelConcurrency = reportedModelSlots(connection.modelInfo, [
12553
+ defaultModelID,
12554
+ thinkingModelID,
12555
+ coderModelID
12556
+ ]);
12557
+ if (modelConcurrency !== undefined) {
12558
+ choice.modelConcurrency = modelConcurrency;
12559
+ }
12360
12560
  return choice;
12361
12561
  }
12562
+ function reportedModelSlots(modelInfo, usedModelIDs) {
12563
+ if (modelInfo === undefined) {
12564
+ return;
12565
+ }
12566
+ const used = new Set(usedModelIDs);
12567
+ const slots = {};
12568
+ for (const model of modelInfo) {
12569
+ if (model.slots !== undefined && used.has(model.modelID)) {
12570
+ slots[model.modelID] = model.slots;
12571
+ }
12572
+ }
12573
+ return Object.keys(slots).length > 0 ? slots : undefined;
12574
+ }
12362
12575
  function instanceSuffix(index) {
12363
12576
  return index < 26 ? String.fromCharCode(97 + index) : String(index + 1);
12364
12577
  }
12365
- async function promptSlotCount(prompt, meta) {
12578
+ async function promptSlotCount(prompt, meta, reported) {
12579
+ const detected = reportedSlotSummary(reported);
12580
+ if (detected !== undefined) {
12581
+ 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");
12582
+ return detected.max;
12583
+ }
12366
12584
  const raw = await prompt.text({
12367
12585
  message: `How many concurrent generation slots does ${meta.label} have? (match its real capacity; leaving one free keeps interactive work responsive)`,
12368
12586
  placeholder: String(DEFAULT_LOCAL_MAX_CONCURRENCY),
@@ -12371,6 +12589,16 @@ async function promptSlotCount(prompt, meta) {
12371
12589
  const parsed = Number.parseInt(raw.trim(), 10);
12372
12590
  return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LOCAL_MAX_CONCURRENCY;
12373
12591
  }
12592
+ function reportedSlotSummary(reported) {
12593
+ const entries = Object.entries(reported ?? {});
12594
+ if (entries.length === 0) {
12595
+ return;
12596
+ }
12597
+ return {
12598
+ summary: entries.map(([modelID, slots]) => `${modelID} = ${slots}`).join(", "),
12599
+ max: Math.max(...entries.map(([, slots]) => slots))
12600
+ };
12601
+ }
12374
12602
  async function promptInstanceId(prompt, meta, defaultId, usedIds) {
12375
12603
  for (;; ) {
12376
12604
  const raw = await prompt.text({
@@ -12392,7 +12620,7 @@ async function resolveRuntimeInstances(deps, meta, info, usedIds) {
12392
12620
  });
12393
12621
  if (!fleet) {
12394
12622
  const choice = await resolveEnabledRuntime(deps, meta, info);
12395
- choice.maxConcurrency = await promptSlotCount(deps.prompt, meta);
12623
+ choice.maxConcurrency = await promptSlotCount(deps.prompt, meta, choice.modelConcurrency);
12396
12624
  usedIds.add(choice.id);
12397
12625
  return [choice];
12398
12626
  }
@@ -12403,7 +12631,7 @@ async function resolveRuntimeInstances(deps, meta, info, usedIds) {
12403
12631
  const choice = await resolveEnabledRuntime(deps, meta, info);
12404
12632
  choice.id = id;
12405
12633
  choice.kind = meta.id;
12406
- choice.maxConcurrency = await promptSlotCount(deps.prompt, meta);
12634
+ choice.maxConcurrency = await promptSlotCount(deps.prompt, meta, choice.modelConcurrency);
12407
12635
  usedIds.add(id);
12408
12636
  instances.push(choice);
12409
12637
  const another = await deps.prompt.confirm({
@@ -16239,7 +16467,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
16239
16467
  // package.json
16240
16468
  var package_default = {
16241
16469
  name: "@jmanuelcorral/openteam",
16242
- version: "0.17.0",
16470
+ version: "0.17.2",
16243
16471
  packageManager: "bun@1.3.14",
16244
16472
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
16245
16473
  license: "MIT",