Соберите общий runtime вокруг registry
Registry - это то, что связывает публичное определение tool с его Zod-валидацией, классом риска и server-only handler в одном месте. Provider adapter получает только определения (то, что видит модель), а runtime владеет исполнением (то, что видит домен). Одна запись - одна ясная граница.
type ToolEntry = {
definition: ToolDefinition;
input: z.ZodType;
risk: "read" | "preview" | "terminal";
execute?: (value: unknown, context: TrustedToolContext) => Promise<unknown>;
};
export const toolRegistry = new Map<string, ToolEntry>([
["policy_search", policySearchEntry],
["order_read", orderReadEntry],
["refund_preview", refundPreviewEntry],
["handoff_prepare", handoffPrepareEntry],
["answer_user", answerUserTerminal],
["ask_clarification", clarificationTerminal],
["request_refund_approval", approvalTerminal],
["request_handoff", handoffTerminal]
]);
export const providerTools = Array.from(toolRegistry.values()).map(
function getDefinition(entry) {
return entry.definition;
}
);Server route для turn собирает всё вместе: проверяет сессию, применяет rate limit, строит доверенный контекст и запускает agent loop с выбранным провайдером.
const turnInput = z.object({
conversation_id: z.string().uuid(),
message: z.string().min(1).max(4_000),
provider: z.enum(["openai", "anthropic", "google"])
}).strict();
export async function POST(request: Request) {
const session = await requireSession(request);
await rateLimit.consume(`support:${session.tenantId}:${session.userId}`);
const body = turnInput.parse(await request.json());
const traceId = crypto.randomUUID();
const context: TrustedToolContext = {
actorId: session.userId,
tenantId: session.tenantId,
locale: session.locale,
traceId
};
const provider = providerFactory.get(body.provider);
const outcome = await runAgent(provider, {
message: body.message,
instructions: buildPrompt(session.locale),
tools: providerTools,
traceId
}, context);
return Response.json({ trace_id: traceId, outcome });
}Заметьте поле provider во входе - оно нужно для лаборатории и cross-provider eval, но в проде так делать нельзя.
Provider выбирается конфигурацией. В реальном продукте не позволяйте произвольному пользователю переключать провайдера. Поле показано для лаборатории и cross-provider eval; production routing определяется серверной политикой и tenant configuration.
Единый runtime собран, и он умеет работать с любым провайдером - потому что вся провайдерская специфика вынесена в адаптеры. Разберём их по одному, начиная с OpenAI.