1import { pipe } from "effect/Function";
2import {
3 DEFAULT_MODEL_BY_PROVIDER,
4 defaultInstanceIdForDriver,
5 resolveProjectProviderInstanceEnabled,
6 type ModelSelection,
7 type ProjectId,
8 type ProviderDriverKind,
9 ProviderInstanceId,
10 type ServerProvider,
11 type ServerProviderModel,
12 type ServerSettings,
13 type ServerProviderState,
14} from "@t3tools/contracts";
15import {
16 normalizeProviderAccentColor,
17 resolveProviderInstanceDisplayName,
18 shouldShowInstanceBadge,
19} from "@t3tools/client-runtime/state/provider-instance-display";
20
21export { normalizeProviderAccentColor, shouldShowInstanceBadge };
22
23/** Placeholder for untargetable drafts. Never persist or dispatch it; disable send until replaced. */
24export const NO_PROVIDER_MODEL_SELECTION: ModelSelection = {
25 instanceId: ProviderInstanceId.make("t3code_no_provider"),
26 model: "",
27};
28
29export interface ProviderInstanceEntry {
30 readonly instanceId: ProviderInstanceId;
31 readonly driverKind: ProviderDriverKind;
32 readonly displayName: string;
33 readonly accentColor?: string | undefined;
34 readonly acpRegistryAgentId?: string | undefined;
35 readonly acpRegistryIconUrl?: string | undefined;
36 readonly continuationGroupKey?: string | undefined;
37 readonly enabled: boolean;
38 readonly installed: boolean;
39 readonly status: ServerProviderState;
40 readonly isDefault: boolean;
41 readonly isAvailable: boolean;
42 readonly snapshot: ServerProvider;
43 readonly models: ReadonlyArray<ServerProviderModel>;
44}
45
46/** Disabled instances may retain ready status until reconciliation. */
47export function isProviderInstancePickerReady(entry: ProviderInstanceEntry): boolean {
48 return entry.enabled && entry.isAvailable && entry.status === "ready";
49}
50
51export function isProviderInstancePickerVisible(entry: ProviderInstanceEntry): boolean {
52 return entry.enabled;
53}
54
55export function deriveProviderInstanceEntries(
56 providers: ReadonlyArray<ServerProvider>,
57): ReadonlyArray<ProviderInstanceEntry> {
58 return providers.map((snapshot) => {
59 const instanceId = snapshot.instanceId;
60 const driverKind = snapshot.driver;
61 const defaultId = defaultInstanceIdForDriver(driverKind);
62 const isDefault = instanceId === defaultId;
63 return {
64 instanceId,
65 driverKind,
66 displayName: resolveProviderInstanceDisplayName(snapshot),
67 accentColor: normalizeProviderAccentColor(snapshot.accentColor),
68 ...(driverKind === "acpRegistry" && snapshot.iconUrl
69 ? { acpRegistryIconUrl: snapshot.iconUrl }
70 : {}),
71 continuationGroupKey: snapshot.continuation?.groupKey,
72 enabled: snapshot.enabled,
73 installed: snapshot.installed,
74 status: snapshot.status,
75 isDefault,
76 isAvailable: snapshot.availability !== "unavailable",
77 snapshot,
78 models: snapshot.models,
79 } satisfies ProviderInstanceEntry;
80 });
81}
82
83/** Instance ids repeat across environments; lookups must stay scoped. */
84export function deriveProviderEntriesByEnvironment(
85 providersByEnvironment: Iterable<
86 readonly [
87 string,
88 ReadonlyArray<ServerProvider>,
89 Pick<ServerSettings, "providerInstances" | "projectSettingsOverrides">?,
90 ]
91 >,
92): ReadonlyMap<string, ReadonlyMap<string, ProviderInstanceEntry>> {
93 const byEnvironment = new Map<string, ReadonlyMap<string, ProviderInstanceEntry>>();
94 for (const [environmentId, providers, settings] of providersByEnvironment) {
95 const derived = deriveProviderInstanceEntries(providers);
96 const entries = settings ? brandProviderInstanceEntries(derived, settings) : derived;
97 byEnvironment.set(
98 environmentId,
99 new Map(entries.map((entry) => [entry.instanceId as string, entry] as const)),
100 );
101 }
102 return byEnvironment;
103}
104
105// The snapshot includes instances enabled by any project; machine settings alone do not.
106export function brandProviderInstanceEntries(
107 entries: ReadonlyArray<ProviderInstanceEntry>,
108 settings: Pick<ServerSettings, "providerInstances">,
109): ReadonlyArray<ProviderInstanceEntry> {
110 return entries.map((entry) => applyProviderInstanceBranding(entry, settings));
111}
112
113/** projectId applies project overrides; explicit null uses machine settings. */
114export function applyProviderInstanceSettings(
115 entries: ReadonlyArray<ProviderInstanceEntry>,
116 settings: Pick<ServerSettings, "providerInstances" | "projectSettingsOverrides">,
117 projectId: ProjectId | null,
118): ReadonlyArray<ProviderInstanceEntry> {
119 return entries.map((entry) => {
120 const enabled = resolveProjectProviderInstanceEnabled(settings, projectId, entry.instanceId);
121 return applyProviderInstanceBranding(
122 enabled === entry.enabled ? entry : { ...entry, enabled },
123 settings,
124 );
125 });
126}
127
128function applyProviderInstanceBranding(
129 entry: ProviderInstanceEntry,
130 settings: Pick<ServerSettings, "providerInstances">,
131): ProviderInstanceEntry {
132 const explicitInstance = Object.hasOwn(settings.providerInstances, entry.instanceId)
133 ? settings.providerInstances[entry.instanceId]
134 : undefined;
135 if (entry.driverKind !== "acpRegistry" || explicitInstance === undefined) return entry;
136 const config =
137 explicitInstance.config !== null && typeof explicitInstance.config === "object"
138 ? (explicitInstance.config as Readonly<Record<string, unknown>>)
139 : null;
140 if (config?.source === "local") {
141 return { ...entry, acpRegistryAgentId: undefined, acpRegistryIconUrl: undefined };
142 }
143 const agentId = config?.agentId;
144 const iconUrl = config?.registryIconUrl;
145 return {
146 ...entry,
147 ...(typeof agentId === "string" && agentId.trim()
148 ? { acpRegistryAgentId: agentId.trim() }
149 : {}),
150 ...(typeof iconUrl === "string" && iconUrl.trim()
151 ? { acpRegistryIconUrl: iconUrl.trim() }
152 : {}),
153 };
154}
155
156/** Picker order: each driver's default instance first, then its custom instances. */
157export function providerInstanceEntriesFor(
158 providers: ReadonlyArray<ServerProvider>,
159 settings: Pick<ServerSettings, "providerInstances" | "projectSettingsOverrides">,
160 projectId: ProjectId | null,
161): ReadonlyArray<ProviderInstanceEntry> {
162 return pipe(
163 deriveProviderInstanceEntries(providers),
164 (entries) => applyProviderInstanceSettings(entries, settings, projectId),
165 sortProviderInstanceEntries,
166 );
167}
168
169function sortProviderInstanceEntries(
170 entries: ReadonlyArray<ProviderInstanceEntry>,
171): ReadonlyArray<ProviderInstanceEntry> {
172 const byKind = new Map<ProviderDriverKind, ProviderInstanceEntry[]>();
173 for (const entry of entries) {
174 const bucket = byKind.get(entry.driverKind);
175 if (bucket) {
176 bucket.push(entry);
177 } else {
178 byKind.set(entry.driverKind, [entry]);
179 }
180 }
181 const sorted: ProviderInstanceEntry[] = [];
182 for (const bucket of byKind.values()) {
183 const defaults = bucket.filter((entry) => entry.isDefault);
184 const customs = bucket.filter((entry) => !entry.isDefault);
185 sorted.push(...defaults, ...customs);
186 }
187 return sorted;
188}
189
190function getProviderInstanceEntry(
191 providers: ReadonlyArray<ServerProvider>,
192 instanceId: ProviderInstanceId,
193): ProviderInstanceEntry | undefined {
194 return deriveProviderInstanceEntries(providers).find((entry) => entry.instanceId === instanceId);
195}
196
197export function getDefaultProviderInstanceModel(
198 providers: ReadonlyArray<ServerProvider>,
199 instanceId: ProviderInstanceId,
200): string | undefined {
201 const entry = getProviderInstanceEntry(providers, instanceId);
202 if (!entry) return undefined;
203 return (
204 entry.models.find((model) => model.isDefault && !model.isCustom)?.slug ??
205 entry.models.find((model) => !model.isCustom)?.slug ??
206 entry.models[0]?.slug ??
207 DEFAULT_MODEL_BY_PROVIDER[entry.driverKind]
208 );
209}
210
211const isSelectableProviderInstanceEntry = (entry: ProviderInstanceEntry): boolean =>
212 entry.enabled && entry.isAvailable;
213
214export function resolveSelectableProviderInstanceEntry(
215 entries: ReadonlyArray<ProviderInstanceEntry>,
216 instanceId: ProviderInstanceId | undefined,
217): ProviderInstanceEntry | undefined {
218 if (instanceId !== undefined) {
219 const requested = entries.find((entry) => entry.instanceId === instanceId);
220 if (requested && isSelectableProviderInstanceEntry(requested)) {
221 return requested;
222 }
223 }
224 return (
225 entries.find(isProviderInstancePickerReady) ??
226 entries.find((entry) => isSelectableProviderInstanceEntry(entry) && entry.status !== "error")
227 );
228}
229
230export function resolveSelectableProviderInstance(
231 providers: ReadonlyArray<ServerProvider>,
232 instanceId: ProviderInstanceId | undefined,
233): ProviderInstanceId | undefined {
234 const entries = deriveProviderInstanceEntries(providers);
235 return resolveSelectableProviderInstanceEntry(entries, instanceId)?.instanceId;
236}
237
238export function resolveDefaultProviderModelSelection(
239 providers: ReadonlyArray<ServerProvider>,
240 selection: ModelSelection | null | undefined,
241): ModelSelection | null {
242 const instanceId = resolveSelectableProviderInstance(providers, selection?.instanceId);
243 if (instanceId === undefined) return null;
244 if (selection?.instanceId === instanceId) return selection;
245 const model = getDefaultProviderInstanceModel(providers, instanceId);
246 return model ? { instanceId, model } : null;
247}
248
249export function resolveProjectDefaultProviderModelSelection(
250 providers: ReadonlyArray<ServerProvider>,
251 settings: ServerSettings,
252 projectId: ProjectId | null,
253): ModelSelection | null {
254 const entries = providerInstanceEntriesFor(providers, settings, projectId);
255 // Matches resolveComposerProviderSelection: the first driver group wins before any other.
256 const entry =
257 resolveSelectableProviderInstanceEntry(
258 entries.filter((candidate) => candidate.driverKind === entries[0]?.driverKind),
259 undefined,
260 ) ?? resolveSelectableProviderInstanceEntry(entries, undefined);
261 const model = entry ? getDefaultProviderInstanceModel(providers, entry.instanceId) : undefined;
262 return entry && model ? { instanceId: entry.instanceId, model } : null;
263}