diff --git a/AGENTS.md b/AGENTS.md
index f88c9056a5..feda2fb478 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -48,20 +48,20 @@ Repository map and Reference Documentation sections below.
**OmniRoute** — unified AI proxy/router. One endpoint, 291 LLM providers, auto-fallback.
-| Layer | Location | Purpose |
-| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
-| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
-| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
-| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
-| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
-| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
-| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
-| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
-| MCP Server | `open-sse/mcp-server/` | 105 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
-| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
-| Skills | `src/lib/skills/` | Extensible skill framework |
-| Memory | `src/lib/memory/` | Persistent conversational memory |
+| Layer | Location | Purpose |
+| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
+| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
+| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
+| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
+| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
+| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
+| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
+| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
+| MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
+| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
+| Skills | `src/lib/skills/` | Extensible skill framework |
+| Memory | `src/lib/memory/` | Persistent conversational memory |
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
diff --git a/README.md b/README.md
index 352619a7f0..5edc2d69c7 100644
--- a/README.md
+++ b/README.md
@@ -188,7 +188,7 @@ curl http://localhost:20128/v1/chat/completions \
-
+
@@ -439,7 +439,7 @@ All **19** strategies — mix & match per combo step:
-
+
📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)
@@ -723,7 +723,7 @@ Expose OmniRoute over **MCP**, **A2A**, a **REST API**, **webhooks** or a **remo
Interface Endpoint / command Use it for
🧰 MCP (stdio) omniroute --mcpPlug into Claude Desktop, Cursor, any MCP client
- 🌊 MCP (HTTP) /api/mcp/streamRemote MCP — 105 tools , 31 scopes, full audit trail
+ 🌊 MCP (HTTP) /api/mcp/streamRemote MCP — 109 tools , 33 scopes, full audit trail
📡 MCP (SSE) /api/mcp/sseStreaming MCP transport
🤝 A2A /.well-known/agent.jsonAgent-to-agent, JSON-RPC 2.0 + SSE, 6 skills
🌐 REST API /v1/*OpenAI-compatible — chat, embeddings, images, audio, OCR
@@ -1110,7 +1110,7 @@ same process on one port, so there is no separate CLI-only package today.
Document Description
API Reference All endpoints with examples
OpenAPI Spec OpenAPI 3.0 specification
- MCP Server 104 MCP tools, IDE configs, Python/TS/Go clients
+ MCP Server 109 MCP tools, IDE configs, Python/TS/Go clients
MCP Server Guide MCP installation, transports, and tool reference
A2A Server JSON-RPC 2.0 protocol, skills, streaming, task mgmt
A2A Server Guide A2A agent card, tasks, skills, and streaming
diff --git a/changelog.d/features/9836-radar-guided-combos.md b/changelog.d/features/9836-radar-guided-combos.md
new file mode 100644
index 0000000000..c813289b35
--- /dev/null
+++ b/changelog.d/features/9836-radar-guided-combos.md
@@ -0,0 +1 @@
+- **feat(radar):** add curated-family combo suggestions, a guided combo page, and the read-only Radar MCP catalog tool ([#9836](https://github.com/diegosouzapw/OmniRoute/pull/9836))
diff --git a/docs/architecture/QUALITY_GATES.md b/docs/architecture/QUALITY_GATES.md
index 06c55e99cf..b7e279525f 100644
--- a/docs/architecture/QUALITY_GATES.md
+++ b/docs/architecture/QUALITY_GATES.md
@@ -186,10 +186,10 @@ Runs on pull requests only.
Runs after `build`. Blocks merge on failure.
-| Suite | Validates | Blocking |
-| ---------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
-| `test:vitest` | MCP server (94 tools), autoCombo, cache — vitest runner | Yes |
-| `test:vitest:ui` | UI component tests — vitest runner | **Advisory** (`continue-on-error: true`) — failing until Fase 6A UI triage |
+| Suite | Validates | Blocking |
+| ---------------- | -------------------------------------------------------- | -------------------------------------------------------------------------- |
+| `test:vitest` | MCP server (109 tools), autoCombo, cache — vitest runner | Yes |
+| `test:vitest:ui` | UI component tests — vitest runner | **Advisory** (`continue-on-error: true`) — failing until Fase 6A UI triage |
### Nightly workflows (scheduled, advisory)
diff --git a/docs/frameworks/MCP-SERVER.md b/docs/frameworks/MCP-SERVER.md
index 710315cf49..7a8e40db39 100644
--- a/docs/frameworks/MCP-SERVER.md
+++ b/docs/frameworks/MCP-SERVER.md
@@ -1,14 +1,14 @@
---
title: "OmniRoute MCP Server Documentation"
-version: 3.8.40
-lastUpdated: 2026-06-28
+version: 3.8.50
+lastUpdated: 2026-08-08
---
# OmniRoute MCP Server Documentation
-> Model Context Protocol server with 105 tools across routing, cache, compression, memory, skills, proxy, pool, and context source operations.
+> Model Context Protocol server with 109 tools across routing, cache, compression, memory, skills, proxy, pool, Radar, and context source operations.
>
-> Source of truth: `open-sse/mcp-server/server.ts` computes **104 unique tools** with `countUniqueMcpTools()`: 42 canonical definitions (including the six CCR lifecycle tools and the agent-skills trio), plus memory (3), skills (4), GitHub skills (3), pool (6), gamification (8), plugins (8), Notion (6), Obsidian (22), and two RTK-only compression tools.
+> Source of truth: `open-sse/mcp-server/server.ts` computes **109 unique tools** with `countUniqueMcpTools()`: 44 canonical definitions (including the six CCR lifecycle tools, the agent-skills trio, and `omniroute_radar_catalog`), plus memory (3), skills (4), GitHub skills (3), pool (6), gamification (8), plugins (8), Notion (6), Obsidian (22), local corpus (3), and two RTK-only compression tools.
## Installation
@@ -64,7 +64,7 @@ Cursor, Cline, and compatible MCP client setup.
---
-## Essential Tools (8) — Phase 1
+## Essential Tools (13) — Phase 1
| Tool | Scopes | Description |
| :------------------------------ | :-------------------- | :------------------------------------------------------------ |
@@ -72,16 +72,15 @@ Cursor, Cline, and compatible MCP client setup.
| `omniroute_list_combos` | `read:combos` | All configured combos with strategies (optional metrics) |
| `omniroute_get_combo_metrics` | `read:combos` | Performance metrics for a specific combo |
| `omniroute_switch_combo` | `write:combos` | Activate or deactivate a combo |
+| `omniroute_create_combo` | `write:combos` | Create a validated combo through the existing combo API |
| `omniroute_check_quota` | `read:quota` | Quota used/total, percent remaining, reset time, token health |
| `omniroute_route_request` | `execute:completions` | Send a chat completion through OmniRoute routing |
| `omniroute_cost_report` | `read:usage` | Cost report by period (session/day/week/month) |
| `omniroute_list_models_catalog` | `read:models` | Full model catalog with capabilities, status, pricing |
-
-## Phase 1 — Search
-
-| Tool | Scopes | Description |
-| :--------------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
-| `omniroute_web_search` | `execute:search` | Web search through OmniRoute search gateway (Serper/Brave/Perplexity/Exa/Tavily/Google PSE/Linkup/SearchAPI/SearXNG) with failover |
+| `omniroute_radar_catalog` | `read:radar` | Local signed Radar catalog; optional provider/family filters |
+| `omniroute_tool_search` | `read:tools` | Discover tools from the registered MCP catalog |
+| `omniroute_web_search` | `execute:search` | Web search through the configured search providers |
+| `omniroute_web_fetch` | `execute:search` | Fetch web content through the configured fetch providers |
## Advanced Tools (11) — Phase 2
@@ -227,7 +226,7 @@ See [AGENT-SKILLS.md](./AGENT-SKILLS.md) for the full catalog and how external a
## Related Frameworks (v3.8.0)
-The MCP tool inventory above (104 unique tools, computed by `countUniqueMcpTools()`) is intentionally
+The MCP tool inventory above (109 unique tools, computed by `countUniqueMcpTools()`) is intentionally
scoped to runtime routing/cache/compression/memory/skills/proxy/context-source operations. Two adjacent
frameworks ship alongside the MCP server in v3.8.0 and are documented separately:
@@ -369,7 +368,7 @@ MCP tool, prompt, and resource registries can compress descriptions at registrat
Description compression shrinks each tool's metadata; **tool-cardinality reduction** goes one step further by reducing _how many_ tools are announced at all. Advertising fewer tools in the `tools/list` manifest cuts the per-request token cost the client's model pays for the tool catalog ("layer 5" compression). The implementation is a pure, stateless filter in `open-sse/mcp-server/toolCardinality.ts` (`reduceToolManifest`), wired into the registration loop in `createMcpServer()` (`open-sse/mcp-server/server.ts`).
-**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 105 tools are announced unchanged.
+**Opt-in, off by default.** The filter only runs when at least one of two environment variables is set; with neither set, all 109 tools are announced unchanged.
| Variable | Mode |
| :--------------- | :-------------------------------------------------------------------------------------- |
diff --git a/docs/frameworks/RADAR.md b/docs/frameworks/RADAR.md
index 1d1d1015d7..1e36a4e228 100644
--- a/docs/frameworks/RADAR.md
+++ b/docs/frameworks/RADAR.md
@@ -290,6 +290,26 @@ The dashboard exposes four local actions:
A feed `enabled: false` remains the safety exception: it wins over a stale local
`enabled: true`, keeps the merged entry disabled, and records `disabledBy: "radar"`.
+### Guided combos and MCP access
+
+Confirmed `familyId` values survive the read-time overlay and drive the pure
+`buildRadarComboSuggestions()` module (`src/lib/radar/comboSuggestions.ts`). A family is suggested
+only when at least two distinct providers have active connections and expose the exact curated model
+ID. Disabled models, inactive providers, missing model IDs, singleton families, and ambiguous
+alias/prefix matches fail closed. Suggestions use the existing `priority` strategy, ordering the
+largest recurring monthly budget first; the UI creates them only through `POST /api/combos`.
+
+The guided UI lives at `/dashboard/radar/combos`. It reads only the local
+`GET /api/radar/catalog` and `GET /api/combos/builder/options` endpoints. It never triggers Radar sync,
+reads provider credentials, or writes directly to the combo database.
+
+MCP clients can read the same local projection with `omniroute_radar_catalog` (`read:radar`). The
+optional `provider`, `familyId`, and `enabledOnly` filters are evaluated after one local
+`GET /api/radar/catalog` read. Its closed output includes catalog metadata plus provider/model,
+display name, `familyId`, quota, capabilities, enabled state, origin, and `disabledBy`; setup URLs,
+steps, connections, e-mail addresses, keys, and referral data are never returned. This tool is
+read-only and never invokes `/api/radar/sync`.
+
### Provenance markers
Every merged entry carries an `origin` field the UI renders as a badge:
@@ -303,7 +323,7 @@ Every merged entry carries an `origin` field the UI renders as a badge:
## Local surfaces — never a feed proxy
-Six local endpoints back the UI, all under `src/app/api/radar/`:
+The local Radar route families below back the UI under `src/app/api/radar/`:
| Route | Method | Purpose |
| ------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------- |
@@ -323,14 +343,14 @@ to the local OmniRoute server. The two modules that touch the Radar service are
always run server-side, never client-side. This keeps the feed URL and any supporter key
out of client-facing network traffic entirely.
-All six endpoints return `404` when `RADAR_ENABLED` is off (see
+All Radar endpoints return `404` when `RADAR_ENABLED` is off (see
[Flag](#flag-radar_enabled-default-off) above), and route error responses through
`buildErrorBody()`/`sanitizeErrorMessage()` per the repo-wide error-sanitization rule
(`docs/security/ERROR_SANITIZATION.md`).
### Authentication
-All six endpoints require authentication via `isAuthenticated()`
+All Radar endpoints require authentication via `isAuthenticated()`
(`src/shared/utils/apiAuth.ts`) — a dashboard session cookie or a management-scoped
API key, the same gate that protects the rest of `/api/settings/*`. The flag-off
`404` check always runs **before** the auth check, so an install with `RADAR_ENABLED`
diff --git a/open-sse/mcp-server/README.md b/open-sse/mcp-server/README.md
index 1ef5f8aa5f..cf3d5ce748 100644
--- a/open-sse/mcp-server/README.md
+++ b/open-sse/mcp-server/README.md
@@ -1,6 +1,6 @@
# OmniRoute MCP Server
-> **Model Context Protocol server** that exposes OmniRoute's gateway intelligence as **104 tools** for AI agents.
+> **Model Context Protocol server** that exposes OmniRoute's gateway intelligence as **109 tools** for AI agents.
>
> **Source of truth for the full tool catalog and REST surface:** [`docs/frameworks/MCP-SERVER.md`](../../docs/frameworks/MCP-SERVER.md). This README focuses on architecture, configuration, and integration examples; the catalog below is a summary subset.
@@ -20,7 +20,7 @@ The MCP Server allows any AI agent (Claude Desktop, Cursor, VS Code Copilot, cus
┌──────────────────────────────────────────────────────────────────┐
│ OmniRoute MCP Server │
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │
-│ │ Scope │ │ 104 MCP Tools │ │ Audit Logger │ │
+│ │ Scope │ │ 109 MCP Tools │ │ Audit Logger │ │
│ │ Enforcement │──│ (core + memory │──│ (SHA-256/SQLite) │ │
│ │ │ │ + skills + …) │ │ │ │
│ └──────────────┘ └────────┬────────┘ └────────────────────┘ │
@@ -120,18 +120,23 @@ omniroute --mcp
## Tool Reference
-### Phase 1: Essential Tools (8)
+### Phase 1: Essential Tools (13)
| # | Tool | Scopes | Description |
| --- | ------------------------------- | --------------------- | -------------------------------------------------------------------------- |
-| 1 | `omniroute_get_health` | `read:health` | Gateway health, uptime, memory, circuit breakers, rate limits, cache stats |
-| 2 | `omniroute_list_combos` | `read:combos` | List all combos (model chains) with strategies and optional metrics |
-| 3 | `omniroute_get_combo_metrics` | `read:combos` | Performance metrics for a specific combo |
-| 4 | `omniroute_switch_combo` | `write:combos` | Activate or deactivate a combo for routing |
-| 5 | `omniroute_check_quota` | `read:quota` | Remaining API quota per provider with token health status |
-| 6 | `omniroute_route_request` | `execute:completions` | Send a chat completion through intelligent routing |
-| 7 | `omniroute_cost_report` | `read:usage` | Cost report by period (session/day/week/month) with per-provider breakdown |
-| 8 | `omniroute_list_models_catalog` | `read:models` | List all available models across providers with capabilities and pricing |
+| 1 | `omniroute_tool_search` | `read:tools` | Discover tools from the registered MCP catalog |
+| 2 | `omniroute_get_health` | `read:health` | Gateway health, uptime, memory, circuit breakers, rate limits, cache stats |
+| 3 | `omniroute_list_combos` | `read:combos` | List all combos (model chains) with strategies and optional metrics |
+| 4 | `omniroute_get_combo_metrics` | `read:combos` | Performance metrics for a specific combo |
+| 5 | `omniroute_switch_combo` | `write:combos` | Activate or deactivate a combo for routing |
+| 6 | `omniroute_create_combo` | `write:combos` | Create a validated combo through the existing combo API |
+| 7 | `omniroute_check_quota` | `read:quota` | Remaining API quota per provider with token health status |
+| 8 | `omniroute_route_request` | `execute:completions` | Send a chat completion through intelligent routing |
+| 9 | `omniroute_cost_report` | `read:usage` | Cost report by period (session/day/week/month) with per-provider breakdown |
+| 10 | `omniroute_list_models_catalog` | `read:models` | List all available models across providers with capabilities and pricing |
+| 11 | `omniroute_radar_catalog` | `read:radar` | Read the local signed Radar catalog with provider/family filters |
+| 12 | `omniroute_web_search` | `execute:search` | Search the web through configured search providers |
+| 13 | `omniroute_web_fetch` | `execute:search` | Fetch web content through configured fetch providers |
### Phase 2: Advanced Tools (8)
diff --git a/open-sse/mcp-server/__tests__/essentialTools.test.ts b/open-sse/mcp-server/__tests__/essentialTools.test.ts
index 88353eed92..c611d5f3c3 100644
--- a/open-sse/mcp-server/__tests__/essentialTools.test.ts
+++ b/open-sse/mcp-server/__tests__/essentialTools.test.ts
@@ -1,7 +1,7 @@
/**
* Unit tests for MCP Essential Tools (Phase 1)
*
- * Tests all 10 essential tool handlers via the tool handler functions.
+ * Tests the essential tool handlers via the tool handler functions.
* The omniroute_web_search tests use InMemoryTransport + Client to exercise
* the actual registered handler (not mockFetch directly).
*/
@@ -22,10 +22,10 @@ describe("MCP Essential Tools", () => {
});
describe("Tool schema validation", () => {
- it("should have exactly 12 essential tools (includes web_search + web_fetch + tool_search)", () => {
- // 11 -> 12: #8925 shipped omniroute_create_combo as a phase-1 tool.
+ it("should have exactly 13 essential tools (including Radar catalog)", () => {
+ // 12 -> 13: F3 shipped omniroute_radar_catalog as a phase-1 read-only tool.
const schemas = MCP_ESSENTIAL_TOOLS;
- expect(schemas).toHaveLength(12);
+ expect(schemas).toHaveLength(13);
});
it("all tools should have omniroute_ prefix", () => {
diff --git a/open-sse/mcp-server/__tests__/radarCatalogTool.test.ts b/open-sse/mcp-server/__tests__/radarCatalogTool.test.ts
new file mode 100644
index 0000000000..d30a59be7e
--- /dev/null
+++ b/open-sse/mcp-server/__tests__/radarCatalogTool.test.ts
@@ -0,0 +1,151 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
+
+import { MCP_SCOPE_LIST, MCP_TOOL_SCOPES } from "../../../src/shared/constants/mcpScopes.ts";
+import { evaluateToolScopes } from "../scopeEnforcement.ts";
+import { getMcpRadarCatalog } from "../radarCatalog.ts";
+import { MCP_ESSENTIAL_TOOLS, MCP_TOOL_MAP } from "../schemas/tools.ts";
+import { createMcpServer } from "../server.ts";
+
+vi.mock("../audit.ts", () => ({
+ logToolCall: vi.fn().mockResolvedValue(undefined),
+}));
+
+const catalog = {
+ entries: [
+ {
+ provider: "groq",
+ modelId: "llama",
+ displayName: "Llama on Groq",
+ familyId: "llama-family",
+ monthlyTokens: 200,
+ creditTokens: 0,
+ freeType: "recurring-daily",
+ poolKey: null,
+ tos: "ok",
+ enabled: true,
+ origin: "radar",
+ capabilities: { tools: true, vision: false, thinking: false },
+ limits: { rpm: 30, rpd: null, tpm: null, tpd: null },
+ setup: { keyUrl: "https://secret.example/key", steps: ["do not expose"] },
+ },
+ {
+ provider: "cerebras",
+ modelId: "llama",
+ displayName: "Llama on Cerebras",
+ familyId: "llama-family",
+ monthlyTokens: 300,
+ creditTokens: 0,
+ freeType: "recurring-daily",
+ poolKey: null,
+ tos: "ok",
+ enabled: false,
+ disabledBy: "radar",
+ origin: "radar",
+ capabilities: { tools: true, vision: false, thinking: true },
+ limits: { rpm: null, rpd: 100, tpm: null, tpd: null },
+ },
+ ],
+ meta: { version: "2026.08.08.1", tier: "community", fetchedAt: "2026-08-08T20:00:00Z" },
+};
+
+describe("omniroute_radar_catalog", () => {
+ it("is a phase-1 read-only registry tool with the dedicated Radar scope", () => {
+ const definition = MCP_TOOL_MAP.omniroute_radar_catalog;
+ expect(definition).toBeDefined();
+ expect(definition.phase).toBe(1);
+ expect(definition.scopes).toEqual(["read:radar"]);
+ expect(definition.auditLevel).toBe("none");
+ expect(definition.sourceEndpoints).toEqual(["/api/radar/catalog"]);
+ expect(MCP_ESSENTIAL_TOOLS).toContain(definition);
+ expect(MCP_SCOPE_LIST).toContain("read:radar");
+ expect(MCP_TOOL_SCOPES.omniroute_radar_catalog).toEqual(["read:radar"]);
+ });
+
+ it("reads only the local catalog and returns a closed filtered projection", async () => {
+ const fetchJson = vi.fn().mockResolvedValue(catalog);
+ const result = await getMcpRadarCatalog(
+ { provider: "groq", familyId: "llama-family", enabledOnly: true },
+ { fetchJson }
+ );
+
+ expect(fetchJson).toHaveBeenCalledOnce();
+ expect(fetchJson).toHaveBeenCalledWith("/api/radar/catalog");
+ expect(result.models).toHaveLength(1);
+ expect(result.models[0]).toEqual({
+ provider: "groq",
+ modelId: "llama",
+ displayName: "Llama on Groq",
+ familyId: "llama-family",
+ quota: {
+ monthlyTokens: 200,
+ creditTokens: 0,
+ freeType: "recurring-daily",
+ limits: { rpm: 30, rpd: null, tpm: null, tpd: null },
+ },
+ capabilities: { tools: true, vision: false, thinking: false },
+ enabled: true,
+ origin: "radar",
+ disabledBy: null,
+ });
+ expect(JSON.stringify(result)).not.toContain("secret.example");
+ expect(JSON.stringify(result)).not.toContain("setup");
+ });
+
+ it("defaults enabledOnly to true and includes disabled models only when explicitly requested", async () => {
+ const fetchJson = vi.fn().mockResolvedValue(catalog);
+ expect((await getMcpRadarCatalog({}, { fetchJson })).models).toHaveLength(1);
+ expect((await getMcpRadarCatalog({ enabledOnly: false }, { fetchJson })).models).toHaveLength(
+ 2
+ );
+ });
+
+ it("allows read:radar and read:* but denies a missing scope when enforcement is active", () => {
+ expect(evaluateToolScopes("omniroute_radar_catalog", ["read:radar"], true).allowed).toBe(true);
+ expect(evaluateToolScopes("omniroute_radar_catalog", ["read:*"], true).allowed).toBe(true);
+ expect(evaluateToolScopes("omniroute_radar_catalog", [], true)).toMatchObject({
+ allowed: false,
+ reason: "missing_scopes",
+ missing: ["read:radar"],
+ });
+ });
+});
+
+describe("omniroute_radar_catalog MCP dispatch", () => {
+ const mockFetch = vi.fn();
+ let client: Client;
+
+ beforeEach(async () => {
+ mockFetch.mockReset();
+ vi.stubGlobal("fetch", mockFetch);
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+ const server = createMcpServer();
+ await server.connect(serverTransport);
+ client = new Client({ name: "radar-catalog-test", version: "1.0.0" });
+ await client.connect(clientTransport);
+ });
+
+ afterEach(async () => {
+ await client.close();
+ vi.unstubAllGlobals();
+ });
+
+ it("registers and dispatches a real read without sync or write", async () => {
+ mockFetch.mockResolvedValueOnce({ ok: true, json: async () => catalog });
+
+ const listed = await client.listTools();
+ expect(listed.tools.some((tool) => tool.name === "omniroute_radar_catalog")).toBe(true);
+
+ const result = await client.callTool({
+ name: "omniroute_radar_catalog",
+ arguments: { enabledOnly: false },
+ });
+ expect(result.isError).toBeFalsy();
+ expect(mockFetch).toHaveBeenCalledOnce();
+ expect(mockFetch.mock.calls[0][0]).toContain("/api/radar/catalog");
+ expect(mockFetch.mock.calls[0][1]).not.toMatchObject({ method: "POST" });
+ const body = JSON.parse((result.content[0] as { text: string }).text);
+ expect(body.models).toHaveLength(2);
+ });
+});
diff --git a/open-sse/mcp-server/radarCatalog.ts b/open-sse/mcp-server/radarCatalog.ts
new file mode 100644
index 0000000000..e17ce724bb
--- /dev/null
+++ b/open-sse/mcp-server/radarCatalog.ts
@@ -0,0 +1,170 @@
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+
+import { logToolCall } from "./audit.ts";
+import { radarCatalogInput, radarCatalogOutput } from "./schemas/radarCatalog.ts";
+import type { McpToolExtraLike } from "./scopeEnforcement.ts";
+import type { TextToolResult } from "./toolResult.ts";
+import { sanitizeErrorMessage } from "../utils/error.ts";
+
+type JsonRecord = Record;
+
+type ScopeEnforcer = (
+ toolName: string,
+ handler: (args: unknown, extra?: McpToolExtraLike) => Promise,
+ toolScopes?: readonly string[]
+) => (args: unknown, extra?: McpToolExtraLike) => Promise;
+
+export interface McpRadarCatalogArgs {
+ provider?: string;
+ familyId?: string;
+ enabledOnly?: boolean;
+}
+
+interface McpRadarCatalogDeps {
+ fetchJson?: (path: string) => Promise;
+}
+
+function record(value: unknown): JsonRecord {
+ return value !== null && typeof value === "object" && !Array.isArray(value)
+ ? (value as JsonRecord)
+ : {};
+}
+
+function text(value: unknown, fallback = ""): string {
+ return typeof value === "string" ? value : fallback;
+}
+
+function number(value: unknown): number {
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
+}
+
+function nullableNumber(value: unknown): number | null {
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
+}
+
+function normalizeMeta(
+ value: unknown
+): { version: string; tier: string; fetchedAt: string } | null {
+ const meta = record(value);
+ if (
+ typeof meta.version !== "string" ||
+ typeof meta.tier !== "string" ||
+ typeof meta.fetchedAt !== "string"
+ ) {
+ return null;
+ }
+ return { version: meta.version, tier: meta.tier, fetchedAt: meta.fetchedAt };
+}
+
+function normalizeEntry(value: unknown) {
+ const entry = record(value);
+ const provider = text(entry.provider).trim();
+ const modelId = text(entry.modelId).trim();
+ if (!provider || !modelId) return null;
+
+ const capabilities = record(entry.capabilities);
+ const limits = record(entry.limits);
+ const origin =
+ entry.origin === "radar" || entry.origin === "local" ? entry.origin : ("baseline" as const);
+ return {
+ provider,
+ modelId,
+ displayName: text(entry.displayName, modelId),
+ familyId: typeof entry.familyId === "string" ? entry.familyId : null,
+ quota: {
+ monthlyTokens: number(entry.monthlyTokens),
+ creditTokens: number(entry.creditTokens),
+ freeType: text(entry.freeType, "unknown"),
+ limits:
+ Object.keys(limits).length > 0
+ ? {
+ rpm: nullableNumber(limits.rpm),
+ rpd: nullableNumber(limits.rpd),
+ tpm: nullableNumber(limits.tpm),
+ tpd: nullableNumber(limits.tpd),
+ }
+ : null,
+ },
+ capabilities:
+ Object.keys(capabilities).length > 0
+ ? {
+ tools: capabilities.tools === true,
+ vision: capabilities.vision === true,
+ thinking: capabilities.thinking === true,
+ }
+ : null,
+ enabled: entry.enabled !== false,
+ origin,
+ disabledBy: entry.disabledBy === "radar" ? ("radar" as const) : null,
+ };
+}
+
+function compareEntries(
+ left: NonNullable>,
+ right: NonNullable>
+): number {
+ return left.provider.localeCompare(right.provider) || left.modelId.localeCompare(right.modelId);
+}
+
+/** Read and project the local Radar catalog without exposing setup or secret-bearing state. */
+export async function getMcpRadarCatalog(
+ args: McpRadarCatalogArgs,
+ deps: McpRadarCatalogDeps = {}
+) {
+ const fetchJson =
+ deps.fetchJson ??
+ ((path: string) => import("./server.ts").then((module) => module.omniRouteFetch(path)));
+ const raw = record(await fetchJson("/api/radar/catalog"));
+ const providerFilter = args.provider?.trim().toLowerCase();
+ const familyFilter = args.familyId?.trim().toLowerCase();
+ const enabledOnly = args.enabledOnly !== false;
+ const entries = Array.isArray(raw.entries) ? raw.entries : [];
+ const models = entries
+ .map(normalizeEntry)
+ .filter((entry): entry is NonNullable => entry !== null)
+ .filter((entry) => !enabledOnly || entry.enabled)
+ .filter((entry) => !providerFilter || entry.provider.toLowerCase() === providerFilter)
+ .filter((entry) => !familyFilter || entry.familyId?.toLowerCase() === familyFilter)
+ .sort(compareEntries);
+
+ return { meta: normalizeMeta(raw.meta), models };
+}
+
+async function handleRadarCatalog(args: {
+ provider?: string;
+ familyId?: string;
+ enabledOnly: boolean;
+}): Promise {
+ const start = Date.now();
+ try {
+ const result = radarCatalogOutput.parse(await getMcpRadarCatalog(args));
+ await logToolCall(
+ "omniroute_radar_catalog",
+ args,
+ { modelCount: result.models.length },
+ Date.now() - start,
+ true
+ );
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
+ } catch (error) {
+ const message = sanitizeErrorMessage(error) || "Failed to read Radar catalog";
+ await logToolCall("omniroute_radar_catalog", args, null, Date.now() - start, false, message);
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
+ }
+}
+
+export function registerRadarCatalogTool(
+ server: McpServer,
+ withScopeEnforcement: ScopeEnforcer
+): void {
+ server.registerTool(
+ "omniroute_radar_catalog",
+ {
+ description: "Reads the local signed Radar catalog with optional provider and family filters",
+ inputSchema: radarCatalogInput,
+ },
+ withScopeEnforcement("omniroute_radar_catalog", (args) =>
+ handleRadarCatalog(radarCatalogInput.parse(args))
+ )
+ );
+}
diff --git a/open-sse/mcp-server/schemas/index.ts b/open-sse/mcp-server/schemas/index.ts
index fe9df69ff2..1c3cbdc029 100644
--- a/open-sse/mcp-server/schemas/index.ts
+++ b/open-sse/mcp-server/schemas/index.ts
@@ -91,6 +91,8 @@ export {
ccrStatsTool,
} from "./tools.ts";
+export { radarCatalogInput, radarCatalogOutput, radarCatalogTool } from "./radarCatalog.ts";
+
// A2A schemas
export {
AgentCardSchema,
diff --git a/open-sse/mcp-server/schemas/radarCatalog.ts b/open-sse/mcp-server/schemas/radarCatalog.ts
new file mode 100644
index 0000000000..1bfb4040e2
--- /dev/null
+++ b/open-sse/mcp-server/schemas/radarCatalog.ts
@@ -0,0 +1,65 @@
+import { z } from "zod";
+
+import type { McpToolDefinition } from "./toolDefinition.ts";
+
+export const radarCatalogInput = z.object({
+ provider: z.string().trim().min(1).max(100).optional().describe("Filter by provider id"),
+ familyId: z.string().trim().min(1).max(120).optional().describe("Filter by curated family id"),
+ enabledOnly: z.boolean().default(true).describe("Exclude models disabled by the Radar feed"),
+});
+
+const radarLimitOutput = z.object({
+ rpm: z.number().nullable(),
+ rpd: z.number().nullable(),
+ tpm: z.number().nullable(),
+ tpd: z.number().nullable(),
+});
+
+export const radarCatalogOutput = z.object({
+ meta: z
+ .object({
+ version: z.string(),
+ tier: z.string(),
+ fetchedAt: z.string(),
+ })
+ .nullable(),
+ models: z.array(
+ z.object({
+ provider: z.string(),
+ modelId: z.string(),
+ displayName: z.string(),
+ familyId: z.string().nullable(),
+ quota: z.object({
+ monthlyTokens: z.number(),
+ creditTokens: z.number(),
+ freeType: z.string(),
+ limits: radarLimitOutput.nullable(),
+ }),
+ capabilities: z
+ .object({
+ tools: z.boolean(),
+ vision: z.boolean(),
+ thinking: z.boolean(),
+ })
+ .nullable(),
+ enabled: z.boolean(),
+ origin: z.enum(["baseline", "radar", "local"]),
+ disabledBy: z.literal("radar").nullable(),
+ })
+ ),
+});
+
+export const radarCatalogTool: McpToolDefinition<
+ typeof radarCatalogInput,
+ typeof radarCatalogOutput
+> = {
+ name: "omniroute_radar_catalog",
+ description:
+ "Reads the local signed Radar catalog with optional provider and curated-family filters. Never syncs or writes data.",
+ inputSchema: radarCatalogInput,
+ outputSchema: radarCatalogOutput,
+ scopes: ["read:radar"],
+ auditLevel: "none",
+ phase: 1,
+ sourceEndpoints: ["/api/radar/catalog"],
+};
diff --git a/open-sse/mcp-server/schemas/tools.ts b/open-sse/mcp-server/schemas/tools.ts
index 4db6a850dc..437a49f56c 100644
--- a/open-sse/mcp-server/schemas/tools.ts
+++ b/open-sse/mcp-server/schemas/tools.ts
@@ -1,5 +1,5 @@
/**
- * MCP Tool Schemas — Contracts for all 23 core and advanced OmniRoute MCP tools.
+ * MCP Tool Schemas — Contracts for the canonical OmniRoute MCP tools.
*
* Defines input/output Zod schemas, descriptions, scopes, and audit levels
* for both essential (Phase 1) and advanced (Phase 2) MCP tools.
@@ -13,11 +13,11 @@ import { z } from "zod";
import { toolSearchTool } from "./toolSearch.ts";
import { pickFastestModelTool } from "./pickFastestModel.ts";
import { CCR_MCP_TOOLS } from "./ccrTools.ts";
+import { radarCatalogTool } from "./radarCatalog.ts";
import {
AUTO_ROUTING_STRATEGY_VALUES,
ROUTING_STRATEGY_VALUES,
} from "../../../src/shared/constants/routingStrategies.ts";
-
// ============ Shared Types ============
// AuditLevel + McpToolDefinition live in the leaf ./toolDefinition.ts so that
// toolSearch.ts can import the type without forming a tools.ts ↔ toolSearch.ts cycle.
@@ -26,8 +26,7 @@ export type { AuditLevel, McpToolDefinition } from "./toolDefinition.ts";
import type { McpToolDefinition } from "./toolDefinition.ts";
export { pickFastestModelInput, pickFastestModelOutput } from "./pickFastestModel.ts";
export * from "./ccrTools.ts";
-
-// ============ Phase 1: Essential Tools (8) ============
+// ============ Phase 1: Essential Tools ============
// --- Tool 1: omniroute_get_health ---
export const getHealthInput = z.object({}).describe("No parameters required");
@@ -432,7 +431,7 @@ export const listModelsCatalogTool: McpToolDefinition<
sourceEndpoints: ["/api/models/catalog", "/v1/models"],
};
-// --- Tool 9: omniroute_web_search ---
+// --- Tool 10: omniroute_web_search ---
export const webSearchInput = z.object({
query: z
.string()
@@ -1512,6 +1511,7 @@ export const MCP_TOOLS = [
routeRequestTool,
costReportTool,
listModelsCatalogTool,
+ radarCatalogTool,
webSearchTool,
webFetchTool,
simulateRouteTool,
diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts
index d61b654f3c..811d87dce0 100644
--- a/open-sse/mcp-server/server.ts
+++ b/open-sse/mcp-server/server.ts
@@ -93,6 +93,8 @@ import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts";
import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
import { getMcpModelsCatalog } from "./catalog.ts";
+import { registerRadarCatalogTool } from "./radarCatalog.ts";
+import type { TextToolResult } from "./toolResult.ts";
export { getMcpModelsCatalog } from "./catalog.ts";
const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl();
@@ -146,11 +148,6 @@ function readMcpAccessibilityConfig(): McpAccessibilityConfig {
}
}
-type TextToolResult = {
- content: Array<{ type: "text"; text: string }>;
- isError?: boolean;
-};
-
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
@@ -815,6 +812,8 @@ export function createMcpServer(): McpServer {
)
);
+ registerRadarCatalogTool(server, withScopeEnforcement);
+
server.registerTool(
"omniroute_simulate_route",
{
diff --git a/open-sse/mcp-server/toolResult.ts b/open-sse/mcp-server/toolResult.ts
new file mode 100644
index 0000000000..ea8535f545
--- /dev/null
+++ b/open-sse/mcp-server/toolResult.ts
@@ -0,0 +1,4 @@
+export type TextToolResult = {
+ content: Array<{ type: "text"; text: string }>;
+ isError?: boolean;
+};
diff --git a/scripts/check/check-docs-counts-sync.mjs b/scripts/check/check-docs-counts-sync.mjs
index c0e4cf83f6..01ad1aeef1 100644
--- a/scripts/check/check-docs-counts-sync.mjs
+++ b/scripts/check/check-docs-counts-sync.mjs
@@ -147,9 +147,10 @@ function readCodeFacts() {
'import {pluginTools} from "./open-sse/mcp-server/tools/pluginTools.ts";',
'import {notionTools} from "./open-sse/mcp-server/tools/notionTools.ts";',
'import {obsidianTools} from "./open-sse/mcp-server/tools/obsidianTools.ts";',
+ 'import {localCorpusTools} from "./open-sse/mcp-server/tools/localCorpusTools.ts";',
'import {compressionTools} from "./open-sse/mcp-server/tools/compressionTools.ts";',
"const cols={MCP_TOOLS,memoryTools,skillTools,agentSkillTools,githubSkillTools,poolTools,",
- "gamificationTools,pluginTools,notionTools,obsidianTools,compressionTools};",
+ "gamificationTools,pluginTools,notionTools,obsidianTools,localCorpusTools,compressionTools};",
"const sc=new Set();",
"for(const col of Object.values(cols))for(const t of Object.values(col))",
"for(const x of (t?.scopes||[]))sc.add(x);",
@@ -313,7 +314,7 @@ export function buildChecks() {
// total ("33 tools (25 CLI Code's …)") are not the MCP aggregate
// per-module rows read "… tool definitions (N tools" / "… management tools
// (N tools" — the word tool(s)/definitions sits right before the paren. The
- // aggregate ("MCP Server (104 tools", "all 104 tools") never does.
+ // aggregate ("MCP Server (109 tools", "all 109 tools") never does.
skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
skipAfter: /^\s*\(\d+ CLI/,
},
diff --git a/src/app/(dashboard)/dashboard/radar/combos/page.tsx b/src/app/(dashboard)/dashboard/radar/combos/page.tsx
new file mode 100644
index 0000000000..04cb3f3e98
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/radar/combos/page.tsx
@@ -0,0 +1,201 @@
+"use client";
+
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useTranslations } from "next-intl";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+import type { ComboBuilderOptionsPayload } from "@/lib/combos/builderOptions";
+import type { MergedEntry } from "@/lib/radar/applyFeed";
+import {
+ buildRadarComboSuggestions,
+ type RadarComboSuggestion,
+} from "@/lib/radar/comboSuggestions";
+import { Card } from "@/shared/components";
+
+interface RadarCatalogPayload {
+ entries?: MergedEntry[];
+ meta?: unknown;
+}
+
+function comboNames(payload: ComboBuilderOptionsPayload): Set {
+ return new Set(payload.comboRefs.map((combo) => combo.name));
+}
+
+export default function RadarCombosPage() {
+ const t = useTranslations("radarCombosPage");
+ const [entries, setEntries] = useState([]);
+ const [providers, setProviders] = useState([]);
+ const [existingNames, setExistingNames] = useState>(new Set());
+ const [createdNames, setCreatedNames] = useState>(new Set());
+ const [hasCatalog, setHasCatalog] = useState(false);
+ const [flagOff, setFlagOff] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [creatingName, setCreatingName] = useState(null);
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ async function load() {
+ try {
+ const [catalogResponse, optionsResponse] = await Promise.all([
+ fetch("/api/radar/catalog"),
+ fetch("/api/combos/builder/options"),
+ ]);
+ if (catalogResponse.status === 404) {
+ setFlagOff(true);
+ return;
+ }
+ if (!catalogResponse.ok || !optionsResponse.ok) throw new Error("load_failed");
+
+ const catalog = (await catalogResponse.json()) as RadarCatalogPayload;
+ const options = (await optionsResponse.json()) as ComboBuilderOptionsPayload;
+ if (!Array.isArray(catalog.entries) || !Array.isArray(options.providers)) {
+ throw new Error("invalid_shape");
+ }
+
+ setEntries(catalog.entries);
+ setProviders(options.providers);
+ setExistingNames(comboNames(options));
+ setHasCatalog(catalog.meta != null);
+ } catch {
+ setError(t("loadFailed"));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ void load();
+ }, [t]);
+
+ const suggestions = useMemo(
+ () => buildRadarComboSuggestions({ entries, providers, existingComboNames: existingNames }),
+ [entries, providers, existingNames]
+ );
+
+ const refreshExistingName = useCallback(async (name: string): Promise => {
+ try {
+ const response = await fetch("/api/combos/builder/options");
+ if (!response.ok) return false;
+ const options = (await response.json()) as ComboBuilderOptionsPayload;
+ if (!Array.isArray(options.comboRefs)) return false;
+ const names = comboNames(options);
+ if (![...names].some((candidate) => candidate.toLowerCase() === name.toLowerCase())) {
+ return false;
+ }
+ setExistingNames(names);
+ return true;
+ } catch {
+ return false;
+ }
+ }, []);
+
+ const createSuggestion = useCallback(
+ async (suggestion: RadarComboSuggestion) => {
+ setCreatingName(suggestion.name);
+ setError("");
+ try {
+ const response = await fetch("/api/combos", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(suggestion.payload),
+ });
+ if (!response.ok) {
+ if (response.status === 400 && (await refreshExistingName(suggestion.name))) return;
+ throw new Error("create_failed");
+ }
+ setExistingNames((current) => new Set([...current, suggestion.name]));
+ setCreatedNames((current) => new Set([...current, suggestion.name]));
+ } catch {
+ setError(t("createFailed"));
+ } finally {
+ setCreatingName(null);
+ }
+ },
+ [refreshExistingName, t]
+ );
+
+ if (flagOff) notFound();
+
+ return (
+
+
+
+ ← {t("backToRadar")}
+
+
+
{t("title")}
+
{t("subtitle")}
+
+
+
+ {error &&
{error}
}
+
+ {loading ? (
+
+ {t("loading")}
+
+ ) : !hasCatalog ? (
+
+ {t("catalogRequired")}
+
+ ) : suggestions.length === 0 ? (
+
+ {t("noSuggestions")}
+
+ ) : (
+
+ {suggestions.map((suggestion) => {
+ const creating = creatingName === suggestion.name;
+ const created = createdNames.has(suggestion.name);
+ return (
+
+
+
+
+ {t("familyLabel")}
+
+
{suggestion.familyId}
+
{t("strategyReason")}
+
+
+
+
{t("modelsLabel")}
+ {suggestion.models.map((model) => (
+
+ {model.providerName}
+
+ {model.qualifiedModel}
+
+
+ ))}
+
+
+
void createSuggestion(suggestion)}
+ disabled={creating || suggestion.alreadyExists}
+ className="self-start px-4 py-2 text-sm font-medium rounded-lg bg-violet-500 text-white hover:bg-violet-600 transition-colors disabled:opacity-50"
+ >
+ {created
+ ? t("created")
+ : suggestion.alreadyExists
+ ? t("alreadyCreated")
+ : creating
+ ? t("generating")
+ : t("generateButton")}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/app/(dashboard)/dashboard/radar/page.tsx b/src/app/(dashboard)/dashboard/radar/page.tsx
index 9153ba254d..75803dc17a 100644
--- a/src/app/(dashboard)/dashboard/radar/page.tsx
+++ b/src/app/(dashboard)/dashboard/radar/page.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslations } from "next-intl";
+import Link from "next/link";
import { notFound } from "next/navigation";
import { Card } from "@/shared/components";
import { shouldAutoSyncOnOpen } from "@/lib/radar/autoSync";
@@ -315,15 +316,25 @@ export default function RadarPage() {
{t("title")}
{t("subtitle")}
- {pageState === "populated" && (
-
- {syncing ? t("syncing") : t("syncNow")}
-
- )}
+
+ {(pageState === "empty" || pageState === "populated") && (
+
+ {t("guidedCombos")}
+
+ )}
+ {pageState === "populated" && (
+
+ {syncing ? t("syncing") : t("syncNow")}
+
+ )}
+
{/* Feed freshness header */}
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index 7c84e6c5a7..9a35e2f896 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "إعداد المزود",
@@ -12280,5 +12281,22 @@
"addConnection": "إضافة اتصال",
"addConnectionDescription": "لا تملك اتصالاً بعد؟ أضف واحدًا في لوحة معلومات المزودين.",
"addConnectionLink": "انتقل إلى المزودين →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json
index 3709ce24b5..4b087ef24f 100644
--- a/src/i18n/messages/az.json
+++ b/src/i18n/messages/az.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Təchizatçı Quraşdırması",
@@ -12280,5 +12281,22 @@
"addConnection": "Bağlantı əlavə et",
"addConnectionDescription": "Hələ bağlantınız yoxdur? Təchizatçılar panelində birini əlavə edin.",
"addConnectionLink": "Təchizatçılara keç →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json
index 83d2ba777d..84a44a67c8 100644
--- a/src/i18n/messages/bg.json
+++ b/src/i18n/messages/bg.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Настройка на доставчика",
@@ -12280,5 +12281,22 @@
"addConnection": "Добавете връзка",
"addConnectionDescription": "Все още нямате връзка? Добавете една в таблото на доставчиците.",
"addConnectionLink": "Отидете на доставчиците →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json
index 6ac5db1b9b..5d9d195ed4 100644
--- a/src/i18n/messages/bn.json
+++ b/src/i18n/messages/bn.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "প্রদানকারী সেটআপ",
@@ -12280,5 +12281,22 @@
"addConnection": "সংযোগ যোগ করুন",
"addConnectionDescription": "এখনও একটি সংযোগ নেই? প্রদানকারীদের ড্যাশবোর্ডে একটি যোগ করুন।",
"addConnectionLink": "প্রদানকারীদের কাছে যান →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json
index 5a6c8ef797..09ae432596 100644
--- a/src/i18n/messages/cs.json
+++ b/src/i18n/messages/cs.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Nastavení poskytovatele",
@@ -12280,5 +12281,22 @@
"addConnection": "Přidat připojení",
"addConnectionDescription": "Ještě nemáte připojení? Přidejte jedno v řídicím panelu poskytovatelů.",
"addConnectionLink": "Přejít na poskytovatele →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json
index 00d2771893..a8aa197cc8 100644
--- a/src/i18n/messages/da.json
+++ b/src/i18n/messages/da.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Udbyder Opsætning",
@@ -12280,5 +12281,22 @@
"addConnection": "Tilføj forbindelse",
"addConnectionDescription": "Har du ikke en forbindelse endnu? Tilføj en i udbyderdashboardet.",
"addConnectionLink": "Gå til udbydere →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json
index 088ee4a22e..40a1e5e313 100644
--- a/src/i18n/messages/de.json
+++ b/src/i18n/messages/de.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Anbieter-Einrichtung",
@@ -12280,5 +12281,22 @@
"addConnection": "Verbindung hinzufügen",
"addConnectionDescription": "Haben Sie noch keine Verbindung? Fügen Sie eine im Anbieter-Dashboard hinzu.",
"addConnectionLink": "Zu den Anbietern →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index ac89199d31..2ff4fdcbe1 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -12305,7 +12305,25 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
},
"radarSetupPage": {
"title": "Provider Setup",
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index a4375b1aea..bd2d6b454e 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Configuración del proveedor",
@@ -12280,5 +12281,22 @@
"addConnection": "Agregar conexión",
"addConnectionDescription": "¿Aún no tienes una conexión? Agrega una en el panel de proveedores.",
"addConnectionLink": "Ir a proveedores →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json
index fea6f33e10..51f69ddae5 100644
--- a/src/i18n/messages/fa.json
+++ b/src/i18n/messages/fa.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "راهاندازی تأمینکننده",
@@ -12280,5 +12281,22 @@
"addConnection": "افزودن اتصال",
"addConnectionDescription": "هنوز اتصالی ندارید؟ یکی را در داشبورد تأمینکنندگان اضافه کنید.",
"addConnectionLink": "به تأمینکنندگان بروید →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json
index 4b598dd04a..2473d88fb2 100644
--- a/src/i18n/messages/fi.json
+++ b/src/i18n/messages/fi.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Palveluntarjoajan asennus",
@@ -12280,5 +12281,22 @@
"addConnection": "Lisää yhteys",
"addConnectionDescription": "Eikö sinulla vielä ole yhteyttä? Lisää yksi palveluntarjoajien hallintapaneelissa.",
"addConnectionLink": "Siirry palveluntarjoajiin →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index 36859e21c4..9c44606d57 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -12279,7 +12279,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Configuration du fournisseur",
@@ -12305,5 +12306,22 @@
"addConnection": "Ajouter une connexion",
"addConnectionDescription": "Vous n'avez pas encore de connexion ? Ajoutez-en une dans le tableau de bord des fournisseurs.",
"addConnectionLink": "Aller aux fournisseurs →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json
index 64e7a6a1ab..12546d9c29 100644
--- a/src/i18n/messages/gu.json
+++ b/src/i18n/messages/gu.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "પ્રદાતા સેટઅપ",
@@ -12280,5 +12281,22 @@
"addConnection": "કનેક્શન ઉમેરો",
"addConnectionDescription": "હજુ સુધી કનેક્શન નથી? પ્રદાતા ડેશબોર્ડમાં એક ઉમેરો.",
"addConnectionLink": "પ્રદાતાઓ પર જાઓ →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json
index d4fc2301cc..8d14ed37f2 100644
--- a/src/i18n/messages/he.json
+++ b/src/i18n/messages/he.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "הגדרת ספק",
@@ -12280,5 +12281,22 @@
"addConnection": "הוסף חיבור",
"addConnectionDescription": "עדיין אין לך חיבור? הוסף אחד בלוח הבקרה של הספקים.",
"addConnectionLink": "עבור לספקים →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json
index 01164266f4..07e921ca43 100644
--- a/src/i18n/messages/hi.json
+++ b/src/i18n/messages/hi.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "प्रदाता सेटअप",
@@ -12280,5 +12281,22 @@
"addConnection": "कनेक्शन जोड़ें",
"addConnectionDescription": "क्या आपके पास अभी तक कोई कनेक्शन नहीं है? प्रदाता डैशबोर्ड में एक जोड़ें।",
"addConnectionLink": "प्रदाताओं पर जाएं →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json
index 206543bb1e..7080943ab7 100644
--- a/src/i18n/messages/hu.json
+++ b/src/i18n/messages/hu.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Szolgáltató beállítása",
@@ -12280,5 +12281,22 @@
"addConnection": "Kapcsolat hozzáadása",
"addConnectionDescription": "Még nincs kapcsolatod? Adj hozzá egyet a szolgáltatók irányítópultján.",
"addConnectionLink": "Menj a szolgáltatókhoz →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json
index 73fd1574ad..4416521072 100644
--- a/src/i18n/messages/id.json
+++ b/src/i18n/messages/id.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Pengaturan Penyedia",
@@ -12280,5 +12281,22 @@
"addConnection": "Tambah koneksi",
"addConnectionDescription": "Belum memiliki koneksi? Tambahkan satu di dasbor penyedia.",
"addConnectionLink": "Pergi ke penyedia →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json
index 1db67ada06..3c451f32b6 100644
--- a/src/i18n/messages/in.json
+++ b/src/i18n/messages/in.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "प्रदाता सेटअप",
@@ -12280,5 +12281,22 @@
"addConnection": "कनेक्शन जोड़ें",
"addConnectionDescription": "क्या आपके पास अभी तक कोई कनेक्शन नहीं है? प्रदाता डैशबोर्ड में एक जोड़ें।",
"addConnectionLink": "प्रदाताओं पर जाएं →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json
index 98f5d1eb0f..1ea2bfe569 100644
--- a/src/i18n/messages/it.json
+++ b/src/i18n/messages/it.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Configurazione del fornitore",
@@ -12280,5 +12281,22 @@
"addConnection": "Aggiungi connessione",
"addConnectionDescription": "Non hai ancora una connessione? Aggiungine una nel dashboard dei fornitori.",
"addConnectionLink": "Vai ai fornitori →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json
index 9cd199e37b..0563a26e74 100644
--- a/src/i18n/messages/ja.json
+++ b/src/i18n/messages/ja.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "プロバイダーセットアップ",
@@ -12280,5 +12281,22 @@
"addConnection": "接続を追加",
"addConnectionDescription": "まだ接続がありませんか?プロバイダーダッシュボードで追加してください。",
"addConnectionLink": "プロバイダーに移動 →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json
index 9203076caf..e6e81411c5 100644
--- a/src/i18n/messages/ko.json
+++ b/src/i18n/messages/ko.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "제공자 설정",
@@ -12280,5 +12281,22 @@
"addConnection": "연결 추가",
"addConnectionDescription": "아직 연결이 없으신가요? 제공자 대시보드에서 추가하세요.",
"addConnectionLink": "제공자로 이동 →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json
index 82f8074832..c48bbef677 100644
--- a/src/i18n/messages/mr.json
+++ b/src/i18n/messages/mr.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "प्रदाता सेटअप",
@@ -12280,5 +12281,22 @@
"addConnection": "कनेक्शन जोडा",
"addConnectionDescription": "अद्याप कनेक्शन नाही? प्रदाता डॅशबोर्डमध्ये एक जोडा.",
"addConnectionLink": "प्रदात्यांकडे जा →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json
index ea1572e6e6..89e6af1a8e 100644
--- a/src/i18n/messages/ms.json
+++ b/src/i18n/messages/ms.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Penyediaan Penyedia",
@@ -12280,5 +12281,22 @@
"addConnection": "Tambah sambungan",
"addConnectionDescription": "Belum mempunyai sambungan? Tambah satu di papan pemuka penyedia.",
"addConnectionLink": "Pergi ke penyedia →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json
index 3d01da6c9b..338422573a 100644
--- a/src/i18n/messages/nl.json
+++ b/src/i18n/messages/nl.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Leverancier Configuratie",
@@ -12280,5 +12281,22 @@
"addConnection": "Voeg verbinding toe",
"addConnectionDescription": "Nog geen verbinding? Voeg er een toe in het leveranciersdashboard.",
"addConnectionLink": "Ga naar leveranciers →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json
index 57a3975e33..2acf920bab 100644
--- a/src/i18n/messages/no.json
+++ b/src/i18n/messages/no.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Leverandøroppsett",
@@ -12280,5 +12281,22 @@
"addConnection": "Legg til tilkobling",
"addConnectionDescription": "Har du ikke en tilkobling ennå? Legg til en i leverandørdashbordet.",
"addConnectionLink": "Gå til leverandører →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json
index 7886a6bc26..a7bcc09ea9 100644
--- a/src/i18n/messages/phi.json
+++ b/src/i18n/messages/phi.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Provider Setup",
@@ -12280,5 +12281,22 @@
"addConnection": "Add connection",
"addConnectionDescription": "Don't have a connection yet? Add one in the providers dashboard.",
"addConnectionLink": "Go to providers →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json
index 56ce285697..9470587d36 100644
--- a/src/i18n/messages/pl.json
+++ b/src/i18n/messages/pl.json
@@ -12276,7 +12276,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Konfiguracja dostawcy",
@@ -12302,5 +12303,22 @@
"addConnection": "Dodaj połączenie",
"addConnectionDescription": "Nie masz jeszcze połączenia? Dodaj je w panelu dostawców.",
"addConnectionLink": "Przejdź do dostawców →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json
index f3a97861ae..b5c5efa60b 100644
--- a/src/i18n/messages/pt-BR.json
+++ b/src/i18n/messages/pt-BR.json
@@ -12305,7 +12305,25 @@
"hiddenModelsTitle": "Modelos ocultos",
"modelDisplayName": "Nome local do modelo",
"modelEnabled": "Ativado localmente",
- "localStateSaveFailed": "Falha ao salvar as configurações locais do Radar"
+ "localStateSaveFailed": "Falha ao salvar as configurações locais do Radar",
+ "guidedCombos": "Combos guiados"
+ },
+ "radarCombosPage": {
+ "title": "Combos guiados pelo Radar",
+ "subtitle": "Crie fallbacks prioritários com modelos gratuitos equivalentes curados pelo Radar.",
+ "backToRadar": "Voltar ao Radar",
+ "loading": "Carregando sugestões de combos...",
+ "familyLabel": "Família de modelos",
+ "modelsLabel": "Ordem de prioridade",
+ "strategyReason": "A estratégia de prioridade usa primeiro a maior franquia gratuita recorrente e depois tenta outros provedores ativos.",
+ "generateButton": "Gerar combo",
+ "generating": "Gerando...",
+ "alreadyCreated": "Já criado",
+ "created": "Combo criado",
+ "noSuggestions": "Nenhuma família elegível tem conexões ativas com pelo menos dois provedores no momento.",
+ "catalogRequired": "Sincronize primeiro o catálogo Radar para carregar as famílias de modelos curadas.",
+ "loadFailed": "Falha ao carregar as sugestões de combos do Radar.",
+ "createFailed": "Falha ao criar o combo. Revise as conexões dos provedores e tente novamente."
},
"radarSetupPage": {
"title": "Configuração do Provedor",
diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json
index f8bbdefccb..a177bce7ba 100644
--- a/src/i18n/messages/pt.json
+++ b/src/i18n/messages/pt.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Configuração do Fornecedor",
@@ -12280,5 +12281,22 @@
"addConnection": "Adicionar conexão",
"addConnectionDescription": "Não tem uma conexão ainda? Adicione uma no painel de fornecedores.",
"addConnectionLink": "Ir para fornecedores →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json
index 5433dc2119..eecd50ccef 100644
--- a/src/i18n/messages/ro.json
+++ b/src/i18n/messages/ro.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Configurare Furnizor",
@@ -12280,5 +12281,22 @@
"addConnection": "Adaugă conexiune",
"addConnectionDescription": "Nu ai o conexiune încă? Adaugă una în tabloul de bord al furnizorilor.",
"addConnectionLink": "Mergi la furnizori →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json
index 3136d2d8a1..74c2abecf3 100644
--- a/src/i18n/messages/ru.json
+++ b/src/i18n/messages/ru.json
@@ -12348,7 +12348,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Настройка провайдера",
@@ -12374,5 +12375,22 @@
"addConnection": "Добавить соединение",
"addConnectionDescription": "У вас еще нет соединения? Добавьте его на панели провайдеров.",
"addConnectionLink": "Перейти к провайдерам →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json
index 23bd038967..ad61e1ec54 100644
--- a/src/i18n/messages/sk.json
+++ b/src/i18n/messages/sk.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Nastavenie poskytovateľa",
@@ -12280,5 +12281,22 @@
"addConnection": "Pridať pripojenie",
"addConnectionDescription": "Ešte nemáte pripojenie? Pridajte jedno v ovládacom paneli poskytovateľov.",
"addConnectionLink": "Prejdite na poskytovateľov →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json
index 80676e0dad..99dc26c545 100644
--- a/src/i18n/messages/sv.json
+++ b/src/i18n/messages/sv.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Leverantörsinstallation",
@@ -12280,5 +12281,22 @@
"addConnection": "Lägg till anslutning",
"addConnectionDescription": "Har du ingen anslutning ännu? Lägg till en i leverantörernas instrumentpanel.",
"addConnectionLink": "Gå till leverantörer →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json
index 203f4ac7ce..0f60456caa 100644
--- a/src/i18n/messages/sw.json
+++ b/src/i18n/messages/sw.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Leverantörsinstallation",
@@ -12280,5 +12281,22 @@
"addConnection": "Lägg till anslutning",
"addConnectionDescription": "Har du ingen anslutning ännu? Lägg till en i leverantörernas instrumentpanel.",
"addConnectionLink": "Gå till leverantörer →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json
index ff0dcbdd27..b08a960d33 100644
--- a/src/i18n/messages/ta.json
+++ b/src/i18n/messages/ta.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "வழங்குநர் அமைப்பு",
@@ -12280,5 +12281,22 @@
"addConnection": "இணைப்பைச் சேர்க்கவும்",
"addConnectionDescription": "இன்னும் ஒரு இணைப்பு இல்லை? வழங்குநர் டாஷ்போர்டில் ஒன்றைச் சேர்க்கவும்.",
"addConnectionLink": "வழங்குநர்களுக்கு செல்லவும் →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json
index 1ac839a4c6..4eebf6458d 100644
--- a/src/i18n/messages/te.json
+++ b/src/i18n/messages/te.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "ప్రొవైడర్ సెటప్",
@@ -12280,5 +12281,22 @@
"addConnection": "కనెక్షన్ జోడించండి",
"addConnectionDescription": "ఇంకా కనెక్షన్ లేదు? ప్రొవైడర్ డాష్బోర్డులో ఒకటి జోడించండి.",
"addConnectionLink": "ప్రొవైడర్లకు వెళ్లండి →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json
index f448f0afa9..d017df86f3 100644
--- a/src/i18n/messages/th.json
+++ b/src/i18n/messages/th.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "การตั้งค่าผู้ให้บริการ",
@@ -12280,5 +12281,22 @@
"addConnection": "เพิ่มการเชื่อมต่อ",
"addConnectionDescription": "ยังไม่มีการเชื่อมต่อ? เพิ่มหนึ่งในแดชบอร์ดผู้ให้บริการ",
"addConnectionLink": "ไปที่ผู้ให้บริการ →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json
index d5d361bcf1..4c7196c193 100644
--- a/src/i18n/messages/tr.json
+++ b/src/i18n/messages/tr.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Sağlayıcı Kurulumu",
@@ -12280,5 +12281,22 @@
"addConnection": "Bağlantı ekle",
"addConnectionDescription": "Henüz bir bağlantınız yok mu? Sağlayıcılar panosunda bir tane ekleyin.",
"addConnectionLink": "Sağlayıcılara git →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json
index 09be04e006..86fa1c32c8 100644
--- a/src/i18n/messages/uk-UA.json
+++ b/src/i18n/messages/uk-UA.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Налаштування постачальника",
@@ -12280,5 +12281,22 @@
"addConnection": "Додати з'єднання",
"addConnectionDescription": "Ще немає з'єднання? Додайте його на панелі постачальників.",
"addConnectionLink": "Перейти до постачальників →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json
index 1b3afb4c38..c9e05f15ec 100644
--- a/src/i18n/messages/ur.json
+++ b/src/i18n/messages/ur.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "فراہم کنندہ سیٹ اپ",
@@ -12280,5 +12281,22 @@
"addConnection": "کنکشن شامل کریں",
"addConnectionDescription": "کیا آپ کے پاس ابھی تک کوئی کنکشن نہیں ہے؟ فراہم کنندہ کے ڈیش بورڈ میں ایک شامل کریں۔",
"addConnectionLink": "فراہم کنندگان پر جائیں →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json
index 5cc7d91c18..c15c386a48 100644
--- a/src/i18n/messages/vi.json
+++ b/src/i18n/messages/vi.json
@@ -12305,7 +12305,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "Thiết lập nhà cung cấp",
@@ -12331,5 +12332,22 @@
"addConnection": "Thêm kết nối",
"addConnectionDescription": "Chưa có kết nối? Thêm một cái trong bảng điều khiển nhà cung cấp.",
"addConnectionLink": "Đi đến các nhà cung cấp →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index 6035a1094d..843ea63891 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "提供者设置",
@@ -12280,5 +12281,22 @@
"addConnection": "添加连接",
"addConnectionDescription": "还没有连接?在提供者仪表板中添加一个。",
"addConnectionLink": "前往提供者 →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json
index c45103f2fa..02a8793b5c 100644
--- a/src/i18n/messages/zh-TW.json
+++ b/src/i18n/messages/zh-TW.json
@@ -12254,7 +12254,8 @@
"hiddenModelsTitle": "Hidden models",
"modelDisplayName": "Local display name",
"modelEnabled": "Enabled locally",
- "localStateSaveFailed": "Failed to save local Radar settings"
+ "localStateSaveFailed": "Failed to save local Radar settings",
+ "guidedCombos": "Guided combos"
},
"radarSetupPage": {
"title": "提供者設置",
@@ -12280,5 +12281,22 @@
"addConnection": "添加連接",
"addConnectionDescription": "還沒有連接?在提供者儀表板中添加一個。",
"addConnectionLink": "前往提供者 →"
+ },
+ "radarCombosPage": {
+ "title": "Radar guided combos",
+ "subtitle": "Create priority fallbacks from equivalent free models curated by Radar.",
+ "backToRadar": "Back to Radar",
+ "loading": "Loading combo suggestions...",
+ "familyLabel": "Model family",
+ "modelsLabel": "Priority order",
+ "strategyReason": "Priority uses the largest recurring free budget first, then falls back across active providers.",
+ "generateButton": "Generate combo",
+ "generating": "Generating...",
+ "alreadyCreated": "Already created",
+ "created": "Combo created",
+ "noSuggestions": "No eligible model family currently has active connections to at least two providers.",
+ "catalogRequired": "Sync the Radar catalog first to load curated model families.",
+ "loadFailed": "Failed to load Radar combo suggestions.",
+ "createFailed": "Failed to create the combo. Review your provider connections and try again."
}
}
diff --git a/src/lib/radar/applyFeed.ts b/src/lib/radar/applyFeed.ts
index 55ba532099..a8e9f07ce7 100644
--- a/src/lib/radar/applyFeed.ts
+++ b/src/lib/radar/applyFeed.ts
@@ -27,6 +27,8 @@ export interface MergedEntry {
provider: string;
modelId: string;
displayName: string;
+ /** Curated cross-provider model family used for Radar combo suggestions. */
+ familyId?: string | null;
monthlyTokens: number;
creditTokens: number;
freeType:
@@ -259,6 +261,9 @@ function mergeOne(
if (!overriddenKeys.has("displayName")) {
result.displayName = feed.displayName;
}
+ if (!overriddenKeys.has("familyId")) {
+ result.familyId = feed.familyId;
+ }
if (!overriddenKeys.has("monthlyTokens")) {
result.monthlyTokens = feedBudgetToMonthlyTokens(feed.budget);
}
@@ -293,6 +298,7 @@ function mergeOne(
// Apply local overrides (rule 1: they win)
if (overrides) {
if (overrides.displayName !== undefined) result.displayName = overrides.displayName;
+ if (overrides.familyId !== undefined) result.familyId = overrides.familyId;
if (overrides.monthlyTokens !== undefined) result.monthlyTokens = overrides.monthlyTokens;
if (overrides.creditTokens !== undefined) result.creditTokens = overrides.creditTokens;
if (overrides.freeType !== undefined) result.freeType = overrides.freeType;
@@ -330,6 +336,7 @@ function feedModelToMerged(
provider: feed.provider,
modelId: feed.modelId,
displayName: overrides?.displayName ?? feed.displayName,
+ familyId: overrides?.familyId ?? feed.familyId,
monthlyTokens: overrides?.monthlyTokens ?? feedBudgetToMonthlyTokens(feed.budget),
creditTokens: overrides?.creditTokens ?? 0,
freeType: overrides?.freeType ?? feed.freeType,
diff --git a/src/lib/radar/comboSuggestions.ts b/src/lib/radar/comboSuggestions.ts
new file mode 100644
index 0000000000..f3fbdf7547
--- /dev/null
+++ b/src/lib/radar/comboSuggestions.ts
@@ -0,0 +1,164 @@
+import type { ComboBuilderProviderOption } from "../combos/builderOptions";
+
+import type { MergedEntry } from "./applyFeed";
+
+export interface RadarComboSuggestionModel {
+ providerId: string;
+ providerName: string;
+ modelId: string;
+ qualifiedModel: string;
+ displayName: string;
+ monthlyTokens: number;
+}
+
+export interface RadarComboSuggestionPayload {
+ name: string;
+ strategy: "priority";
+ models: Array<{
+ kind: "model";
+ providerId: string;
+ model: string;
+ weight: 0;
+ }>;
+}
+
+export interface RadarComboSuggestion {
+ familyId: string;
+ name: string;
+ alreadyExists: boolean;
+ models: RadarComboSuggestionModel[];
+ payload: RadarComboSuggestionPayload;
+}
+
+export interface BuildRadarComboSuggestionsInput {
+ entries: readonly MergedEntry[];
+ providers: readonly ComboBuilderProviderOption[];
+ existingComboNames: Iterable;
+}
+
+function compareText(left: string, right: string): number {
+ if (left < right) return -1;
+ if (left > right) return 1;
+ return 0;
+}
+
+function normalizedIdentity(value: string | null | undefined): string {
+ return value?.trim().toLowerCase() ?? "";
+}
+
+function resolveProvider(
+ providerIdentity: string,
+ providers: readonly ComboBuilderProviderOption[]
+): ComboBuilderProviderOption | null {
+ const identity = normalizedIdentity(providerIdentity);
+ if (!identity) return null;
+
+ const active = providers.filter((provider) => provider.activeConnectionCount > 0);
+ const selectors: Array<(provider: ComboBuilderProviderOption) => string | null | undefined> = [
+ (provider) => provider.providerId,
+ (provider) => provider.alias,
+ (provider) => provider.prefix,
+ ];
+
+ for (const select of selectors) {
+ const matches = active.filter((provider) => normalizedIdentity(select(provider)) === identity);
+ if (matches.length === 1) return matches[0];
+ if (matches.length > 1) return null;
+ }
+
+ return null;
+}
+
+function stableHash(value: string): string {
+ let hash = 0x811c9dc5;
+ for (let index = 0; index < value.length; index += 1) {
+ hash ^= value.charCodeAt(index);
+ hash = Math.imul(hash, 0x01000193);
+ }
+ return (hash >>> 0).toString(16).padStart(8, "0");
+}
+
+function comboNameForFamily(familyId: string): string {
+ const safeFamily =
+ familyId
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9._-]+/g, "-")
+ .replace(/^-+|-+$/g, "") || "family";
+ const fullName = `radar-${safeFamily}`;
+ if (fullName.length <= 100) return fullName;
+
+ const hash = stableHash(familyId);
+ const availableFamilyLength = 100 - "radar-".length - 1 - hash.length;
+ return `radar-${safeFamily.slice(0, availableFamilyLength)}-${hash}`;
+}
+
+function compareModels(left: RadarComboSuggestionModel, right: RadarComboSuggestionModel): number {
+ return (
+ right.monthlyTokens - left.monthlyTokens ||
+ compareText(left.providerId, right.providerId) ||
+ compareText(left.modelId, right.modelId)
+ );
+}
+
+/** Build pure, deterministic combo proposals from curated Radar families and live provider options. */
+export function buildRadarComboSuggestions(
+ input: BuildRadarComboSuggestionsInput
+): RadarComboSuggestion[] {
+ const families = new Map>();
+
+ for (const entry of input.entries) {
+ const familyId = entry.familyId?.trim();
+ if (!familyId || entry.enabled === false) continue;
+
+ const provider = resolveProvider(entry.provider, input.providers);
+ if (!provider) continue;
+ const model = provider.models.find((candidate) => candidate.id === entry.modelId);
+ if (!model) continue;
+
+ const candidate: RadarComboSuggestionModel = {
+ providerId: provider.providerId,
+ providerName: provider.displayName,
+ modelId: entry.modelId,
+ qualifiedModel: model.qualifiedModel,
+ displayName: entry.displayName,
+ monthlyTokens: entry.monthlyTokens,
+ };
+ const byProvider = families.get(familyId) ?? new Map();
+ const current = byProvider.get(provider.providerId);
+ if (!current || compareModels(candidate, current) < 0) {
+ byProvider.set(provider.providerId, candidate);
+ }
+ families.set(familyId, byProvider);
+ }
+
+ const existingNames = new Set(
+ [...input.existingComboNames].map((name) => normalizedIdentity(name)).filter(Boolean)
+ );
+ const suggestions: RadarComboSuggestion[] = [];
+
+ for (const familyId of [...families.keys()].sort(compareText)) {
+ const models = [...(families.get(familyId)?.values() ?? [])].sort(compareModels);
+ if (models.length < 2) continue;
+
+ const name = comboNameForFamily(familyId);
+ suggestions.push({
+ familyId,
+ name,
+ alreadyExists: existingNames.has(normalizedIdentity(name)),
+ models,
+ payload: {
+ name,
+ strategy: "priority",
+ models: models.map((model) => ({
+ kind: "model",
+ providerId: model.providerId,
+ model: model.qualifiedModel,
+ weight: 0,
+ })),
+ },
+ });
+ }
+
+ return suggestions;
+}
diff --git a/src/shared/constants/mcpScopes.ts b/src/shared/constants/mcpScopes.ts
index d5babbb63c..c03e4e5797 100644
--- a/src/shared/constants/mcpScopes.ts
+++ b/src/shared/constants/mcpScopes.ts
@@ -15,6 +15,7 @@ export const MCP_SCOPE_LIST = [
"read:quota",
"read:usage",
"read:models",
+ "read:radar",
"execute:completions",
"execute:search",
"write:budget",
@@ -44,6 +45,7 @@ export const MCP_TOOL_SCOPES: Record = {
omniroute_web_fetch: ["execute:search"],
omniroute_cost_report: ["read:usage"],
omniroute_list_models_catalog: ["read:models"],
+ omniroute_radar_catalog: ["read:radar"],
// Phase 2: Advanced Tools
omniroute_simulate_route: ["read:health", "read:combos"],
diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts
index 27deae3057..7f7aaaaffd 100644
--- a/tests/unit/check-docs-counts-sync.test.ts
+++ b/tests/unit/check-docs-counts-sync.test.ts
@@ -148,7 +148,7 @@ test("free-tier gate passes when a file carries no headline at all", () => {
// --- 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→109 MCP tools, 30→33 scopes, 26→33 CLI tools.
import { makeNumberClaimValidator } from "../../scripts/check/check-docs-counts-sync.mjs";
const makeValidator = makeNumberClaimValidator as (
@@ -157,19 +157,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(109, {
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 (109 tools)").ok, true);
+ assert.equal(v("with 109 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(109, {
what: "MCP tools",
pattern: /(\d+) tools/gi,
skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
diff --git a/tests/unit/radar-apply-feed.test.ts b/tests/unit/radar-apply-feed.test.ts
index 868d0cd76a..cd1a3fc012 100644
--- a/tests/unit/radar-apply-feed.test.ts
+++ b/tests/unit/radar-apply-feed.test.ts
@@ -746,6 +746,40 @@ test("FIX2 feedModelToMerged path: contextWindow/capabilities/limits/setup survi
});
});
+test("F3 mergeOne path: familyId survives the feed merge over a baseline entry", () => {
+ const result = applyFeed({
+ baseline: makeBaseline(),
+ feed: [
+ makeFeedModel({
+ provider: "groq",
+ modelId: "llama-3.3-70b-versatile",
+ familyId: "llama-3.3-70b",
+ }),
+ ],
+ localOverrides: new Map(),
+ tombstones: new Set(),
+ });
+
+ assert.equal(result.find((entry) => entry.provider === "groq")?.familyId, "llama-3.3-70b");
+});
+
+test("F3 feedModelToMerged path: familyId survives for a feed-only entry", () => {
+ const result = applyFeed({
+ baseline: [],
+ feed: [
+ makeFeedModel({
+ provider: "new-provider",
+ modelId: "shared-model",
+ familyId: "shared-family",
+ }),
+ ],
+ localOverrides: new Map(),
+ tombstones: new Set(),
+ });
+
+ assert.equal(result[0]?.familyId, "shared-family");
+});
+
// ===========================================================================
// Feed `enabled:false` is the safety exception to local override precedence:
// a model confirmed dead upstream must not be resurrected locally.
diff --git a/tests/unit/radar-combo-suggestions.test.ts b/tests/unit/radar-combo-suggestions.test.ts
new file mode 100644
index 0000000000..9a83b3555b
--- /dev/null
+++ b/tests/unit/radar-combo-suggestions.test.ts
@@ -0,0 +1,134 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import type { ComboBuilderProviderOption } from "../../src/lib/combos/builderOptions.ts";
+import type { MergedEntry } from "../../src/lib/radar/applyFeed.ts";
+import { buildRadarComboSuggestions } from "../../src/lib/radar/comboSuggestions.ts";
+
+function entry(
+ overrides: Partial & Pick
+): MergedEntry {
+ return {
+ provider: overrides.provider,
+ modelId: overrides.modelId,
+ displayName: overrides.displayName ?? overrides.modelId,
+ monthlyTokens: overrides.monthlyTokens ?? 100,
+ creditTokens: 0,
+ freeType: "recurring-daily",
+ poolKey: null,
+ tos: "ok",
+ enabled: true,
+ origin: "radar",
+ familyId: "shared-family",
+ ...overrides,
+ };
+}
+
+function provider(
+ providerId: string,
+ modelId: string,
+ overrides: Partial = {}
+): ComboBuilderProviderOption {
+ return {
+ providerId,
+ providerType: providerId,
+ displayName: providerId.toUpperCase(),
+ alias: providerId,
+ icon: "api",
+ color: "#000000",
+ source: "system",
+ acceptsArbitraryModel: false,
+ connectionCount: 1,
+ activeConnectionCount: 1,
+ modelCount: 1,
+ connections: [],
+ models: [
+ {
+ id: modelId,
+ qualifiedModel: `${providerId}/${modelId}`,
+ name: modelId,
+ source: "system",
+ sources: ["system"],
+ },
+ ],
+ ...overrides,
+ };
+}
+
+test("two active providers in one family create one deterministic priority suggestion", () => {
+ const suggestions = buildRadarComboSuggestions({
+ entries: [
+ entry({ provider: "groq", modelId: "llama", monthlyTokens: 200 }),
+ entry({ provider: "cerebras", modelId: "llama", monthlyTokens: 300 }),
+ ],
+ providers: [provider("groq", "llama"), provider("cerebras", "llama")],
+ existingComboNames: [],
+ });
+
+ assert.equal(suggestions.length, 1);
+ assert.equal(suggestions[0].familyId, "shared-family");
+ assert.equal(suggestions[0].name, "radar-shared-family");
+ assert.equal(suggestions[0].alreadyExists, false);
+ assert.deepEqual(suggestions[0].payload, {
+ name: "radar-shared-family",
+ strategy: "priority",
+ models: [
+ { kind: "model", providerId: "cerebras", model: "cerebras/llama", weight: 0 },
+ { kind: "model", providerId: "groq", model: "groq/llama", weight: 0 },
+ ],
+ });
+});
+
+test("ineligible entries fail closed while alias and prefix match exact provider models", () => {
+ const suggestions = buildRadarComboSuggestions({
+ entries: [
+ entry({ provider: "gq", modelId: "llama", monthlyTokens: 500 }),
+ entry({ provider: "cb", modelId: "llama", monthlyTokens: 400 }),
+ entry({ provider: "inactive", modelId: "llama", monthlyTokens: 900 }),
+ entry({ provider: "disabled", modelId: "llama", enabled: false }),
+ entry({ provider: "missing-model", modelId: "other" }),
+ entry({ provider: "singleton", modelId: "solo", familyId: "solo-family" }),
+ ],
+ providers: [
+ provider("groq", "llama", { alias: "gq" }),
+ provider("cerebras", "llama", { prefix: "cb" }),
+ provider("inactive", "llama", { activeConnectionCount: 0 }),
+ provider("disabled", "llama"),
+ provider("missing-model", "llama"),
+ provider("singleton", "solo"),
+ ],
+ existingComboNames: new Set(["RADAR-SHARED-FAMILY"]),
+ });
+
+ assert.equal(suggestions.length, 1);
+ assert.equal(suggestions[0].alreadyExists, true);
+ assert.deepEqual(
+ suggestions[0].models.map((model) => model.providerId),
+ ["groq", "cerebras"]
+ );
+});
+
+test("ambiguous provider aliases, duplicate providers, empty families and unsafe names are closed", () => {
+ const longFamily = `Family / ${"x".repeat(120)}`;
+ const suggestions = buildRadarComboSuggestions({
+ entries: [
+ entry({ provider: "ambiguous", modelId: "m", familyId: "ambiguous-family" }),
+ entry({ provider: "one", modelId: "m", familyId: longFamily, monthlyTokens: 200 }),
+ entry({ provider: "two", modelId: "m", familyId: longFamily, monthlyTokens: 100 }),
+ entry({ provider: "one", modelId: "m", familyId: longFamily, monthlyTokens: 50 }),
+ entry({ provider: "one", modelId: "blank", familyId: " " }),
+ ],
+ providers: [
+ provider("ambiguous-a", "m", { alias: "ambiguous" }),
+ provider("ambiguous-b", "m", { alias: "ambiguous" }),
+ provider("one", "m"),
+ provider("two", "m"),
+ ],
+ existingComboNames: [],
+ });
+
+ assert.equal(suggestions.length, 1);
+ assert.ok(suggestions[0].name.length <= 100);
+ assert.match(suggestions[0].name, /^[a-zA-Z0-9_/.\-\[\] ]+$/);
+ assert.equal(new Set(suggestions[0].models.map((model) => model.providerId)).size, 2);
+});
diff --git a/tests/unit/radar-combos-page.test.ts b/tests/unit/radar-combos-page.test.ts
new file mode 100644
index 0000000000..de41e5d86a
--- /dev/null
+++ b/tests/unit/radar-combos-page.test.ts
@@ -0,0 +1,90 @@
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import test from "node:test";
+
+const pagePath = path.join(process.cwd(), "src/app/(dashboard)/dashboard/radar/combos/page.tsx");
+const radarPagePath = path.join(process.cwd(), "src/app/(dashboard)/dashboard/radar/page.tsx");
+
+function pageSource(): string {
+ return fs.existsSync(pagePath) ? fs.readFileSync(pagePath, "utf8") : "";
+}
+
+test("Radar exposes the guided combos page from its catalog", () => {
+ assert.ok(fs.existsSync(pagePath), "missing /dashboard/radar/combos page");
+ assert.match(fs.readFileSync(radarPagePath, "utf8"), /href="\/dashboard\/radar\/combos"/);
+});
+
+test("guided combos reuse only the local catalog, builder options, and combo writer", () => {
+ const source = pageSource();
+ assert.match(source, /fetch\("\/api\/radar\/catalog"\)/);
+ assert.match(source, /fetch\("\/api\/combos\/builder\/options"\)/);
+ assert.match(source, /fetch\("\/api\/combos",\s*\{/);
+ assert.match(source, /method:\s*"POST"/);
+ assert.doesNotMatch(source, /\/api\/radar\/sync/);
+ assert.doesNotMatch(source, /localDb|getDbInstance|createCombo\(/);
+});
+
+test("guided combos render family, provider models, strategy reason and created state", () => {
+ const source = pageSource();
+ assert.match(source, /buildRadarComboSuggestions/);
+ for (const key of [
+ "familyLabel",
+ "modelsLabel",
+ "strategyReason",
+ "generateButton",
+ "alreadyCreated",
+ "noSuggestions",
+ "catalogRequired",
+ "loadFailed",
+ "createFailed",
+ ]) {
+ assert.match(source, new RegExp(`t\\("${key}"`), `missing UI key ${key}`);
+ }
+});
+
+test("every locale carries the Radar combos namespace and English/pt-BR have real copy", () => {
+ const messagesDir = path.join(process.cwd(), "src/i18n/messages");
+ const files = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json"));
+ const requiredKeys = [
+ "title",
+ "subtitle",
+ "backToRadar",
+ "loading",
+ "familyLabel",
+ "modelsLabel",
+ "strategyReason",
+ "generateButton",
+ "generating",
+ "alreadyCreated",
+ "created",
+ "noSuggestions",
+ "catalogRequired",
+ "loadFailed",
+ "createFailed",
+ ];
+
+ for (const file of files) {
+ const messages = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf8")) as {
+ radarCombosPage?: Record;
+ };
+ for (const key of requiredKeys) {
+ const value = messages.radarCombosPage?.[key];
+ assert.equal(typeof value, "string", `${file}: missing radarCombosPage.${key}`);
+ assert.ok((value as string).trim().length > 0, `${file}: empty radarCombosPage.${key}`);
+ }
+ }
+
+ for (const locale of ["en", "pt-BR"]) {
+ const messages = JSON.parse(
+ fs.readFileSync(path.join(messagesDir, `${locale}.json`), "utf8")
+ ) as { radarCombosPage: Record };
+ for (const key of requiredKeys) {
+ assert.doesNotMatch(
+ messages.radarCombosPage[key],
+ /^__MISSING__:/,
+ `${locale}: placeholder at radarCombosPage.${key}`
+ );
+ }
+ }
+});