Ctrl K
ComunidadGuías…

lib/ai-surfaces.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * The AI ​​surfaces that an account can pass on its BYOK key (MIN-366).
 * This module remains pure: the API, client settings and server runtime
 * share exactly the same identifiers and the same default values.
 */

import type { AgentProviderId } from "./agent-providers";

export const AI_SURFACES = ["agent", "assistant", "automations", "voice", "feedback"] as const;

export type AiSurface = (typeof AI_SURFACES)[number];


/**
 * The call type that answers for a whole surface when a caller does not name
 * one: the conversation model for Numo, the main model of each other surface
 * for its capability.
 */
export function usageKeyFor(
  surface: AiSurface | undefined,
  capability: "text" | "transcription" | "embedding" = "text",
): ByokModelKey {
  switch (surface) {
    case "automations":
      return capability === "embedding" ? "issue_embedding_model" : "smart_fill_model";
    case "voice":
      return capability === "transcription" ? "transcription_model" : "dictate_model";
    case "feedback":
      return capability === "embedding" ? "feedback_embedding_model" : "feedback_analysis_model";
    default:
      return "assistant_model";
  }
}

/** The managed provider, next to the account's own credentials. */
export const MINDDY_CLOUD_PROVIDER = "minddy" as const;

/** A provider an account can choose for a usage or a conversation. */
export type AiProviderChoice = typeof MINDDY_CLOUD_PROVIDER | AgentProviderId;

/**
 * Call types whose template may differ. They correspond to the real
 * platform settings in app_config; OpenRouter BYOK can therefore take the
 * Minddy value of the same name without a fragile correspondence table.
 */
export const BYOK_MODEL_KEYS = [
  "assistant_model",
  "conversation_title_model",
  "web_search_model",
  "numo_wake_model",
  "home_brief_model",
  "smart_assign_model",
  "smart_fill_model",
  "smart_fill_title_model",
  "import_map_model",
  "brief_model",
  "issue_embedding_model",
  "issue_relation_model",
  "dictate_model",
  "transcription_model",
  "feedback_analysis_model",
  "feedback_embedding_model",
] as const;

export type ByokModelKey = (typeof BYOK_MODEL_KEYS)[number];

/**
 * The model roles (MIN-954). Every AI call is one of four kinds, and each
 * kind runs ONE model, chosen once: the LLM (Numo and every text task, from
 * conversation titles to Smart Fill and the feedback review), the embedding
 * model (issues and feedback alike), speech to text, and the decision model
 * (instance-wide, OpenRouter only). The call types above stay as call sites
 * and ledger labels; they no longer choose a model or a provider of their own.
 */
export const AI_MODEL_ROLES = ["llm", "embedding", "transcription"] as const;
export type AiModelRole = (typeof AI_MODEL_ROLES)[number];

/** The call type that stands for a role: its model and provider are the role's. */
export const ROLE_USAGE_KEYS: Record<AiModelRole, ByokModelKey> = {
  llm: "assistant_model",
  embedding: "issue_embedding_model",
  transcription: "transcription_model",
};

/** The role of a call type. */
export function roleOfUsageKey(usageKey: ByokModelKey): AiModelRole {
  if (usageKey === "issue_embedding_model" || usageKey === "feedback_embedding_model") return "embedding";
  if (usageKey === "transcription_model") return "transcription";
  return "llm";
}

/** The call type whose route and model a call type follows. */
export function roleUsageKey(usageKey: ByokModelKey): ByokModelKey {
  return ROLE_USAGE_KEYS[roleOfUsageKey(usageKey)];
}
export type ByokFeatureModels = Partial<Record<ByokModelKey, string>>;

export interface AiSurfaceDefinition {
  id: AiSurface;
  modelKeys: readonly ByokModelKey[];
}

export const AI_SURFACE_DEFINITIONS: readonly AiSurfaceDefinition[] = [
  // The agent model and its reasoning already have their dedicated group just below
  // surfaces in /settings; we do not make two concurrent controls.
  { id: "agent", modelKeys: [] },
  {
    id: "assistant",
    modelKeys: ["assistant_model", "conversation_title_model", "web_search_model", "numo_wake_model", "home_brief_model"],
  },
  {
    id: "automations",
    modelKeys: [
      "smart_assign_model",
      "smart_fill_model",
      "smart_fill_title_model",
      "import_map_model",
      "brief_model",
      "issue_embedding_model",
      "issue_relation_model",
    ],
  },
  { id: "voice", modelKeys: ["dictate_model", "transcription_model"] },
  {
    id: "feedback",
    modelKeys: ["feedback_analysis_model", "feedback_embedding_model"],
  },
] as const;

const AI_SURFACE_SET = new Set<string>(AI_SURFACES);
const BYOK_MODEL_KEY_SET = new Set<string>(BYOK_MODEL_KEYS);

export function isAiSurface(value: string): value is AiSurface {
  return AI_SURFACE_SET.has(value);
}

export function isByokModelKey(value: string): value is ByokModelKey {
  return BYOK_MODEL_KEY_SET.has(value);
}

/** App_config key of the pre-selected model for a native provider. */
export function byokFeatureDefaultModelKey(provider: string, modelKey: ByokModelKey): string {
  return `byok_default_${provider}_${modelKey}`;
}

/** Validation stricte d'un payload API. Un tableau vide signifie « quota Minddy partout ». */
export function parseAiSurfaces(value: unknown): AiSurface[] | null {
  if (!Array.isArray(value)) return null;
  const result: AiSurface[] = [];
  for (const entry of value) {
    if (typeof entry !== "string" || !isAiSurface(entry) || result.includes(entry)) return null;
    result.push(entry);
  }
  return result;
}

/**
 * Normalizes the template map sent by the account. An empty string clears the
 * choice and lets the default admin win; unknown keys are refused.
 */
export function parseByokFeatureModels(value: unknown): ByokFeatureModels | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const result: ByokFeatureModels = {};
  for (const [key, raw] of Object.entries(value)) {
    // Older clients may resend this retired override while saving another model.
    if (key === "smart_triage_model") continue;
    if (!isByokModelKey(key) || typeof raw !== "string" || raw.length > 300) return null;
    const model = raw.trim();
    if (model) result[key] = model;
  }
  return result;
}

export function surfaceForModelKey(modelKey: ByokModelKey): AiSurface {
  const definition = AI_SURFACE_DEFINITIONS.find((entry) => entry.modelKeys.includes(modelKey));
  if (!definition) throw new Error(`No AI surface for model key ${modelKey}`);
  return definition.id;
}

/** Chains and routines are automations even if they run the same agent. */
export function surfaceForAgentRun(run: {
  chain_id?: string | null;
  routine_id?: string | null;
}): Extract<AiSurface, "agent" | "automations"> {
  return run.chain_id || run.routine_id ? "automations" : "agent";
}

/**
 * Provider surface for a frozen code worker. Legacy automation runs retain the
 * surface they launched with; every run created under the account-only rule
 * uses the agent surface regardless of its trigger.
 */
export function workerModelSurfaceForAgentRun(run: {
  chain_id?: string | null;
  routine_id?: string | null;
  worker_model_source?: string | null;
}): Extract<AiSurface, "agent" | "automations"> {
  return run.worker_model_source === "account" ? "agent" : surfaceForAgentRun(run);
}