diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 325b2b4f9a..2cabb3a30b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -293,16 +293,16 @@ src/ # TypeScript (.ts / .tsx) ├── mitm/ # MITM proxy (cert, DNS, target routing) ├── shared/ │ ├── components/ # React components (.tsx) -│ ├── constants/ # Provider definitions (290), MCP scopes, 19 routing strategies +│ ├── constants/ # Provider definitions (329), MCP scopes, 19 routing strategies │ ├── utils/ # Circuit breaker, sanitizer, auth helpers │ └── validation/ # Zod v4 schemas └── sse/ # SSE proxy pipeline open-sse/ # @omniroute/open-sse workspace -├── executors/ # 14 provider-specific request executors +├── executors/ # 89 executor implementation modules ├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.) -├── mcp-server/ # MCP server (104 tools, 3 transports, 31 scopes) -├── services/ # 36+ services (combo, autoCombo, rateLimitManager, etc.) +├── mcp-server/ # MCP server (107 unique tools, 3 transports, 32 scopes) +├── services/ # 178 top-level services (combo, autoCombo, rateLimitManager, etc.) ├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama) ├── transformer/ # Responses API transformer └── utils/ # 22 utility modules (stream, TLS, proxy, logging) diff --git a/SECURITY.md b/SECURITY.md index 0dadb47aec..59298ced57 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -48,7 +48,7 @@ Request → CORS → Authz pipeline (classify → policies → enforce) | **Authz Pipeline** | Route classification (PUBLIC / CLIENT_API / MANAGEMENT) — see `docs/architecture/AUTHZ_GUIDE.md` | | **Route Guard Tiers** | 3-tier model for management routes (LOCAL_ONLY / ALWAYS_PROTECTED / MANAGEMENT) — see `docs/security/ROUTE_GUARD_TIERS.md` | | **Manage-Scope MCP** | Remote `/api/mcp/*` access gated by API keys with `manage` scope; `/api/cli-tools/runtime/*` stays strict-loopback. See ROUTE_GUARD_TIERS | -| **MCP Scopes** | ~13 granular scopes (read:health, write:combos, execute:completions, etc.) — see `docs/frameworks/MCP-SERVER.md` | +| **MCP Scopes** | 32 granular scopes (read:health, write:combos, execute:completions, etc.) — see `docs/frameworks/MCP-SERVER.md` | ### 🛡️ Encryption at Rest diff --git a/bin/cli/CONVENTIONS.md b/bin/cli/CONVENTIONS.md index e46caa0b27..c65ea033ef 100644 --- a/bin/cli/CONVENTIONS.md +++ b/bin/cli/CONVENTIONS.md @@ -136,7 +136,7 @@ export const RETRY_DEFAULTS = { - Every user-facing string goes through `t("module.key", vars)`. - Catalogs live in `bin/cli/locales/{locale}.json` (nested objects). - 42 files ship out-of-the-box: `en`, `pt-BR`, and 40 additional locales. + 43 files ship out-of-the-box: `en`, `pt-BR`, and 41 additional locales. 11 locales are scaffold-only (empty `{}`); all keys fall back to `en` automatically. - Detection order: `--lang` flag → `OMNIROUTE_LANG` env → `LC_ALL` → `LC_MESSAGES` → `LANG` → `en`. - Locale persisted via `config lang set ` — saves `OMNIROUTE_LANG` to `~/.omniroute/.env`. diff --git a/bin/cli/README.md b/bin/cli/README.md index 00c9dd06e4..c3b979da01 100644 --- a/bin/cli/README.md +++ b/bin/cli/README.md @@ -22,9 +22,9 @@ bin/cli/ ├── provider-test.mjs ← testProviderApiKey() ├── settings-store.mjs ← DB CRUD for key_value settings ├── locales/ -│ ├── en.json ← English strings (source of truth, 42+ locales) +│ ├── en.json ← English strings (source of truth, 43 locales) │ ├── pt-BR.json ← Portuguese (Brazil) — fully translated -│ └── {locale}.json ← 40 additional locales (ar, az, de, es, fr, ja, zh-CN, …) +│ └── {locale}.json ← 42 additional locales (ar, az, de, es, fr, ja, zh-CN, …) ├── scripts/ │ └── generate-locales.mjs ← scaffold new locale files from config/i18n.json └── commands/ diff --git a/docs/architecture/RESILIENCE_GUIDE.md b/docs/architecture/RESILIENCE_GUIDE.md index e2f9d3dfd2..2529e41b38 100644 --- a/docs/architecture/RESILIENCE_GUIDE.md +++ b/docs/architecture/RESILIENCE_GUIDE.md @@ -335,4 +335,4 @@ default `test:integration`, chaos and heap self-skip (without `RUN_CHAOS_INT`/`- - [Architecture Guide](./ARCHITECTURE.md) — System architecture and internals - [User Guide](../guides/USER_GUIDE.md) — Providers, combos, CLI integration -- [Auto-Combo Engine](../routing/AUTO-COMBO.md) — 12-factor scoring, mode packs +- [Auto-Combo Engine](../routing/AUTO-COMBO.md) — 13-factor scoring, mode packs diff --git a/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md b/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md index 9dc5d955cb..3a25f85f0a 100644 --- a/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md +++ b/docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md @@ -1,7 +1,7 @@ --- title: "OmniRoute vs Alternatives" -version: 3.8.43 -lastUpdated: 2026-07-01 +version: 3.8.50 +lastUpdated: 2026-08-02 --- # OmniRoute vs Alternatives @@ -13,16 +13,16 @@ Objective feature comparison vs popular open-source AI routers. | Feature | OmniRoute 3.8 | LiteLLM 1.x | OpenRouter (SaaS) | Portkey | | -------------------------------------------------- | :----------------------------------------------: | :------------: | :---------------: | :---------: | -| **Providers** | **237+** | ~100 | ~50 | ~30 | -| **Free-tier providers** | **90+** | n/a | passthrough | n/a | +| **Providers** | **329** | ~100 | ~50 | ~30 | +| **Free/no-auth catalog entries** | **155** | n/a | passthrough | n/a | | **Self-hostable** | ✅ | ✅ | ❌ | ⚠ paid | -| **OAuth providers (Claude, Codex, Copilot, etc.)** | **15+** | partial | ❌ | ❌ | -| **Auto-fallback combos** | **17 strategies** | priority-based | tier-based | weighted | +| **OAuth catalog entries** | **23** | partial | ❌ | ❌ | +| **Auto-fallback combos** | **19 strategies** | priority-based | tier-based | weighted | | **Fusion (parallel panel + judge synthesis)** | ✅ | ❌ | ❌ | ❌ | | **Tier 1/2/3 fallback (subscription→cheap→free)** | ✅ + UI | manual | n/a | manual | -| **Token compression** | 10-engine pipeline (RTK + Caveman + LLMLingua-2) | none | none | none | +| **Token compression** | 12-engine stack (RTK + Caveman + LLMLingua) | none | none | none | | **Multimodal generation (speech/music/video)** | ✅ | ❌ | passthrough | ❌ | -| **Built-in MCP server** | ✅ 99 tools, 32 scopes | ❌ | ❌ | ❌ | +| **Built-in MCP server** | ✅ 107 tools, 32 scopes | ❌ | ❌ | ❌ | | **A2A protocol** | ✅ 6 skills | ❌ | ❌ | ❌ | | **Memory (FTS5 + vector)** | ✅ | ❌ | ❌ | ❌ | | **Guardrails (PII, injection, vision)** | ✅ | partial | ❌ | ✅ paid | @@ -34,14 +34,14 @@ Objective feature comparison vs popular open-source AI routers. | **CLI with system tray (no Electron)** | ✅ | ❌ | n/a | n/a | | **CLI machine-ID auto-auth** | ✅ | ❌ | n/a | n/a | | **Dashboard** | Next.js 16 | basic | proprietary | proprietary | -| **i18n** | **42+ locales** | ❌ | ❌ | ⚠ | -| **Public agent skills (SKILL.md)** | ✅ 43 | ❌ | ❌ | ❌ | +| **i18n** | **43 locales** | ❌ | ❌ | ⚠ | +| **Public agent skills (SKILL.md)** | ✅ 45 | ❌ | ❌ | ❌ | | **Tunnel support (Cloudflared, Tailscale, Ngrok)** | ✅ | ❌ | n/a | n/a | | **License** | MIT | MIT | proprietary | proprietary | ## When to choose OmniRoute -- You self-host and want **maximum provider coverage** (237+, 90+ with a free tier) +- You self-host and want **maximum provider coverage** (329 providers, 155 free/no-auth catalog entries) - You need a **built-in MCP server** (LLM tools, memory, skills exposed as tools) - You need **A2A protocol** for agent-to-agent workflows - You want **fingerprint stealth** (JA3/JA4) to avoid detection by upstream CAPTCHAs @@ -67,4 +67,4 @@ Objective feature comparison vs popular open-source AI routers. --- -_Last updated: 2026-06-28. Submit corrections via PR to keep this table accurate._ +_Last updated: 2026-08-02. Submit corrections via PR to keep this table accurate._ diff --git a/docs/compression/COMPRESSION_GUIDE.md b/docs/compression/COMPRESSION_GUIDE.md index ea70ebbfac..12b380379e 100644 --- a/docs/compression/COMPRESSION_GUIDE.md +++ b/docs/compression/COMPRESSION_GUIDE.md @@ -199,7 +199,7 @@ In `Dashboard → Context & Cache → Compression Combos`, assign a compression combo: ```txt -Combo: "free-forever" +Combo: "free-tier-fallback" Compression Combo: "coding-agent-stack" Pipeline: RTK -> Caveman Targets: diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md index 0fa9f35bca..7b6fd18f9a 100644 --- a/docs/diagrams/README.md +++ b/docs/diagrams/README.md @@ -16,7 +16,7 @@ Mermaid sources (`.mmd`) and exported SVGs for OmniRoute v3.8.0 architecture flo | [auto-combo-12factor.mmd](./auto-combo-12factor.mmd) | [SVG](./exported/auto-combo-12factor.svg) | docs/routing/AUTO-COMBO.md | | [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md | | [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md | -| [mcp-tools-104.mmd](./mcp-tools-104.mmd) | [SVG](./exported/mcp-tools-104.svg) | docs/frameworks/MCP-SERVER.md | +| [mcp-tools-107.mmd](./mcp-tools-107.mmd) | [SVG](./exported/mcp-tools-107.svg) | docs/frameworks/MCP-SERVER.md | | [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md | | [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md | | [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md | @@ -34,11 +34,11 @@ inside GitHub's `` sandbox: | [combo-always-on.svg](./combo-always-on.svg) | style reference | Animated priority-combo fallback (4 layers, 16s loop). Edit the SVG directly — there is no `.mmd` source. | | [cli-terminal.svg](./cli-terminal.svg) | README.md (root) | Compact half-height animated terminal (1200×350): 3 real CLI commands cycling with typewriter + scrolling subcommand ticker; first frame = completed providers screen. Edit the SVG directly — there is no `.mmd` source. | | [compression-pipeline.svg](./compression-pipeline.svg) | README.md (root) | Animated 10-engine compression funnel (8s loop). Edit the SVG directly — there is no `.mmd` source. | -| [free-tier-budget.svg](./free-tier-budget.svg) | README.md (root) | Animated free-tier budget card (~1.4B/mo headline, 19-pool budget bar, per-model grid, signup credits, 10s loop). Edit the SVG directly — there is no `.mmd` source. | -| [readme-hero.svg](./readme-hero.svg) | README.md (root) | Animated hero card (tagline, 268-provider/90+ free headline, full-width compression bar demo, 6 stat chips). Edit the SVG directly — there is no `.mmd` source. | +| [free-tier-budget.svg](./free-tier-budget.svg) | README.md (root) | Animated free-tier budget card (~1.53B/mo quantified headline, 19-pool budget bar, per-model grid, signup credits, 10s loop). Edit the SVG directly — there is no `.mmd` source. | +| [readme-hero.svg](./readme-hero.svg) | README.md (root) | Animated hero card (tagline, live provider/free-access headline, full-width compression bar demo, 6 stat chips). Edit the SVG directly — there is no `.mmd` source. | | [promise-pillars.svg](./promise-pillars.svg) | README.md (root) | Animated "The Promise" 6-pillar card (12s border-highlight sweep). Edit the SVG directly — there is no `.mmd` source. | | [why-pain-fix.svg](./why-pain-fix.svg) | README.md (root) | Animated "Why OmniRoute" 10-row pain-vs-fix ledger (15s green row sweep). Edit the SVG directly — there is no `.mmd` source. | -| [strategies-grid.svg](./strategies-grid.svg) | README.md (root) | Animated 6×3 grid of all 18 routing-strategy flows (one micro-stage per strategy, staggered dot loops). Edit the SVG directly — there is no `.mmd` source. | +| [strategies-grid.svg](./strategies-grid.svg) | README.md (root) | Animated grid illustrating 18 of the 19 routing strategies; `cache-optimized` remains documented in the adjacent table. Edit the SVG directly — there is no `.mmd` source. | | [privacy-local.svg](./privacy-local.svg) | README.md (root) | Animated "Private & Local-First" 11-row guarantee ledger with receipt chips (16s green row sweep). Edit the SVG directly — there is no `.mmd` source. | | [resilience-layers.svg](./resilience-layers.svg) | README.md (root) | Animated 3-layer resilience card (breaker states CLOSED→OPEN→HALF-OPEN, key cooldown with ×2 backoff, model lockout — 18s loops). Edit the SVG directly — there is no `.mmd` source. | diff --git a/docs/diagrams/combo-always-on.svg b/docs/diagrams/combo-always-on.svg index 0278375d73..182c3bcc21 100644 --- a/docs/diagrams/combo-always-on.svg +++ b/docs/diagrams/combo-always-on.svg @@ -1,4 +1,5 @@ - + + Animated priority combo that tries two subscriptions, a low-cost API, and a listed free-access route while clearly stating that fallback does not guarantee availability. @@ -122,7 +123,7 @@ 4 kr/claude-sonnet-4.5 - FREE · unlimited · never fails + FREE ACCESS · limits apply @@ -214,8 +215,8 @@ fails → next - - UPTIME + + FALLBACK COVERAGE @@ -224,7 +225,7 @@ - ALWAYS ON + FALLBACK READY - Result: 4 layers of fallback = zero downtime + Result: 4 fallback layers = broader resilience · availability is not guaranteed diff --git a/docs/diagrams/exported/auto-combo-12factor.svg b/docs/diagrams/exported/auto-combo-12factor.svg index 6d88a323aa..d0f2e00d7c 100644 --- a/docs/diagrams/exported/auto-combo-12factor.svg +++ b/docs/diagrams/exported/auto-combo-12factor.svg @@ -1 +1 @@ -

12-factor scoring weights (sum = 1.0)

health (0.20)

quota (0.15)

costInv (0.15)

latencyInv (0.12)

taskFit (0.08)

stability (0.05)

tierPriority (0.05)

tierAffinity (0.05)

specificityMatch (0.05)

contextAffinity (0.05)

connectionDensity (0.05)

resetWindowAffinity (0.00)

Incoming request

Eligible candidates
(provider × model × account)

Compute composite score
per candidate

Sort by score
(desc)

Pick top-N targets

Dispatch sequentially
(short-circuit on success)

\ No newline at end of file +

13-factor scoring weights (sum = 1.0)

health (0.20)

quota (0.15)

costInv (0.15)

latencyInv (0.12)

taskFit (0.08)

stability (0.05)

tierPriority (0.05)

tierAffinity (0.05)

specificityMatch (0.05)

contextAffinity (0.05)

connectionDensity (0.05)

cacheAffinity (0.00)

resetWindowAffinity (0.00)

Incoming request

Eligible candidates
(provider × model × account)

Compute composite score
per candidate

Sort by score
(desc)

Pick top-N targets

Dispatch sequentially
(short-circuit on success)

\ No newline at end of file diff --git a/docs/diagrams/exported/mcp-tools-104.svg b/docs/diagrams/exported/mcp-tools-104.svg deleted file mode 100644 index 3f750ae1fd..0000000000 --- a/docs/diagrams/exported/mcp-tools-104.svg +++ /dev/null @@ -1 +0,0 @@ -

MCP Server
104 tools total

Core (42)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch

Memory (3)
memory_search, memory_save,
memory_delete

Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall
+ agent-skill cli-registry discovery

GitHub-Skills (3)
marketplace search,
install, update

Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status

Gamification (8)

Plugins (8)

Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks

Obsidian (22)
vault read/write, search,
frontmatter, daily notes, …

Compression Studios (+2)
beyond the 11 in Core

\ No newline at end of file diff --git a/docs/diagrams/exported/mcp-tools-107.svg b/docs/diagrams/exported/mcp-tools-107.svg new file mode 100644 index 0000000000..e49849311c --- /dev/null +++ b/docs/diagrams/exported/mcp-tools-107.svg @@ -0,0 +1 @@ +

MCP Server
107 tools total

Base registry (42)
routing, cache, compression, quota,
proxy, web search/fetch, tool search

Memory (3)
memory_search, memory_add,
memory_clear

Skills (4)
skills_list, skills_enable,
skills_execute, skills_executions
+ agent-skill catalog discovery

GitHub-Skills (3)
search, scan, install

Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status

Gamification (8)

Plugins (8)

Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks

Obsidian (22)
vault read/write, search,
frontmatter, daily notes, …

Local corpus (3)
status, search, read

Compression Studios (+2)
beyond the 11 in Core

\ No newline at end of file diff --git a/docs/diagrams/exported/mcp-tools-99.svg b/docs/diagrams/exported/mcp-tools-99.svg deleted file mode 100644 index 53b38bb50c..0000000000 --- a/docs/diagrams/exported/mcp-tools-99.svg +++ /dev/null @@ -1 +0,0 @@ -

MCP Server
99 tools total

Core (36)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch

Memory (3)
memory_search, memory_save,
memory_delete

Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall

Agent-Skills (3)
cli-registry discovery
(also in Core)

GitHub Skills (3)
search, scan, install

Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status

Gamification (8)

Plugins (8)

Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks

Obsidian (22)
vault read/write, search,
frontmatter, daily notes, ...

Local Corpus (3)
status, search, read

\ No newline at end of file diff --git a/docs/diagrams/exported/request-pipeline.svg b/docs/diagrams/exported/request-pipeline.svg index 9de7fb82a0..d025124a9f 100644 --- a/docs/diagrams/exported/request-pipeline.svg +++ b/docs/diagrams/exported/request-pipeline.svg @@ -1 +1 @@ -

yes

no

combo

single

Client
(IDE/CLI/SDK)

Next.js Route
/v1/chat/completions

CORS preflight

Zod validation
(request body)

AuthZ pipeline
(extractApiKey +
isValidApiKey)

API key policy
(allowlist + scopes)

Prompt-injection
guardrail

handleChatCore

Cache hit?

Return cached

Rate limit
(per-key, per-IP)

Combo
target?

resolveComboTargets
(18 strategies)

handleSingleModel
(per target)

translateRequest
(OpenAI↔Claude↔Gemini)

getExecutor
(31 executors)

Upstream Provider
(177 providers)

SSE / JSON

responsesTransformer
(Responses↔Chat)

\ No newline at end of file +

yes

no

combo

single

Client
(IDE/CLI/SDK)

Next.js Route
/v1/chat/completions

CORS preflight

Zod validation
(request body)

AuthZ pipeline
(extractApiKey +
isValidApiKey)

API key policy
(allowlist + scopes)

Prompt-injection
guardrail

handleChatCore

Cache hit?

Return cached

Rate limit
(per-key, per-IP)

Combo
target?

resolveComboTargets
(19 strategies)

handleSingleModel
(per target)

translateRequest
(OpenAI↔Claude↔Gemini)

getExecutor
(89 executor modules)

Upstream Provider
(329 catalog entries)

SSE / JSON

responsesTransformer
(Responses↔Chat)

\ No newline at end of file diff --git a/docs/diagrams/mcp-tools-104.mmd b/docs/diagrams/mcp-tools-107.mmd similarity index 54% rename from docs/diagrams/mcp-tools-104.mmd rename to docs/diagrams/mcp-tools-107.mmd index 27967da552..b0bb2d2346 100644 --- a/docs/diagrams/mcp-tools-104.mmd +++ b/docs/diagrams/mcp-tools-107.mmd @@ -1,20 +1,21 @@ %% MCP Server tool inventory by category %% Reflects: open-sse/mcp-server/ — TOTAL_MCP_TOOL_COUNT in server.ts (countUniqueMcpTools) %% Unique contributions: MCP_TOOLS 42 + memory 3 + skill 4 + githubSkill 3 + pool 6 -%% + gamification 8 + plugin 8 + notion 6 + obsidian 22 + compression 2 = 104 -%% (agentSkillTools fully overlaps the skill registry; 11 of the 13 compression +%% + gamification 8 + plugin 8 + notion 6 + obsidian 22 + localCorpus 3 + compression 2 = 107 +%% (agentSkillTools fully overlaps the base registry; 11 of the 13 compression %% tools live in the base registry — duplicates counted once) %% Regenerate the SVG with: npm run docs:render-diagrams -%% v3.8.49 +%% v3.8.50 flowchart LR - MCP["MCP Server

104 tools total"] - MCP --> Core["Core (42)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch"] - MCP --> Mem["Memory (3)
memory_search, memory_save,
memory_delete"] - MCP --> Skl["Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall
+ agent-skill cli-registry discovery"] - MCP --> GSkl["GitHub-Skills (3)
marketplace search,
install, update"] + MCP["MCP Server
107 tools total"] + MCP --> Core["Base registry (42)
routing, cache, compression, quota,
proxy, web search/fetch, tool search"] + MCP --> Mem["Memory (3)
memory_search, memory_add,
memory_clear"] + MCP --> Skl["Skills (4)
skills_list, skills_enable,
skills_execute, skills_executions
+ agent-skill catalog discovery"] + MCP --> GSkl["GitHub-Skills (3)
search, scan, install"] MCP --> Pool["Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status"] MCP --> Gam["Gamification (8)"] MCP --> Plg["Plugins (8)"] MCP --> Notion["Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks"] MCP --> Obs["Obsidian (22)
vault read/write, search,
frontmatter, daily notes, …"] + MCP --> Corpus["Local corpus (3)
status, search, read"] MCP --> Cmp["Compression Studios (+2)
beyond the 11 in Core"] diff --git a/docs/diagrams/mcp-tools-99.mmd b/docs/diagrams/mcp-tools-99.mmd deleted file mode 100644 index 3b9bec1b46..0000000000 --- a/docs/diagrams/mcp-tools-99.mmd +++ /dev/null @@ -1,18 +0,0 @@ -%% MCP Server tool inventory by category -%% Reflects: open-sse/mcp-server/ - TOTAL_MCP_TOOL_COUNT in server.ts -%% 102 definitions; 99 unique after the 3 agentSkill definitions duplicated in MCP_TOOLS are de-duplicated -%% Regenerate the SVG with: npm run docs:render-diagrams -%% v3.8.49 -flowchart LR - MCP["MCP Server
99 tools total"] - MCP --> Core["Core (36)
routing, cache, compression,
quota, proxy, tunnels, search, web_fetch"] - MCP --> Mem["Memory (3)
memory_search, memory_save,
memory_delete"] - MCP --> Skl["Skills (4)
skill_invoke, skill_list,
skill_diagnose, skill_uninstall"] - MCP --> ASkl["Agent-Skills (3)
cli-registry discovery
(also in Core)"] - MCP --> GSkl["GitHub Skills (3)
search, scan, install"] - MCP --> Pool["Pool (6)
pool_status, pool_sessions, pool_reset,
pool_warm, pool_health, browser_pool_status"] - MCP --> Gam["Gamification (8)"] - MCP --> Plg["Plugins (8)"] - MCP --> Notion["Notion (6)
search, get_page, query_database,
get_database, list_block_children,
append_blocks"] - MCP --> Obs["Obsidian (22)
vault read/write, search,
frontmatter, daily notes, ..."] - MCP --> Local["Local Corpus (3)
status, search, read"] diff --git a/docs/diagrams/request-pipeline.mmd b/docs/diagrams/request-pipeline.mmd index 2933c645c3..9c776e17a8 100644 --- a/docs/diagrams/request-pipeline.mmd +++ b/docs/diagrams/request-pipeline.mmd @@ -13,12 +13,12 @@ flowchart LR Cache -->|yes| Return["Return cached"] Cache -->|no| Rate["Rate limit
(per-key, per-IP)"] Rate --> Combo{"Combo
target?"} - Combo -->|combo| Resolve["resolveComboTargets
(18 strategies)"] + Combo -->|combo| Resolve["resolveComboTargets
(19 strategies)"] Resolve --> Single["handleSingleModel
(per target)"] Combo -->|single| Single Single --> Translate["translateRequest
(OpenAI↔Claude↔Gemini)"] - Translate --> Exec["getExecutor
(31 executors)"] - Exec --> Upstream["Upstream Provider
(177 providers)"] + Translate --> Exec["getExecutor
(89 executor modules)"] + Exec --> Upstream["Upstream Provider
(329 catalog entries)"] Upstream --> Stream["SSE / JSON"] Stream --> Transformer["responsesTransformer
(Responses↔Chat)"] Transformer --> Client diff --git a/docs/diagrams/strategies-grid.svg b/docs/diagrams/strategies-grid.svg index 6ef2ae7f7c..d518f85d06 100644 --- a/docs/diagrams/strategies-grid.svg +++ b/docs/diagrams/strategies-grid.svg @@ -1,5 +1,5 @@ - - Animated grid of 18 routing-strategy tiles; static tracks show each flow on the first frame while a small dot repeatedly travels the path each strategy takes. + + Animated grid of 18 routing-strategy tiles; static tracks show each flow on the first frame while a small dot repeatedly travels the path each illustrated strategy takes. @@ -13,7 +13,7 @@ - COMBO ROUTING · ALL 18 STRATEGY FLOWS + COMBO ROUTING · 18 OF 19 STRATEGY FLOWS ● request → ▮ targets — each tile animates the path its strategy takes · green outline = the pick @@ -95,7 +95,7 @@ auto 72916455 -live 12-factor scoring +live 13-factor scoring fusion diff --git a/docs/diagrams/tier-cascade.svg b/docs/diagrams/tier-cascade.svg index 7944762286..5dfe2314fd 100644 --- a/docs/diagrams/tier-cascade.svg +++ b/docs/diagrams/tier-cascade.svg @@ -1,4 +1,5 @@ + Animated request path that tries subscription, API-key, low-cost, and free-access tiers in sequence, subject to upstream availability and limits. @@ -255,15 +256,15 @@ - + - ALWAYS ON + LIMITS APPLY - AUTO-FALLBACK CASCADE · NEVER STOP CODING + AUTO-FALLBACK CASCADE · UPSTREAM AVAILABILITY VARIES diff --git a/docs/frameworks/A2A-SERVER.md b/docs/frameworks/A2A-SERVER.md index ea451e42d3..68e68421b9 100644 --- a/docs/frameworks/A2A-SERVER.md +++ b/docs/frameworks/A2A-SERVER.md @@ -148,9 +148,9 @@ OmniRoute exposes 6 A2A skills wired in `src/lib/a2a/taskExecution.ts::A2A_SKILL | Provider Discovery | `provider-discovery` | Lists installed providers with capabilities, free-tier flags, OAuth status | providers, discovery | "What providers are available?" | | Cost Analysis | `cost-analysis` | Estimates cost of a request/conversation given the catalog + recent usage | cost, usage | "Estimate cost for this conversation" | | Health Report | `health-report` | Aggregates circuit breaker, cooldown, lockout state per provider | health, resilience | "Show health status of all providers" | -| List Capabilities | `list-capabilities` | Returns the full 42-entry Agent Skills catalog as a markdown table with raw SKILL.md URLs for context injection | catalog, discovery, skills | "List all OmniRoute capabilities" | +| List Capabilities | `list-capabilities` | Returns the full 45-entry Agent Skills catalog (23 API + 21 CLI + 1 config) as a markdown table with raw SKILL.md URLs for context injection | catalog, discovery, skills | "List all OmniRoute capabilities" | -> Note: the Agent Card description currently advertises "36+ providers" (`src/app/.well-known/agent.json/route.ts:26` and `:55`). The actual catalog has grown to 180+ providers — the string should be updated in a follow-up change (tracked as a separate doc/code TODO; not modified here). +> The Agent Card should be kept aligned with the live 329-provider catalog; provider counts and free/no-auth metadata are sourced from the runtime registry. ### `list-capabilities` Skill Detail diff --git a/docs/frameworks/AGENT-SKILLS.md b/docs/frameworks/AGENT-SKILLS.md index b24f3c85a9..d06fcde1c4 100644 --- a/docs/frameworks/AGENT-SKILLS.md +++ b/docs/frameworks/AGENT-SKILLS.md @@ -1,13 +1,13 @@ --- title: "OmniRoute Agent Skills Catalog" -version: 3.8.40 -lastUpdated: 2026-06-28 +version: 3.8.50 +lastUpdated: 2026-08-02 --- # OmniRoute Agent Skills Catalog > **Source of truth:** `src/lib/agentSkills/` (catalog, generator, parsers) + `skills/` directory (SKILL.md files) -> **Last updated:** 2026-06-28 — v3.8.40 +> **Last updated:** 2026-08-02 — v3.8.50 Agent Skills are structured SKILL.md files that teach external agents, MCP clients, and A2A orchestrators how to use OmniRoute's REST API and CLI. Unlike [Omni Skills](./SKILLS.md) (which are LLM tool definitions executed inside OmniRoute), Agent Skills are a _documentation catalog_ — static markdown that can be fed directly into agent context. @@ -15,7 +15,7 @@ Agent Skills are structured SKILL.md files that teach external agents, MCP clien ## Overview -The catalog contains **42 canonical Agent Skills** (22 REST API + 20 CLI). Each skill has: +The catalog contains **45 Agent Skills** (23 REST API + 21 CLI + 1 configuration workflow). Each skill has: - A **canonical ID** (`omni-auth`, `cli-serve`, etc.) - A **SKILL.md** file in `skills/{id}/SKILL.md` with YAML frontmatter (`name`, `description`) + rich markdown body @@ -27,7 +27,7 @@ The catalog contains **42 canonical Agent Skills** (22 REST API + 20 CLI). Each ## Architecture ``` -src/shared/constants/agentSkills.ts — 42-entry curated list (name/desc/category/area/icon) +src/shared/constants/agentSkills.ts — 45-entry curated list (name/desc/category/area/icon) src/lib/agentSkills/ catalog.ts — getCatalog(), getSkillById(), filterCatalog(), computeCoverage() generator.ts — generateAgentSkills() writes SKILL.md to skills/{id}/ @@ -36,7 +36,7 @@ src/lib/agentSkills/ schemas.ts — Zod schemas: AgentSkillSchema, SkillCoverageSchema, etc. types.ts — TypeScript interfaces: AgentSkill, SkillCoverage, etc. -skills/{id}/SKILL.md — Generated + curated markdown files (42 total) +skills/{id}/SKILL.md — Generated + curated markdown files (45 total) src/app/api/agent-skills/ route.ts — GET /api/agent-skills @@ -92,7 +92,7 @@ The generator preserves content between `` and ` ## Overview -Create and manage routing combos with 14 strategies (priority, weighted, round-robin, Auto-combo, etc.). Configure fallback chains, test routing outcomes, and retrieve combo metrics. +Create and manage routing combos with 19 strategies (priority, weighted, round-robin, Auto-combo, and more). Configure fallback chains, test routing outcomes, and retrieve combo metrics. ## Authentication @@ -147,28 +147,33 @@ curl -X POST $OMNIROUTE_URL/api/combos \ }' ``` -## 14 routing strategies +## 19 public routing strategies -| Strategy | Description | -| ------------------- | -------------------------------------------------- | -| `priority` | Always use target[0]; fall back on error | -| `weighted` | Distribute by weight percentage | -| `round-robin` | Rotate targets in order | -| `fill-first` | Fill quota of target[0] before spilling | -| `least-used` | Route to target with fewest active requests | -| `cost-optimized` | Pick cheapest target for the token estimate | -| `auto` | 9-factor scoring: cost + latency + quota + circuit | -| `random` | Uniform random selection | -| `strict-random` | Random without repeating until all used | -| `p2c` | Power-of-2-choices: sample 2, pick better | -| `reset-aware` | Prefer targets near quota reset time | -| `lkgp` | Last-known-good-provider sticky routing | -| `context-optimized` | Pick best model for context length | -| `context-relay` | Chain models for very long contexts | +| Strategy | Description | +| --- | --- | +| `priority` | Always use target[0]; fall back on error | +| `weighted` | Distribute by weight percentage | +| `round-robin` | Rotate targets in order | +| `context-relay` | Chain models for very long contexts | +| `fill-first` | Fill quota of target[0] before spilling | +| `p2c` | Power-of-two choices: sample two targets, pick the better candidate | +| `random` | Uniform random selection | +| `least-used` | Route to the target with the lowest observed usage | +| `cost-optimized` | Pick the cheapest eligible target for the token estimate | +| `reset-aware` | Prefer targets based on quota-reset state | +| `reset-window` | Order targets by their configured reset window | +| `headroom` | Prefer targets with more remaining quota headroom | +| `strict-random` | Random without repeating until all targets have been used | +| `auto` | Auto-Combo scoring across 13 factors | +| `lkgp` | Last-known-good-provider sticky routing | +| `context-optimized` | Pick the best model for the request's context size | +| `cache-optimized` | Prefer targets with stronger cache affinity | +| `fusion` | Run a panel in parallel and synthesize one judged response | +| `pipeline` | Run a configured sequence of targets as a pipeline | ## Auto-combo (recommended for production) -Auto-combo scores each candidate on 9 factors every request: +Auto-combo scores each candidate on 13 factors every request: ```bash curl -X POST $OMNIROUTE_URL/api/combos \ @@ -236,6 +241,6 @@ omniroute_best_combo_for_task → get recommendation by task type ## Errors - `404 combo not found` → check `id` from `/api/combos` -- `400 invalid strategy` → use one of the 14 strategies above +- `400 invalid strategy` → use one of the 19 strategies above - `409 name conflict` → combo name already exists diff --git a/skills/omni-github-skills/SKILL.md b/skills/omni-github-skills/SKILL.md index 355bfe6d61..e4dc612b50 100644 --- a/skills/omni-github-skills/SKILL.md +++ b/skills/omni-github-skills/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-github-skills -description: Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. +description: Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across many tool and provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. --- ## Overview -Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. +Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across many tool and provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories. ## Authentication diff --git a/skills/omni-mcp/SKILL.md b/skills/omni-mcp/SKILL.md index e65e4795c8..5022628aa6 100644 --- a/skills/omni-mcp/SKILL.md +++ b/skills/omni-mcp/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-mcp -description: "Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes." +description: "Connect to the OmniRoute MCP server (107 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 32 permission scopes." --- ## Overview -Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes. +Connect to the OmniRoute MCP server (107 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 32 permission scopes. ## Authentication @@ -63,7 +63,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: } ``` -## Available tools (37 total) +## Core tool examples (live catalog: 107 tools) + +The table below shows representative core tools; the server's discovery response is authoritative and includes all 107 tools. | Scope | Tools | | --------- | -------------------------------------------------------------------------------------------------- | @@ -82,7 +84,7 @@ Full list: `GET $OMNIROUTE_URL/api/mcp/tools` ## Scopes -Tools are grouped into 13 scopes (chat-only, memory-readonly, full-admin, etc.). +Tools are grouped into 32 scopes (chat-only, memory-readonly, full-admin, etc.). Pass scope name as `--scope` arg or via `X-Omniroute-Scope` header. ## Reference diff --git a/skills/omni-providers/SKILL.md b/skills/omni-providers/SKILL.md index cb5f0abc92..8a973dc354 100644 --- a/skills/omni-providers/SKILL.md +++ b/skills/omni-providers/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-providers -description: Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations (OpenAI, Anthropic, Gemini, and 160+). +description: "Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations across OmniRoute's 327-provider catalog." --- ## Overview -Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations (OpenAI, Anthropic, Gemini, and 160+). +Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations across OmniRoute's 327-provider catalog. ## Authentication diff --git a/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx b/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx index 6493362ec0..b1d0eb3e83 100644 --- a/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx +++ b/src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient.tsx @@ -38,6 +38,10 @@ function CoverageBarSkeleton(): JSX.Element {
+
+
+
+
); } @@ -189,8 +193,12 @@ export function AgentSkillsPageClient(): JSX.Element { }); const selectedMarkdown = selectedId ? (markdownCache.get(selectedId) ?? null) : null; - const coverageTotal = coverage !== null ? coverage.api.have + coverage.cli.have : null; - const showGenerateButton = coverageTotal !== null && coverageTotal < 42; + const coverageTotal = + coverage !== null ? coverage.api.have + coverage.cli.have + coverage.config.have : null; + const catalogTotal = + coverage !== null ? coverage.api.total + coverage.cli.total + coverage.config.total : null; + const showGenerateButton = + coverageTotal !== null && catalogTotal !== null && coverageTotal < catalogTotal; return (
diff --git a/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx b/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx index 07846e8240..32974ffe7f 100644 --- a/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx +++ b/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx @@ -23,7 +23,7 @@ function trackColor(have: number, total: number): string { export function CoverageBar({ coverage }: CoverageBarProps): JSX.Element { const t = useTranslations("agentSkills"); - const { api, cli } = coverage; + const { api, cli, config } = coverage; return (
@@ -49,6 +49,28 @@ export function CoverageBar({ coverage }: CoverageBarProps): JSX.Element {
+
+ + {t("categoryConfig")} {config.have}/{config.total} + +
+
0 ? (config.have / config.total) * 100 : 0}%` }} + /> +
+ + {config.total > 0 ? Math.round((config.have / config.total) * 100) : 0}% + +
+
{t("categoryCli")} {cli.have}/{cli.total} diff --git a/src/lib/a2a/skills/listCapabilities.ts b/src/lib/a2a/skills/listCapabilities.ts index de4c111f35..765f820c53 100644 --- a/src/lib/a2a/skills/listCapabilities.ts +++ b/src/lib/a2a/skills/listCapabilities.ts @@ -1,7 +1,7 @@ /** * A2A Skill: List Capabilities * - * Returns the full catalog of OmniRoute agent skills (22 API + 20 CLI + config) + * Returns the full catalog of OmniRoute agent skills (23 API + 21 CLI + 1 config) * as a markdown table with raw SKILL.md URLs for orchestrating agents. */ @@ -14,7 +14,8 @@ export interface ListCapabilitiesResult { metadata: { coverage: { api: { have: number; total: 23 }; - cli: { have: number; total: 20 }; + cli: { have: number; total: 21 }; + config: { have: number; total: 1 }; }; totalSkills: number; generatedAt: string; @@ -47,7 +48,7 @@ export async function executeListCapabilities(_task: A2ATask): Promise s.category === opts.category); @@ -122,7 +125,7 @@ export function filterCatalog(opts: { category?: "api" | "cli"; area?: string }) } /** - * Computes coverage stats: filesystem has SKILL.md vs catalog declares 42. + * Computes coverage stats: filesystem has SKILL.md vs the 45-entry catalog. * Reads `skills/` relative to the project root (CWD). */ export function computeCoverage(): SkillCoverage { @@ -149,8 +152,8 @@ export function computeCoverage(): SkillCoverage { const configHave = catalog.filter((s) => s.category === "config" && presentIds.has(s.id)).length; return { - // Totals derive from the id lists — hardcoded 23/20 went stale the first - // time the catalog grew (cli-skill-collector registration, 2026-07-15). + // Totals derive from the canonical id lists so catalog additions cannot + // leave the coverage contract behind. api: { have: apiHave, total: API_SKILL_IDS.length }, cli: { have: cliHave, total: CLI_SKILL_IDS.length }, config: { have: configHave, total: configTotal }, diff --git a/src/lib/agentSkills/generator.ts b/src/lib/agentSkills/generator.ts index 5da9691504..bdcb0a307c 100644 --- a/src/lib/agentSkills/generator.ts +++ b/src/lib/agentSkills/generator.ts @@ -1,5 +1,5 @@ /** - * generator.ts — idempotent SKILL.md generator for all 42 agent skills. + * generator.ts — idempotent SKILL.md generator for all 45 agent skills. * * Usage (library): * import { generateAgentSkills, buildSkillMarkdown } from "@/lib/agentSkills/generator"; diff --git a/src/lib/agentSkills/schemas.ts b/src/lib/agentSkills/schemas.ts index dfdeef8988..4617a6316b 100644 --- a/src/lib/agentSkills/schemas.ts +++ b/src/lib/agentSkills/schemas.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -export const SkillCategorySchema = z.enum(["api", "cli"]); +export const SkillCategorySchema = z.enum(["api", "cli", "config"]); export const AgentSkillSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/), @@ -19,7 +19,8 @@ export const AgentSkillSchema = z.object({ export const SkillCoverageSchema = z.object({ api: z.object({ have: z.number().int().nonnegative(), total: z.literal(23) }), - cli: z.object({ have: z.number().int().nonnegative(), total: z.literal(20) }), + cli: z.object({ have: z.number().int().nonnegative(), total: z.literal(21) }), + config: z.object({ have: z.number().int().nonnegative(), total: z.literal(1) }), totalSkills: z.number().int().nonnegative(), generatedAt: z.string().datetime(), }); diff --git a/src/lib/agentSkills/types.ts b/src/lib/agentSkills/types.ts index 7054a10fd4..a73880b794 100644 --- a/src/lib/agentSkills/types.ts +++ b/src/lib/agentSkills/types.ts @@ -1,7 +1,7 @@ export type SkillCategory = "api" | "cli" | "config" | "external"; export type SkillArea = - // API areas (22) + // API areas (23) | "auth" | "providers" | "models" @@ -28,7 +28,7 @@ export type SkillArea = | "github-skills" // Config skills | "config-codex-cli" - // CLI families (20) + // CLI families (21) | "cli-serve" | "cli-health" | "cli-providers" diff --git a/src/shared/constants/agentSkills.ts b/src/shared/constants/agentSkills.ts index 91fee363fd..77fa85fab6 100644 --- a/src/shared/constants/agentSkills.ts +++ b/src/shared/constants/agentSkills.ts @@ -35,11 +35,11 @@ export interface CuratedSkillEntry { isNew?: boolean; } -// ── Canonical 42-entry curated list (D28) ──────────────────────────────────── +// ── Canonical 45-entry curated list (D28) ──────────────────────────────────── -/** Curated metadata for all 42 agent skills. Source-of-truth for the catalog. */ +/** Curated metadata for all 45 agent skills. Source-of-truth for the catalog. */ export const CURATED_SKILLS: CuratedSkillEntry[] = [ - // ── API Skills (22) ───────────────────────────────────────────────────────── + // ── API Skills (23) ───────────────────────────────────────────────────────── { id: "omni-auth", @@ -55,7 +55,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-providers", name: "Providers", description: - "Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations (OpenAI, Anthropic, Gemini, and 160+).", + "Manage provider connections, API keys, OAuth flows, and connection tests via the REST API. List, add, update, remove, and test AI provider integrations across OmniRoute's 327-provider catalog.", category: "api", area: "providers", icon: "key", @@ -73,7 +73,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-combos-routing", name: "Combos & Routing", description: - "Create and manage routing combos with 14 strategies (priority, weighted, round-robin, Auto-combo, etc.). Configure fallback chains, test routing outcomes, and retrieve combo metrics.", + "Create and manage routing combos with 19 strategies (priority, weighted, round-robin, Auto-combo, and more). Configure fallback chains, test routing outcomes, and retrieve combo metrics.", category: "api", area: "combos-routing", icon: "route", @@ -212,7 +212,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-mcp", name: "MCP Server", description: - "Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes.", + "Connect to the OmniRoute MCP server (107 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 32 permission scopes.", category: "api", area: "mcp", icon: "electrical_services", @@ -245,7 +245,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ icon: "hub", }, - // ── CLI Skills (20) ────────────────────────────────────────────────────────── + // ── CLI Skills (21) ────────────────────────────────────────────────────────── { id: "cli-serve", @@ -457,7 +457,7 @@ export const CURATED_SKILLS: CuratedSkillEntry[] = [ id: "omni-github-skills", name: "GitHub Skill Discovery", description: - "Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across 160+ provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories.", + "Search, score, scan, and import agent skills from GitHub repositories that contain SKILL.md, CLAUDE.md, .cursorrules, and similar agent skill files. Discover community skills across many tool and provider categories, evaluate relevance with heuristic scoring, check for malware or hardcoded secrets, and install into Hermes, Claude Code, Gemini CLI, or OpenCode agent directories.", category: "api", area: "github-skills", icon: "explore", diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index 35b3be51d1..0ec376305a 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -34,6 +34,8 @@ export const PROVIDER_ENDPOINTS = { "chat-oripe": "https://api.oriper.com/v1/chat/completions", freeinference: "https://freeinference.org/v1/chat/completions", "free-ai": "https://api.free.ai/v1/chat/", + "void-ai": "https://api.voidai.app/v1/chat/completions", + helixmind: "https://helixmind.online/v1/chat/completions", glm: "https://api.z.ai/api/anthropic/v1/messages", glmt: "https://api.z.ai/api/anthropic/v1/messages", "bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index a20078ee1a..48841ddcae 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -120,6 +120,8 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([ "chat-oripe", "freeinference", "free-ai", + "void-ai", + "helixmind", ]);; diff --git a/src/shared/constants/providers/apikey/frontier-labs.ts b/src/shared/constants/providers/apikey/frontier-labs.ts index 15a91af1bf..60139bde84 100644 --- a/src/shared/constants/providers/apikey/frontier-labs.ts +++ b/src/shared/constants/providers/apikey/frontier-labs.ts @@ -63,7 +63,7 @@ export const APIKEY_PROVIDERS_FRONTIER = { website: "https://aistudio.google.com", hasFree: true, freeNote: - "Free forever: 1,500 req/day for Gemini 2.5 Flash — no credit card, get key at aistudio.google.com", + "Free tier available through Google AI Studio; current per-model quotas and regional limits apply", }, groq: { id: "groq", @@ -85,7 +85,8 @@ export const APIKEY_PROVIDERS_FRONTIER = { textIcon: "BB", website: "https://blackbox.ai", hasFree: true, - freeNote: "Free tier: unlimited basic chat plus Minimax-M2.5, no credit card required", + freeNote: + "Limited free access is available through Blackbox; model availability and account limits apply", }, xai: { id: "xai", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 40855ff9d0..3a88b74d9e 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -1215,4 +1215,34 @@ export const APIKEY_PROVIDERS_GATEWAYS = { authHint: "Get API key at naga.ac — Google/GitHub/Discord signup available.", }, + "void-ai": { + id: "void-ai", + alias: "void-ai", + name: "Void AI", + icon: "science", + color: "#111827", + textIcon: "VA", + passthroughModels: true, + website: "https://voidai.app", + hasFree: true, + freeNote: + "The public model catalog marks some models with a free plan requirement, but access is conditional and no numeric quota is confirmed.", + apiHint: + "Use https://api.voidai.app/v1 only after confirming authentication, account eligibility and terms. Treat this integration as experimental until the blocked documentation becomes public.", + }, + helixmind: { + id: "helixmind", + alias: "helixmind", + name: "HelixMind", + icon: "hub", + color: "#4F46E5", + textIcon: "HM", + passthroughModels: true, + website: "https://helixmind.online", + hasFree: false, + freeNote: + "Previously circulated 3 RPM/50 RPD and no-card claims were not confirmed during the 2026-08-02 audit; current quota and billing require account verification.", + apiHint: + "Create a helix- key and use https://helixmind.online/v1. OpenAI requests use Bearer authentication; the Anthropic-compatible messages endpoint accepts x-api-key.", + }, }; diff --git a/src/shared/constants/providers/apikey/inference-hosts.ts b/src/shared/constants/providers/apikey/inference-hosts.ts index 678fbc7c6a..2830e691a3 100644 --- a/src/shared/constants/providers/apikey/inference-hosts.ts +++ b/src/shared/constants/providers/apikey/inference-hosts.ts @@ -127,7 +127,8 @@ export const APIKEY_PROVIDERS_INFERENCE = { textIcon: "SF", website: "https://cloud.siliconflow.com", hasFree: true, - freeNote: "$1 free credits plus permanently free models after identity verification", + freeNote: + "$1 free credits plus currently listed $0 models after identity verification; availability and limits may change", }, hyperbolic: { id: "hyperbolic", diff --git a/tests/e2e/agent-skills-page.spec.ts b/tests/e2e/agent-skills-page.spec.ts index 7538940e3b..0683d593d4 100644 --- a/tests/e2e/agent-skills-page.spec.ts +++ b/tests/e2e/agent-skills-page.spec.ts @@ -7,7 +7,7 @@ type AgentSkill = { id: string; name: string; description: string; - category: "api" | "cli"; + category: "api" | "cli" | "config"; area: string; icon: string; endpoints?: string[]; @@ -19,13 +19,14 @@ type AgentSkill = { type SkillCoverage = { api: { have: number; total: number }; cli: { have: number; total: number }; + config: { have: number; total: number }; totalSkills: number; generatedAt: string; }; function makeAgentSkills(): AgentSkill[] { const skills: AgentSkill[] = []; - for (let i = 0; i < 22; i++) { + for (let i = 0; i < 23; i++) { skills.push({ id: `omni-skill-${i}`, name: `API Skill ${i}`, @@ -38,7 +39,7 @@ function makeAgentSkills(): AgentSkill[] { githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/omni-skill-${i}/SKILL.md`, }); } - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 21; i++) { skills.push({ id: `cli-skill-${i}`, name: `CLI Skill ${i}`, @@ -51,13 +52,25 @@ function makeAgentSkills(): AgentSkill[] { githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/cli-skill-${i}/SKILL.md`, }); } + skills.push({ + id: "config-codex-cli", + name: "Config: Codex CLI", + description: "Configure Codex CLI to use OmniRoute.", + category: "config", + area: "config-codex-cli", + icon: "terminal", + rawUrl: + "https://raw.githubusercontent.com/example/OmniRoute/main/skills/config-codex-cli/SKILL.md", + githubUrl: "https://github.com/example/OmniRoute/blob/main/skills/config-codex-cli/SKILL.md", + }); return skills; } const FULL_COVERAGE: SkillCoverage = { - api: { have: 22, total: 22 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + api: { have: 23, total: 23 }, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; @@ -80,9 +93,7 @@ async function fulfillText(route: Route, body: string, status = 200) { test.describe("Agent Skills page", () => { test.setTimeout(600_000); - test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({ - page, - }) => { + test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({ page }) => { const skills = makeAgentSkills(); await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => { @@ -101,7 +112,7 @@ test.describe("Agent Skills page", () => { await expect(conceptCard).toBeVisible({ timeout: 15_000 }); }); - test("grid shows 42 skill cards when filter is 'all'", async ({ page }) => { + test("grid shows 45 skill cards when filter is 'all'", async ({ page }) => { const skills = makeAgentSkills(); await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => { @@ -122,12 +133,10 @@ test.describe("Agent Skills page", () => { }); const cards = page.locator("[data-testid^='skill-card-']"); - await expect(cards).toHaveCount(42, { timeout: 15_000 }); + await expect(cards).toHaveCount(45, { timeout: 15_000 }); }); - test("clicking omni-skill-0 card renders markdown in preview pane", async ({ - page, - }) => { + test("clicking omni-skill-0 card renders markdown in preview pane", async ({ page }) => { const skills = makeAgentSkills(); const mockMarkdown = "# API Skill 0\n\nThis skill manages connections."; @@ -199,7 +208,7 @@ test.describe("Agent Skills page", () => { expect( finalUrl.includes("/dashboard/omni-skills") || finalUrl.includes("/login") || - finalUrl.includes("/onboarding"), + finalUrl.includes("/onboarding") ).toBe(true); }); }); diff --git a/tests/integration/agent-skills-content.test.ts b/tests/integration/agent-skills-content.test.ts index a3e4f4a305..404fd8c85a 100644 --- a/tests/integration/agent-skills-content.test.ts +++ b/tests/integration/agent-skills-content.test.ts @@ -2,7 +2,7 @@ * Integration tests for Agent Skills content integrity. * * Verifies: - * 1. All 44 skill IDs from catalog have skills/{id}/ folder with SKILL.md. + * 1. All 45 skill IDs from catalog have skills/{id}/ folder with SKILL.md. * 2. Zero omniroute-* folders remain (post-prune: old omniroute-* skill dirs were removed). * 3. 12 specific IDs have ... blocks: * omni-mcp, omni-compression, cli-providers, cli-eval, omni-agents-a2a, @@ -15,7 +15,8 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = await import("../../src/lib/agentSkills/catalog.ts"); +const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = + await import("../../src/lib/agentSkills/catalog.ts"); const SKILLS_DIR = path.resolve(process.cwd(), "skills"); const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[]; @@ -37,9 +38,9 @@ const CUSTOM_BLOCK_IDS = [ "config-codex-cli", ] as const; -// ── §1: All 42 catalog IDs have skills/{id}/SKILL.md ───────────────────────── +// ── §1: All 45 catalog IDs have skills/{id}/SKILL.md ───────────────────────── -test("all 44 catalog IDs have a skills/{id}/ directory", () => { +test("all 45 catalog IDs have a skills/{id}/ directory", () => { const missing: string[] = []; for (const id of ALL_IDS) { const dirPath = path.join(SKILLS_DIR, id); @@ -50,7 +51,7 @@ test("all 44 catalog IDs have a skills/{id}/ directory", () => { assert.deepEqual(missing, [], `Missing skill directories: ${missing.join(", ")}`); }); -test("all 44 catalog IDs have a skills/{id}/SKILL.md file", () => { +test("all 45 catalog IDs have a skills/{id}/SKILL.md file", () => { const missing: string[] = []; for (const id of ALL_IDS) { const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); @@ -75,7 +76,7 @@ test("skills/ has zero omniroute-* directories (all pruned)", () => { assert.deepEqual( omniRouteDirs, [], - `Found omniroute-* directories that should have been pruned: ${omniRouteDirs.join(", ")}`, + `Found omniroute-* directories that should have been pruned: ${omniRouteDirs.join(", ")}` ); }); @@ -85,11 +86,7 @@ test("skills/ directory only contains expected catalog IDs plus README", () => { const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); const expectedSet = new Set(ALL_IDS); const unexpected = dirs.filter((d) => !expectedSet.has(d)); - assert.deepEqual( - unexpected, - [], - `Unexpected directories in skills/: ${unexpected.join(", ")}`, - ); + assert.deepEqual(unexpected, [], `Unexpected directories in skills/: ${unexpected.join(", ")}`); }); // ── §3: 10 specific IDs have custom blocks ─────────────────────────────────── @@ -97,18 +94,15 @@ test("skills/ directory only contains expected catalog IDs plus README", () => { for (const id of CUSTOM_BLOCK_IDS) { test(`skills/${id}/SKILL.md has block`, () => { const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); - assert.ok( - fs.existsSync(skillPath), - `skills/${id}/SKILL.md does not exist`, - ); + assert.ok(fs.existsSync(skillPath), `skills/${id}/SKILL.md does not exist`); const content = fs.readFileSync(skillPath, "utf-8"); assert.ok( content.includes(""), - `skills/${id}/SKILL.md missing block`, + `skills/${id}/SKILL.md missing block` ); assert.ok( content.includes(""), - `skills/${id}/SKILL.md missing block`, + `skills/${id}/SKILL.md missing block` ); }); } @@ -130,7 +124,7 @@ test("exactly 13 skills have custom blocks", () => { assert.deepEqual( withCustomBlocks.sort(), expectedIds, - `Expected exactly these 13 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`, + `Expected exactly these 13 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}` ); }); diff --git a/tests/integration/agent-skills-discovery.test.ts b/tests/integration/agent-skills-discovery.test.ts index 6cc9a8dc52..8caf9c6874 100644 --- a/tests/integration/agent-skills-discovery.test.ts +++ b/tests/integration/agent-skills-discovery.test.ts @@ -2,10 +2,10 @@ * Integration tests for Agent Skills discovery. * * Verifies: - * 1. Every ID in API_SKILL_IDS + CLI_SKILL_IDS has a skills//SKILL.md on disk. + * 1. Every catalog ID has a skills//SKILL.md on disk. * 2. Each SKILL.md has valid frontmatter (name + description) and body ≥ 100 chars. - * 3. MCP tool omniroute_agent_skills_list handler returns 44 entries. - * 4. A2A skill list-capabilities returns 1 artifact with 44 lines. + * 3. MCP tool omniroute_agent_skills_list handler returns 45 entries. + * 4. A2A skill list-capabilities returns one artifact containing all 45 entries. * * Does NOT spin up a server — tests handlers directly via imports. */ @@ -15,7 +15,8 @@ import fs from "node:fs"; import path from "node:path"; // Dynamic imports for ESM + tsx compatibility -const { API_SKILL_IDS, CLI_SKILL_IDS } = await import("../../src/lib/agentSkills/catalog.ts"); +const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = + await import("../../src/lib/agentSkills/catalog.ts"); const { agentSkillTools } = await import("../../open-sse/mcp-server/tools/agentSkillTools.ts"); const { executeListCapabilities } = await import("../../src/lib/a2a/skills/listCapabilities.ts"); import type { A2ATask } from "../../src/lib/a2a/taskManager.ts"; @@ -41,7 +42,7 @@ function parseSkillMarkdown(content: string): { name: string; description: strin // ── §1: Filesystem — every skill ID has a SKILL.md ─────────────────────────── -const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[]; +const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[]; test("skills/ directory exists and is readable", () => { assert.ok(fs.existsSync(SKILLS_DIR), `skills/ directory not found at ${SKILLS_DIR}`); @@ -69,8 +70,19 @@ test("every CLI skill ID has skills//SKILL.md on disk", () => { assert.deepEqual(missing, [], `Missing CLI SKILL.md files: ${missing.join(", ")}`); }); -test("total skill count is exactly 44 (23 API + 21 CLI)", () => { - assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 44); +test("every config skill ID has skills//SKILL.md on disk", () => { + const missing: string[] = []; + for (const id of CONFIG_SKILL_IDS) { + const skillPath = path.join(SKILLS_DIR, id, "SKILL.md"); + if (!fs.existsSync(skillPath)) { + missing.push(id); + } + } + assert.deepEqual(missing, [], `Missing config SKILL.md files: ${missing.join(", ")}`); +}); + +test("total skill count is exactly 45 (23 API + 21 CLI + 1 config)", () => { + assert.equal(ALL_IDS.length, 45); }); // ── §2: Frontmatter validation ──────────────────────────────────────────────── @@ -120,14 +132,14 @@ test("each SKILL.md body is at least 100 chars", () => { // ── §3: MCP tool omniroute_agent_skills_list ───────────────────────────────── -test("MCP omniroute_agent_skills_list handler returns count 45 (44 + config)", async () => { +test("MCP omniroute_agent_skills_list handler returns count 45", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); assert.equal(result.count, 45, `Expected 45 but got ${result.count}`); assert.ok(Array.isArray(result.skills)); assert.equal(result.skills.length, 45); }); -test("MCP omniroute_agent_skills_list result has all 42 IDs", async () => { +test("MCP omniroute_agent_skills_list result has all 45 IDs", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); const returnedIds = new Set(result.skills.map((s: { id: string }) => s.id)); for (const id of ALL_IDS) { @@ -145,7 +157,7 @@ test("A2A list-capabilities returns exactly 1 artifact", async () => { assert.equal(result.artifacts[0].type, "text", "Artifact type should be 'text'"); }); -test("A2A list-capabilities artifact content contains 42 skill IDs as table rows", async () => { +test("A2A list-capabilities artifact content contains 45 skill IDs as table rows", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; const rows = content @@ -154,15 +166,15 @@ test("A2A list-capabilities artifact content contains 42 skill IDs as table rows (line) => line.startsWith("| ") && !line.startsWith("| ID") && !line.startsWith("| ---") ); // Each skill row starts with "| |" - assert.ok(rows.length >= 42, `Expected at least 42 data rows but got ${rows.length}`); + assert.equal(rows.length, 45, `Expected 45 data rows but got ${rows.length}`); }); -test("A2A list-capabilities metadata.totalSkills === 45 (44 + config)", async () => { +test("A2A list-capabilities metadata.totalSkills === 45", async () => { const result = await executeListCapabilities(stubTask); assert.equal(result.metadata.totalSkills, 45); }); -test("A2A list-capabilities artifact contains all 42 skill IDs", async () => { +test("A2A list-capabilities artifact contains all 45 skill IDs", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; const missing: string[] = []; diff --git a/tests/unit/agentSkillTools-mcp.test.ts b/tests/unit/agentSkillTools-mcp.test.ts index e344f14928..7c6ed46e5f 100644 --- a/tests/unit/agentSkillTools-mcp.test.ts +++ b/tests/unit/agentSkillTools-mcp.test.ts @@ -77,13 +77,21 @@ test("omniroute_agent_skills_list({category:'cli'}) returns exactly 21 entries", assert.ok(result.skills.every((s: { category: string }) => s.category === "cli")); }); +test("omniroute_agent_skills_list({category:'config'}) returns exactly 1 entry", async () => { + const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "config" }); + assert.equal(result.count, 1, `Expected 1 config skill but got ${result.count}`); + assert.ok(result.skills.every((s: { category: string }) => s.category === "config")); +}); + test("omniroute_agent_skills_list result includes coverage shape", async () => { const result = await agentSkillTools.omniroute_agent_skills_list.handler({}); assert.ok(result.coverage != null, "coverage should be present"); assert.ok(typeof result.coverage.api === "object"); assert.ok(typeof result.coverage.cli === "object"); + assert.ok(typeof result.coverage.config === "object"); assert.equal(result.coverage.api.total, 23); assert.equal(result.coverage.cli.total, 21); + assert.equal(result.coverage.config.total, 1); assert.ok(typeof result.coverage.totalSkills === "number"); assert.ok(typeof result.coverage.generatedAt === "string"); }); @@ -94,7 +102,7 @@ test("omniroute_agent_skills_list skill entries have required fields", async () assert.ok(typeof first.id === "string" && first.id.length > 0); assert.ok(typeof first.name === "string" && first.name.length > 0); assert.ok(typeof first.description === "string"); - assert.ok(first.category === "api" || first.category === "cli"); + assert.ok(first.category === "api" || first.category === "cli" || first.category === "config"); assert.ok(typeof first.area === "string"); assert.ok(typeof first.rawUrl === "string"); assert.ok(typeof first.githubUrl === "string"); @@ -105,6 +113,11 @@ test("AgentSkillsListSchema parses valid category filter", () => { assert.equal(parsed.category, "api"); }); +test("AgentSkillsListSchema parses config category filter", () => { + const parsed = AgentSkillsListSchema.parse({ category: "config" }); + assert.equal(parsed.category, "config"); +}); + test("AgentSkillsListSchema rejects invalid category", () => { assert.throws(() => AgentSkillsListSchema.parse({ category: "unknown" })); }); @@ -140,6 +153,14 @@ test("omniroute_agent_skills_get({id:'cli-serve'}) resolves correct cli skill me assert.ok(typeof skill!.name === "string" && skill!.name.length > 0); }); +test("omniroute_agent_skills_get({id:'config-codex-cli'}) resolves config skill metadata", async () => { + const { getSkillById } = await import("../../src/lib/agentSkills/catalog.ts"); + const skill = getSkillById("config-codex-cli"); + assert.ok(skill != null, "config-codex-cli should exist in catalog"); + assert.equal(skill!.id, "config-codex-cli"); + assert.equal(skill!.category, "config"); +}); + test("omniroute_agent_skills_get with invalid id throws Error", async () => { await assert.rejects( () => agentSkillTools.omniroute_agent_skills_get.handler({ id: "non-existent-skill-xyz" }), @@ -167,14 +188,17 @@ test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => { assert.ok(result != null); assert.ok(typeof result.api === "object"); assert.ok(typeof result.cli === "object"); + assert.ok(typeof result.config === "object"); assert.equal(result.api.total, 23); assert.equal(result.cli.total, 21); + assert.equal(result.config.total, 1); assert.ok(typeof result.api.have === "number"); assert.ok(typeof result.cli.have === "number"); assert.ok(result.api.have >= 0 && result.api.have <= 23); assert.ok(result.cli.have >= 0 && result.cli.have <= 21); + assert.ok(result.config.have >= 0 && result.config.have <= 1); assert.ok(typeof result.totalSkills === "number"); - assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0)); + assert.equal(result.totalSkills, result.api.have + result.cli.have + result.config.have); assert.ok(typeof result.generatedAt === "string"); // Validate ISO datetime format assert.ok(!isNaN(Date.parse(result.generatedAt)), "generatedAt should be valid ISO datetime"); diff --git a/tests/unit/agentSkills-catalog.test.ts b/tests/unit/agentSkills-catalog.test.ts index 7ee190554d..378e5ea03e 100644 --- a/tests/unit/agentSkills-catalog.test.ts +++ b/tests/unit/agentSkills-catalog.test.ts @@ -9,6 +9,7 @@ const { computeCoverage, refreshCatalog, API_SKILL_IDS, + CONFIG_SKILL_IDS, CLI_SKILL_IDS, } = await import("../../src/lib/agentSkills/catalog.ts"); const agentSkillsConstants = await import("../../src/shared/constants/agentSkills.ts"); @@ -25,11 +26,11 @@ test("API_SKILL_IDS has exactly 23 entries", () => { assert.equal(API_SKILL_IDS.length, 23); }); -test("CLI_SKILL_IDS has exactly 20 entries", () => { +test("CLI_SKILL_IDS has exactly 21 entries", () => { assert.equal(CLI_SKILL_IDS.length, 21); }); -test("getCatalog() contains exactly 22 api skills", () => { +test("getCatalog() contains exactly 23 api skills", () => { const apiSkills = getCatalog().filter((s) => s.category === "api"); assert.equal(apiSkills.length, 23); }); @@ -39,6 +40,15 @@ test("getCatalog() contains exactly 21 cli skills", () => { assert.equal(cliSkills.length, 21); }); +test("CONFIG_SKILL_IDS has exactly 1 entry", () => { + assert.equal(CONFIG_SKILL_IDS.length, 1); +}); + +test("getCatalog() contains exactly 1 config skill", () => { + const configSkills = getCatalog().filter((s) => s.category === "config"); + assert.equal(configSkills.length, 1); +}); + // ─── ID format ──────────────────────────────────────────────────────────────── test("all skill IDs match regex ^[a-z][a-z0-9-]*$", () => { @@ -218,6 +228,11 @@ test("computeCoverage() returns valid SkillCoverage shape", () => { assert.ok(typeof cov.cli.have === "number"); assert.ok(cov.cli.have >= 0 && cov.cli.have <= 21); + assert.ok(typeof cov.config === "object"); + assert.equal(cov.config.total, 1); + assert.ok(typeof cov.config.have === "number"); + assert.ok(cov.config.have >= 0 && cov.config.have <= 1); + assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0)); // generatedAt must be a valid ISO datetime string @@ -227,7 +242,7 @@ test("computeCoverage() returns valid SkillCoverage shape", () => { ); }); -test("computeCoverage() api.have + cli.have = totalSkills", () => { +test("computeCoverage() category totals add up to totalSkills", () => { const cov = computeCoverage(); assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0)); }); diff --git a/tests/unit/agentSkills-routes.test.ts b/tests/unit/agentSkills-routes.test.ts index 1de6eccf2d..1af09c6cfd 100644 --- a/tests/unit/agentSkills-routes.test.ts +++ b/tests/unit/agentSkills-routes.test.ts @@ -5,7 +5,7 @@ * Auth tested via requireManagementAuth with live DB in temp directory. * * Coverage goals: - * - GET /api/agent-skills — happy path (43 skills), filters, invalid category + * - GET /api/agent-skills — happy path (45 skills), filters, invalid category * - GET /api/agent-skills/[id] — found, 404 not found * - GET /api/agent-skills/[id]/raw — found, 404 not found, 502 on GitHub failure * - GET /api/agent-skills/coverage — happy path @@ -55,7 +55,7 @@ function makeRequest( method: string, url: string, body?: unknown, - headers: Record = {}, + headers: Record = {} ): Request { return new Request(url, { method, @@ -120,7 +120,10 @@ test("GET /api/agent-skills?category=api — returns 23 api skills", async () => assert.equal(res.status, 200); const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; assert.equal(body.count, 23); - assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category"); + assert.ok( + body.skills.every((s) => s.category === "api"), + "All skills should be api category" + ); }); test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => { @@ -130,7 +133,23 @@ test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => assert.equal(res.status, 200); const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; assert.equal(body.count, 21); - assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category"); + assert.ok( + body.skills.every((s) => s.category === "cli"), + "All skills should be cli category" + ); +}); + +test("GET /api/agent-skills?category=config — returns 1 config skill", async () => { + const req = makeRequest("GET", "http://localhost/api/agent-skills?category=config"); + const res = await listRoute.GET(req); + + assert.equal(res.status, 200); + const body = (await res.json()) as { skills: Array<{ category: string }>; count: number }; + assert.equal(body.count, 1); + assert.ok( + body.skills.every((s) => s.category === "config"), + "All skills should be config category" + ); }); test("GET /api/agent-skills?area=providers — returns only providers area skills", async () => { @@ -154,7 +173,7 @@ test("GET /api/agent-skills?category=invalid — returns 400 with sanitized erro // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -192,7 +211,7 @@ test("GET /api/agent-skills/[id] — returns 404 with sanitized error for unknow // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -213,7 +232,7 @@ test("GET /api/agent-skills/[id]/raw — returns 404 with sanitized error for un // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -230,14 +249,14 @@ test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (n // Either 200 (network available) or 502 (no network) is acceptable assert.ok( res.status === 200 || res.status === 502 || res.status === 500, - `Expected 200, 502, or 500 but got ${res.status}`, + `Expected 200, 502, or 500 but got ${res.status}` ); if (res.status === 200) { const contentType = res.headers.get("content-type") ?? ""; assert.ok( contentType.includes("text/markdown"), - `Expected text/markdown content-type, got: ${contentType}`, + `Expected text/markdown content-type, got: ${contentType}` ); const cacheControl = res.headers.get("cache-control") ?? ""; assert.ok(cacheControl.includes("max-age=3600"), "Cache-Control should include max-age=3600"); @@ -247,7 +266,7 @@ test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (n assert.ok(body.error); assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); } }); @@ -291,7 +310,7 @@ test("POST /api/agent-skills/generate — 401 when auth is required and no token // requireManagementAuth returns 401 or 403 when auth is required and no token assert.ok( res.status === 401 || res.status === 403, - `Expected 401 or 403 without auth, got ${res.status}`, + `Expected 401 or 403 without auth, got ${res.status}` ); const body = (await res.json()) as { error: { message: string } | string }; @@ -300,7 +319,7 @@ test("POST /api/agent-skills/generate — 401 when auth is required and no token typeof body.error === "string" ? body.error : (body.error as { message: string }).message; assert.ok( !errorMsg.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${errorMsg}"`, + `Error message must not contain stack trace: "${errorMsg}"` ); }); @@ -318,7 +337,7 @@ test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean // But with invalid body, 400 should come first assert.ok( res.status === 400 || res.status === 503, - `Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}`, + `Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}` ); const body = (await res.json()) as { error: { message: string } }; @@ -326,7 +345,7 @@ test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -345,7 +364,7 @@ test("POST /api/agent-skills/generate — 503 when generator module unavailable // Both are valid depending on merge state. assert.ok( res.status === 200 || res.status === 503, - `Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}`, + `Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}` ); const body = (await res.json()) as Record; @@ -355,7 +374,7 @@ test("POST /api/agent-skills/generate — 503 when generator module unavailable // Hard Rule #12: no stack trace exposure assert.ok( !err.message.match(/\bat \/|\bat file:\/\//), - `503 error message must not contain stack trace: "${err.message}"`, + `503 error message must not contain stack trace: "${err.message}"` ); } else { // 200: body should look like a GeneratorReport @@ -380,7 +399,7 @@ test("POST /api/agent-skills/generate — 400 when request body is not JSON", as // Hard Rule #12: no stack trace exposure assert.ok( !body.error.message.match(/\bat \/|\bat file:\/\//), - `Error message must not contain stack trace: "${body.error.message}"`, + `Error message must not contain stack trace: "${body.error.message}"` ); }); @@ -394,23 +413,20 @@ test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' p // Collect error responses from various bad inputs const errorResponses: Response[] = [ // Invalid category query - await listRoute.GET( - makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val"), - ), + await listRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val")), // Unknown skill id await idRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id"), { params: Promise.resolve({ id: "unknown-id" }), }), // Unknown raw skill id - await rawRoute.GET( - makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"), - { params: Promise.resolve({ id: "unknown-id" }) }, - ), + await rawRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"), { + params: Promise.resolve({ id: "unknown-id" }), + }), // Invalid generate body (non-boolean) await generateRoute.POST( makeRequest("POST", "http://localhost/api/agent-skills/generate", { dryRun: 42, - }), + }) ), ]; @@ -419,14 +435,14 @@ test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' p const contentType = res.headers.get("content-type") ?? ""; assert.ok( contentType.includes("application/json") || contentType.includes("json"), - `Error response must be JSON, got content-type: ${contentType}`, + `Error response must be JSON, got content-type: ${contentType}` ); const body = (await res.json()) as { error?: { message?: string } }; const message = body?.error?.message ?? ""; assert.ok( !message.match(/\bat \/|\bat file:\/\//), - `Stack trace detected in error response (status ${res.status}): "${message}"`, + `Stack trace detected in error response (status ${res.status}): "${message}"` ); } }); diff --git a/tests/unit/agentSkills-schemas.test.ts b/tests/unit/agentSkills-schemas.test.ts index a793f7675e..ea40f9b090 100644 --- a/tests/unit/agentSkills-schemas.test.ts +++ b/tests/unit/agentSkills-schemas.test.ts @@ -136,8 +136,9 @@ test("AgentSkillSchema — .parse throws on invalid input", () => { test("SkillCoverageSchema — valid coverage parses successfully", () => { const input = { api: { have: 23, total: 23 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -147,8 +148,9 @@ test("SkillCoverageSchema — valid coverage parses successfully", () => { test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => { const input = { api: { have: 21, total: 21 }, - cli: { have: 20, total: 20 }, - totalSkills: 41, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 44, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -158,8 +160,9 @@ test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => { test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => { const input = { api: { have: 23, total: 23 }, - cli: { have: 19, total: 19 }, - totalSkills: 41, + cli: { have: 20, total: 20 }, + config: { have: 1, total: 1 }, + totalSkills: 44, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -169,8 +172,9 @@ test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => { test("SkillCoverageSchema — invalid datetime fails", () => { const input = { api: { have: 23, total: 23 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: "not-a-date", }; const result = SkillCoverageSchema.safeParse(input); @@ -180,8 +184,9 @@ test("SkillCoverageSchema — invalid datetime fails", () => { test("SkillCoverageSchema — negative have value fails", () => { const input = { api: { have: -1, total: 23 }, - cli: { have: 20, total: 20 }, - totalSkills: 42, + cli: { have: 21, total: 21 }, + config: { have: 1, total: 1 }, + totalSkills: 45, generatedAt: new Date().toISOString(), }; const result = SkillCoverageSchema.safeParse(input); @@ -207,6 +212,14 @@ test("ListQuerySchema — valid category parses successfully", () => { } }); +test("ListQuerySchema — config category parses successfully", () => { + const result = ListQuerySchema.safeParse({ category: "config" }); + assert.equal(result.success, true); + if (result.success) { + assert.equal(result.data.category, "config"); + } +}); + test("ListQuerySchema — invalid category fails", () => { const result = ListQuerySchema.safeParse({ category: "invalid" }); assert.equal(result.success, false); diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts index 9e2afca449..5557e5f10c 100644 --- a/tests/unit/check-docs-counts-sync.test.ts +++ b/tests/unit/check-docs-counts-sync.test.ts @@ -8,6 +8,9 @@ import { tallyDrift, readProviderTotal, countLocales, + readMcpFactsFromSource, + listLocalizedDocs, + makeRequiredCountsValidator, } from "../../scripts/check/check-docs-counts-sync.mjs"; // Explicit types for the .mjs exports — keep the test at 0 no-explicit-any warnings. @@ -24,6 +27,11 @@ const tally = tallyDrift as ( ) => { strict: number; soft: number; lines: string[] }; const readTotal = readProviderTotal as () => number; const locales = countLocales as () => number; +const mcpFacts = readMcpFactsFromSource as () => { tools: number; scopes: number } | null; +const localizedDocs = listLocalizedDocs as (relativePath: string) => string[]; +const requireCounts = makeRequiredCountsValidator as ( + requirements: { label: string; value: number }[] +) => (content: string) => { ok: boolean; detail: string }; const here = path.dirname(fileURLToPath(import.meta.url)); const GATE = path.resolve(here, "../../scripts/check/check-docs-counts-sync.mjs"); @@ -85,13 +93,38 @@ test("a missing file (null content) registers drift, not a crash", () => { // --- live source readers (smoke) ----------------------------------------------------- test("readProviderTotal reads a real, positive total from the catalog", () => { - assert.ok(readTotal() > 100, "provider catalog total should be > 100"); + assert.ok(readTotal() >= 300, "live provider catalog total should be at least 300"); }); test("countLocales reads a real, positive locale count from config/i18n.json", () => { assert.ok(locales() >= 40, "i18n config should define at least 40 locales"); }); +test("source-only MCP fallback matches the canonical inventory and scope union", () => { + assert.deepEqual(mcpFacts(), { tools: 107, scopes: 32 }); +}); + +test("localized-doc discovery returns every locale root document", () => { + const readmes = localizedDocs("README.md"); + assert.equal(readmes.length, 42); + assert.ok(readmes.includes("docs/i18n/pt-BR/README.md")); + assert.ok(readmes.includes("docs/i18n/zh-CN/README.md")); +}); + +test("required-count validator reports precisely which live markers are missing", () => { + const validate = requireCounts([ + { label: "providers", value: 327 }, + { label: "MCP tools", value: 107 }, + { label: "MCP scopes", value: 32 }, + ]); + assert.equal(validate("327 providers; 107 tools; 32 scopes").ok, true); + const stale = validate("290 providers; 104 tools; 31 scopes"); + assert.equal(stale.ok, false); + assert.match(stale.detail, /providers=327/); + assert.match(stale.detail, /MCP tools=107/); + assert.match(stale.detail, /MCP scopes=32/); +}); + // --- live gate smoke ----------------------------------------------------------------- test("the gate exits 0 against the current (synced) repo state", () => { @@ -105,6 +138,7 @@ test("the gate exits 0 against the current (synced) repo state", () => { // down to 1.37B, because no gate watched that number. import { checkFreeTierHeadline, + checkFreeTierInventory, extractHeadlineClaims, } from "../../scripts/check/check-docs-counts-sync.mjs"; @@ -146,9 +180,27 @@ test("free-tier gate passes when a file carries no headline at all", () => { assert.equal(checkHeadline("no figures here", TOTALS).ok, true); }); +const checkInventory = checkFreeTierInventory as ( + content: string, + totals: { pools: number; models: number } +) => { ok: boolean; detail: string }; + +test("free-tier inventory gate accepts the live pool/model counts", () => { + assert.equal( + checkInventory("43 provider pools / 522 model budget entries", { pools: 43, models: 522 }).ok, + true + ); +}); + +test("free-tier inventory gate rejects stale model counts", () => { + const result = checkInventory("43 provider pools / 516 models", { pools: 43, models: 522 }); + assert.equal(result.ok, false); + assert.match(result.detail, /live catalog has 43 pools \/ 522 model budget entries/); +}); + // --- Generic numeric-claim gate (engines / MCP tools / scopes / CLI) -------- // Extends the same drift guard to the counts that silently drifted in v3.8.49: -// 11→12 engines, 94→104 MCP tools, 30→31 scopes, 26→33 CLI tools. +// 11→12 engines, 94→107 MCP tools, 30→32 scopes, 26→33 CLI tools. import { makeNumberClaimValidator } from "../../scripts/check/check-docs-counts-sync.mjs"; const makeValidator = makeNumberClaimValidator as ( @@ -157,19 +209,19 @@ const makeValidator = makeNumberClaimValidator as ( ) => (content: string) => { ok: boolean; detail: string }; test("MCP-tools gate accepts the aggregate and rejects a stale one", () => { - const v = makeValidator(104, { + const v = makeValidator(107, { what: "MCP tools", pattern: /(\d+) tools/gi, skipBefore: /(tools?|definitions?)\s*\(\s*$/i, skipAfter: /^\s*\(\d+ CLI/, }); - assert.equal(v("MCP Server (104 tools)").ok, true); - assert.equal(v("with 104 tools total").ok, true); + assert.equal(v("MCP Server (107 tools)").ok, true); + assert.equal(v("with 107 tools total").ok, true); assert.equal(v("MCP Server (94 tools)").ok, false); }); test("MCP-tools gate ignores per-module counts and the CLI catalog total", () => { - const v = makeValidator(104, { + const v = makeValidator(107, { what: "MCP tools", pattern: /(\d+) tools/gi, skipBefore: /(tools?|definitions?)\s*\(\s*$/i, diff --git a/tests/unit/free-tier-providers-wave5-integration.test.ts b/tests/unit/free-tier-providers-wave5-integration.test.ts new file mode 100644 index 0000000000..48d24c2081 --- /dev/null +++ b/tests/unit/free-tier-providers-wave5-integration.test.ts @@ -0,0 +1,101 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { DefaultExecutor, getExecutor, hasSpecializedExecutor } = + await import("../../open-sse/executors/index.ts"); +const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts"); +const { isValidModel } = await import("../../src/shared/constants/models.ts"); +const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts"); +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts"); + +const providers = [ + { + id: "void-ai", + endpoint: "https://api.voidai.app/v1/chat/completions", + modelsUrl: "https://api.voidai.app/v1/models", + hasFree: true, + }, + { + id: "helixmind", + endpoint: "https://helixmind.online/v1/chat/completions", + modelsUrl: "https://helixmind.online/v1/models", + hasFree: false, + }, +] as const; + +for (const { id, endpoint, modelsUrl, hasFree } of providers) { + test(`${id} is fully wired through the public provider interfaces`, () => { + const registry = REGISTRY[id]; + const metadata = APIKEY_PROVIDERS[id]; + + assert.ok(registry); + assert.ok(metadata); + assert.equal(registry.id, id); + assert.equal(registry.alias, id); + assert.equal(registry.format, "openai"); + assert.equal(registry.executor, "default"); + assert.equal(registry.authType, "apikey"); + assert.equal(registry.authHeader, "bearer"); + assert.equal(registry.baseUrl, endpoint); + assert.equal(registry.modelsUrl, modelsUrl); + assert.equal(registry.passthroughModels, true); + assert.deepEqual(registry.models, []); + + assert.equal(PROVIDER_ENDPOINTS[id], endpoint); + assert.equal(metadata.id, id); + assert.equal(metadata.alias, id); + assert.equal(metadata.hasFree, hasFree); + assert.equal(metadata.passthroughModels, true); + assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); + assert.equal(hasSpecializedExecutor(id), false); + + const executor = getExecutor(id); + assert.ok(executor instanceof DefaultExecutor); + assert.equal(executor.buildUrl("live-model", false), endpoint); + assert.equal(isValidModel(id, "future/live-catalog-model"), true); + + const discovery = deriveConfigFromRegistryModelsUrl(id); + assert.ok(discovery); + assert.equal(discovery.url, modelsUrl); + assert.deepEqual(discovery.parseResponse({ object: "list", data: [{ id: "live-model" }] }), [ + { id: "live-model" }, + ]); + }); +} + +test("Void AI metadata keeps the free-plan signal conditional", () => { + const metadata = APIKEY_PROVIDERS["void-ai"]; + + assert.match(metadata.freeNote ?? "", /free plan/i); + assert.match(metadata.freeNote ?? "", /conditional/i); + assert.match(metadata.freeNote ?? "", /no numeric quota/i); + assert.match(metadata.apiHint ?? "", /authentication.*account.*terms/i); +}); + +test("HelixMind exposes its verified alternate API surfaces without reviving old quota claims", () => { + const registry = REGISTRY.helixmind; + const metadata = APIKEY_PROVIDERS.helixmind; + + assert.equal(registry.responsesBaseUrl, "https://helixmind.online/v1/responses"); + assert.deepEqual(registry.alternateFormats, [ + { + format: "claude", + baseUrl: "https://helixmind.online/v1/messages", + authHeader: "x-api-key", + label: "Anthropic-compatible", + }, + { + format: "openai-responses", + baseUrl: "https://helixmind.online/v1/responses", + authHeader: "bearer", + label: "OpenAI Responses", + }, + ]); + assert.match(metadata.freeNote ?? "", /3 RPM\/50 RPD/i); + assert.match(metadata.freeNote ?? "", /no-card/i); + assert.match(metadata.freeNote ?? "", /not confirmed/i); + assert.doesNotMatch(metadata.freeNote ?? "", /free forever|unlimited/i); +}); diff --git a/tests/unit/listCapabilities-a2a.test.ts b/tests/unit/listCapabilities-a2a.test.ts index 60e636b163..64b1968653 100644 --- a/tests/unit/listCapabilities-a2a.test.ts +++ b/tests/unit/listCapabilities-a2a.test.ts @@ -3,7 +3,7 @@ * * Verifies: * - Return shape matches §3.7 contract - * - Markdown table contains all 44 skill IDs + * - Markdown table contains all 45 skill IDs * - Coverage bounds are within declared totals * - metadata.source === "agent-skills-catalog" * - metadata.generatedAt is an ISO datetime string @@ -13,7 +13,11 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import type { A2ATask } from "../../src/lib/a2a/taskManager.js"; import { executeListCapabilities } from "../../src/lib/a2a/skills/listCapabilities.js"; -import { API_SKILL_IDS, CLI_SKILL_IDS } from "../../src/lib/agentSkills/catalog.js"; +import { + API_SKILL_IDS, + CLI_SKILL_IDS, + CONFIG_SKILL_IDS, +} from "../../src/lib/agentSkills/catalog.js"; // Minimal stub — executeListCapabilities only receives the task arg but does not use it const stubTask = {} as A2ATask; @@ -31,20 +35,21 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () = const { metadata } = result; assert.ok(metadata, "metadata exists"); assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches"); - assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45 (44 + config)"); + assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45"); assert.ok(metadata.coverage, "metadata.coverage exists"); assert.ok(metadata.coverage.api, "metadata.coverage.api exists"); assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists"); assert.equal(metadata.coverage.api.total, 23, "api.total === 23"); - assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20"); + assert.equal(metadata.coverage.cli.total, 21, "cli.total === 21"); + assert.equal(metadata.coverage.config.total, 1, "config.total === 1"); }); -test("executeListCapabilities markdown table contains all 44 API+CLI skill IDs", async () => { +test("executeListCapabilities markdown table contains all 45 skill IDs", async () => { const result = await executeListCapabilities(stubTask); const content = result.artifacts[0].content; - const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[]; - assert.equal(allIds.length, 44, "API+CLI catalog declares 44 skill IDs"); + const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[]; + assert.equal(allIds.length, 45, "catalog declares 45 skill IDs"); for (const id of allIds) { assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`); @@ -65,6 +70,13 @@ test("metadata.coverage.cli.have is within [0, 21]", async () => { assert.ok(cli.have <= 21, "cli.have <= 21"); }); +test("metadata.coverage.config.have is within [0, 1]", async () => { + const result = await executeListCapabilities(stubTask); + const { config } = result.metadata.coverage; + assert.ok(config.have >= 0, "config.have >= 0"); + assert.ok(config.have <= 1, "config.have <= 1"); +}); + test("metadata.generatedAt is a valid ISO datetime", async () => { const result = await executeListCapabilities(stubTask); const { generatedAt } = result.metadata;