merge origin/master into gitea/new_feature

Bring local branch up to v0.5.35 while keeping xAI image/edit, SuperGrok
quota tracking, per-provider timeouts, and pinned model-test actions.
This commit is contained in:
2026-07-17 15:31:47 +07:00
224 changed files with 21037 additions and 1643 deletions

View File

@@ -0,0 +1,871 @@
/**
* Public AI API catalog for the dashboard API Explorer.
* Single source of truth for endpoint docs + form fields used by ApiExplorerModal.
*/
/** @typedef {"string"|"number"|"boolean"|"select"|"textarea"|"json"|"file"|"messages"} FieldType */
/**
* @typedef {Object} ApiField
* @property {string} key
* @property {string} label
* @property {FieldType} type
* @property {boolean} [required]
* @property {string} [description]
* @property {*} [default]
* @property {string[]} [options]
* @property {number} [min]
* @property {number} [max]
* @property {number} [step]
* @property {string} [placeholder]
* @property {string} [accept] - file accept attr
* @property {"body"|"query"|"header"} [location] - default body
* @property {boolean} [advanced] - hide behind "Advanced"
*/
/**
* @typedef {Object} ApiEndpoint
* @property {string} id
* @property {string} method
* @property {string} path
* @property {string} label
* @property {string} description
* @property {string} icon
* @property {string} category
* @property {"json"|"multipart"|"none"} contentType
* @property {boolean} [needsModel]
* @property {string} [modelsKind] - /v1/models/{kind} query
* @property {ApiField[]} fields
* @property {string} [defaultResponse]
* @property {boolean} [supportsStream]
*/
/** @type {ApiEndpoint[]} */
export const API_CATALOG = [
{
id: "models",
method: "GET",
path: "/v1/models",
label: "List Models",
description: "List chat/LLM models (default). Combos appear with owned_by: combo.",
icon: "list_alt",
category: "Discovery",
contentType: "none",
fields: [],
defaultResponse: `{\n "object": "list",\n "data": [\n { "id": "openai/gpt-4o", "object": "model", "owned_by": "openai" }\n ]\n}`,
},
{
id: "models-kind",
method: "GET",
path: "/v1/models/{kind}",
label: "List Models by Kind",
description: "List models for a capability kind: image, tts, stt, embedding, web, etc.",
icon: "category",
category: "Discovery",
contentType: "none",
fields: [
{
key: "kind",
label: "Kind",
type: "select",
required: true,
location: "path",
default: "image",
options: ["image", "tts", "stt", "embedding", "web", "image-to-text"],
description: "Path segment: /v1/models/{kind}. Chat models use GET /v1/models (no kind).",
},
],
defaultResponse: `{\n "object": "list",\n "data": [{ "id": "openai/dall-e-3", "object": "model", "owned_by": "openai" }]\n}`,
},
{
id: "models-info",
method: "GET",
path: "/v1/models/info",
label: "Model Info",
description: "Per-model metadata: params, options, contextWindow, capabilities.",
icon: "info",
category: "Discovery",
contentType: "none",
fields: [
{
key: "id",
label: "Model ID",
type: "string",
required: true,
location: "query",
placeholder: "openai/gpt-4o",
description: "Query: ?id=provider/model",
},
],
defaultResponse: `{\n "id": "openai/gpt-4o",\n "kind": "llm",\n "endpoint": "/v1/chat/completions",\n "contextWindow": 128000\n}`,
},
{
id: "chat-completions",
method: "POST",
path: "/v1/chat/completions",
label: "Chat Completions",
description: "OpenAI-compatible chat/code generation with streaming + auto-fallback.",
icon: "chat",
category: "Chat",
contentType: "json",
needsModel: true,
modelsKind: "llm",
supportsStream: true,
fields: [
{
key: "messages",
label: "Messages",
type: "messages",
required: true,
default: [{ role: "user", content: "Hello! Say hi in one sentence." }],
description: "Array of { role, content }",
},
{
key: "stream",
label: "Stream",
type: "boolean",
default: false,
description: "SSE streaming response",
},
{
key: "temperature",
label: "Temperature",
type: "number",
default: "",
min: 0,
max: 2,
step: 0.1,
advanced: true,
},
{
key: "top_p",
label: "Top P",
type: "number",
default: "",
min: 0,
max: 1,
step: 0.05,
advanced: true,
},
{
key: "max_tokens",
label: "Max Tokens",
type: "number",
default: "",
min: 1,
advanced: true,
},
{
key: "max_completion_tokens",
label: "Max Completion Tokens",
type: "number",
default: "",
min: 1,
advanced: true,
},
{
key: "presence_penalty",
label: "Presence Penalty",
type: "number",
default: "",
min: -2,
max: 2,
step: 0.1,
advanced: true,
},
{
key: "frequency_penalty",
label: "Frequency Penalty",
type: "number",
default: "",
min: -2,
max: 2,
step: 0.1,
advanced: true,
},
{
key: "seed",
label: "Seed",
type: "number",
default: "",
advanced: true,
},
{
key: "stop",
label: "Stop",
type: "json",
default: "",
placeholder: '["\\n"] or "stop"',
advanced: true,
description: "String or array of stop sequences",
},
{
key: "tools",
label: "Tools",
type: "json",
default: "",
placeholder: "[{ type: \"function\", function: {...} }]",
advanced: true,
},
{
key: "tool_choice",
label: "Tool Choice",
type: "json",
default: "",
placeholder: '"auto" | "none" | { type, function }',
advanced: true,
},
{
key: "response_format",
label: "Response Format",
type: "json",
default: "",
placeholder: '{ "type": "json_object" }',
advanced: true,
},
{
key: "reasoning_effort",
label: "Reasoning Effort",
type: "select",
default: "",
options: ["", "low", "medium", "high"],
advanced: true,
},
{
key: "thinking",
label: "Thinking",
type: "json",
default: "",
placeholder: '{ "type": "enabled", "budget_tokens": 1024 }',
advanced: true,
},
{
key: "n",
label: "n",
type: "number",
default: "",
min: 1,
max: 8,
advanced: true,
},
{
key: "user",
label: "User",
type: "string",
default: "",
advanced: true,
},
],
defaultResponse: `{\n "id": "chatcmpl-...",\n "object": "chat.completion",\n "choices": [{ "message": { "role": "assistant", "content": "..." }, "finish_reason": "stop" }],\n "usage": { "prompt_tokens": 8, "completion_tokens": 2, "total_tokens": 10 }\n}`,
},
{
id: "messages",
method: "POST",
path: "/v1/messages",
label: "Messages (Anthropic)",
description: "Anthropic Messages API format. Same models, Claude-compatible tools/clients.",
icon: "forum",
category: "Chat",
contentType: "json",
needsModel: true,
modelsKind: "llm",
supportsStream: true,
fields: [
{
key: "messages",
label: "Messages",
type: "messages",
required: true,
default: [{ role: "user", content: "Hello! Say hi in one sentence." }],
},
{
key: "max_tokens",
label: "Max Tokens",
type: "number",
required: true,
default: 1024,
min: 1,
},
{
key: "stream",
label: "Stream",
type: "boolean",
default: false,
},
{
key: "system",
label: "System",
type: "textarea",
default: "",
placeholder: "You are a helpful assistant.",
advanced: true,
},
{
key: "temperature",
label: "Temperature",
type: "number",
default: "",
min: 0,
max: 1,
step: 0.1,
advanced: true,
},
{
key: "top_p",
label: "Top P",
type: "number",
default: "",
min: 0,
max: 1,
step: 0.05,
advanced: true,
},
{
key: "top_k",
label: "Top K",
type: "number",
default: "",
min: 0,
advanced: true,
},
{
key: "stop_sequences",
label: "Stop Sequences",
type: "json",
default: "",
placeholder: '["\\n\\nHuman:"]',
advanced: true,
},
{
key: "tools",
label: "Tools",
type: "json",
default: "",
advanced: true,
},
{
key: "tool_choice",
label: "Tool Choice",
type: "json",
default: "",
advanced: true,
},
{
key: "thinking",
label: "Thinking",
type: "json",
default: "",
placeholder: '{ "type": "enabled", "budget_tokens": 1024 }',
advanced: true,
},
{
key: "anthropic-version",
label: "anthropic-version",
type: "string",
location: "header",
default: "2023-06-01",
advanced: true,
},
],
defaultResponse: `{\n "id": "msg_...",\n "type": "message",\n "role": "assistant",\n "content": [{ "type": "text", "text": "Hello!" }],\n "stop_reason": "end_turn",\n "usage": { "input_tokens": 8, "output_tokens": 2 }\n}`,
},
{
id: "responses",
method: "POST",
path: "/v1/responses",
label: "Responses API",
description: "OpenAI Responses format (Codex / OpenClaw). input can be string or items[].",
icon: "reply",
category: "Chat",
contentType: "json",
needsModel: true,
modelsKind: "llm",
supportsStream: true,
fields: [
{
key: "input",
label: "Input",
type: "textarea",
required: true,
default: "Hello! Say hi in one sentence.",
description: "String or JSON array of input items",
},
{
key: "stream",
label: "Stream",
type: "boolean",
default: false,
},
{
key: "instructions",
label: "Instructions",
type: "textarea",
default: "",
advanced: true,
},
{
key: "temperature",
label: "Temperature",
type: "number",
default: "",
min: 0,
max: 2,
step: 0.1,
advanced: true,
},
{
key: "max_output_tokens",
label: "Max Output Tokens",
type: "number",
default: "",
min: 1,
advanced: true,
},
{
key: "tools",
label: "Tools",
type: "json",
default: "",
advanced: true,
},
{
key: "reasoning",
label: "Reasoning",
type: "json",
default: "",
placeholder: '{ "effort": "medium" }',
advanced: true,
},
],
defaultResponse: `{\n "id": "resp_...",\n "object": "response",\n "status": "completed",\n "output": [{ "type": "message", "content": [{ "type": "output_text", "text": "..." }] }]\n}`,
},
{
id: "count-tokens",
method: "POST",
path: "/v1/messages/count_tokens",
label: "Count Tokens",
description: "Estimate token count for Anthropic-style messages (local estimate).",
icon: "tag",
category: "Chat",
contentType: "json",
needsModel: true,
modelsKind: "llm",
fields: [
{
key: "messages",
label: "Messages",
type: "messages",
required: true,
default: [{ role: "user", content: "Hello world" }],
},
{
key: "system",
label: "System",
type: "textarea",
default: "",
advanced: true,
},
],
defaultResponse: `{\n "input_tokens": 12\n}`,
},
{
id: "embeddings",
method: "POST",
path: "/v1/embeddings",
label: "Embeddings",
description: "Vector embeddings for RAG / semantic search.",
icon: "scatter_plot",
category: "Embeddings",
contentType: "json",
needsModel: true,
modelsKind: "embedding",
fields: [
{
key: "input",
label: "Input",
type: "textarea",
required: true,
default: "Hello world",
description: "String or JSON array of strings",
},
{
key: "encoding_format",
label: "Encoding",
type: "select",
default: "float",
options: ["float", "base64"],
},
{
key: "dimensions",
label: "Dimensions",
type: "number",
default: "",
min: 1,
advanced: true,
description: "OpenAI text-embedding-3-* only",
},
],
defaultResponse: `{\n "object": "list",\n "data": [{ "object": "embedding", "index": 0, "embedding": [0.01, -0.02, "..."] }],\n "usage": { "prompt_tokens": 2, "total_tokens": 2 }\n}`,
},
{
id: "images-generations",
method: "POST",
path: "/v1/images/generations",
label: "Image Generation",
description: "Text-to-image (DALL·E, Imagen, FLUX, Codex, MiniMax…). Supports binary output.",
icon: "brush",
category: "Media",
contentType: "json",
needsModel: true,
modelsKind: "image",
fields: [
{
key: "prompt",
label: "Prompt",
type: "textarea",
required: true,
default: "A cute cat wearing a hat, watercolor style",
},
{
key: "n",
label: "n",
type: "number",
default: 1,
min: 1,
max: 4,
},
{
key: "size",
label: "Size",
type: "select",
default: "auto",
options: ["auto", "1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"],
},
{
key: "aspect_ratio",
label: "Aspect Ratio",
type: "select",
default: "",
options: ["", "auto", "1:1", "16:9", "9:16", "4:3", "3:2", "2:3", "9:19.5", "20:9"],
advanced: true,
},
{
key: "resolution",
label: "Resolution",
type: "select",
default: "",
options: ["", "1k", "2k"],
advanced: true,
},
{
key: "quality",
label: "Quality",
type: "select",
default: "auto",
options: ["auto", "low", "medium", "high", "standard", "hd"],
advanced: true,
},
{
key: "background",
label: "Background",
type: "select",
default: "auto",
options: ["auto", "transparent", "opaque"],
advanced: true,
},
{
key: "style",
label: "Style",
type: "select",
default: "",
options: ["", "vivid", "natural"],
advanced: true,
},
{
key: "response_format",
label: "Response Format",
type: "select",
default: "url",
options: ["url", "b64_json", "binary"],
description: "binary uses ?response_format=binary (raw image bytes)",
},
{
key: "output_format",
label: "Codec",
type: "select",
default: "png",
options: ["png", "jpeg", "webp"],
advanced: true,
},
{
key: "image",
label: "Reference Image",
type: "image",
default: "",
placeholder: "https://... or upload an image",
advanced: true,
description: "Edit / img2img when provider supports it. Paste URL or upload file (stored as data URL).",
accept: "image/*",
},
],
defaultResponse: `{\n "created": 1735000000,\n "data": [{ "url": "https://..." }]\n}`,
},
{
id: "audio-speech",
method: "POST",
path: "/v1/audio/speech",
label: "Text to Speech",
description: "OpenAI / ElevenLabs / Edge / Google / Deepgram voices → audio bytes.",
icon: "record_voice_over",
category: "Media",
contentType: "json",
needsModel: true,
modelsKind: "tts",
fields: [
{
key: "input",
label: "Text",
type: "textarea",
required: true,
default: "Hello from 9Router!",
},
{
key: "response_format",
label: "Response Format",
type: "select",
location: "query",
default: "mp3",
options: ["mp3", "json"],
description: "mp3 = raw audio; json = { audio: base64, format }",
},
{
key: "language",
label: "Language Hint",
type: "string",
default: "",
placeholder: "en, vi, ...",
advanced: true,
description: "Optional language hint (e.g. Gemini)",
},
{
key: "voice",
label: "Voice",
type: "string",
default: "",
placeholder: "alloy (provider-dependent)",
advanced: true,
},
{
key: "speed",
label: "Speed",
type: "number",
default: "",
min: 0.25,
max: 4,
step: 0.05,
advanced: true,
},
],
defaultResponse: "(binary audio/mp3)",
},
{
id: "audio-voices",
method: "GET",
path: "/v1/audio/voices",
label: "List Voices",
description: "List TTS voices for elevenlabs, edge-tts, deepgram, inworld, local-device.",
icon: "voice_selection",
category: "Media",
contentType: "none",
fields: [
{
key: "provider",
label: "Provider",
type: "select",
required: true,
location: "query",
default: "edge-tts",
options: ["edge-tts", "elevenlabs", "deepgram", "inworld", "local-device"],
},
{
key: "lang",
label: "Language",
type: "string",
location: "query",
default: "",
placeholder: "vi, en, ...",
advanced: true,
},
],
defaultResponse: `{\n "object": "list",\n "data": [{ "id": "...", "model": "edge-tts/vi-VN-HoaiMyNeural" }]\n}`,
},
{
id: "audio-transcriptions",
method: "POST",
path: "/v1/audio/transcriptions",
label: "Speech to Text",
description: "Whisper-compatible multipart transcription.",
icon: "mic",
category: "Media",
contentType: "multipart",
needsModel: true,
modelsKind: "stt",
fields: [
{
key: "file",
label: "Audio File",
type: "file",
required: true,
accept: "audio/*,.mp3,.wav,.m4a,.webm,.ogg,.flac",
},
{
key: "language",
label: "Language",
type: "string",
default: "",
placeholder: "en, vi, ...",
},
{
key: "prompt",
label: "Prompt",
type: "textarea",
default: "",
advanced: true,
},
{
key: "response_format",
label: "Response Format",
type: "select",
default: "json",
options: ["json", "text", "verbose_json", "srt", "vtt"],
},
{
key: "temperature",
label: "Temperature",
type: "number",
default: "",
min: 0,
max: 1,
step: 0.1,
advanced: true,
},
],
defaultResponse: `{\n "text": "..."\n}`,
},
{
id: "search",
method: "POST",
path: "/v1/search",
label: "Web Search",
description: "Tavily / Exa / Brave / Serper / SearXNG / Google PSE / You.com…",
icon: "search",
category: "Web",
contentType: "json",
needsModel: true,
modelsKind: "web",
fields: [
{
key: "query",
label: "Query",
type: "textarea",
required: true,
default: "What is the latest news about AI?",
},
{
key: "max_results",
label: "Max Results",
type: "number",
default: 5,
min: 1,
max: 100,
},
{
key: "search_type",
label: "Search Type",
type: "select",
default: "web",
options: ["web", "news"],
},
{
key: "country",
label: "Country",
type: "string",
default: "",
advanced: true,
},
{
key: "language",
label: "Language",
type: "string",
default: "",
advanced: true,
},
{
key: "time_range",
label: "Time Range",
type: "string",
default: "",
placeholder: "day, week, month, year",
advanced: true,
},
{
key: "domain_filter",
label: "Domain Filter",
type: "string",
default: "",
placeholder: "example.com",
advanced: true,
},
],
defaultResponse: `{\n "provider": "tavily",\n "query": "...",\n "results": [{ "title": "...", "url": "...", "snippet": "..." }]\n}`,
},
{
id: "web-fetch",
method: "POST",
path: "/v1/web/fetch",
label: "Web Fetch",
description: "URL → markdown / text / HTML via Firecrawl, Jina, Tavily, Exa.",
icon: "language",
category: "Web",
contentType: "json",
needsModel: true,
modelsKind: "web",
fields: [
{
key: "url",
label: "URL",
type: "string",
required: true,
default: "https://9router.com",
placeholder: "https://example.com",
},
{
key: "format",
label: "Format",
type: "select",
default: "markdown",
options: ["markdown", "text", "html"],
},
{
key: "max_characters",
label: "Max Characters",
type: "number",
default: 0,
min: 0,
advanced: true,
description: "0 = no truncate",
},
],
defaultResponse: `{\n "provider": "jina-reader",\n "url": "...",\n "title": "...",\n "content": { "format": "markdown", "text": "..." }\n}`,
},
];
export const API_CATEGORIES = [...new Set(API_CATALOG.map((e) => e.category))];
export function getApiEndpointById(id) {
return API_CATALOG.find((e) => e.id === id) || null;
}
export function getApiEndpointsByCategory(category) {
return API_CATALOG.filter((e) => e.category === category);
}

View File

@@ -62,6 +62,9 @@ export const MITM_TOOLS = {
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5", alias: "claude-haiku-4.5" },
{ id: "deepseek-3.2", name: "DeepSeek 3.2", alias: "deepseek-3.2" },
{ id: "minimax-m2.1", name: "MiniMax M2.1", alias: "minimax-m2.1" },
{ id: "gpt-5.6-sol", name: "GPT 5.6 Sol", alias: "gpt-5.6-sol", contextLength: 272000, rateMultiplier: 2.4 },
{ id: "gpt-5.6-terra", name: "GPT 5.6 Terra", alias: "gpt-5.6-terra", contextLength: 272000, rateMultiplier: 1.2 },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", alias: "gpt-5.6-luna", contextLength: 272000, rateMultiplier: 0.6 },
{ id: "simple-task", name: "Qwen3 Coder Next", alias: "simple-task" },
],
},
@@ -95,13 +98,15 @@ export const CLI_TOOLS = {
model: "ANTHROPIC_MODEL",
opusModel: "ANTHROPIC_DEFAULT_OPUS_MODEL",
sonnetModel: "ANTHROPIC_DEFAULT_SONNET_MODEL",
fableModel: "ANTHROPIC_DEFAULT_FABLE_MODEL",
haikuModel: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
},
modelAliases: ["default", "sonnet", "opus", "haiku", "opusplan"],
modelAliases: ["default", "sonnet", "opus", "fable", "haiku", "opusplan"],
settingsFile: "~/.claude/settings.json",
defaultModels: [
{ id: "opus", name: "Claude Opus", alias: "opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-6" },
{ id: "sonnet", name: "Claude Sonnet", alias: "sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-4-6" },
{ id: "fable", name: "Claude Fable", alias: "fable", envKey: "ANTHROPIC_DEFAULT_FABLE_MODEL", defaultValue: "cc/claude-fable-5" },
{ id: "opus", name: "Claude Opus", alias: "opus", envKey: "ANTHROPIC_DEFAULT_OPUS_MODEL", defaultValue: "cc/claude-opus-4-8" },
{ id: "sonnet", name: "Claude Sonnet", alias: "sonnet", envKey: "ANTHROPIC_DEFAULT_SONNET_MODEL", defaultValue: "cc/claude-sonnet-5" },
{ id: "haiku", name: "Claude Haiku", alias: "haiku", envKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL", defaultValue: "cc/claude-haiku-4-5-20251001" },
],
},
@@ -358,6 +363,30 @@ amp --model "{{model}}"
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", alias: "gemini", defaultValue: "gemini/gemini-3.1-pro" },
],
},
"grok-build": {
id: "grok-build",
name: "Grok Build",
image: "/providers/grok-cli.png",
color: "#1DA1F2",
description: "xAI Grok Build TUI coding agent",
configType: "custom",
docsUrl: "https://x.ai/cli",
defaultCommand: "grok",
notes: [
{
type: "info",
text: "Grok Build uses ~/.grok/config.toml. 9Router writes a [model.9router] custom model and sets it as the default.",
},
{
type: "info",
text: "After Apply, run grok (or /model 9router) to use the routed model. Switch back anytime with /model grok-build.",
},
{
type: "warning",
text: "Config path: Linux/macOS ~/.grok/config.toml • Windows %USERPROFILE%\\.grok\\config.toml",
},
],
},
// HIDDEN: gemini-cli
// "gemini-cli": {
// id: "gemini-cli",

View File

@@ -33,4 +33,5 @@ export const LOCALE_FLAGS = {
"fi": "🇫🇮",
"da": "🇩🇰",
"no": "🇳🇴",
"fa": "🇮🇷",
};

View File

@@ -77,7 +77,7 @@ export const MEDIA_PROVIDER_KINDS = [
{ id: "stt", label: "Speech To Text", icon: "mic", endpoint: { method: "POST", path: "/v1/audio/transcriptions" } },
{ id: "webSearch", label: "Web Search", icon: "travel_explore", endpoint: { method: "POST", path: "/v1/search" } },
{ id: "webFetch", label: "Web Fetch", icon: "language", endpoint: { method: "POST", path: "/v1/web/fetch" } },
{ id: "video", label: "Video", icon: "movie", endpoint: { method: "POST", path: "/v1/video/generations" } },
{ id: "video", label: "Video", icon: "movie", endpoint: { method: "POST", path: "/v1/videos/generations" } },
{ id: "music", label: "Music", icon: "music_note", endpoint: { method: "POST", path: "/v1/audio/music" } },
];