mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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:
364
src/lib/playground/codeExport.ts
Normal file
364
src/lib/playground/codeExport.ts
Normal 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"),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user