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).
This commit is contained in:
diegosouzapw
2026-05-27 21:28:47 -03:00
parent 722e9f41cd
commit 25613e6176

View File

@@ -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<string, unknown>;
};
}
/** 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<PlaygroundEndpoint, string> = {
"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<string, unknown> {
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<string, unknown> = {
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<string, unknown> = {
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: "<audio-file-binary>",
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<string, unknown> = {
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<string, unknown> = {
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<ExportLanguage, string> {
return {
curl: exportCode(state, "curl"),
python: exportCode(state, "python"),
typescript: exportCode(state, "typescript"),
};
}