@hilbras/omnihilbras 2.9.11 → 2.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/app/package.json +4 -3
- package/cli/package.json +1 -1
- package/open-sse/executors/webcookie-agent.js +36 -5
- package/open-sse/providers/registry/ollama-local.js +1 -0
- package/open-sse/providers/registry/ollama.js +1 -0
- package/open-sse/services/agent/runAgentTools.js +1 -1
- package/package.json +3 -1
- package/skills/SKILLS_INDEX.json +67 -0
- package/tests/translator/__snapshots__/golden-url-header.test.js.snap +112 -12
- package/tests/unit/custom-server-peer-headers.test.js +1 -1
- package/tests/unit/observability-repo.test.js +155 -0
- package/tests/unit/skill-store.test.js +81 -0
- package/tests/unit/skills-index.test.js +20 -0
package/cli/app/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "omnihilbras
|
|
3
|
-
"version": "2.9.
|
|
2
|
+
"name": "@hilbras/omnihilbras",
|
|
3
|
+
"version": "2.9.12",
|
|
4
4
|
"description": "OmniHilbras web dashboard",
|
|
5
|
-
"private": true,
|
|
6
5
|
"scripts": {
|
|
7
6
|
"dev": "next dev --port 20127",
|
|
8
7
|
"dev:webpack": "next dev --webpack --port 20127",
|
|
8
|
+
"prebuild": "node scripts/build-skills-index.mjs",
|
|
9
9
|
"build": "next build --webpack",
|
|
10
10
|
"postbuild": "node scripts/copy-standalone-assets.mjs",
|
|
11
11
|
"postbuild:bun": "node scripts/copy-standalone-assets.mjs",
|
|
12
12
|
"start": "node custom-server.js --port 20127",
|
|
13
13
|
"dev:bun": "bun --bun next dev --webpack --port 20127",
|
|
14
|
+
"prebuild:bun": "node scripts/build-skills-index.mjs",
|
|
14
15
|
"build:bun": "bun --bun next build --webpack",
|
|
15
16
|
"start:bun": "bun ./.next/standalone/custom-server.js",
|
|
16
17
|
"cli:pack": "npm --prefix cli run pack:cli",
|
package/cli/package.json
CHANGED
|
@@ -176,13 +176,30 @@ export class WebCookieAgentExecutor extends BaseExecutor {
|
|
|
176
176
|
const created = Math.floor(Date.now() / 1000);
|
|
177
177
|
const finishReason = agentResult.finished === "cap" ? "length" : "stop";
|
|
178
178
|
|
|
179
|
+
// Collect the actual tool-call objects from the conversation so the UI
|
|
180
|
+
// can render what the agent did (name, args, result snippets).
|
|
181
|
+
const agentToolCalls = [];
|
|
182
|
+
for (const msg of agentResult.messages || []) {
|
|
183
|
+
if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
|
|
184
|
+
for (const tc of msg.tool_calls) {
|
|
185
|
+
agentToolCalls.push({
|
|
186
|
+
id: tc.id || `call_${agentToolCalls.length + 1}`,
|
|
187
|
+
type: "function",
|
|
188
|
+
function: {
|
|
189
|
+
name: tc.function?.name || tc.name || "tool",
|
|
190
|
+
arguments: typeof tc.function?.arguments === "string"
|
|
191
|
+
? tc.function.arguments
|
|
192
|
+
: JSON.stringify(tc.function?.arguments || {}),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
179
199
|
const finalMessage = {
|
|
180
200
|
role: "assistant",
|
|
181
201
|
content,
|
|
182
|
-
|
|
183
|
-
...(agentResult.toolCalls > 0
|
|
184
|
-
? { _agent: { toolCalls: agentResult.toolCalls, rounds: agentResult.rounds, finished: agentResult.finished } }
|
|
185
|
-
: {}),
|
|
202
|
+
...(agentToolCalls.length > 0 ? { tool_calls: agentToolCalls } : {}),
|
|
186
203
|
};
|
|
187
204
|
|
|
188
205
|
const baseModel = model || "webcookie-agent";
|
|
@@ -195,10 +212,24 @@ export class WebCookieAgentExecutor extends BaseExecutor {
|
|
|
195
212
|
id: cid, object: "chat.completion.chunk", created, model: baseModel,
|
|
196
213
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null, logprobs: null }],
|
|
197
214
|
})));
|
|
215
|
+
|
|
216
|
+
// Emit the final content + tool_calls in one chunk. The agent loop
|
|
217
|
+
// runs to completion before streaming starts, so all tool calls are
|
|
218
|
+
// known at this point.
|
|
219
|
+
const delta = { content };
|
|
220
|
+
if (finalMessage.tool_calls?.length) {
|
|
221
|
+
delta.tool_calls = finalMessage.tool_calls.map((tc, i) => ({
|
|
222
|
+
index: i,
|
|
223
|
+
id: tc.id,
|
|
224
|
+
type: tc.type,
|
|
225
|
+
function: tc.function,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
198
228
|
controller.enqueue(encoder.encode(sseChunk({
|
|
199
229
|
id: cid, object: "chat.completion.chunk", created, model: baseModel,
|
|
200
|
-
choices: [{ index: 0, delta
|
|
230
|
+
choices: [{ index: 0, delta, finish_reason: null, logprobs: null }],
|
|
201
231
|
})));
|
|
232
|
+
|
|
202
233
|
controller.enqueue(encoder.encode(sseChunk({
|
|
203
234
|
id: cid, object: "chat.completion.chunk", created, model: baseModel,
|
|
204
235
|
choices: [{ index: 0, delta: {}, finish_reason: finishReason, logprobs: null }],
|
|
@@ -32,6 +32,7 @@ export default {
|
|
|
32
32
|
{ id: "minimax-m3", name: "MiniMax M3" },
|
|
33
33
|
{ id: "deepseek-v4.1-flash:cloud", name: "DeepSeek V4.1 Flash" },
|
|
34
34
|
],
|
|
35
|
+
modelsFetcher: { url: "https://ollama.com/api/tags", type: "openai" },
|
|
35
36
|
serviceKinds: ["llm", "webFetch"],
|
|
36
37
|
fetchConfig: {
|
|
37
38
|
baseUrl: "https://ollama.com/api/web_fetch",
|
|
@@ -37,7 +37,7 @@ function agentApiBaseUrl() {
|
|
|
37
37
|
// at import) so this module stays importable in tests without the local DB.
|
|
38
38
|
async function agentApiToken() {
|
|
39
39
|
try {
|
|
40
|
-
const { getCliToken } = await import("
|
|
40
|
+
const { getCliToken } = await import("../../../src/lib/auth/cliToken.js");
|
|
41
41
|
return await getCliToken();
|
|
42
42
|
} catch {
|
|
43
43
|
return process.env.OMNIHILBRAS_AGENT_API_KEY?.trim() || "dummy";
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hilbras/omnihilbras",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.13",
|
|
4
4
|
"description": "OmniHilbras web dashboard",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev --port 20127",
|
|
7
7
|
"dev:webpack": "next dev --webpack --port 20127",
|
|
8
|
+
"prebuild": "node scripts/build-skills-index.mjs",
|
|
8
9
|
"build": "next build --webpack",
|
|
9
10
|
"postbuild": "node scripts/copy-standalone-assets.mjs",
|
|
10
11
|
"postbuild:bun": "node scripts/copy-standalone-assets.mjs",
|
|
11
12
|
"start": "node custom-server.js --port 20127",
|
|
12
13
|
"dev:bun": "bun --bun next dev --webpack --port 20127",
|
|
14
|
+
"prebuild:bun": "node scripts/build-skills-index.mjs",
|
|
13
15
|
"build:bun": "bun --bun next build --webpack",
|
|
14
16
|
"start:bun": "bun ./.next/standalone/custom-server.js",
|
|
15
17
|
"cli:pack": "npm --prefix cli run pack:cli",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skills": [
|
|
3
|
+
{
|
|
4
|
+
"id": "omnihilbras",
|
|
5
|
+
"name": "omnihilbras",
|
|
6
|
+
"description": "Entry point for OmniHilbras — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions OmniHilbras, OMNIHILBRAS_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.",
|
|
7
|
+
"sha256": "3faaa6ca6cf2245130c399a58c11ce972075ec5a9924ffa4aaac509ce4c5a8bc",
|
|
8
|
+
"content": "---\nname: omnihilbras\ndescription: Entry point for OmniHilbras — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions OmniHilbras, OMNIHILBRAS_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.\n---\n\n# OmniHilbras\n\nLocal/remote AI gateway exposing OpenAI-compatible REST. One key, many providers, auto-fallback.\n\n## Setup\n\n```bash\nexport OMNIHILBRAS_URL=\"http://localhost:20128\" # or VPS / tunnel URL\nexport OMNIHILBRAS_KEY=\"sk-...\" # from Dashboard → Keys (only if requireApiKey=true)\n```\n\nAll requests: `${OMNIHILBRAS_URL}/v1/...` with header `Authorization: Bearer ${OMNIHILBRAS_KEY}` (omit if auth disabled).\n\nVerify: `curl $OMNIHILBRAS_URL/api/health` → `{\"ok\":true}`\n\n## Discover models\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models # chat/LLM (default)\ncurl $OMNIHILBRAS_URL/v1/models/image # image-gen\ncurl $OMNIHILBRAS_URL/v1/models/tts # text-to-speech\ncurl $OMNIHILBRAS_URL/v1/models/embedding # embeddings\ncurl $OMNIHILBRAS_URL/v1/models/web # web search + fetch (entries have `kind` field)\ncurl $OMNIHILBRAS_URL/v1/models/stt # speech-to-text\ncurl $OMNIHILBRAS_URL/v1/models/image-to-text # vision\n```\n\nUse `data[].id` as `model` field in requests. Combos appear with `owned_by:\"combo\"`.\n\nResponse shape:\n```json\n{ \"object\": \"list\", \"data\": [\n { \"id\": \"openai/gpt-5\", \"object\": \"model\", \"owned_by\": \"openai\", \"created\": 1735000000 },\n { \"id\": \"tavily/search\", \"object\": \"model\", \"kind\": \"webSearch\", \"owned_by\": \"tavily\", \"created\": 1735000000 }\n]}\n```\n\n## Capability skills\n\nWhen the user needs a specific capability, fetch that skill's `SKILL.md` from its raw URL:\n\n| Capability | Raw URL |\n|---|---|\n| Chat / code-gen | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-chat/SKILL.md |\n| Image generation | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-image/SKILL.md |\n| Text-to-speech | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-tts/SKILL.md |\n| Speech-to-text | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-stt/SKILL.md |\n| Embeddings | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-embeddings/SKILL.md |\n| Web search | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-web-search/SKILL.md |\n| Web fetch (URL → markdown) | https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras-web-fetch/SKILL.md |\n\n## Errors\n\n- 401 → set/refresh `OMNIHILBRAS_KEY` (Dashboard → Keys)\n- 400 `Invalid model format` → check `model` exists in `/v1/models/<kind>`\n- 503 `All accounts unavailable` → wait `retry-after` or add another provider account\n"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "omnihilbras-chat",
|
|
12
|
+
"name": "omnihilbras-chat",
|
|
13
|
+
"description": "Chat / code generation via OmniHilbras using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through OmniHilbras.",
|
|
14
|
+
"sha256": "79d79bd8b301f7d6e2ca9d4f12ce46e72e8ecb4be8e22168d0226ba3ab12ce94",
|
|
15
|
+
"content": "---\nname: omnihilbras-chat\ndescription: Chat / code generation via OmniHilbras using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through OmniHilbras.\n---\n\n# OmniHilbras — Chat\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Endpoints\n\n- `POST $OMNIHILBRAS_URL/v1/chat/completions` — OpenAI format\n- `POST $OMNIHILBRAS_URL/v1/messages` — Anthropic format\n\n## Discover\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models | jq '.data[].id'\n# Per-model metadata (contextWindow, params)\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=openai/gpt-4o\"\n```\n\nCombos (e.g. `vip`, `mycodex`) auto-fallback through multiple providers.\n\n## OpenAI format\n\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/chat/completions \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"openai/gpt-5\",\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}],\"stream\":false}'\n```\n\nJS (OpenAI SDK):\n\n```js\nimport OpenAI from \"openai\";\nconst client = new OpenAI({ baseURL: `${process.env.OMNIHILBRAS_URL}/v1`, apiKey: process.env.OMNIHILBRAS_KEY });\nconst res = await client.chat.completions.create({\n model: \"openai/gpt-5\",\n messages: [{ role: \"user\", content: \"Hi\" }],\n stream: true,\n});\nfor await (const chunk of res) process.stdout.write(chunk.choices[0]?.delta?.content || \"\");\n```\n\n## Anthropic format\n\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/messages \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"anthropic-version: 2023-06-01\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"cc/claude-opus-4-7\",\"max_tokens\":1024,\"messages\":[{\"role\":\"user\",\"content\":\"Hi\"}]}'\n```\n\n## Response shape\n\nOpenAI (`/v1/chat/completions`):\n```json\n{ \"id\": \"chatcmpl-...\", \"object\": \"chat.completion\", \"model\": \"openai/gpt-5\",\n \"choices\": [{ \"index\": 0, \"message\": { \"role\": \"assistant\", \"content\": \"Hello!\" }, \"finish_reason\": \"stop\" }],\n \"usage\": { \"prompt_tokens\": 8, \"completion_tokens\": 2, \"total_tokens\": 10 } }\n```\n\nStreaming (`stream:true`) emits SSE: `data: {choices:[{delta:{content:\"...\"}}]}\\n\\n` ... `data: [DONE]\\n\\n`.\n\nAnthropic (`/v1/messages`):\n```json\n{ \"id\": \"msg_...\", \"type\": \"message\", \"role\": \"assistant\", \"model\": \"cc/claude-opus-4-7\",\n \"content\": [{ \"type\": \"text\", \"text\": \"Hello!\" }],\n \"stop_reason\": \"end_turn\", \"usage\": { \"input_tokens\": 8, \"output_tokens\": 2 } }\n```\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "omnihilbras-embeddings",
|
|
19
|
+
"name": "omnihilbras-embeddings",
|
|
20
|
+
"description": "Generate vector embeddings via OmniHilbras /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text.",
|
|
21
|
+
"sha256": "97fc86d31317bd23dfb248a34ba5c3c92d162ae3d67151a338f667683f01beab",
|
|
22
|
+
"content": "---\nname: omnihilbras-embeddings\ndescription: Generate vector embeddings via OmniHilbras /v1/embeddings using OpenAI / Gemini / Mistral / Voyage / Nvidia / GitHub embedding models for RAG, semantic search, similarity. Use when the user wants embeddings, vectors, RAG, semantic search, or to embed text.\n---\n\n# OmniHilbras — Embeddings\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models/embedding | jq '.data[].id'\n# Per-model dimensions\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=openai/text-embedding-3-small\"\n```\n\n## Endpoint\n\n`POST $OMNIHILBRAS_URL/v1/embeddings`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/embedding` |\n| `input` | yes | string OR array of strings |\n| `encoding_format` | no | `float` (default) / `base64` |\n| `dimensions` | no | OpenAI v3 only |\n\n## Examples\n\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/embeddings \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"openai/text-embedding-3-small\",\"input\":[\"hello\",\"world\"]}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.OMNIHILBRAS_URL}/v1/embeddings`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${process.env.OMNIHILBRAS_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"gemini/text-embedding-004\", input: \"RAG chunk text\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].embedding.length); // dimension\n```\n\n## Response shape\n\n```json\n{ \"object\": \"list\", \"model\": \"openai/text-embedding-3-small\",\n \"data\": [\n { \"object\": \"embedding\", \"index\": 0, \"embedding\": [0.0123, -0.045, ...] },\n { \"object\": \"embedding\", \"index\": 1, \"embedding\": [...] }\n ],\n \"usage\": { \"prompt_tokens\": 5, \"total_tokens\": 5 } }\n```\n\n## Provider quirks\n\n| Provider | Notes |\n|---|---|\n| `openai`, `openrouter`, `mistral`, `voyage-ai`, `fireworks`, `together`, `nebius`, `github`, `nvidia`, `jina-ai` | Native OpenAI shape — `dimensions` works only on OpenAI v3 (`text-embedding-3-*`) |\n| `gemini`, `google_ai_studio` | Server auto-converts to `embedContent`/`batchEmbedContents` — send OpenAI shape |\n| `openai-compatible-*`, `custom-embedding-*` | Custom `baseUrl` from credentials |\n\nBatch (`input` as array) is faster; some providers cap batch size.\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "omnihilbras-image",
|
|
26
|
+
"name": "omnihilbras-image",
|
|
27
|
+
"description": "Generate images via OmniHilbras /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img).",
|
|
28
|
+
"sha256": "559b4e82dc861854e40151f5665d90c7d064387aa6fcf24ebceb43600de46737",
|
|
29
|
+
"content": "---\nname: omnihilbras-image\ndescription: Generate images via OmniHilbras /v1/images/generations using OpenAI / Gemini Imagen / DALL-E / FLUX / MiniMax / SDWebUI / ComfyUI / Codex models. Use when the user wants to create, generate, draw, or render an image, picture, or text-to-image (txt2img).\n---\n\n# OmniHilbras — Image Generation\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models/image | jq '.data[].id'\n# Per-model params/options (size enum, quality enum, capabilities like edit)\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=openai/dall-e-3\"\n```\n\n## Endpoint\n\n`POST $OMNIHILBRAS_URL/v1/images/generations`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/image` |\n| `prompt` | yes | image description |\n| `n` | no | count (provider-dependent) |\n| `size` | no | `1024x1024`, `1792x1024`, ... |\n| `quality` | no | `standard` / `hd` (OpenAI) |\n| `response_format` | no | `url` (default) or `b64_json` |\n\nAdd query `?response_format=binary` to receive raw image bytes (handy for saving file).\n\n## Examples\n\nSave to file (binary):\n\n```bash\ncurl -X POST \"$OMNIHILBRAS_URL/v1/images/generations?response_format=binary\" \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"gemini/gemini-3-pro-image-preview\",\"prompt\":\"watercolor mountains at sunrise\",\"size\":\"1024x1024\"}' \\\n --output out.png\n```\n\nJS (URL response):\n\n```js\nconst r = await fetch(`${process.env.OMNIHILBRAS_URL}/v1/images/generations`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${process.env.OMNIHILBRAS_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"gemini/gemini-3-pro-image-preview\", prompt: \"neon city\", size: \"1024x1024\" }),\n});\nconst { data } = await r.json();\nconsole.log(data[0].url || data[0].b64_json.slice(0, 40));\n```\n\n## Response shape\n\nJSON (default `response_format=url`):\n```json\n{ \"created\": 1735000000, \"data\": [{ \"url\": \"https://...\" }] }\n```\n\n`response_format=b64_json`:\n```json\n{ \"created\": 1735000000, \"data\": [{ \"b64_json\": \"iVBORw0KGgo...\" }] }\n```\n\nQuery `?response_format=binary` returns raw image bytes (Content-Type `image/png` or `image/jpeg`).\n\n## Provider quirks\n\nCommon fields above work everywhere. These add/override:\n\n| Provider | Extra/changed fields | Notes |\n|---|---|---|\n| `openai`, `minimax`, `openrouter`, `recraft` | `quality`, `style`, `response_format` | Standard OpenAI shape |\n| `gemini` (nano-banana) | — | Only `prompt`; ignores `size`/`n` |\n| `codex` (gpt-5.4-image) | `image`, `images[]`, `image_detail`, `output_format`, `background` | SSE stream; **ChatGPT Plus/Pro required** |\n| `huggingface` | — | Only `prompt`; returns single image |\n| `nanobanana` | `image`, `images[]` (edit mode) | `size` → aspect ratio; async polling |\n| `fal-ai` | `image` (img2img) | `n` → `num_images`; `size` → ratio; async |\n| `stability-ai` | `style` (preset), `output_format` | `size` → `aspect_ratio` |\n| `black-forest-labs` (FLUX) | `image` (ref) | `size` → exact `width`/`height`; async |\n| `runwayml` | `image` (ref) | `size` → ratio; async; video models exist |\n| `sdwebui`, `comfyui` | — | Localhost noAuth (`:7860` / `:8188`) |\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "omnihilbras-stt",
|
|
33
|
+
"name": "omnihilbras-stt",
|
|
34
|
+
"description": "Speech-to-text via OmniHilbras /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files.",
|
|
35
|
+
"sha256": "5f49ab037d003ce5f48fd4d59a95182b562f751f1ad975a1068317bd963806a4",
|
|
36
|
+
"content": "---\nname: omnihilbras-stt\ndescription: Speech-to-text via OmniHilbras /v1/audio/transcriptions using OpenAI Whisper / Groq / Gemini / Deepgram / AssemblyAI / NVIDIA / HuggingFace models. Use when the user wants to transcribe audio, convert speech to text, or get subtitles from audio files.\n---\n\n# OmniHilbras — Speech-to-Text\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models/stt | jq '.data[].id'\n# Per-model params (language, response_format, prompt, temperature support)\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=openai/whisper-1\"\n```\n\n`model` = STT model ID (e.g. `openai/whisper-1`, `groq/whisper-large-v3`, `deepgram/nova-3`, `gemini/gemini-2.5-flash`).\n\n## Endpoint\n\n`POST $OMNIHILBRAS_URL/v1/audio/transcriptions` (OpenAI Whisper compatible, `multipart/form-data`)\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | from `/v1/models/stt` |\n| `file` | yes | audio file (mp3, wav, m4a, webm, ogg, flac) |\n| `language` | no | ISO-639-1 (e.g. `en`, `vi`) |\n| `prompt` | no | hint text to guide transcription |\n| `response_format` | no | `json` (default) / `text` / `verbose_json` / `srt` / `vtt` |\n| `temperature` | no | 0–1 |\n\n## Examples\n\n```bash\ncurl -X POST \"$OMNIHILBRAS_URL/v1/audio/transcriptions\" \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -F \"model=openai/whisper-1\" \\\n -F \"file=@audio.mp3\" \\\n -F \"language=vi\"\n```\n\nJS (Node):\n\n```js\nimport { createReadStream } from \"node:fs\";\nconst form = new FormData();\nform.append(\"model\", \"groq/whisper-large-v3-turbo\");\nform.append(\"file\", new Blob([await (await import(\"node:fs/promises\")).readFile(\"audio.mp3\")]), \"audio.mp3\");\nconst r = await fetch(`${process.env.OMNIHILBRAS_URL}/v1/audio/transcriptions`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${process.env.OMNIHILBRAS_KEY}` },\n body: form,\n});\nconst { text } = await r.json();\nconsole.log(text);\n```\n\n## Response shape\n\nDefault (`response_format=json`):\n```json\n{ \"text\": \"Xin chào, đây là bản ghi âm.\" }\n```\n\n`verbose_json` adds `language`, `duration`, `segments[]` with timestamps.\n`srt` / `vtt` return subtitle text.\n\n## Provider quirks\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe` | Native OpenAI shape |\n| `groq` | `whisper-large-v3`, `whisper-large-v3-turbo`, `distil-whisper-large-v3-en` | Fastest; OpenAI shape |\n| `gemini` | `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` | Server converts to `generateContent` with audio inline |\n| `deepgram` | `nova-3`, `nova-2`, `whisper-large` | Token auth; server adapts response |\n| `assemblyai` | `universal-3-pro`, `universal-2` | Async upload+poll handled server-side |\n| `nvidia` | `nvidia/parakeet-ctc-1.1b-asr` | NIM endpoint |\n| `huggingface` | `openai/whisper-large-v3`, `openai/whisper-small` | HF Inference API |\n| `elevenlabs` | `scribe_v1`, `scribe_v2` | Whisper-compatible shape; `xi-api-key` auth, not Bearer. Extra params: `timestamps_granularity` (`word`/`character`/`none`), `tag_audio_events`, and speaker labelling via **either** `diarize=true` **or** `num_speakers` (1–32) — sending both makes the request invalid, so `diarize` wins and `num_speakers` is dropped. Blank `language` is omitted upstream for auto-detect. `srt`/`vtt` and `verbose_json` segments are served from Scribe's own `additional_formats` render, so `segments[]` is omitted when the upstream render is unavailable rather than synthesized. Does not accept `temperature` or `prompt` — those are not forwarded. |\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "omnihilbras-tts",
|
|
40
|
+
"name": "omnihilbras-tts",
|
|
41
|
+
"description": "Text-to-speech via OmniHilbras /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud.",
|
|
42
|
+
"sha256": "a95bf665df429861c53f46c64799140dd0bda750edca89a26aa177ebf87bd0bb",
|
|
43
|
+
"content": "---\nname: omnihilbras-tts\ndescription: Text-to-speech via OmniHilbras /v1/audio/speech using OpenAI / ElevenLabs / Deepgram / Edge TTS / Google TTS / Hyperbolic / Inworld voices. Use when the user wants to convert text to speech, generate audio, voiceover, narrate, or read text aloud.\n---\n\n# OmniHilbras — Text-to-Speech\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Discover\n\n```bash\n# 1) List models\ncurl $OMNIHILBRAS_URL/v1/models/tts | jq '.data[].id'\n# 2) Per-model metadata (params, voicesUrl if voice-by-id)\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=el/eleven_multilingual_v2\"\n# 3) List voices (elevenlabs, edge-tts, deepgram, inworld, local-device). Optional ?lang=vi\ncurl \"$OMNIHILBRAS_URL/v1/audio/voices?provider=edge-tts&lang=vi\" | jq '.data[].model'\n```\n\n`model` field in `/v1/audio/speech` = voice ID directly (e.g. `edge-tts/vi-VN-HoaiMyNeural`, `el/<voice_id>`, or `openai/tts-1` model+default voice).\n\n## Endpoint\n\n`POST $OMNIHILBRAS_URL/v1/audio/speech`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | yes | voice ID from `/v1/models/tts` |\n| `input` | yes | text to speak |\n\nQuery `?response_format=mp3` (default, raw bytes) or `?response_format=json` (`{audio: base64, format}`).\n\n## Examples\n\nSave MP3:\n\n```bash\ncurl -X POST \"$OMNIHILBRAS_URL/v1/audio/speech\" \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"openai/tts-1\",\"input\":\"Hello world\"}' \\\n --output speech.mp3\n```\n\nJS (save file):\n\n```js\nimport { writeFile } from \"node:fs/promises\";\nconst r = await fetch(`${process.env.OMNIHILBRAS_URL}/v1/audio/speech`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${process.env.OMNIHILBRAS_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"el/eleven_multilingual_v2\", input: \"Xin chào\" }),\n});\nawait writeFile(\"speech.mp3\", Buffer.from(await r.arrayBuffer()));\n```\n\n## Response shape\n\nDefault → raw audio bytes (Content-Type `audio/mp3`).\n\n`?response_format=json`:\n```json\n{ \"audio\": \"SUQzBAAAA...\", \"format\": \"mp3\" }\n```\n\n## Provider quirks (model format)\n\n| Provider | `model` format | Notes |\n|---|---|---|\n| `openai` | `tts-1/alloy` (model/voice) or just voice | Default model `gpt-4o-mini-tts` |\n| `elevenlabs` | `<model_id>/<voice_id>` or `<voice_id>` | Default model `eleven_flash_v2_5`; list voices in Dashboard |\n| `openrouter` | `openai/gpt-4o-mini-tts/alloy` | Streamed via chat-completions audio modality |\n| `edge-tts` | voice id e.g. `vi-VN-HoaiMyNeural` | **noAuth**; default `vi-VN-HoaiMyNeural` |\n| `google-tts` | language code e.g. `en`, `vi` | **noAuth** |\n| `local-device` | OS voice name (`say -v ?` / SAPI) | **noAuth**; needs `ffmpeg` |\n| `deepgram` | `aura-asteria-en` etc | Token auth |\n| `nvidia`, `inworld`, `cartesia`, `playht` | `model/voice` | Provider-specific auth header |\n| `coqui`, `tortoise` | speaker / voice id | Localhost noAuth |\n| `hyperbolic` | model id | Body = `{text}` only |\n"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "omnihilbras-video",
|
|
47
|
+
"name": "omnihilbras-video",
|
|
48
|
+
"description": "Generate videos via OmniHilbras /v1/videos/generations using xAI Grok Imagine (grok-imagine-video). Async job flow - submit, poll request_id until done, download MP4. Use when the user wants to create, generate, or render a video, text-to-video (txt2vid), or image-to-video.",
|
|
49
|
+
"sha256": "38586127031e374e2f792bed8514466d050eda87aa6face94bd54c3cec21515b",
|
|
50
|
+
"content": "---\nname: omnihilbras-video\ndescription: Generate videos via OmniHilbras /v1/videos/generations using xAI Grok Imagine (grok-imagine-video). Async job flow - submit, poll request_id until done, download MP4. Use when the user wants to create, generate, or render a video, text-to-video (txt2vid), or image-to-video.\n---\n\n# OmniHilbras — Video Generation (xAI Grok Imagine)\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\nRequires a connected **xAI account** in the OmniHilbras dashboard — either **Grok Build OAuth** (SuperGrok / X Premium+ subscription sign-in) or a direct **xAI API key** from console.x.ai. The two are separate auth types with separate billing; the dashboard shows which one each connection uses.\n\n## Endpoints (async job flow)\n\nVideo generation is **asynchronous**: the POST returns a `request_id` immediately, then you poll until the job is `done` or `failed`.\n\n| Endpoint | Purpose |\n|---|---|\n| `POST /v1/videos/generations` | text-to-video / image-to-video |\n| `POST /v1/videos/edits` | edit an existing video |\n| `POST /v1/videos/extensions` | extend an existing video |\n| `GET /v1/videos/{request_id}` | poll job status |\n\nRequest fields (passed through to xAI unchanged — see https://docs.x.ai/developers/rest-api-reference/inference/videos):\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` | no | `xai/grok-imagine-video` (prefix is stripped before upstream) |\n| `prompt` | yes for T2V | video description |\n| `duration` | no | seconds |\n| `aspect_ratio` | no | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `3:2`, `2:3` |\n| `resolution` | no | `480p`, `720p`, `1080p` |\n| `image` | no | `{ \"url\": \"https://… or data:image/…;base64,…\" }` for image-to-video |\n| `video` | edits/extensions | `{ \"url\": \"…mp4\" }` or `{ \"file_id\": \"…\" }` |\n\n## Examples\n\nSubmit a job:\n\n```bash\ncurl -X POST \"$OMNIHILBRAS_URL/v1/videos/generations\" \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"xai/grok-imagine-video\",\"prompt\":\"A cinematic tracking shot through a neon city at night\",\"duration\":8,\"aspect_ratio\":\"16:9\",\"resolution\":\"720p\"}'\n# → {\"request_id\":\"abc123\"} (response header x-omnihilbras-connection-id: <id>)\n```\n\nPoll until done (echo the connection header back so the same account polls the job):\n\n```bash\ncurl \"$OMNIHILBRAS_URL/v1/videos/abc123\" \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"x-connection-id: <id from create response>\"\n# → {\"status\":\"pending\",\"progress\":42}\n# → {\"status\":\"done\",\"video\":{\"url\":\"https://…mp4\",\"duration\":8},\"model\":\"grok-imagine-video\"}\n# → {\"status\":\"failed\",\"error\":{\"code\":\"…\",\"message\":\"…\"}}\n```\n\nDownload: fetch `video.url` from the `done` response.\n\n## CLI one-shot\n\n```bash\nomnihilbras xai video \\\n --prompt \"A cinematic tracking shot through a neon city at night\" \\\n --output video.mp4\n# options: --model --duration --aspect-ratio --resolution --image --timeout --port --api-key\n```\n\nSubmits, polls with progress, downloads to `video.mp4.part`, atomically renames on success. Ctrl+C cancels cleanly; non-zero exit on failure.\n\n## Notes & limits\n\n- Jobs are **account-bound** upstream: poll with the same connection that created the job (`x-connection-id` header, value from the create response's `x-omnihilbras-connection-id`).\n- Creation POSTs are **never auto-retried** (a retry could create and bill two videos). Only a 401→token-refresh→single-retry is performed, which upstream rejects before job creation.\n- Video models are tagged `kind: \"video\"` and are excluded from chat model lists and chat fallback combos.\n- Grok Build **subscription OAuth** tokens are sent to the same `api.x.ai/v1/videos` endpoints as API keys; whether a given subscription tier includes video-generation quota is controlled by xAI and is not verified by OmniHilbras — a `403`/`permission_denied` from upstream means the connected account has no video access.\n"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "omnihilbras-web-fetch",
|
|
54
|
+
"name": "omnihilbras-web-fetch",
|
|
55
|
+
"description": "Fetch URL → markdown / text / HTML via OmniHilbras /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown.",
|
|
56
|
+
"sha256": "0f21778c450430d35c110cc41071182a2682f302ae2b3ca0f26182115a8a69bb",
|
|
57
|
+
"content": "---\nname: omnihilbras-web-fetch\ndescription: Fetch URL → markdown / text / HTML via OmniHilbras /v1/web/fetch using Ollama Cloud / Firecrawl / Jina Reader / Tavily Extract / Exa Contents. Use when the user wants to scrape a webpage, extract URL content, read article, or convert a URL to markdown.\n---\n\n# OmniHilbras — Web Fetch\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models/web | jq '.data[] | select(.kind==\"webFetch\") | .id'\n# Per-provider params\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=firecrawl/fetch\"\n```\n\nIDs end in `/fetch` (e.g. `firecrawl/fetch`, `jina/fetch`). `fetch-combo` chains providers with auto-fallback.\n\n## Endpoint\n\n`POST $OMNIHILBRAS_URL/v1/web/fetch`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `firecrawl` or `jina-reader`) |\n| `url` | yes | URL to extract |\n| `format` | no | `markdown` (default) / `text` / `html` |\n| `max_characters` | no | truncate output |\n\n## Examples\n\n### Jina Reader\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/web/fetch \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"jina-reader\",\"url\":\"https://omnihilbras.com\",\"format\":\"markdown\"}'\n```\n\n### Exa\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/web/fetch \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"exa\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Firecrawl\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/web/fetch \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"firecrawl\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Tavily\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/web/fetch \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"tavily\",\"url\":\"https://example.com\",\"format\":\"markdown\",\"max_characters\":0}'\n```\n\n### Ollama Cloud\n\nUses the API key from the existing `ollama` connection.\n\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/web/fetch \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"ollama\",\"url\":\"https://example.com\",\"format\":\"markdown\"}'\n```\n\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.OMNIHILBRAS_URL}/v1/web/fetch`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${process.env.OMNIHILBRAS_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"fetch-combo\", url: \"https://example.com\", format: \"markdown\", max_characters: 5000 }),\n});\nconst { data } = await r.json();\nconsole.log(data.title, data.content.length);\n```\n\n## Response shape\n\n```json\n{\n \"provider\": \"jina-reader\",\n \"url\": \"...\",\n \"title\": \"...\",\n \"content\": { \"format\": \"markdown\", \"text\": \"...\", \"length\": 1234 },\n \"links\": [\"https://example.com/related\"],\n \"metadata\": { \"author\": null, \"published_at\": null, \"language\": null },\n \"usage\": { \"fetch_cost_usd\": 0 },\n \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700 }\n}\n```\n\n`links` is included when the upstream provider returns discovered page links (currently Ollama Cloud).\n\n## Provider quirks\n\n| Provider | Auth | Best for |\n|---|---|---|\n| `firecrawl` | Bearer | JS-rendered pages, `format=markdown/html` |\n| `jina-reader` | Bearer (optional) | Free tier (~1M chars/mo); fastest plain markdown |\n| `tavily` | Bearer | Bulk extract; returns `raw_content` |\n| `exa` | `x-api-key` | Pre-indexed pages; fast text extraction |\n| `ollama` | Bearer | Markdown plus page title and discovered links; uses the Ollama Cloud key |\n"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "omnihilbras-web-search",
|
|
61
|
+
"name": "omnihilbras-web-search",
|
|
62
|
+
"description": "Web and X search via OmniHilbras /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts.",
|
|
63
|
+
"sha256": "fe116258a5127faecf750e7469d8fbea0f926157263739265e0fced3cfcb851d",
|
|
64
|
+
"content": "---\nname: omnihilbras-web-search\ndescription: Web and X search via OmniHilbras /v1/search using Tavily / Exa / Brave / Serper / SearXNG / Google PSE / Linkup / SearchAPI / You.com / Perplexity / Xquik. Use when the user wants to search the web, find articles, or search public X posts.\n---\n\n# OmniHilbras — Web Search\n\nRequires `OMNIHILBRAS_URL` (and `OMNIHILBRAS_KEY` if auth enabled). See https://raw.githubusercontent.com/hilbras/omnihilbras/refs/heads/main/skills/omnihilbras/SKILL.md for setup.\n\n## Discover\n\n```bash\ncurl $OMNIHILBRAS_URL/v1/models/web | jq '.data[] | select(.kind==\"webSearch\") | .id'\n# Per-provider params (searchTypes, maxResults, required options like cx for google-pse)\ncurl \"$OMNIHILBRAS_URL/v1/models/info?id=tavily/search\"\n```\n\nIDs end in `/search` (e.g. `tavily/search`). Combos (`owned_by:\"combo\"`) chain providers with auto-fallback.\n\n## Endpoint\n\n`POST $OMNIHILBRAS_URL/v1/search`\n\n| Field | Required | Notes |\n|---|---|---|\n| `model` (or `provider`) | yes | from `/v1/models/web` (e.g. `tavily` or `brave`) |\n| `query` | yes | search query |\n| `max_results` | no | default 5 |\n| `search_type` | no | `web` (default) / `news` / `x` for Xquik |\n| `country`, `language`, `time_range`, `domain_filter` | no | provider-dependent |\n\n## Examples\n\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/search \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"tavily\",\"query\":\"OmniHilbras open source\",\"max_results\":5}'\n```\n\nJS:\n\n```js\nconst r = await fetch(`${process.env.OMNIHILBRAS_URL}/v1/search`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${process.env.OMNIHILBRAS_KEY}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ model: \"search-combo\", query: \"latest LLM benchmarks\", max_results: 10 }),\n});\nconsole.log(await r.json());\n```\n\nX search with Xquik:\n\n```bash\ncurl -X POST $OMNIHILBRAS_URL/v1/search \\\n -H \"Authorization: Bearer $OMNIHILBRAS_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"xquik\",\"query\":\"from:github release\",\"max_results\":10,\"provider_options\":{\"queryType\":\"Latest\"}}'\n```\n\nAdd the Xquik API key in OmniHilbras's provider settings. Xquik charges 1 credit per returned post. Continue a search by passing `pagination.next_cursor` as `provider_options.cursor`.\n\nXquik responses include provider pagination and credit usage:\n\n```json\n{\n \"pagination\": { \"has_more\": true, \"next_cursor\": \"cursor-2\" },\n \"usage\": { \"queries_used\": 1, \"search_cost_usd\": null, \"provider_credits_used\": 10 }\n}\n```\n\n## Response shape\n\n```json\n{\n \"provider\": \"tavily\",\n \"query\": \"OmniHilbras open source\",\n \"results\": [\n {\n \"title\": \"...\", \"url\": \"https://...\", \"display_url\": \"github.com/...\",\n \"snippet\": \"...\", \"position\": 1, \"score\": 0.92,\n \"published_at\": null, \"favicon_url\": null, \"content\": null,\n \"metadata\": { \"author\": null, \"language\": null, \"source_type\": null, \"image_url\": null },\n \"citation\": { \"provider\": \"tavily\", \"retrieved_at\": \"2026-...\", \"rank\": 1 }\n }\n ],\n \"answer\": null,\n \"usage\": { \"queries_used\": 1, \"search_cost_usd\": 0.008 },\n \"metrics\": { \"response_time_ms\": 850, \"upstream_latency_ms\": 700, \"total_results_available\": 12 },\n \"errors\": []\n}\n```\n\n## Provider quirks\n\nAll accept `query` + `max_results`. Optional fields vary:\n\n| Provider | Supports | Required extras |\n|---|---|---|\n| `tavily` | country, domain_filter, news topic | — |\n| `exa` | domain_filter (incl/excl), news category | — |\n| `brave-search` | country, language | — |\n| `serper` | country, language, news endpoint | — |\n| `perplexity` | country, language, domain_filter | — |\n| `linkup` | domain_filter, time_range | `depth: fast/standard/deep` (option) |\n| `google-pse` | country, language, time_range, offset | **`cx` required** (providerOptions) |\n| `searchapi` | country, language, pagination | — |\n| `youcom` | country, language, time_range, domain_filter, full_page | — |\n| `searxng` | language, time_range | Self-hosted, **noAuth** |\n| `xquik` | X/Twitter search operators, language, cursor pagination | `queryType: Latest/Top`, `cursor` (options) |\n\nProvider IS the model — `\"provider\":\"tavily\" ≡ \"model\":\"tavily\"`.\n"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
@@ -139,6 +139,31 @@ exports[`GOLDEN buildHeaders (default executor providers) > anthropic → header
|
|
|
139
139
|
}
|
|
140
140
|
`;
|
|
141
141
|
|
|
142
|
+
exports[`GOLDEN buildHeaders (default executor providers) > anyrouter → headers (apiKey / oauth) 1`] = `
|
|
143
|
+
{
|
|
144
|
+
"apiKey": {
|
|
145
|
+
"Accept": "text/event-stream",
|
|
146
|
+
"Authorization": "Bearer <TOK>",
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
"HTTP-Referer": "https://endpoint-proxy.local",
|
|
149
|
+
"X-Title": "Endpoint Proxy",
|
|
150
|
+
},
|
|
151
|
+
"nonStream": {
|
|
152
|
+
"Authorization": "Bearer <TOK>",
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
"HTTP-Referer": "https://endpoint-proxy.local",
|
|
155
|
+
"X-Title": "Endpoint Proxy",
|
|
156
|
+
},
|
|
157
|
+
"oauth": {
|
|
158
|
+
"Accept": "text/event-stream",
|
|
159
|
+
"Authorization": "Bearer <TOK>",
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"HTTP-Referer": "https://endpoint-proxy.local",
|
|
162
|
+
"X-Title": "Endpoint Proxy",
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
|
|
142
167
|
exports[`GOLDEN buildHeaders (default executor providers) > api-airforce → headers (apiKey / oauth) 1`] = `
|
|
143
168
|
{
|
|
144
169
|
"apiKey": {
|
|
@@ -624,32 +649,50 @@ exports[`GOLDEN buildHeaders (default executor providers) > codebuddy-cn → hea
|
|
|
624
649
|
"Accept": "text/event-stream",
|
|
625
650
|
"Authorization": "Bearer <TOK>",
|
|
626
651
|
"Content-Type": "application/json",
|
|
627
|
-
"User-Agent": "CLI/2.
|
|
652
|
+
"User-Agent": "CLI/2.177.0 CodeBuddy/2.177.0",
|
|
628
653
|
"X-IDE-Name": "CLI",
|
|
629
654
|
"X-IDE-Type": "CLI",
|
|
655
|
+
"X-IDE-Version": "2.177.0",
|
|
630
656
|
"X-Product": "SaaS",
|
|
657
|
+
"x-agent-intent": "craft",
|
|
658
|
+
"x-agent-purpose": "conversation",
|
|
659
|
+
"x-agent-runtime": "other",
|
|
660
|
+
"x-agent-type": "main",
|
|
631
661
|
"x-codebuddy-request": "1",
|
|
662
|
+
"x-private-data": "false",
|
|
632
663
|
"x-requested-with": "XMLHttpRequest",
|
|
633
664
|
},
|
|
634
665
|
"nonStream": {
|
|
635
666
|
"Authorization": "Bearer <TOK>",
|
|
636
667
|
"Content-Type": "application/json",
|
|
637
|
-
"User-Agent": "CLI/2.
|
|
668
|
+
"User-Agent": "CLI/2.177.0 CodeBuddy/2.177.0",
|
|
638
669
|
"X-IDE-Name": "CLI",
|
|
639
670
|
"X-IDE-Type": "CLI",
|
|
671
|
+
"X-IDE-Version": "2.177.0",
|
|
640
672
|
"X-Product": "SaaS",
|
|
673
|
+
"x-agent-intent": "craft",
|
|
674
|
+
"x-agent-purpose": "conversation",
|
|
675
|
+
"x-agent-runtime": "other",
|
|
676
|
+
"x-agent-type": "main",
|
|
641
677
|
"x-codebuddy-request": "1",
|
|
678
|
+
"x-private-data": "false",
|
|
642
679
|
"x-requested-with": "XMLHttpRequest",
|
|
643
680
|
},
|
|
644
681
|
"oauth": {
|
|
645
682
|
"Accept": "text/event-stream",
|
|
646
683
|
"Authorization": "Bearer <TOK>",
|
|
647
684
|
"Content-Type": "application/json",
|
|
648
|
-
"User-Agent": "CLI/2.
|
|
685
|
+
"User-Agent": "CLI/2.177.0 CodeBuddy/2.177.0",
|
|
649
686
|
"X-IDE-Name": "CLI",
|
|
650
687
|
"X-IDE-Type": "CLI",
|
|
688
|
+
"X-IDE-Version": "2.177.0",
|
|
651
689
|
"X-Product": "SaaS",
|
|
690
|
+
"x-agent-intent": "craft",
|
|
691
|
+
"x-agent-purpose": "conversation",
|
|
692
|
+
"x-agent-runtime": "other",
|
|
693
|
+
"x-agent-type": "main",
|
|
652
694
|
"x-codebuddy-request": "1",
|
|
695
|
+
"x-private-data": "false",
|
|
653
696
|
"x-requested-with": "XMLHttpRequest",
|
|
654
697
|
},
|
|
655
698
|
}
|
|
@@ -661,32 +704,50 @@ exports[`GOLDEN buildHeaders (default executor providers) > codebuddy-intl → h
|
|
|
661
704
|
"Accept": "text/event-stream",
|
|
662
705
|
"Authorization": "Bearer <TOK>",
|
|
663
706
|
"Content-Type": "application/json",
|
|
664
|
-
"User-Agent": "
|
|
665
|
-
"X-IDE-Name": "
|
|
666
|
-
"X-IDE-Type": "
|
|
707
|
+
"User-Agent": "CLI/2.177.0 CodeBuddy/2.177.0",
|
|
708
|
+
"X-IDE-Name": "CLI",
|
|
709
|
+
"X-IDE-Type": "CLI",
|
|
710
|
+
"X-IDE-Version": "2.177.0",
|
|
667
711
|
"X-Product": "SaaS",
|
|
712
|
+
"x-agent-intent": "craft",
|
|
713
|
+
"x-agent-purpose": "conversation",
|
|
714
|
+
"x-agent-runtime": "other",
|
|
715
|
+
"x-agent-type": "main",
|
|
668
716
|
"x-codebuddy-request": "1",
|
|
717
|
+
"x-private-data": "false",
|
|
669
718
|
"x-requested-with": "XMLHttpRequest",
|
|
670
719
|
},
|
|
671
720
|
"nonStream": {
|
|
672
721
|
"Authorization": "Bearer <TOK>",
|
|
673
722
|
"Content-Type": "application/json",
|
|
674
|
-
"User-Agent": "
|
|
675
|
-
"X-IDE-Name": "
|
|
676
|
-
"X-IDE-Type": "
|
|
723
|
+
"User-Agent": "CLI/2.177.0 CodeBuddy/2.177.0",
|
|
724
|
+
"X-IDE-Name": "CLI",
|
|
725
|
+
"X-IDE-Type": "CLI",
|
|
726
|
+
"X-IDE-Version": "2.177.0",
|
|
677
727
|
"X-Product": "SaaS",
|
|
728
|
+
"x-agent-intent": "craft",
|
|
729
|
+
"x-agent-purpose": "conversation",
|
|
730
|
+
"x-agent-runtime": "other",
|
|
731
|
+
"x-agent-type": "main",
|
|
678
732
|
"x-codebuddy-request": "1",
|
|
733
|
+
"x-private-data": "false",
|
|
679
734
|
"x-requested-with": "XMLHttpRequest",
|
|
680
735
|
},
|
|
681
736
|
"oauth": {
|
|
682
737
|
"Accept": "text/event-stream",
|
|
683
738
|
"Authorization": "Bearer <TOK>",
|
|
684
739
|
"Content-Type": "application/json",
|
|
685
|
-
"User-Agent": "
|
|
686
|
-
"X-IDE-Name": "
|
|
687
|
-
"X-IDE-Type": "
|
|
740
|
+
"User-Agent": "CLI/2.177.0 CodeBuddy/2.177.0",
|
|
741
|
+
"X-IDE-Name": "CLI",
|
|
742
|
+
"X-IDE-Type": "CLI",
|
|
743
|
+
"X-IDE-Version": "2.177.0",
|
|
688
744
|
"X-Product": "SaaS",
|
|
745
|
+
"x-agent-intent": "craft",
|
|
746
|
+
"x-agent-purpose": "conversation",
|
|
747
|
+
"x-agent-runtime": "other",
|
|
748
|
+
"x-agent-type": "main",
|
|
689
749
|
"x-codebuddy-request": "1",
|
|
750
|
+
"x-private-data": "false",
|
|
690
751
|
"x-requested-with": "XMLHttpRequest",
|
|
691
752
|
},
|
|
692
753
|
}
|
|
@@ -1774,6 +1835,31 @@ exports[`GOLDEN buildHeaders (default executor providers) > tokenrouter → head
|
|
|
1774
1835
|
}
|
|
1775
1836
|
`;
|
|
1776
1837
|
|
|
1838
|
+
exports[`GOLDEN buildHeaders (default executor providers) > vengetic → headers (apiKey / oauth) 1`] = `
|
|
1839
|
+
{
|
|
1840
|
+
"apiKey": {
|
|
1841
|
+
"Accept": "text/event-stream",
|
|
1842
|
+
"Authorization": "Bearer <TOK>",
|
|
1843
|
+
"Content-Type": "application/json",
|
|
1844
|
+
"HTTP-Referer": "https://endpoint-proxy.local",
|
|
1845
|
+
"X-Title": "Endpoint Proxy",
|
|
1846
|
+
},
|
|
1847
|
+
"nonStream": {
|
|
1848
|
+
"Authorization": "Bearer <TOK>",
|
|
1849
|
+
"Content-Type": "application/json",
|
|
1850
|
+
"HTTP-Referer": "https://endpoint-proxy.local",
|
|
1851
|
+
"X-Title": "Endpoint Proxy",
|
|
1852
|
+
},
|
|
1853
|
+
"oauth": {
|
|
1854
|
+
"Accept": "text/event-stream",
|
|
1855
|
+
"Authorization": "Bearer <TOK>",
|
|
1856
|
+
"Content-Type": "application/json",
|
|
1857
|
+
"HTTP-Referer": "https://endpoint-proxy.local",
|
|
1858
|
+
"X-Title": "Endpoint Proxy",
|
|
1859
|
+
},
|
|
1860
|
+
}
|
|
1861
|
+
`;
|
|
1862
|
+
|
|
1777
1863
|
exports[`GOLDEN buildHeaders (default executor providers) > venice → headers (apiKey / oauth) 1`] = `
|
|
1778
1864
|
{
|
|
1779
1865
|
"apiKey": {
|
|
@@ -1978,6 +2064,13 @@ exports[`GOLDEN buildUrl (default executor providers) > anthropic → url (strea
|
|
|
1978
2064
|
}
|
|
1979
2065
|
`;
|
|
1980
2066
|
|
|
2067
|
+
exports[`GOLDEN buildUrl (default executor providers) > anyrouter → url (stream + non-stream) 1`] = `
|
|
2068
|
+
{
|
|
2069
|
+
"nonStream": "https://anyrouter.dev/v1/chat/completions",
|
|
2070
|
+
"stream": "https://anyrouter.dev/v1/chat/completions",
|
|
2071
|
+
}
|
|
2072
|
+
`;
|
|
2073
|
+
|
|
1981
2074
|
exports[`GOLDEN buildUrl (default executor providers) > api-airforce → url (stream + non-stream) 1`] = `
|
|
1982
2075
|
{
|
|
1983
2076
|
"nonStream": "https://api.airforce/v1/chat/completions",
|
|
@@ -2503,6 +2596,13 @@ exports[`GOLDEN buildUrl (default executor providers) > tokenrouter → url (str
|
|
|
2503
2596
|
}
|
|
2504
2597
|
`;
|
|
2505
2598
|
|
|
2599
|
+
exports[`GOLDEN buildUrl (default executor providers) > vengetic → url (stream + non-stream) 1`] = `
|
|
2600
|
+
{
|
|
2601
|
+
"nonStream": "https://api.vengetic.biz.id/v1/chat/completions",
|
|
2602
|
+
"stream": "https://api.vengetic.biz.id/v1/chat/completions",
|
|
2603
|
+
}
|
|
2604
|
+
`;
|
|
2605
|
+
|
|
2506
2606
|
exports[`GOLDEN buildUrl (default executor providers) > venice → url (stream + non-stream) 1`] = `
|
|
2507
2607
|
{
|
|
2508
2608
|
"nonStream": "https://api.venice.ai/api/v1/chat/completions",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import http from "node:http";
|
|
6
|
-
import { __test__ as requestDetails } from "@/lib/db/repos/
|
|
6
|
+
import { __test__ as requestDetails } from "@/lib/db/repos/observabilityRepo.js";
|
|
7
7
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Repo + helpers behind /api/observability/*.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
|
6
|
+
import { parseLogFilter, redactPayloads, toCsv, toJsonl } from "@/lib/observability/logQuery.js";
|
|
7
|
+
|
|
8
|
+
const originalDataDir = process.env.DATA_DIR;
|
|
9
|
+
let tempDir;
|
|
10
|
+
let db;
|
|
11
|
+
|
|
12
|
+
function insert(adapter, row) {
|
|
13
|
+
adapter.run(
|
|
14
|
+
`INSERT INTO requestDetails(id, timestamp, provider, model, connectionId, status, data) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
|
15
|
+
[row.id, row.timestamp, row.provider, row.model, row.connectionId || null, row.status, JSON.stringify(row)]
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "omnihilbras-observability-"));
|
|
21
|
+
process.env.DATA_DIR = tempDir;
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
db = await import("@/lib/db/index.js");
|
|
24
|
+
await db.initDb();
|
|
25
|
+
await db.updateSettings({ enableObservability: true });
|
|
26
|
+
db.invalidateObservabilityConfig();
|
|
27
|
+
|
|
28
|
+
const { getAdapter } = await import("@/lib/db/driver.js");
|
|
29
|
+
const adapter = await getAdapter();
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
insert(adapter, {
|
|
32
|
+
id: "a", timestamp: new Date(now - 60_000).toISOString(), provider: "openai", model: "gpt-4", status: "success",
|
|
33
|
+
latency: { total: 100 }, tokens: { prompt_tokens: 10, completion_tokens: 5 },
|
|
34
|
+
});
|
|
35
|
+
insert(adapter, {
|
|
36
|
+
id: "b", timestamp: new Date(now - 120_000).toISOString(), provider: "openai", model: "gpt-4", status: "error",
|
|
37
|
+
latency: { total: 300 }, tokens: {},
|
|
38
|
+
});
|
|
39
|
+
insert(adapter, {
|
|
40
|
+
id: "c", timestamp: new Date(now - 3 * 86400_000).toISOString(), provider: "claude", model: "sonnet", status: "success",
|
|
41
|
+
latency: { total: 200 }, tokens: { input_tokens: 7, output_tokens: 3 },
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
47
|
+
if (originalDataDir === undefined) delete process.env.DATA_DIR;
|
|
48
|
+
else process.env.DATA_DIR = originalDataDir;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("observability stats", () => {
|
|
52
|
+
it("24h window counts only recent rows, with error rate and per-provider groups", async () => {
|
|
53
|
+
const s = await db.getObservabilityStats("24h");
|
|
54
|
+
expect(s.requests).toBe(2);
|
|
55
|
+
expect(s.errors).toBe(1);
|
|
56
|
+
expect(s.errorRate).toBe(0.5);
|
|
57
|
+
expect(s.promptTokens).toBe(10);
|
|
58
|
+
expect(s.completionTokens).toBe(5);
|
|
59
|
+
expect(s.avgLatency).toBe(200);
|
|
60
|
+
expect(s.byProvider).toEqual([{ key: "openai", requests: 2, errors: 1, tokens: 15, avgLatency: 200 }]);
|
|
61
|
+
expect(s.retention.records).toBe(3);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("all period includes input_tokens/output_tokens shapes", async () => {
|
|
65
|
+
const s = await db.getObservabilityStats("all");
|
|
66
|
+
expect(s.period).toBe("all");
|
|
67
|
+
expect(s.requests).toBe(3);
|
|
68
|
+
expect(s.promptTokens).toBe(17);
|
|
69
|
+
expect(s.completionTokens).toBe(8);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("observability deletes and filters", () => {
|
|
74
|
+
it("filters by model and status", async () => {
|
|
75
|
+
const res = await db.getRequestLogEntries({ model: "gpt-4", status: "error" });
|
|
76
|
+
expect(res.details.map((d) => d.id)).toEqual(["b"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("distinct facets", async () => {
|
|
80
|
+
expect(await db.getDistinctModels()).toEqual(["gpt-4", "sonnet"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("deleteRequestLogsBefore removes only older rows", async () => {
|
|
84
|
+
const deleted = await db.deleteRequestLogsBefore(new Date(Date.now() - 86400_000));
|
|
85
|
+
expect(deleted).toBe(1);
|
|
86
|
+
expect(await db.getRequestLogDetailById("c")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("deleteRequestLogBatch and deleteRequestLogById", async () => {
|
|
90
|
+
expect(await db.deleteRequestLogBatch(["a", "missing"])).toBe(1);
|
|
91
|
+
expect(await db.deleteRequestLogById("b")).toBe(true);
|
|
92
|
+
expect(await db.deleteRequestLogById("b")).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("logQuery helpers", () => {
|
|
97
|
+
it("redacts every payload field but keeps metadata", () => {
|
|
98
|
+
const out = redactPayloads({ id: "x", model: "m", request: { messages: [] }, response: { content: "secret" } });
|
|
99
|
+
expect(out).toEqual({ id: "x", model: "m", request: { redacted: true }, response: { redacted: true } });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects invalid dates and skips empty values", () => {
|
|
103
|
+
expect(parseLogFilter(new URLSearchParams("startDate=nope")).error).toMatch(/startDate/);
|
|
104
|
+
expect(parseLogFilter({ provider: "", model: "gpt-4" }).filter).toEqual({ model: "gpt-4" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("CSV escapes quotes and commas; JSONL is one object per line", () => {
|
|
108
|
+
const rows = [{ id: "1", model: 'a,"b"', tokens: { input_tokens: 4 } }];
|
|
109
|
+
const csv = toCsv(rows).split("\n");
|
|
110
|
+
expect(csv[0]).toMatch(/^timestamp,id,provider,model/);
|
|
111
|
+
expect(csv[1]).toContain('"a,""b"""');
|
|
112
|
+
expect(csv[1]).toContain(",4,");
|
|
113
|
+
expect(toJsonl(rows)).toBe(JSON.stringify(rows[0]) + "\n");
|
|
114
|
+
expect(toJsonl([])).toBe("");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("observability on/off precedence", () => {
|
|
119
|
+
const saved = { obs: process.env.OBSERVABILITY_ENABLED, logs: process.env.ENABLE_REQUEST_LOGS };
|
|
120
|
+
const restore = () => {
|
|
121
|
+
for (const [key, value] of [["OBSERVABILITY_ENABLED", saved.obs], ["ENABLE_REQUEST_LOGS", saved.logs]]) {
|
|
122
|
+
if (value === undefined) delete process.env[key];
|
|
123
|
+
else process.env[key] = value;
|
|
124
|
+
}
|
|
125
|
+
db.invalidateObservabilityConfig();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
it("UI toggle decides; ENABLE_REQUEST_LOGS (file logs) does not affect it", async () => {
|
|
129
|
+
try {
|
|
130
|
+
process.env.ENABLE_REQUEST_LOGS = "false";
|
|
131
|
+
await db.updateSettings({ enableObservability: true });
|
|
132
|
+
db.invalidateObservabilityConfig();
|
|
133
|
+
expect((await db.getObservabilityConfig()).enabled).toBe(true);
|
|
134
|
+
|
|
135
|
+
process.env.ENABLE_REQUEST_LOGS = "true";
|
|
136
|
+
await db.updateSettings({ enableObservability: false });
|
|
137
|
+
db.invalidateObservabilityConfig();
|
|
138
|
+
expect((await db.getObservabilityConfig()).enabled).toBe(false);
|
|
139
|
+
} finally {
|
|
140
|
+
restore();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("OBSERVABILITY_ENABLED=false forces recording off over the UI toggle", async () => {
|
|
145
|
+
try {
|
|
146
|
+
process.env.OBSERVABILITY_ENABLED = "FALSE";
|
|
147
|
+
await db.updateSettings({ enableObservability: true });
|
|
148
|
+
db.invalidateObservabilityConfig();
|
|
149
|
+
expect(db.isObservabilityForcedOff()).toBe(true);
|
|
150
|
+
expect((await db.getObservabilityConfig()).enabled).toBe(false);
|
|
151
|
+
} finally {
|
|
152
|
+
restore();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Skill Marketplace service behind /api/skills.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
const originalDataDir = process.env.DATA_DIR;
|
|
8
|
+
let tempDir;
|
|
9
|
+
let store;
|
|
10
|
+
let db;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "omnihilbras-skills-"));
|
|
14
|
+
process.env.DATA_DIR = tempDir;
|
|
15
|
+
vi.resetModules();
|
|
16
|
+
db = await import("@/lib/db/index.js");
|
|
17
|
+
await db.initDb();
|
|
18
|
+
store = await import("@/lib/skills/skillStore.js");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
+
if (originalDataDir === undefined) delete process.env.DATA_DIR;
|
|
24
|
+
else process.env.DATA_DIR = originalDataDir;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("skill store", () => {
|
|
28
|
+
it("catalog lists every bundled skill, entry skill first, none installed", async () => {
|
|
29
|
+
const all = await store.fetchManifest();
|
|
30
|
+
expect(all.length).toBeGreaterThanOrEqual(9);
|
|
31
|
+
expect(all[0].id).toBe("omnihilbras");
|
|
32
|
+
expect(all.every((s) => !s.installed)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("search matches text and filters by category", async () => {
|
|
36
|
+
expect((await store.searchSkills("transcribe")).map((s) => s.id)).toContain("omnihilbras-stt");
|
|
37
|
+
const web = await store.searchSkills("", "web");
|
|
38
|
+
expect(web.map((s) => s.id).sort()).toEqual(["omnihilbras-web-fetch", "omnihilbras-web-search"]);
|
|
39
|
+
expect(await store.searchSkills("no-such-skill-xyz")).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("install writes SKILL.md under DATA_DIR and records it", async () => {
|
|
43
|
+
const skill = await store.installSkill("omnihilbras-chat");
|
|
44
|
+
const file = path.join(tempDir, "skills", "omnihilbras-chat", "SKILL.md");
|
|
45
|
+
expect(skill.installed).toBe(true);
|
|
46
|
+
expect(skill.path).toBe(file);
|
|
47
|
+
expect(fs.readFileSync(file, "utf8")).toBe(store.getSkillText("omnihilbras-chat"));
|
|
48
|
+
expect((await store.listInstalledSkills()).map((s) => s.id)).toEqual(["omnihilbras-chat"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("flags an update when the installed version differs, and update clears it", async () => {
|
|
52
|
+
const row = await db.getInstalledSkill("omnihilbras-chat");
|
|
53
|
+
await db.saveInstalledSkill("omnihilbras-chat", { ...row, sha256: "old" });
|
|
54
|
+
expect((await store.getSkill("omnihilbras-chat")).updateAvailable).toBe(true);
|
|
55
|
+
const updated = await store.installSkill("omnihilbras-chat");
|
|
56
|
+
expect(updated.updateAvailable).toBe(false);
|
|
57
|
+
expect(updated.installedAt).toBe(row.installedAt);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("a row whose file was deleted by hand reads as not installed", async () => {
|
|
61
|
+
await store.installSkill("omnihilbras-tts");
|
|
62
|
+
fs.rmSync(path.join(tempDir, "skills", "omnihilbras-tts"), { recursive: true });
|
|
63
|
+
expect((await store.getSkill("omnihilbras-tts")).installed).toBe(false);
|
|
64
|
+
expect((await store.listInstalledSkills()).map((s) => s.id)).not.toContain("omnihilbras-tts");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("uninstall removes the file and the row", async () => {
|
|
68
|
+
expect(await store.uninstallSkill("omnihilbras-chat")).toBe(true);
|
|
69
|
+
expect(fs.existsSync(path.join(tempDir, "skills", "omnihilbras-chat"))).toBe(false);
|
|
70
|
+
expect(await db.getInstalledSkill("omnihilbras-chat")).toBeNull();
|
|
71
|
+
expect(await store.uninstallSkill("omnihilbras-chat")).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects ids that are not catalog keys or could escape the install dir", async () => {
|
|
75
|
+
expect(await store.installSkill("not-in-catalog")).toBeNull();
|
|
76
|
+
for (const id of ["../etc", "a/b", "", "UPPER", ".hidden"]) {
|
|
77
|
+
expect(store.isValidSkillId(id)).toBe(false);
|
|
78
|
+
expect(await store.uninstallSkill(id)).toBe(false);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// skills/SKILLS_INDEX.json is generated by `prebuild`; this fails when a
|
|
2
|
+
// SKILL.md changed without regenerating it (run: node scripts/build-skills-index.mjs).
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { buildSkillsIndex, parseFrontmatter } from "../../scripts/build-skills-index.mjs";
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(__dirname, "../..");
|
|
9
|
+
|
|
10
|
+
describe("skills index", () => {
|
|
11
|
+
it("matches the skills/ folder", () => {
|
|
12
|
+
const committed = JSON.parse(fs.readFileSync(path.join(root, "skills/SKILLS_INDEX.json"), "utf8"));
|
|
13
|
+
expect(committed).toEqual(buildSkillsIndex(path.join(root, "skills")));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("parses frontmatter name and description", () => {
|
|
17
|
+
expect(parseFrontmatter("---\nname: x\ndescription: \"Does y: z\"\n---\n# body")).toEqual({ name: "x", description: "Does y: z" });
|
|
18
|
+
expect(parseFrontmatter("# no frontmatter")).toEqual({});
|
|
19
|
+
});
|
|
20
|
+
});
|