@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.
- package/.opencode/openteam.example.json +7 -2
- package/README.es.md +4 -3
- package/README.md +4 -3
- package/dist/certificates/graph-release-certificate.json +2 -2
- package/dist/certificates/graph-shadow-certificate.json +2 -2
- package/dist/cli/setupAdapters.d.ts.map +1 -1
- package/dist/cli.js +273 -45
- package/dist/commands/doctor.d.ts +12 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/setup.d.ts +62 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/config/errors.d.ts.map +1 -1
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +207 -39
- package/dist/local/lemonade.d.ts +5 -1
- package/dist/local/lemonade.d.ts.map +1 -1
- package/dist/local/slotPool.d.ts +58 -0
- package/dist/local/slotPool.d.ts.map +1 -0
- package/dist/local/types.d.ts +19 -0
- package/dist/local/types.d.ts.map +1 -1
- package/dist/orchestrator/coordinator.d.ts +9 -0
- package/dist/orchestrator/coordinator.d.ts.map +1 -1
- package/dist/orchestrator/runtimeSelection.d.ts +7 -1
- package/dist/orchestrator/runtimeSelection.d.ts.map +1 -1
- package/dist/router/chooseModel.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -36,13 +36,18 @@
|
|
|
36
36
|
{
|
|
37
37
|
"id": "lemonade",
|
|
38
38
|
"enabled": false,
|
|
39
|
-
"baseURL": "http://localhost:
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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": "
|
|
44
|
+
"digest": "1f51bbf60529fba584b0d050615a88ae6456b85bd19b91aca60c80b650161333"
|
|
45
45
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"rawFindings": 0
|
|
21
21
|
},
|
|
22
22
|
"overhead": {
|
|
23
|
-
"p95Millis": 0.
|
|
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": "
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
2304
|
-
queuedCount: limiter.queuedCount(
|
|
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:
|
|
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
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
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
|
-
|
|
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
|
|
12438
|
+
const first = preferredModelID(models, modelInfo);
|
|
12247
12439
|
const opts = {
|
|
12248
12440
|
message: `Default local model for ${meta.label}`,
|
|
12249
|
-
choices: models
|
|
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
|
|
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,
|
|
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
|
|
12509
|
+
let probedRuntime;
|
|
12317
12510
|
if (deps.probeRuntime !== undefined) {
|
|
12318
12511
|
try {
|
|
12319
|
-
|
|
12320
|
-
models = probed.models;
|
|
12512
|
+
probedRuntime = await deps.probeRuntime(meta.id, baseURL);
|
|
12321
12513
|
} catch {}
|
|
12322
12514
|
}
|
|
12323
|
-
return { baseURL,
|
|
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((
|
|
12338
|
-
value:
|
|
12339
|
-
label:
|
|
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.
|
|
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",
|