From 25613e61763799ea218a7e993ddfe9a4e396ebd8 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 27 May 2026 21:28:47 -0300 Subject: [PATCH] feat(playground): add codeExport.ts generator (curl/python/typescript) Implements the shared codeExport.ts foundation for the Playground Studio. Generates curl/python/typescript snippets for all 10 endpoints (chat.completions, completions, embeddings, images, audio.transcriptions, audio.speech, moderations, rerank, search, web.fetch). Always uses $OMNIROUTE_API_KEY placeholder (D11). --- src/lib/playground/codeExport.ts | 364 +++++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 src/lib/playground/codeExport.ts diff --git a/src/lib/playground/codeExport.ts b/src/lib/playground/codeExport.ts new file mode 100644 index 0000000000..01bc457cc1 --- /dev/null +++ b/src/lib/playground/codeExport.ts @@ -0,0 +1,364 @@ +// src/lib/playground/codeExport.ts +import { z } from "zod"; + +/** + * Endpoint suportado pelo Playground Studio. Reflete os 10 endpoints da aba API + * + os endpoints consumidos pelas tabs Chat/Compare/Build/Search/Scrape. + */ +export type PlaygroundEndpoint = + | "chat.completions" + | "completions" + | "embeddings" + | "images" + | "audio.transcriptions" + | "audio.speech" + | "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", + "completions", + "embeddings", + "images", + "audio.transcriptions", + "audio.speech", + "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", + completions: "/v1/completions", + embeddings: "/v1/embeddings", + images: "/v1/images/generations", + "audio.transcriptions": "/v1/audio/transcriptions", + "audio.speech": "/v1/audio/speech", + 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 "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 "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"), + }; +}