@north-light/crouter 0.3.161 → 0.3.162
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/api/client.d.ts +3 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/modelauth.d.ts +32 -0
- package/dist/api/dto/modelauth.js +2 -1
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +55 -395
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +204 -419
- package/dist/clients/attach/viewer.js +592 -592
- package/dist/core/__tests__/broker-launch-candidates.test.js +13 -3
- package/dist/core/__tests__/broker-sdk-wiring.test.js +53 -25
- package/dist/core/__tests__/full/broker-provider-retry.test.js +6 -122
- package/dist/core/__tests__/model-pin-durability.test.js +7 -5
- package/dist/core/__tests__/model-routes.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes.test.js +41 -0
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/auth-file.d.ts +3 -2
- package/dist/core/auth-file.js +5 -3
- package/dist/core/canvas/types.d.ts +6 -1
- package/dist/core/config.d.ts +3 -1
- package/dist/core/config.js +88 -2
- package/dist/core/model-routes.d.ts +37 -0
- package/dist/core/model-routes.js +140 -0
- package/dist/core/runtime/broker.d.ts +0 -4
- package/dist/core/runtime/broker.js +116 -372
- package/dist/core/runtime/launch.d.ts +9 -11
- package/dist/core/runtime/launch.js +15 -21
- package/dist/core/runtime/managed-provider-cooling.d.ts +0 -14
- package/dist/core/runtime/managed-provider-cooling.js +0 -19
- package/dist/core/runtime/model-swap.js +7 -1
- package/dist/core/runtime/promote.js +1 -0
- package/dist/core/subscription-state.d.ts +12 -36
- package/dist/core/subscription-state.js +19 -203
- package/dist/daemon/api/__tests__/full/api-server.test.js +9 -6
- package/dist/daemon/api/__tests__/full/b10-attach-modelauth.test.js +123 -1
- package/dist/daemon/api/handlers/modelauth.js +51 -2
- package/dist/daemon/api/handlers/nodes.js +2 -0
- package/dist/types.d.ts +17 -0
- package/dist/web-client/assets/{index-B76ZKfT_.js → index-NIuSCOHM.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/core/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
5
|
+
import { emitEvent } from './events/emit.js';
|
|
5
6
|
import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
6
7
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
|
|
7
8
|
import { profileRoot } from './profiles/manifest.js';
|
|
@@ -172,6 +173,83 @@ function mergeModelLadders(raw, base = defaultModelLaddersConfig()) {
|
|
|
172
173
|
}
|
|
173
174
|
return out;
|
|
174
175
|
}
|
|
176
|
+
function validStrength(value) {
|
|
177
|
+
return value === 'ultra' || value === 'strong' || value === 'medium' || value === 'light';
|
|
178
|
+
}
|
|
179
|
+
/** Validate and overlay route entries. Invalid declarations are loud but local:
|
|
180
|
+
* a typo must not prevent unrelated launch configuration from resolving. */
|
|
181
|
+
function mergeModelRoutes(raw, base = {}) {
|
|
182
|
+
const out = { ...base };
|
|
183
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
184
|
+
return out;
|
|
185
|
+
for (const [routeId, value] of Object.entries(raw)) {
|
|
186
|
+
const patch = value;
|
|
187
|
+
const invalid = (rule) => { emitEvent({ level: 'warn', event: 'config.model_routes.invalid', fields: { route_id: routeId, rule } }); };
|
|
188
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
189
|
+
invalid('route must be an object');
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const route = { ...out[routeId], ...patch, models: { ...out[routeId]?.models, ...patch.models } };
|
|
193
|
+
if (typeof route.family !== 'string' || route.family.trim() === '') {
|
|
194
|
+
invalid('family must be a non-empty string');
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (typeof route.credentialSource !== 'string' || route.credentialSource.trim() === '') {
|
|
198
|
+
invalid('credentialSource must be a non-empty string');
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (typeof route.providerId !== 'string' || route.providerId.trim() === '') {
|
|
202
|
+
invalid('providerId must be a non-empty string');
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!route.models || typeof route.models !== 'object' || Array.isArray(route.models)) {
|
|
206
|
+
invalid('models must be an object');
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const models = {};
|
|
210
|
+
let rule;
|
|
211
|
+
for (const [strength, spec] of Object.entries(route.models)) {
|
|
212
|
+
if (!validStrength(strength)) {
|
|
213
|
+
rule = `models.${strength} is not a model strength`;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (typeof spec !== 'string' || spec.trim() === '') {
|
|
217
|
+
rule = `models.${strength} must be a non-empty provider/id ref`;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
const slash = spec.indexOf('/');
|
|
221
|
+
if (slash <= 0 || slash === spec.length - 1) {
|
|
222
|
+
rule = `models.${strength} must be provider/id`;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (spec.slice(0, slash) !== route.providerId) {
|
|
226
|
+
rule = `models.${strength} provider prefix must equal providerId`;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
models[strength] = spec;
|
|
230
|
+
}
|
|
231
|
+
if (rule !== undefined) {
|
|
232
|
+
invalid(rule);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out[routeId] = { family: route.family, credentialSource: route.credentialSource, providerId: route.providerId, models };
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
function mergeModelRouting(raw, base) {
|
|
240
|
+
if (raw === undefined)
|
|
241
|
+
return base;
|
|
242
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
243
|
+
return base;
|
|
244
|
+
const value = raw;
|
|
245
|
+
const strings = (items) => Array.isArray(items) && items.every((item) => typeof item === 'string' && item.trim() !== '') ? [...items] : undefined;
|
|
246
|
+
const strengths = Array.isArray(value.strengthFallback) && value.strengthFallback.every((item) => validStrength(item)) ? [...value.strengthFallback] : undefined;
|
|
247
|
+
return {
|
|
248
|
+
...(strings(value.credentialSourceOrder) !== undefined ? { credentialSourceOrder: strings(value.credentialSourceOrder) } : {}),
|
|
249
|
+
...(strings(value.familyOrder) !== undefined ? { familyOrder: strings(value.familyOrder) } : {}),
|
|
250
|
+
...(strengths !== undefined ? { strengthFallback: strengths } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
175
253
|
/** True when `pattern` (a `spawnEnv.allow` entry) matches EVERY possible env
|
|
176
254
|
* var name — a bare `*`, or any run of nothing but `*` characters (`**`,
|
|
177
255
|
* `***`, …). This is the one glob shape that reopens the inherit-all hole
|
|
@@ -362,10 +440,12 @@ function mergeConfig(partial) {
|
|
|
362
440
|
const fold_finished_tools = typeof partial.fold_finished_tools === 'boolean' ? partial.fold_finished_tools : defaults.fold_finished_tools;
|
|
363
441
|
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
364
442
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
443
|
+
const modelRoutes = mergeModelRoutes(partial.modelRoutes);
|
|
444
|
+
const modelRouting = mergeModelRouting(partial.modelRouting, undefined);
|
|
365
445
|
const kinds = mergeKinds(partial.kinds);
|
|
366
446
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
367
447
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
368
|
-
return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, kinds, remoteCanvas, spawnEnv };
|
|
448
|
+
return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
369
449
|
}
|
|
370
450
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
371
451
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -450,18 +530,24 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
450
530
|
const projectRawsFarthestFirst = readRawProjectScopeConfigs(targetCwd, targetProfileId);
|
|
451
531
|
let kinds = mergeKinds(userRaw?.kinds, defaults.kinds);
|
|
452
532
|
let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
|
|
533
|
+
let modelRoutes = mergeModelRoutes(userRaw?.modelRoutes);
|
|
534
|
+
let modelRouting = mergeModelRouting(userRaw?.modelRouting, undefined);
|
|
453
535
|
let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
|
|
454
536
|
if (profileRaw !== null) {
|
|
455
537
|
kinds = mergeKinds(profileRaw.kinds, kinds);
|
|
456
538
|
modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
|
|
539
|
+
modelRoutes = mergeModelRoutes(profileRaw.modelRoutes, modelRoutes);
|
|
540
|
+
modelRouting = mergeModelRouting(profileRaw.modelRouting, modelRouting);
|
|
457
541
|
spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
|
|
458
542
|
}
|
|
459
543
|
for (const raw of projectRawsFarthestFirst) {
|
|
460
544
|
kinds = mergeKinds(raw.kinds, kinds);
|
|
461
545
|
modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
|
|
546
|
+
modelRoutes = mergeModelRoutes(raw.modelRoutes, modelRoutes);
|
|
547
|
+
modelRouting = mergeModelRouting(raw.modelRouting, modelRouting);
|
|
462
548
|
spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
|
|
463
549
|
}
|
|
464
|
-
return { kinds, modelLadders, spawnEnv };
|
|
550
|
+
return { kinds, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), spawnEnv };
|
|
465
551
|
}
|
|
466
552
|
/** The effective `KindConfig` for one full kind string (top-level, e.g.
|
|
467
553
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ModelStrength } from '../types.js';
|
|
2
|
+
export type ModelRequest = {
|
|
3
|
+
kind: 'logical';
|
|
4
|
+
family: string;
|
|
5
|
+
strength: ModelStrength;
|
|
6
|
+
spec: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'concrete';
|
|
9
|
+
spec: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'exact';
|
|
12
|
+
spec: string;
|
|
13
|
+
};
|
|
14
|
+
export type RoutedCandidate = {
|
|
15
|
+
routeId: string;
|
|
16
|
+
credentialSource: string;
|
|
17
|
+
family: string;
|
|
18
|
+
strength: ModelStrength;
|
|
19
|
+
providerId: string;
|
|
20
|
+
modelId: string;
|
|
21
|
+
thinkingLevel?: string;
|
|
22
|
+
rank: number;
|
|
23
|
+
};
|
|
24
|
+
export type RouteAvailability = {
|
|
25
|
+
authenticated: boolean;
|
|
26
|
+
coolingUntil?: number;
|
|
27
|
+
registered: boolean;
|
|
28
|
+
};
|
|
29
|
+
export interface ModelRegistryLike {
|
|
30
|
+
find(providerId: string, modelId: string): unknown;
|
|
31
|
+
hasConfiguredAuth(model: unknown): boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Expand config-derived route order only. Availability never affects rank. */
|
|
34
|
+
export declare function expandModelCandidates(request: ModelRequest, cwd?: string, profileId?: string | null): RoutedCandidate[];
|
|
35
|
+
/** Probe the common availability facts consumed unchanged by resolveProviderCandidates. */
|
|
36
|
+
export declare function probeRouteAvailability(candidate: RoutedCandidate, registry: ModelRegistryLike, now?: number): RouteAvailability;
|
|
37
|
+
export declare function modelRequestFromConfig(model: string | undefined, modelIntent: string | undefined, exact: boolean): ModelRequest | undefined;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readMergedLaunchConfig } from './config.js';
|
|
2
|
+
import { isManagedProvider, readRotationConfig, readSubscriptionPool } from './subscription-state.js';
|
|
3
|
+
const STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
4
|
+
const THINKING_SUFFIXES = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
5
|
+
function parseSpec(spec) {
|
|
6
|
+
const slash = spec.indexOf('/');
|
|
7
|
+
if (slash <= 0 || slash === spec.length - 1)
|
|
8
|
+
return undefined;
|
|
9
|
+
const providerId = spec.slice(0, slash);
|
|
10
|
+
const rawId = spec.slice(slash + 1);
|
|
11
|
+
const colon = rawId.lastIndexOf(':');
|
|
12
|
+
const suffix = colon > 0 ? rawId.slice(colon + 1).toLowerCase() : undefined;
|
|
13
|
+
return {
|
|
14
|
+
providerId,
|
|
15
|
+
modelId: suffix !== undefined && THINKING_SUFFIXES.has(suffix) ? rawId.slice(0, colon) : rawId,
|
|
16
|
+
...(suffix !== undefined && THINKING_SUFFIXES.has(suffix) && suffix !== 'off' ? { thinkingLevel: suffix } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function synthesizeRoutes(ladders) {
|
|
20
|
+
const route = (family) => {
|
|
21
|
+
const providerId = ladders[family].strong.slice(0, ladders[family].strong.indexOf('/'));
|
|
22
|
+
return { family, credentialSource: 'default', providerId, models: ladders[family] };
|
|
23
|
+
};
|
|
24
|
+
return { anthropic: route('anthropic'), openai: route('openai') };
|
|
25
|
+
}
|
|
26
|
+
function ordered(first, rest) {
|
|
27
|
+
return [first, ...rest.filter((item) => item !== first)];
|
|
28
|
+
}
|
|
29
|
+
function strengths(requested, configured) {
|
|
30
|
+
return ordered(requested, configured).filter((value, index, all) => all.indexOf(value) === index);
|
|
31
|
+
}
|
|
32
|
+
/** Expand config-derived route order only. Availability never affects rank. */
|
|
33
|
+
export function expandModelCandidates(request, cwd, profileId) {
|
|
34
|
+
const cfg = readMergedLaunchConfig(cwd, profileId ?? null);
|
|
35
|
+
const routes = cfg.modelRoutes && Object.keys(cfg.modelRoutes).length > 0 ? cfg.modelRoutes : synthesizeRoutes(cfg.modelLadders);
|
|
36
|
+
const routeEntries = Object.entries(routes);
|
|
37
|
+
const fallback = cfg.modelRouting?.strengthFallback ?? [readRotationConfig().defaultFallbackStrength ?? 'strong'];
|
|
38
|
+
const declarationFamilies = routeEntries.map(([, route]) => route.family).filter((value, index, all) => all.indexOf(value) === index);
|
|
39
|
+
const declarationSources = routeEntries.map(([, route]) => route.credentialSource).filter((value, index, all) => all.indexOf(value) === index);
|
|
40
|
+
if (request.kind === 'exact') {
|
|
41
|
+
const parsed = parseSpec(request.spec);
|
|
42
|
+
return parsed ? [{ routeId: 'exact', credentialSource: 'exact', family: parsed.providerId, strength: 'strong', ...parsed, rank: 0 }] : [];
|
|
43
|
+
}
|
|
44
|
+
const concrete = request.kind === 'concrete' ? parseSpec(request.spec) : undefined;
|
|
45
|
+
const logical = request.kind === 'logical' ? request : undefined;
|
|
46
|
+
const matchesConcrete = (spec, exactThinking) => {
|
|
47
|
+
const parsed = parseSpec(spec);
|
|
48
|
+
return parsed !== undefined
|
|
49
|
+
&& concrete !== undefined
|
|
50
|
+
&& parsed.providerId === concrete.providerId
|
|
51
|
+
&& parsed.modelId === concrete.modelId
|
|
52
|
+
&& (!exactThinking || parsed.thinkingLevel === concrete.thinkingLevel);
|
|
53
|
+
};
|
|
54
|
+
const concreteRoute = concrete === undefined ? undefined : routeEntries.find(([, route]) => Object.values(route.models).some((spec) => matchesConcrete(spec, false)));
|
|
55
|
+
const requestedStrength = logical?.strength ?? (() => {
|
|
56
|
+
if (!concrete)
|
|
57
|
+
return 'strong';
|
|
58
|
+
// Mirrors the former ladder back-map: preserve a precise thinking-level
|
|
59
|
+
// match, then map a drifting/omitted suffix to the weakest same-model cell.
|
|
60
|
+
for (const exactThinking of [true, false]) {
|
|
61
|
+
for (const strength of exactThinking ? STRENGTHS : [...STRENGTHS].reverse()) {
|
|
62
|
+
if (routeEntries.some(([, route]) => {
|
|
63
|
+
const spec = route.models[strength];
|
|
64
|
+
return spec !== undefined && matchesConcrete(spec, exactThinking);
|
|
65
|
+
}))
|
|
66
|
+
return strength;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 'strong';
|
|
70
|
+
})();
|
|
71
|
+
const requestedFamily = logical?.family ?? concreteRoute?.[1].family ?? concrete?.providerId ?? 'anthropic';
|
|
72
|
+
const sourceOrder = concrete ? declarationSources : (cfg.modelRouting?.credentialSourceOrder ?? declarationSources);
|
|
73
|
+
const familyOrder = concrete ? [requestedFamily] : (cfg.modelRouting?.familyOrder ?? declarationFamilies);
|
|
74
|
+
const emitted = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const append = (routeId, route, strength, spec) => {
|
|
77
|
+
const parsed = parseSpec(spec);
|
|
78
|
+
if (!parsed)
|
|
79
|
+
return;
|
|
80
|
+
if (concrete && parsed.modelId !== concrete.modelId)
|
|
81
|
+
return;
|
|
82
|
+
const key = `${parsed.providerId}/${parsed.modelId}:${parsed.thinkingLevel ?? ''}`;
|
|
83
|
+
if (seen.has(key))
|
|
84
|
+
return;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
emitted.push({ routeId, credentialSource: route.credentialSource, family: route.family, strength, ...parsed, rank: emitted.length });
|
|
87
|
+
};
|
|
88
|
+
// Concrete requests retain their exact model/provider first even if a route has
|
|
89
|
+
// not been authored for it; declared routes then offer that identical model in
|
|
90
|
+
// other credential sources only.
|
|
91
|
+
if (concrete) {
|
|
92
|
+
const [routeId, route] = concreteRoute ?? [
|
|
93
|
+
'concrete',
|
|
94
|
+
{ family: requestedFamily, credentialSource: 'default', providerId: concrete.providerId, models: { [requestedStrength]: request.spec } },
|
|
95
|
+
];
|
|
96
|
+
append(routeId, route, requestedStrength, request.spec);
|
|
97
|
+
}
|
|
98
|
+
for (const credentialSource of sourceOrder) {
|
|
99
|
+
for (const strength of concrete ? [requestedStrength] : strengths(requestedStrength, fallback)) {
|
|
100
|
+
for (const family of ordered(requestedFamily, familyOrder)) {
|
|
101
|
+
for (const [routeId, route] of routeEntries) {
|
|
102
|
+
if (route.credentialSource !== credentialSource || route.family !== family)
|
|
103
|
+
continue;
|
|
104
|
+
const spec = route.models[strength];
|
|
105
|
+
if (spec)
|
|
106
|
+
append(routeId, route, strength, spec);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return emitted.map((candidate, rank) => ({ ...candidate, rank }));
|
|
112
|
+
}
|
|
113
|
+
/** Probe the common availability facts consumed unchanged by resolveProviderCandidates. */
|
|
114
|
+
export function probeRouteAvailability(candidate, registry, now = Date.now()) {
|
|
115
|
+
const model = registry.find(candidate.providerId, candidate.modelId);
|
|
116
|
+
if (model === undefined)
|
|
117
|
+
return { registered: false, authenticated: false };
|
|
118
|
+
if (!isManagedProvider(candidate.providerId))
|
|
119
|
+
return { registered: true, authenticated: registry.hasConfiguredAuth(model) };
|
|
120
|
+
const pool = readSubscriptionPool(candidate.providerId).filter((entry) => entry.authFailure === undefined);
|
|
121
|
+
const cooling = pool.length > 0 && pool.every((entry) => (entry.rateLimitedUntil || 0) > now);
|
|
122
|
+
return {
|
|
123
|
+
registered: true,
|
|
124
|
+
authenticated: pool.length > 0,
|
|
125
|
+
...(cooling ? { coolingUntil: Math.min(...pool.map((entry) => entry.rateLimitedUntil || 0)) } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function modelRequestFromConfig(model, modelIntent, exact) {
|
|
129
|
+
if (!model)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (exact)
|
|
132
|
+
return { kind: 'exact', spec: model };
|
|
133
|
+
if (modelIntent) {
|
|
134
|
+
const slash = modelIntent.indexOf('/');
|
|
135
|
+
const strength = slash > 0 ? modelIntent.slice(slash + 1) : '';
|
|
136
|
+
if (STRENGTHS.includes(strength))
|
|
137
|
+
return { kind: 'logical', family: modelIntent.slice(0, slash), strength: strength, spec: model };
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'concrete', spec: model };
|
|
140
|
+
}
|
|
@@ -3,7 +3,6 @@ import { type AgentSessionRuntime, type AgentSessionServices, type CreateAgentSe
|
|
|
3
3
|
import { type ImageContent } from '@earendil-works/pi-ai';
|
|
4
4
|
import { type BrokerSdkConfig } from './launch.js';
|
|
5
5
|
import { type BrokerEngine } from './broker-sdk.js';
|
|
6
|
-
import { type ManagedProviderRotationPolicyControl } from './managed-provider-cooling.js';
|
|
7
6
|
import { type KnownStreamWaitControl } from './stream-watchdog.js';
|
|
8
7
|
import { FrameDecoder, type BrokerSnapshot, type BrokerToClient, type ClientRole, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from './broker-protocol.js';
|
|
9
8
|
/**
|
|
@@ -231,9 +230,6 @@ export interface BrokerDialogDeps {
|
|
|
231
230
|
/** Broadcast a non-blocking display frame (setStatus/setWidget/setTitle) to ALL
|
|
232
231
|
* viewers — the relay path for pi's fire-and-forget extension-UI surface. */
|
|
233
232
|
broadcast: (frame: BrokerToClient) => void;
|
|
234
|
-
/** Live provider-fallback policy. This is a callback because an explicit
|
|
235
|
-
* set_model updates the running broker's pin state without a revive. */
|
|
236
|
-
providerRotationPolicy?: ManagedProviderRotationPolicyControl;
|
|
237
233
|
/** Optional broker-owned control for a deliberate, deadline-bounded provider
|
|
238
234
|
* wait: pause the dead-stream watchdog while the provider extension sleeps to
|
|
239
235
|
* a known finite future deadline, returning a release fn that resumes it. Put
|