import Anthropic from "@anthropic-ai/sdk" import { isEncodable, type Encodable } from "@automate.ax/codec" import { isPlainObject } from "@zachsents/zippy" import type { CamelCasedPropertiesDeep } from "type-fest" import * as z from "zod" const ANTHROPIC_SECRET_SCHEMA = z.object({ apiKey: z.string().min(1), workspaceId: z.string().min(1).optional(), }) /** Camel-cased, codec-safe form of an official Anthropic SDK value. */ export type AnthropicValue = CamelCasedPropertiesDeep & Encodable /** Remaps Anthropic's bracketed timestamp filters to public camelCase keys. */ type RemapAnthropicQueryKeys = { [TKey in keyof T as TKey extends `created_at[${infer TOperator}]` ? `createdAt${Capitalize}` : TKey]: T[TKey] } /** Camel-cased official Anthropic parameter type accepted by an action. */ export type AnthropicInput = { [TKey in keyof CamelCasedPropertiesDeep< RemapAnthropicQueryKeys> >]: CamelCasedPropertiesDeep>>[TKey] } /** * Creates an authenticated Managed Agents client with bounded provider calls. * * @param secret - Resolved Anthropic account secret. */ export function getAnthropicApi(secret: Record) { const { apiKey, workspaceId } = ANTHROPIC_SECRET_SCHEMA.parse(secret) return new Anthropic({ apiKey, defaultHeaders: workspaceId ? { "anthropic-workspace-id": workspaceId } : undefined, maxRetries: 0, timeout: 50_000, }) } /** Runtime schema for an encodable Anthropic response. */ export function anthropicOutputSchema(): z.ZodType> { return z.custom>((value) => isEncodable(value), { message: "Expected an encodable Anthropic response.", }) } /** * Runtime schema for a camel-cased Anthropic parameter object. * * @param schema - Additional public input validation. * @param allowedKeys - Complete public top-level key set. */ export function anthropicInputSchema( schema: z.ZodType, allowedKeys: readonly string[], ): z.ZodType> { const allowedKeySet = new Set(allowedKeys) return z.custom>( (value) => isPlainObject(value) && isEncodable(value) && Object.keys(value).every((key) => allowedKeySet.has(key)) && schema.safeParse(value).success, { message: "Expected valid Anthropic parameters." }, ) } /** * Converts public camelCase input to Anthropic's wire keys. * * @param value - Public action input. */ export function toAnthropic(value: AnthropicInput): T { return z.custom().parse(transformKeys(value, "snake")) } /** * Converts an Anthropic response to stable public camelCase keys. * * @param value - Official client response. */ export function fromAnthropic(value: T): AnthropicValue { return anthropicOutputSchema().parse(transformKeys(value, "camel")) } /** * Returns a provider page without exposing SDK pagination objects. * * @param page - Official client page. * @param page.data - Page resources. * @param page.next_page - Forward cursor. * @param page.prev_page - Backward cursor. */ export function fromAnthropicPage(page: { data: T[] next_page?: string | null prev_page?: string | null }) { return fromAnthropic({ data: page.data, next_page: page.next_page ?? null, ...(Object.hasOwn(page, "prev_page") && { prev_page: page.prev_page ?? null, }), }) } const PRESERVED_RECORD_KEYS = new Set(["input", "inputSchema", "metadata"]) /** * Recursively converts provider-owned object keys. * * @param value - Value to traverse. * @param direction - Target key convention. */ function transformKeys(value: unknown, direction: "camel" | "snake"): unknown { if (Array.isArray(value)) { return value.map((item) => transformKeys(item, direction)) } if (!isPlainObject(value)) return value return Object.fromEntries( Object.entries(value).map(([key, item]) => { const transformedKey = direction === "camel" ? toCamelCase(key) : toSnakeCase(key) return [ transformedKey, PRESERVED_RECORD_KEYS.has(key) || PRESERVED_RECORD_KEYS.has(transformedKey) ? item : transformKeys(item, direction), ] }), ) } /** * Converts one snake-case key to camel case. * * @param value - Provider key. */ function toCamelCase(value: string): string { const queryOperator = /^(.+)\[(gt|gte|lt|lte)\]$/.exec(value) if (queryOperator) { return `${toCamelCase(queryOperator[1]!)}${queryOperator[2]![0]!.toUpperCase()}${queryOperator[2]!.slice(1)}` } return value.replace(/_([a-z0-9])/g, (_match, character: string) => character.toUpperCase(), ) } /** * Converts one camel-case key to snake case. * * @param value - Public key. */ function toSnakeCase(value: string): string { const queryOperator = /^(.*At)(Gte|Gt|Lte|Lt)$/.exec(value) if (queryOperator) { return `${toSnakeCase(queryOperator[1]!)}[${queryOperator[2]!.toLowerCase()}]` } return value.replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`) }