mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
409 lines
12 KiB
TypeScript
409 lines
12 KiB
TypeScript
// 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<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",
|
|
"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<PlaygroundEndpoint, string> = {
|
|
"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<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 "responses": {
|
|
const body: Record<string, unknown> = {
|
|
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<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 "video": {
|
|
const body: Record<string, unknown> = {
|
|
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<string, unknown> = {
|
|
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<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"),
|
|
};
|
|
}
|