443 lines · 15.9 KB · TypeScript
RawDownload
import {  ANTIGRAVITY_DEFAULT_MODEL,  DEFAULT_TEXT_GENERATION_MODEL,  DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER,  defaultInstanceIdForDriver,  type ModelSelection,  ProviderDriverKind,  ProviderInstanceId,  type ServerProvider,  type ServerSettingsPatch,} from "@t3tools/contracts";import {  type CustomModelDefinition,  createModelSelection,  normalizeCustomModelSlug,  readCustomModelEntries,  resolveSelectableModel,} from "@t3tools/shared/model";import { getComposerProviderState } from "./components/chat/composerProviderState";import { UnifiedSettings } from "@t3tools/contracts/settings";import * as Arr from "effect/Array";import * as Result from "effect/Result";import {  getDefaultServerModel,  getProviderModels,  resolveSelectableProvider,} from "./providerModels";import { ModelEsque } from "./components/chat/providerIconUtils";import {  type ProviderInstanceEntry,  deriveProviderInstanceEntries,  NO_PROVIDER_MODEL_SELECTION,} from "./providerInstances";import { sortModelsForProviderInstance } from "./modelOrdering"; const MAX_CUSTOM_MODEL_COUNT = 32;export const MAX_CUSTOM_MODEL_LENGTH = 256;const DEFAULT_TEXT_GENERATION_INSTANCE_ID = ProviderInstanceId.make("codex"); /** * Resolve the custom-model list for a given instance, preferring the * instance's own `providerInstances[id].config.customModels` blob when * present and falling back to the legacy per-kind * `settings.providers[kind].customModels` bucket for default instances only. * * The Settings UI promotes the legacy bucket into an explicit * `providerInstances[defaultId]` entry on every edit (the "migrate on * first write" scheme documented in * `ProviderInstanceRegistryHydration`), so this helper exists primarily * so readers pick up that promotion immediately — and so first-time * viewers on pre-migration settings still see their legacy list on * default slots. Custom instances intentionally do not read the legacy * per-driver bucket; otherwise one custom model added to `claude_openrouter` * can appear on the stock `claudeAgent` instance. */function readInstanceCustomModels(  settings: UnifiedSettings,  instanceId: ProviderInstanceId,  driverKind: ProviderDriverKind,): ReadonlyArray<CustomModelDefinition> {  if (driverKind === "antigravity") return [];  const instance = settings.providerInstances?.[instanceId];  const config = instance?.config;  if (config !== null && typeof config === "object") {    const value = (config as Record<string, unknown>).customModels;    if (Array.isArray(value)) {      return readCustomModelEntries(value);    }  }  const defaultInstanceId = defaultInstanceIdForDriver(driverKind);  if (instanceId !== defaultInstanceId) {    return [];  }  const legacyProviders = settings.providers as Record<    string,    { readonly customModels: ReadonlyArray<unknown> } | undefined  >;  return readCustomModelEntries(legacyProviders[driverKind]?.customModels ?? []);} export interface AppModelOption {  slug: string;  name: string;  shortName?: string;  subProvider?: string;  aliases?: ReadonlyArray<string>;  badge?: "new";  isCustom: boolean;  isDefault?: boolean;  isLegacy?: boolean;  isUnavailable?: boolean;} function appendUnavailableDynamicModelSelection(  options: AppModelOption[],  rawModels: ReadonlyArray<ServerProvider["models"][number]>,  provider: ProviderDriverKind,  selectedModel: string | null | undefined,  hiddenModels: ReadonlyArray<string>,): AppModelOption[] {  if (provider !== "opencode" && provider !== "antigravity") return options;  const slug = normalizeCustomModelSlug(selectedModel);  if (!slug) return options;  if (provider === "antigravity" && slug === ANTIGRAVITY_DEFAULT_MODEL) return options;   // A model that exists in the raw catalog can be absent from `options`  // because the user hid it. Keep that preference authoritative.  if (resolveSelectableModel(provider, slug, rawModels) !== null) return options;  if (hiddenModels.includes(slug)) return options;  if (options.some((option) => option.slug === slug)) return options;   return [...options, { slug, name: slug, isCustom: false, isUnavailable: true }];} function toAppModelOption(model: ServerProvider["models"][number]): AppModelOption {  const option: AppModelOption = {    slug: model.slug,    name: model.name,    isCustom: model.isCustom,  };  if (model.shortName) option.shortName = model.shortName;  if (model.subProvider) option.subProvider = model.subProvider;  if (model.aliases) option.aliases = model.aliases;  if (model.badge) option.badge = model.badge;  if (model.isDefault) option.isDefault = true;  if (model.isLegacy) option.isLegacy = true;  return option;} function readInstanceModelPreferences(  settings: UnifiedSettings,  instanceId: ProviderInstanceId,): { readonly hiddenModels: ReadonlyArray<string>; readonly modelOrder: ReadonlyArray<string> } {  return (    settings.providerModelPreferences?.[instanceId] ?? {      hiddenModels: [],      modelOrder: [],    }  );} function applyInstanceModelPreferences(  options: ReadonlyArray<AppModelOption>,  preferences: {    readonly hiddenModels: ReadonlyArray<string>;    readonly modelOrder: ReadonlyArray<string>;  },): AppModelOption[] {  const hiddenModels = new Set(preferences.hiddenModels);  return sortModelsForProviderInstance(    options.filter((option) => option.isCustom || !hiddenModels.has(option.slug)),    { modelOrder: preferences.modelOrder },  );} function normalizeCustomModelEntries(  models: ReadonlyArray<CustomModelDefinition>,  builtInModelSlugs: ReadonlySet<string>,): CustomModelDefinition[] {  const normalizedModels: CustomModelDefinition[] = [];  const seen = new Set<string>();   for (const candidate of models) {    if (      candidate.slug.length > MAX_CUSTOM_MODEL_LENGTH ||      builtInModelSlugs.has(candidate.slug) ||      seen.has(candidate.slug)    ) {      continue;    }     seen.add(candidate.slug);    normalizedModels.push(candidate);    if (normalizedModels.length >= MAX_CUSTOM_MODEL_COUNT) {      break;    }  }   return normalizedModels;} function getAppModelOptions(  settings: UnifiedSettings,  providers: ReadonlyArray<ServerProvider>,  provider: ProviderDriverKind,  selectedModel?: string | null,): AppModelOption[] {  const rawModels = getProviderModels(providers, provider);  // Server-reported custom rows mirror settings and can lag a removal, so  // only built-ins are taken from the snapshot; custom rows are rebuilt from  // settings below.  const options: AppModelOption[] = rawModels    .filter((model) => !model.isCustom)    .map(toAppModelOption);  const seen = new Set(options.map((option) => option.slug));  const builtInModelSlugs = new Set(    Arr.filterMap(getProviderModels(providers, provider), (model) =>      model.isCustom ? Result.failVoid : Result.succeed(model.slug),    ),  );   // Read from the default instance's config first (that's where edits  // now land), falling back to the legacy per-kind bucket so unmigrated  // settings and the initial render before the first write both still  // see the user's authored custom models.  const defaultInstanceId = defaultInstanceIdForDriver(provider);  const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider);  for (const entry of normalizeCustomModelEntries(customModels, builtInModelSlugs)) {    if (seen.has(entry.slug)) {      continue;    }     seen.add(entry.slug);    options.push({ slug: entry.slug, name: entry.name, isCustom: true });  }   const preferences = readInstanceModelPreferences(settings, defaultInstanceId);  return appendUnavailableDynamicModelSelection(    applyInstanceModelPreferences(options, preferences),    rawModels,    provider,    selectedModel,    preferences.hiddenModels,  );} /** * Instance-scoped variant of {@link getAppModelOptions}. Built-in models * come from the instance's own `entry.models` snapshot (rather than the * first-matching-kind fallback in `getProviderModels`), so each custom * instance gets the precise model list its driver reported. Custom model * slugs come from the instance's own `providerInstances[id].config.customModels` * when present, falling back to the legacy per-kind * `settings.providers[driverKind].customModels` bucket for default * instances only. This keeps two instances of the same kind from leaking * custom slugs into each other. Custom rows reported by the server are * ignored so a slug removed in Settings disappears without waiting for the * next provider probe. */export function getAppModelOptionsForInstance(  settings: UnifiedSettings,  entry: ProviderInstanceEntry,  selectedModel?: string | null,): AppModelOption[] {  const options: AppModelOption[] = entry.models    .filter((model) => !model.isCustom)    .map(toAppModelOption);  const seen = new Set(options.map((option) => option.slug));  const builtInModelSlugs = new Set(    Arr.filterMap(entry.models, (model) =>      model.isCustom ? Result.failVoid : Result.succeed(model.slug),    ),  );   const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);  for (const custom of normalizeCustomModelEntries(customModels, builtInModelSlugs)) {    if (seen.has(custom.slug)) {      continue;    }     seen.add(custom.slug);    options.push({ slug: custom.slug, name: custom.name, isCustom: true });  }   const preferences = readInstanceModelPreferences(settings, entry.instanceId);  return appendUnavailableDynamicModelSelection(    applyInstanceModelPreferences(options, preferences),    entry.models,    entry.driverKind,    selectedModel,    preferences.hiddenModels,  );} export function resolveAppModelSelection(  provider: ProviderDriverKind,  settings: UnifiedSettings,  providers: ReadonlyArray<ServerProvider>,  selectedModel: string | null | undefined,): string {  const resolvedProvider = resolveSelectableProvider(providers, provider);  const options = getAppModelOptions(settings, providers, resolvedProvider, selectedModel);  return (    resolveSelectableModel(resolvedProvider, selectedModel, options) ??    getDefaultServerModel(providers, resolvedProvider)  );} export function resolveAppModelSelectionForInstance(  instanceId: ProviderInstanceId,  settings: UnifiedSettings,  providers: ReadonlyArray<ServerProvider>,  selectedModel: string | null | undefined,  resolutionOptions?: { readonly preserveUnavailableSelection?: boolean },): string | null {  const entry = deriveProviderInstanceEntries(providers).find(    (candidate) => candidate.instanceId === instanceId,  );  if (!entry) return null;  const options = getAppModelOptionsForInstance(    settings,    entry,    resolutionOptions?.preserveUnavailableSelection ? selectedModel : null,  );  const resolvedSelection = resolveSelectableModel(entry.driverKind, selectedModel, options);  if (resolvedSelection) {    return resolvedSelection;  }  if (    resolutionOptions?.preserveUnavailableSelection &&    (entry.driverKind === "opencode" || entry.driverKind === "antigravity")  ) {    const unavailableSelection = normalizeCustomModelSlug(selectedModel);    const hiddenModels = readInstanceModelPreferences(settings, entry.instanceId).hiddenModels;    if (      unavailableSelection &&      !hiddenModels.includes(unavailableSelection) &&      resolveSelectableModel(entry.driverKind, selectedModel, entry.models) === null &&      (entry.driverKind !== "antigravity" || unavailableSelection !== ANTIGRAVITY_DEFAULT_MODEL)    ) {      return unavailableSelection;    }  }  return options.find((option) => option.isDefault)?.slug ?? options[0]?.slug ?? null;} /** * Instance-keyed model options map. Each configured instance gets its own * option list so the model picker can show the same driver's built-in and * custom instances side by side without collapsing them. */export function getCustomModelOptionsByInstance(  settings: UnifiedSettings,  providers: ReadonlyArray<ServerProvider>,  selectedInstanceId?: ProviderInstanceId | null,  selectedModel?: string | null,): ReadonlyMap<ProviderInstanceId, ReadonlyArray<ModelEsque>> {  const out = new Map<ProviderInstanceId, ReadonlyArray<ModelEsque>>();  for (const entry of deriveProviderInstanceEntries(providers)) {    out.set(      entry.instanceId,      getAppModelOptionsForInstance(        settings,        entry,        entry.instanceId === selectedInstanceId ? selectedModel : null,      ),    );  }  return out;} /** * Drop the opencode "plan" agent option from a stored model selection. * Used when legacy plan mode is turned off so server-side text-generation * tasks (title, branch, PR) cannot keep dispatching the plan agent. */export function withoutPlanAgentSelection(  selection: ModelSelection | null | undefined,): ModelSelection | null | undefined {  if (!selection?.options) {    return selection;  }  const options = selection.options.filter(    (option) => !(option.id === "agent" && option.value === "plan"),  );  if (options.length === selection.options.length) {    return selection;  }  return createModelSelection(selection.instanceId, selection.model, options);} // The dropdown hides the opencode "plan" agent while legacy plan mode is off,// but the persisted text-generation selections are only healed when the toggle// flips. Users who already have plan mode off and a stored "plan" selection// never trip the toggle handler, so resolve the heal once per settings load.export function resolvePlanAgentHealPatch(input: {  readonly planModeEnabled: boolean;  readonly textGenerationModelSelection: ModelSelection | null | undefined;  readonly sourceControlWriterModelSelection: ModelSelection | null | undefined;}): ServerSettingsPatch | null {  if (input.planModeEnabled) {    return null;  }  const healedText = withoutPlanAgentSelection(input.textGenerationModelSelection);  const healedSourceControl = withoutPlanAgentSelection(input.sourceControlWriterModelSelection);  const patch: ServerSettingsPatch = {    ...(healedText && healedText !== input.textGenerationModelSelection      ? { textGenerationModelSelection: healedText }      : {}),    ...(healedSourceControl && healedSourceControl !== input.sourceControlWriterModelSelection      ? { sourceControlWriterModelSelection: healedSourceControl }      : {}),  };  return Object.keys(patch).length > 0 ? patch : null;} export function resolveAppModelSelectionState(  settings: UnifiedSettings,  providers: ReadonlyArray<ServerProvider>,): ModelSelection {  const selection = settings.textGenerationModelSelection ?? {    instanceId: DEFAULT_TEXT_GENERATION_INSTANCE_ID,    model: DEFAULT_TEXT_GENERATION_MODEL,  };  const supportedProviders = providers.filter(    (provider) => provider.supportsTextGeneration !== false,  );  const entries = deriveProviderInstanceEntries(supportedProviders);  const selectedEntry = entries.find(    (entry) => entry.instanceId === selection.instanceId && entry.enabled && entry.isAvailable,  );  const entry =    selectedEntry ?? entries.find((candidate) => candidate.enabled && candidate.isAvailable);  if (entry) {    // When the instance changed due to fallback (e.g. selected instance was disabled),    // don't carry over the old instance's model — use the fallback instance's default.    const selectedModel = selectedEntry ? selection.model : null;    const model =      resolveAppModelSelectionForInstance(        entry.instanceId,        settings,        supportedProviders,        selectedModel,      ) ??      entry.models[0]?.slug ??      DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER[entry.driverKind];    if (!model) {      return createModelSelection(entry.instanceId, "", []);    }    const provider = entry.driverKind;    const { modelOptionsForDispatch } = getComposerProviderState({      provider,      model,      models: entry.models,      modelOptions: selectedEntry ? selection.options : undefined,      planModeEnabled: settings.planModeEnabled,    });     return createModelSelection(entry.instanceId, model, modelOptionsForDispatch);  }   return NO_PROVIDER_MODEL_SELECTION;}