// src/lib/playground/codeExport.ts import { z } from "zod"; /** * Endpoint suportado pelo Playground Studio. Reflete os 13 endpoints da API OmniRoute * consumíveis pelas tabs Chat/Compare/Build/Search/Scrape via Export Code. * * D4-rev2 (2026-05-28): contrato expandido de 10 → 13 incluindo `responses`, `video`, * `music` para refletir a API real (`/v1/responses`, `/v1/videos/generations`, * `/v1/music/generations`). A tab API (Monaco editor) mantém seu próprio sistema * de endpoint values (D14) e não consome este type. */ export type PlaygroundEndpoint = | "chat.completions" | "responses" | "completions" | "embeddings" | "images" | "audio.transcriptions" | "audio.speech" | "video" | "music" | "moderations" | "rerank" | "search" | "web.fetch"; /** Linguagens de export suportadas. */ export type ExportLanguage = "curl" | "python" | "typescript"; /** Mensagem chat single-turn ou multi-turn. */ export interface ChatMessage { role: "system" | "user" | "assistant" | "tool"; content: string | Array<{ type: string; [k: string]: unknown }>; name?: string; tool_call_id?: string; } /** Tool definition no formato OpenAI Function Calling. */ export interface ToolDefinition { type: "function"; function: { name: string; description?: string; parameters: Record; }; } /** Estado completo capturável pelo Playground (subset de campos por endpoint). */ export interface PlaygroundState { endpoint: PlaygroundEndpoint; baseUrl: string; // ex.: "http://localhost:20128" model?: string; // não aplicável a web.fetch systemPrompt?: string; messages?: ChatMessage[]; // chat/completions prompt?: string; // completions/embeddings query?: string; // search/rerank url?: string; // web.fetch params?: Partial<{ temperature: number; max_tokens: number; top_p: number; presence_penalty: number; frequency_penalty: number; seed: number; stop: string | string[]; response_format: { type: "text" | "json_object" | "json_schema"; json_schema?: unknown }; }>; tools?: ToolDefinition[]; stream?: boolean; // Search-specific searchProvider?: string; searchType?: "web" | "news"; maxResults?: number; // Scrape-specific fetchProvider?: "firecrawl" | "jina-reader" | "tavily-search"; fetchFormat?: "markdown" | "html" | "links" | "screenshot"; fetchDepth?: 0 | 1 | 2; // Rerank-specific rerankModel?: string; documents?: string[]; } export const PlaygroundStateSchema = z.object({ endpoint: z.enum([ "chat.completions", "responses", "completions", "embeddings", "images", "audio.transcriptions", "audio.speech", "video", "music", "moderations", "rerank", "search", "web.fetch", ]), baseUrl: z.string().min(1), model: z.string().optional(), systemPrompt: z.string().optional(), messages: z.array(z.any()).optional(), prompt: z.string().optional(), query: z.string().optional(), url: z.string().optional(), params: z.record(z.string(), z.any()).optional(), tools: z.array(z.any()).optional(), stream: z.boolean().optional(), searchProvider: z.string().optional(), searchType: z.enum(["web", "news"]).optional(), maxResults: z.number().int().optional(), fetchProvider: z.enum(["firecrawl", "jina-reader", "tavily-search"]).optional(), fetchFormat: z.enum(["markdown", "html", "links", "screenshot"]).optional(), fetchDepth: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(), rerankModel: z.string().optional(), documents: z.array(z.string()).optional(), }); /** Constante: placeholder de API key — NUNCA embutir key real. */ export const API_KEY_PLACEHOLDER = "$OMNIROUTE_API_KEY"; /** * Resolve o path HTTP a partir do endpoint (ex.: "chat.completions" → "/v1/chat/completions"). * Exportado para reuso em testes. */ export function endpointToPath(endpoint: PlaygroundEndpoint): string { const map: Record = { "chat.completions": "/v1/chat/completions", responses: "/v1/responses", completions: "/v1/completions", embeddings: "/v1/embeddings", images: "/v1/images/generations", "audio.transcriptions": "/v1/audio/transcriptions", "audio.speech": "/v1/audio/speech", video: "/v1/videos/generations", music: "/v1/music/generations", moderations: "/v1/moderations", rerank: "/v1/rerank", search: "/v1/search", "web.fetch": "/v1/web/fetch", }; return map[endpoint]; } /** * Build the request body for a given endpoint+state. * Internal helper — returns plain object suitable for JSON.stringify. */ function buildBody(state: PlaygroundState): Record { const { endpoint, model, params, tools, stream } = state; switch (endpoint) { case "chat.completions": { let messages: ChatMessage[]; if (state.messages && state.messages.length > 0) { messages = state.messages; } else if (state.systemPrompt) { messages = [ { role: "system", content: state.systemPrompt }, { role: "user", content: state.prompt ?? "Hello!" }, ]; } else { messages = [{ role: "user", content: state.prompt ?? "Hello!" }]; } const body: Record = { model: model ?? "gpt-4o-mini", messages, stream: stream ?? false, }; if (params) Object.assign(body, params); if (tools && tools.length > 0) body.tools = tools; return body; } case "responses": { const body: Record = { model: model ?? "gpt-4o-mini", input: state.prompt ?? "Hello!", stream: stream ?? false, }; if (state.systemPrompt) body.instructions = state.systemPrompt; if (params) Object.assign(body, params); if (tools && tools.length > 0) body.tools = tools; return body; } case "completions": { const body: Record = { model: model ?? "gpt-3.5-turbo-instruct", prompt: state.prompt ?? "Hello,", stream: stream ?? false, }; if (params) Object.assign(body, params); return body; } case "embeddings": { return { model: model ?? "text-embedding-3-small", input: state.prompt ?? "Hello world", }; } case "images": { return { model: model ?? "dall-e-3", prompt: state.prompt ?? "A beautiful sunset", n: 1, size: "1024x1024", }; } case "audio.transcriptions": { // Note: actual usage needs multipart/form-data; shown as JSON for documentation return { model: model ?? "whisper-1", file: "", language: "en", }; } case "audio.speech": { return { model: model ?? "tts-1", input: state.prompt ?? "Hello, world!", voice: "alloy", }; } case "video": { const body: Record = { model: model ?? "sora-1.0", prompt: state.prompt ?? "A cinematic shot of a city at sunset", }; if (params) Object.assign(body, params); return body; } case "music": { const body: Record = { model: model ?? "music-1", prompt: state.prompt ?? "An upbeat lo-fi instrumental", }; if (params) Object.assign(body, params); return body; } case "moderations": { return { model: model ?? "text-moderation-latest", input: state.prompt ?? "Hello world", }; } case "rerank": { return { model: state.rerankModel ?? model ?? "rerank-english-v3.0", query: state.query ?? "search query", documents: state.documents ?? ["Document 1 text", "Document 2 text"], top_n: 3, }; } case "search": { const body: Record = { query: state.query ?? "search query", }; if (model) body.model = model; if (state.searchProvider) body.provider = state.searchProvider; if (state.searchType) body.search_type = state.searchType; if (state.maxResults) body.max_results = state.maxResults; return body; } case "web.fetch": { const body: Record = { url: state.url ?? "https://example.com", }; if (state.fetchProvider) body.provider = state.fetchProvider; if (state.fetchFormat) body.format = state.fetchFormat; if (state.fetchDepth != null) body.depth = state.fetchDepth; return body; } } } /** * Escape a string for safe embedding inside single-quoted shell literals. */ function escSingleQuote(s: string): string { return s.replace(/'/g, "'\\''"); } /** * Generate curl snippet for a given endpoint. */ function buildCurlSnippet(state: PlaygroundState): string { const path = endpointToPath(state.endpoint); const url = `${state.baseUrl}${path}`; const body = buildBody(state); const bodyJson = JSON.stringify(body, null, 2); const lines: string[] = [ `# Set your API key: export OMNIROUTE_API_KEY="your-key-here"`, `curl -s -X POST \\`, ` "${url}" \\`, ` -H "Authorization: Bearer ${API_KEY_PLACEHOLDER}" \\`, ` -H "Content-Type: application/json" \\`, ` -d '${escSingleQuote(JSON.stringify(body))}'`, ]; // Also show pretty body as a comment for readability const prettyLines = bodyJson.split("\n"); const commentBlock = prettyLines.map((l) => `# ${l}`).join("\n"); return `${lines.join("\n")}\n\n# Request body (pretty-printed for reference):\n${commentBlock}`; } /** * Generate Python (requests) snippet for a given endpoint. */ function buildPythonSnippet(state: PlaygroundState): string { const path = endpointToPath(state.endpoint); const url = `${state.baseUrl}${path}`; const body = buildBody(state); const bodyJson = JSON.stringify(body, null, 2); const lines: string[] = [ `# Set your API key: export ${API_KEY_PLACEHOLDER}="your-key-here"`, `import os`, `import json`, `import requests`, ``, `api_key = os.environ["OMNIROUTE_API_KEY"]`, ``, `url = "${url}"`, `headers = {`, ` "Authorization": f"Bearer {api_key}",`, ` "Content-Type": "application/json",`, `}`, ``, `data = json.loads("""`, bodyJson, `""")`, ``, `response = requests.post(url, headers=headers, json=data)`, `print(response.json())`, ]; return lines.join("\n"); } /** * Generate TypeScript (fetch) snippet for a given endpoint. */ function buildTypescriptSnippet(state: PlaygroundState): string { const path = endpointToPath(state.endpoint); const url = `${state.baseUrl}${path}`; const body = buildBody(state); const bodyJson = JSON.stringify(body, null, 2); const lines: string[] = [ `// Set your API key: export ${API_KEY_PLACEHOLDER}="your-key-here"`, `const apiKey = process.env.OMNIROUTE_API_KEY ?? "";`, ``, `const url = "${url}";`, `const body = ${bodyJson};`, ``, `const response = await fetch(url, {`, ` method: "POST",`, ` headers: {`, ` "Authorization": \`Bearer \${apiKey}\`,`, ` "Content-Type": "application/json",`, ` },`, ` body: JSON.stringify(body),`, `});`, ``, `const data = await response.json();`, `console.log(data);`, ]; return lines.join("\n"); } /** * Gera código para uma linguagem específica a partir do estado atual. * Sempre usa `API_KEY_PLACEHOLDER` para a API key (D11). */ export function exportCode(state: PlaygroundState, language: ExportLanguage): string { switch (language) { case "curl": return buildCurlSnippet(state); case "python": return buildPythonSnippet(state); case "typescript": return buildTypescriptSnippet(state); } } /** Gera os 3 snippets de uma vez (atalho para o ExportCodeModal de UI). */ export function exportAllLanguages(state: PlaygroundState): Record { return { curl: exportCode(state, "curl"), python: exportCode(state, "python"), typescript: exportCode(state, "typescript"), }; }