/** * 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 = { 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>; 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(AI_SURFACES); const BYOK_MODEL_KEY_SET = new Set(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 { 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 { return run.worker_model_source === "account" ? "agent" : surfaceForAgentRun(run); }