Skip to content
Source
Sign inSign up

modelOptions.ts

501 lines17.6 KB

1import {
2 normalizeProviderAccentColor,
3 resolveProviderInstanceDisplayName,
4 shouldShowInstanceBadge,
5} from "@t3tools/client-runtime/state/provider-instance-display";
6​
7import type {
8 ModelCapabilities,
9 ModelSelection,
10 ProjectId,
11 RuntimeMode,
12 ServerConfig as T3ServerConfig,
13} from "@t3tools/contracts";
14import {
15 disallowedProviderMessage,
16 isManagedDriverAllowed,
17 isProviderDriverKind,
18 resolveProjectProviderInstanceEnabled,
19} from "@t3tools/contracts";
20import {
21 buildExplicitProviderOptionSelectionsFromDescriptors,
22 getProviderOptionDescriptors,
23} from "@t3tools/shared/model";
24import { resolveModelShortName } from "@t3tools/client-runtime/provider-clients";
25import { resolveProvidersForCwd } from "@t3tools/client-runtime/providerSkills";
26​
27export type ModelOption = {
28 readonly key: string;
29 readonly label: string;
30 readonly shortName?: string;
31 readonly subtitle: string;
32 readonly providerKey: string;
33 readonly providerLabel: string;
34 readonly providerDriver: string;
35 readonly supportedRuntimeModes?: ReadonlyArray<RuntimeMode>;
36 readonly providerIconUrl?: string | undefined;
37 readonly providerBadge?: {
38 readonly displayName: string;
39 readonly accentColor?: string;
40 readonly showBadge: boolean;
41 };
42 readonly isDefault: boolean;
43 readonly isLegacy: boolean;
44 readonly isUnavailable?: boolean;
45 readonly isHidden?: boolean;
46 readonly canHide?: boolean;
47 readonly capabilities: ModelCapabilities | null;
48 readonly selection: ModelSelection;
49 readonly providerUpdateRequired?: ProviderUpdateRequired;
50};
51​
52type ProviderUpdateRequired = Pick<
53 T3ServerConfig["providers"][number],
54 "driver" | "updateRequiredModels"
55>;
56​
57export type ProviderGroup = {
58 readonly providerKey: string;
59 readonly providerLabel: string;
60 readonly models: ReadonlyArray<ModelOption>;
61 readonly updateRequired?: ProviderUpdateRequired;
62};
63​
64function providerDisplayLabel(provider: {
65 readonly displayName?: string | undefined;
66 readonly driver: string;
67 readonly instanceId: string;
68}): string {
69 if (provider.displayName) return provider.displayName;
70 if (provider.driver === "codex") return "Codex";
71 if (provider.driver === "claudeAgent") return "Claude";
72 if (provider.driver === "pi") return "Pi";
73 return provider.instanceId;
75​
76function isProviderEnabledForProject(
77 config: T3ServerConfig | null | undefined,
78 provider:
79 | { readonly instanceId: ModelSelection["instanceId"]; readonly enabled: boolean }
80 | undefined,
81 projectId: ProjectId | null,
82): boolean {
83 if (!provider) {
84 return false;
85 }
86 return config?.settings
87 ? resolveProjectProviderInstanceEnabled(config.settings, projectId, provider.instanceId)
88 : provider.enabled;
90​
91function isProjectDisabledOverride(
92 config: T3ServerConfig | null | undefined,
93 instanceId: ModelSelection["instanceId"],
94 projectId: ProjectId | null,
95): boolean {
96 if (!projectId || !config?.settings) {
97 return false;
98 }
99 return (
100 config.settings.projectSettingsOverrides[projectId]?.providerInstances?.[instanceId]
101 ?.enabled === false
102 );
104​
105function isDisabledForProject(
106 config: T3ServerConfig | null | undefined,
107 provider:
108 | { readonly instanceId: ModelSelection["instanceId"]; readonly enabled: boolean }
109 | undefined,
110 instanceId: ModelSelection["instanceId"],
111 projectId: ProjectId | null,
112): boolean {
113 if (isProjectDisabledOverride(config, instanceId, projectId)) {
114 return true;
115 }
116 return provider !== undefined && !isProviderEnabledForProject(config, provider, projectId);
118​
119function selectionDriver(
120 config: T3ServerConfig | null | undefined,
121 instanceId: ModelSelection["instanceId"],
122): string | undefined {
123 return (
124 config?.providers.find((candidate) => candidate.instanceId === instanceId)?.driver ??
125 config?.settings?.providerInstances[instanceId]?.driver
126 );
128​
129/** Whether the environment's managed policy leaves this driver out; the server refuses its turns. */
130function isDriverDisallowed(config: T3ServerConfig | null | undefined, driver: string | undefined) {
131 return (
132 driver !== undefined &&
133 isProviderDriverKind(driver) &&
134 !isManagedDriverAllowed(config?.managedPolicy, driver)
135 );
137​
138function normalizeSelectionOptions(
139 selection: ModelSelection,
140 capabilities: ModelCapabilities | null,
141): ModelSelection {
142 if (!capabilities) {
143 return selection;
144 }
145 if (!selection.options?.length) {
146 return { instanceId: selection.instanceId, model: selection.model };
147 }
148 const options = buildExplicitProviderOptionSelectionsFromDescriptors(
149 getProviderOptionDescriptors({
150 caps: capabilities,
151 selections: selection.options,
152 }),
153 selection.options,
154 );
155 return options
156 ? { ...selection, options }
157 : {
158 instanceId: selection.instanceId,
159 model: selection.model,
160 };
162​
163/** The config as a project at `cwd` sees it, with that project's own provider models. */
164export function serverConfigForCwd<C extends T3ServerConfig | null | undefined>(
165 config: C,
166 cwd: string | null,
167): C {
168 if (!config) return config;
169 const providers = resolveProvidersForCwd(config.providers, cwd);
170 return providers === config.providers ? config : { ...config, providers };
172​
173/** Require explicit projectId or null so omissions cannot bypass project provider disablement. */
174export function isModelSelectionUnavailable(
175 config: T3ServerConfig | null | undefined,
176 selection: ModelSelection | null | undefined,
177 projectId: ProjectId | null,
178): boolean {
179 if (!config || !selection) {
180 return false;
181 }
182 const provider = config.providers.find(
183 (candidate) => candidate.instanceId === selection.instanceId,
184 );
185 if (isDisabledForProject(config, provider, selection.instanceId, projectId)) {
186 return true;
187 }
188 const driver =
189 provider?.driver ?? config.settings?.providerInstances[selection.instanceId]?.driver;
190 if (isDriverDisallowed(config, driver)) {
191 return true;
192 }
193 return (
194 driver === "antigravity" &&
195 (!provider ||
196 !provider.installed ||
197 provider.auth.status === "unauthenticated" ||
198 provider.availability === "unavailable" ||
199 !provider.models.some((model) => model.slug === selection.model))
200 );
202​
203export function describeUnavailableModelSelection(
204 config: T3ServerConfig | null | undefined,
205 selection: ModelSelection,
206 projectId: ProjectId | null,
207): { readonly title: string; readonly detail: string } {
208 const provider = config?.providers.find(
209 (candidate) => candidate.instanceId === selection.instanceId,
210 );
211 const driver = selectionDriver(config, selection.instanceId);
212 if (isDriverDisallowed(config, driver)) {
213 const label = providerDisplayLabel({
214 driver: driver ?? selection.instanceId,
215 displayName:
216 provider?.displayName ??
217 config?.settings?.providerInstances[selection.instanceId]?.displayName,
218 instanceId: selection.instanceId,
219 });
220 return {
221 title: "Provider not allowed",
222 detail: `${disallowedProviderMessage(config?.managedPolicy?.organization, label)} Choose another model.`,
223 };
224 }
225 return isDisabledForProject(config, provider, selection.instanceId, projectId)
226 ? {
227 title: "Model turned off for this project",
228 detail: "Turn it back on from web or desktop, or choose another model.",
229 }
230 : {
231 title: "Antigravity model unavailable",
232 detail: "Set up Antigravity on web or desktop, or choose another model.",
233 };
235​
236export function resolveSelectableModelSelection(
237 config: T3ServerConfig | null | undefined,
238 selection: ModelSelection | null,
239 projectId: ProjectId | null,
240): ModelSelection | null {
241 if (!selection || !config) {
242 return selection;
243 }
244 const provider = config.providers.find(
245 (candidate) => candidate.instanceId === selection.instanceId,
246 );
247 const driver =
248 provider?.driver ?? config.settings?.providerInstances[selection.instanceId]?.driver;
249 if (driver === "antigravity") {
250 return isProjectDisabledOverride(config, selection.instanceId, projectId) ? null : selection;
251 }
252 return provider &&
253 isProviderEnabledForProject(config, provider, projectId) &&
254 provider.installed &&
255 provider.auth.status !== "unauthenticated"
256 ? selection
257 : null;
259​
260export function resolveDefaultableModelSelection(
261 config: T3ServerConfig | null | undefined,
262 selection: ModelSelection | null,
263 projectId: ProjectId | null,
264): ModelSelection | null {
265 const usable = resolveSelectableModelSelection(config, selection, projectId);
266 if (!usable || !config) {
267 return usable;
268 }
269 const provider = config.providers.find((candidate) => candidate.instanceId === usable.instanceId);
270 const model = provider?.models.find((candidate) => candidate.slug === usable.model);
271 return provider?.driver !== "antigravity" && model?.isLegacy === true ? null : usable;
273​
274export function resolveNewTaskModelSelection(input: {
275 readonly draftSelection: ModelSelection | null;
276 readonly projectDefaultSelection: ModelSelection | null;
277 readonly stickySelection: ModelSelection | null;
278 readonly modelOptions: ReadonlyArray<ModelOption>;
279}): ModelSelection | null {
280 return (
281 input.draftSelection ??
282 input.projectDefaultSelection ??
283 input.stickySelection ??
284 input.modelOptions.find((option) => option.isDefault && !option.isUnavailable)?.selection ??
285 input.modelOptions.find((option) => !option.isUnavailable)?.selection ??
286 null
287 );
289​
290export function buildModelOptions(
291 config: T3ServerConfig | null | undefined,
292 fallbackModelSelection: ModelSelection | null,
293 providerInstanceId: ModelSelection["instanceId"] | undefined,
294 projectId: ProjectId | null,
295 includeHidden = false,
296): ReadonlyArray<ModelOption> {
297 const options = new Map<string, ModelOption>();
298 const lockedProvider = config?.providers.find(
299 (provider) => provider.instanceId === providerInstanceId,
300 );
301 const matchesLockedProvider = (provider: T3ServerConfig["providers"][number]) => {
302 if (providerInstanceId === undefined || provider.instanceId === providerInstanceId) return true;
303 if (!lockedProvider || provider.driver !== lockedProvider.driver) return false;
304 const groupKey = lockedProvider.continuation?.groupKey;
305 if (groupKey) return provider.continuation?.groupKey === groupKey;
306 return provider.driver !== "antigravity";
307 };
308 const registryAgentId = (instanceId: ModelSelection["instanceId"]) => {
309 const configValue = config?.settings?.providerInstances[instanceId]?.config;
310 if (configValue === null || typeof configValue !== "object") return undefined;
311 const agentId = "agentId" in configValue ? configValue.agentId : undefined;
312 return "source" in configValue && configValue.source === "local"
313 ? undefined
314 : typeof agentId === "string"
315 ? agentId.trim() || undefined
316 : undefined;
317 };
318 const badgeEntries = (config?.providers ?? []).map((provider) => ({
319 driverKind: provider.driver,
320 enabled: provider.enabled,
321 acpRegistryAgentId: registryAgentId(provider.instanceId),
322 }));
323 const providerBadgeFor = (instanceId: ModelSelection["instanceId"]) => {
324 const snapshot = config?.providers.find((provider) => provider.instanceId === instanceId);
325 const instance = config?.settings?.providerInstances[instanceId];
326 const driver = snapshot?.driver ?? instance?.driver;
327 if (!driver) return undefined;
328 const accentColor = normalizeProviderAccentColor(
329 snapshot?.accentColor ?? instance?.accentColor,
330 );
331 return {
332 displayName: resolveProviderInstanceDisplayName({
333 instanceId,
334 driver,
335 displayName: snapshot?.displayName ?? instance?.displayName,
336 }),
337 ...(accentColor ? { accentColor } : {}),
338 showBadge: shouldShowInstanceBadge(
339 {
340 driverKind: driver,
341 accentColor,
342 acpRegistryAgentId: registryAgentId(instanceId),
343 enabled: snapshot?.enabled ?? instance?.enabled ?? false,
344 },
345 badgeEntries,
346 ),
347 };
348 };
349​
350 for (const provider of config?.providers ?? []) {
351 if (
352 !matchesLockedProvider(provider) ||
353 isDriverDisallowed(config, provider.driver) ||
354 !isProviderEnabledForProject(config, provider, projectId) ||
355 !provider.installed ||
356 provider.auth.status === "unauthenticated" ||
357 (provider.driver === "antigravity" && provider.availability === "unavailable")
358 ) {
359 continue;
360 }
361​
362 const providerLabel = providerDisplayLabel(provider);
363 const providerBadge = providerBadgeFor(provider.instanceId);
364 const updateRequired = provider.updateRequiredModels?.length
365 ? { driver: provider.driver, updateRequiredModels: provider.updateRequiredModels }
366 : undefined;
367 const preferences = config?.settings?.providerModelPreferences?.[provider.instanceId];
368 const hiddenModels = new Set(preferences?.hiddenModels ?? []);
369 const order = new Map((preferences?.modelOrder ?? []).map((slug, index) => [slug, index]));
370 const models = provider.models.filter(
371 (model) => includeHidden || model.isCustom || !hiddenModels.has(model.slug),
372 );
373 if (order.size > 0)
374 models.sort((a, b) => (order.get(a.slug) ?? Infinity) - (order.get(b.slug) ?? Infinity));
375 for (const model of models) {
376 const key = `${provider.instanceId}:${model.slug}`;
377 const shortName = resolveModelShortName(model, provider.driver);
378 options.set(key, {
379 key,
380 label: model.name,
381 ...(shortName ? { shortName } : {}),
382 subtitle: model.subProvider ?? "",
383 providerKey: provider.instanceId,
384 providerLabel,
385 providerDriver: provider.driver,
386 providerBadge,
387 ...(provider.supportedRuntimeModes === undefined
388 ? {}
389 : { supportedRuntimeModes: provider.supportedRuntimeModes }),
390 ...(provider.iconUrl ? { providerIconUrl: provider.iconUrl } : {}),
391 isHidden: !model.isCustom && hiddenModels.has(model.slug),
392 canHide: !model.isCustom,
393 isDefault: model.isDefault === true,
394 isLegacy: model.isLegacy === true,
395 ...(updateRequired ? { providerUpdateRequired: updateRequired } : {}),
396 capabilities: model.capabilities,
397 selection: normalizeSelectionOptions(
398 {
399 instanceId: provider.instanceId,
400 model: model.slug,
401 },
402 model.capabilities,
403 ),
404 });
405 }
406 }
407​
408 if (
409 fallbackModelSelection &&
410 (providerInstanceId === undefined ||
411 fallbackModelSelection.instanceId === providerInstanceId) &&
412 (!config?.settings?.providerModelPreferences?.[
413 fallbackModelSelection.instanceId
414 ]?.hiddenModels.includes(fallbackModelSelection.model) ||
415 config?.providers
416 .find((provider) => provider.instanceId === fallbackModelSelection.instanceId)
417 ?.models.find((model) => model.slug === fallbackModelSelection.model)?.isCustom)
418 ) {
419 const key = `${fallbackModelSelection.instanceId}:${fallbackModelSelection.model}`;
420 const existing = options.get(key);
421 if (existing) {
422 options.set(key, {
423 ...existing,
424 selection:
425 existing.providerDriver === "antigravity"
426 ? fallbackModelSelection
427 : normalizeSelectionOptions(fallbackModelSelection, existing.capabilities),
428 });
429 } else {
430 const provider = config?.providers.find(
431 (candidate) => candidate.instanceId === fallbackModelSelection.instanceId,
432 );
433 const instanceConfig = config?.settings?.providerInstances[fallbackModelSelection.instanceId];
434 const model = provider?.models.find(
435 (candidate) => candidate.slug === fallbackModelSelection.model,
436 );
437 const providerDriver =
438 provider?.driver ?? instanceConfig?.driver ?? fallbackModelSelection.instanceId;
439 const providerLabel = providerDisplayLabel({
440 driver: providerDriver,
441 displayName: provider?.displayName ?? instanceConfig?.displayName,
442 instanceId: fallbackModelSelection.instanceId,
443 });
444 const shortName = model
445 ? resolveModelShortName(model, isProviderDriverKind(providerDriver) ? providerDriver : null)
446 : undefined;
447 options.set(key, {
448 key,
449 label: model?.name ?? fallbackModelSelection.model,
450 ...(shortName ? { shortName } : {}),
451 subtitle: model?.subProvider ?? "",
452 providerKey: fallbackModelSelection.instanceId,
453 providerLabel,
454 providerDriver,
455 providerBadge: providerBadgeFor(fallbackModelSelection.instanceId),
456 isDefault: false,
457 isLegacy: model?.isLegacy === true,
458 ...(isModelSelectionUnavailable(config, fallbackModelSelection, projectId) ||
459 (provider !== undefined && !isProviderEnabledForProject(config, provider, projectId)) ||
460 provider?.updateRequiredModels?.some((gated) => gated.slug === fallbackModelSelection.model)
461 ? { isUnavailable: true }
462 : {}),
463 capabilities: model?.capabilities ?? null,
464 selection: fallbackModelSelection,
465 });
466 }
467 }
468​
469 return [...options.values()];
471​
472export function groupByProvider(options: ReadonlyArray<ModelOption>): ReadonlyArray<ProviderGroup> {
473 const groups = new Map<
474 string,
475 {
476 providerLabel: string;
477 models: ModelOption[];
478 updateRequired: ProviderUpdateRequired | undefined;
479 }
480 >();
481 for (const option of options) {
482 const existing = groups.get(option.providerKey);
483 if (existing) {
484 existing.models.push(option);
485 existing.updateRequired ??= option.providerUpdateRequired;
486 } else {
487 groups.set(option.providerKey, {
488 providerLabel: option.providerLabel,
489 models: [option],
490 updateRequired: option.providerUpdateRequired,
491 });
492 }
493 }
494​
495 return [...groups.entries()].map(([providerKey, group]) => ({
496 providerKey,
497 providerLabel: group.providerLabel,
498 models: group.models,
499 ...(group.updateRequired ? { updateRequired: group.updateRequired } : {}),
500 }));