lib/ai-surfaces.ts
Rama main · réplica de lectura del repositorio original.
/**
* 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);
}