From 5eb3057be6f3e640a16daaf6125426b84e55f376 Mon Sep 17 00:00:00 2001 From: Joseph Perla Date: Thu, 23 Jul 2026 13:17:45 -0700 Subject: [PATCH] Add TrustedRouter model provider --- README.md | 2 +- coworker/providers/matrix.py | 18 ++++++++++++++++++ coworker/providers/registry.py | 18 ++++++++++++++++-- surfaces/gui/e2e/fixtures.ts | 2 ++ surfaces/gui/e2e/onboarding.spec.ts | 1 + surfaces/gui/src/api.test.ts | 10 ++++++++++ surfaces/gui/src/api.ts | 1 + surfaces/gui/src/providers/logos.ts | 3 +++ .../gui/src/providers/logos/trustedrouter.svg | 8 ++++++++ tests/test_provider_router.py | 8 ++++++++ tests/test_provider_verify.py | 9 +++++++++ 11 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 surfaces/gui/src/api.test.ts create mode 100644 surfaces/gui/src/providers/logos/trustedrouter.svg diff --git a/README.md b/README.md index d96cf139..9eddac4a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Under the hood: Model access is yours: pick a provider, paste your key, switch anytime. Supported out of the box: -**OpenAI · Anthropic · Google Gemini · Inkling (Thinking Machines) · GLM (Z.ai) · DeepSeek · Kimi (Moonshot) · Qwen · MiniMax · Mistral · Grok (xAI)** - plus open-weight models via **Together** and **Fireworks**, and fully local models via **Ollama**. +**OpenAI · Anthropic · Google Gemini · Inkling (Thinking Machines) · GLM (Z.ai) · DeepSeek · Kimi (Moonshot) · Qwen · MiniMax · Mistral · Grok (xAI)** - plus multi-model routing through **[TrustedRouter](https://trustedrouter.com)**, open-weight models via **Together** and **Fireworks**, and fully local models via **Ollama**. A curated model list marks what we've verified for tool-calling work. Adding any model string works at your own risk. diff --git a/coworker/providers/matrix.py b/coworker/providers/matrix.py index 9b40cacf..224ac727 100644 --- a/coworker/providers/matrix.py +++ b/coworker/providers/matrix.py @@ -88,6 +88,24 @@ class ModelEntry: "qwen:qwen3-max": ModelEntry("Qwen3 Max · Alibaba"), "xai:grok-4.3": ModelEntry("Grok 4.3 · xAI"), "mistral:mistral-large-latest": ModelEntry("Mistral Large · Mistral"), + # -- multi-model gateways --------------------------------------------------- + # The first prefix selects OpenWorker's provider; the remaining slash-delimited id is sent + # unchanged to TrustedRouter's OpenAI-compatible API. + "trustedrouter:trustedrouter/auto": ModelEntry( + "Auto · TrustedRouter (automatic fallback)" + ), + "trustedrouter:trustedrouter/zdr": ModelEntry( + "ZDR · TrustedRouter (zero-retention routes)" + ), + "trustedrouter:trustedrouter/e2e": ModelEntry( + "E2E · TrustedRouter (confidential routes)" + ), + "trustedrouter:trustedrouter/cheap": ModelEntry( + "Cheap · TrustedRouter (cost-optimized)" + ), + "trustedrouter:trustedrouter/plato": ModelEntry( + "Plato · TrustedRouter (open-weight advisor)" + ), # -- resellers (their model namespaces, verbatim) ----------------------------- "together:thinkingmachines/Inkling": ModelEntry("Inkling · via Together"), "together:zai-org/GLM-5.2": ModelEntry("GLM-5.2 · via Together"), diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index b5c628c6..58118e57 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -8,8 +8,9 @@ Today: `openai` (the default, with an optional custom endpoint that covers Azure OpenAI's `/openai/v1` and any OpenAI-compliant gateway), `anthropic` (native Messages API via -`AnthropicProvider`), `gemini` (native Google GenAI API via `GeminiProvider`), and `ollama` -(local, OpenAI-compatible `/v1`). Bedrock/Vertex auth for Claude is future work. +`AnthropicProvider`), `gemini` (native Google GenAI API via `GeminiProvider`), `trustedrouter` +(OpenAI-compatible multi-model routing), and `ollama` (local, OpenAI-compatible `/v1`). +Bedrock/Vertex auth for Claude is future work. """ from __future__ import annotations @@ -315,6 +316,17 @@ def _compat( env_key="META_API_KEY", endpoint_help="Prefilled with the Meta Model API endpoint (public preview, US-only as of 2026-07).", ), + _compat( + "trustedrouter", + "TrustedRouter", + base_url="https://api.trustedrouter.com/v1", + recommended_model="trustedrouter/auto", + env_key="TRUSTEDROUTER_API_KEY", + endpoint_help=( + "TrustedRouter's attested OpenAI-compatible API. Create a key at " + "https://trustedrouter.com/console/api-keys." + ), + ), # Resellers: many labs' models behind one key, using THEIR model namespaces (the curated # ids + display labels live in providers/matrix.py). TODO: add Groq and OpenRouter here # (+ their matrix rows) once the current provider surface is tested — deliberately @@ -388,6 +400,8 @@ def detect_provider(api_key: str) -> Optional[str]: return "anthropic" if key.startswith("AIza"): return "gemini" + if key.startswith("sk-tr-"): + return "trustedrouter" if key.startswith(("sk-", "sk_")): return "openai" return None diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index 7df802f5..e6f78ddd 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -329,6 +329,8 @@ const PROVIDERS = [ { name: "openai", title: "OpenAI", needs_key: true, fields: [{ key: "api_key", label: "OpenAI API key", secret: true, required: true, help: "", placeholder: "sk-…" }], configured: true, values: {}, suggested_models: ["gpt-5.5"], key_set_at: "2026-06-12", last_used_at: Math.floor(Date.now() / 1000) - 7200 }, // anthropic: configured but never used ("Not used yet"). { name: "anthropic", title: "Claude (Anthropic)", needs_key: true, fields: [{ key: "api_key", label: "API key", secret: true, required: true, help: "", placeholder: "sk-…" }], configured: true, values: {}, suggested_models: ["claude-opus-4-8"], key_set_at: null, last_used_at: null }, + // trustedrouter: first-class OpenAI-compatible multi-model backend. + { name: "trustedrouter", title: "TrustedRouter", needs_key: true, blurb: "Uses TrustedRouter's OpenAI-compatible API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "TrustedRouter API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "TrustedRouter's attested OpenAI-compatible API.", placeholder: "https://api.trustedrouter.com/v1", default: "https://api.trustedrouter.com/v1" }], configured: false, values: {}, suggested_models: ["trustedrouter/auto", "trustedrouter/zdr"], key_set_at: null, last_used_at: null }, // zai: an OpenAI-compatible vendor — unconfigured, with a prefilled editable endpoint + blurb. { name: "zai", title: "Z AI (GLM)", needs_key: true, blurb: "Uses Z AI's OpenAI-compatible API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "Z AI API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "Prefilled with Z AI's international endpoint.", placeholder: "https://api.z.ai/api/paas/v4", default: "https://api.z.ai/api/paas/v4" }], configured: false, values: {}, suggested_models: ["glm-5.2"], key_set_at: null, last_used_at: null }, // ollama: keyless local provider — "configured" without proving anything runs; the diff --git a/surfaces/gui/e2e/onboarding.spec.ts b/surfaces/gui/e2e/onboarding.spec.ts index fc832d8f..a57129fa 100644 --- a/surfaces/gui/e2e/onboarding.spec.ts +++ b/surfaces/gui/e2e/onboarding.spec.ts @@ -24,6 +24,7 @@ test("provider gallery: cards wear their state; Next arms off stored credentials // "is OpenAI already connected?" — is answered by the gallery itself). await expect(page.getByTestId("ob-provider-openai")).toContainText("✓ Connected"); await expect(page.getByTestId("ob-provider-anthropic")).toContainText("✓ Connected"); + await expect(page.getByTestId("ob-provider-trustedrouter")).toContainText("Not set up"); await expect(page.getByTestId("ob-provider-zai")).toContainText("Not set up"); await expect(page.getByTestId("ob-provider-ollama")).toContainText("No key needed"); // Recognition-first order: anthropic before openai before the OpenAI-compat tail. diff --git a/surfaces/gui/src/api.test.ts b/surfaces/gui/src/api.test.ts new file mode 100644 index 00000000..d6d01840 --- /dev/null +++ b/surfaces/gui/src/api.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; + +import { detectProvider } from "./api"; + +describe("detectProvider", () => { + it("recognizes TrustedRouter keys before the generic OpenAI prefix", () => { + expect(detectProvider("sk-tr-v1-test")).toBe("trustedrouter"); + expect(detectProvider("sk-proj-test")).toBe("openai"); + }); +}); diff --git a/surfaces/gui/src/api.ts b/surfaces/gui/src/api.ts index c700cdd0..62a8af71 100644 --- a/surfaces/gui/src/api.ts +++ b/surfaces/gui/src/api.ts @@ -1325,6 +1325,7 @@ export function detectProvider(apiKey: string): string | null { if (!key) return null; if (key.startsWith("sk-ant-")) return "anthropic"; if (key.startsWith("AIza")) return "gemini"; + if (key.startsWith("sk-tr-")) return "trustedrouter"; if (key.startsWith("sk-") || key.startsWith("sk_")) return "openai"; return null; } diff --git a/surfaces/gui/src/providers/logos.ts b/surfaces/gui/src/providers/logos.ts index 14c10332..72b519a6 100644 --- a/surfaces/gui/src/providers/logos.ts +++ b/surfaces/gui/src/providers/logos.ts @@ -19,6 +19,7 @@ import qwen from "./logos/qwen.svg"; import minimax from "./logos/minimax.svg"; import xai from "./logos/xai.svg"; import meta from "./logos/meta.svg"; +import trustedrouter from "./logos/trustedrouter.svg"; export const PROVIDER_LOGOS: Record = { anthropic, @@ -35,6 +36,7 @@ export const PROVIDER_LOGOS: Record = { qwen, minimax, xai, + trustedrouter, }; export const PROVIDER_ORDER = [ @@ -52,6 +54,7 @@ export const PROVIDER_ORDER = [ "qwen", "minimax", "xai", + "trustedrouter", ]; export function providerRank(name: string): number { diff --git a/surfaces/gui/src/providers/logos/trustedrouter.svg b/surfaces/gui/src/providers/logos/trustedrouter.svg new file mode 100644 index 00000000..eac56de4 --- /dev/null +++ b/surfaces/gui/src/providers/logos/trustedrouter.svg @@ -0,0 +1,8 @@ + + TrustedRouter + + + + + + diff --git a/tests/test_provider_router.py b/tests/test_provider_router.py index 81e53d30..49aec2cb 100644 --- a/tests/test_provider_router.py +++ b/tests/test_provider_router.py @@ -115,6 +115,9 @@ def test_router_routes_and_strips_prefix(monkeypatch): router.complete(model="gpt-5.5", messages=[]) # bare → default openai assert state["latest"]["openai"].models == ["gpt-5.5"] + router.complete(model="trustedrouter:trustedrouter/auto", messages=[]) + assert state["latest"]["trustedrouter"].models == ["trustedrouter/auto"] + def test_router_caches_and_invalidates(monkeypatch): state = _patch_build(monkeypatch) @@ -416,6 +419,10 @@ def test_provider_builders(monkeypatch): assert o._base_url == "https://my.azure.example/openai/v1" assert build_provider_client("openai", {}, None)._base_url is None + tr = build_provider_client("trustedrouter", {"api_key": "sk-tr-v1-test"}, None) + assert tr._api_key == "sk-tr-v1-test" + assert tr._base_url == "https://api.trustedrouter.com/v1" + def test_anthropic_gemini_capabilities(): for m in ("anthropic:claude-sonnet-4-6", "gemini:gemini-2.5-flash"): @@ -504,6 +511,7 @@ def test_provider_suggested_models(tmp_path, monkeypatch): mgr = SessionManager(data_dir=tmp_path) provs = {p["name"]: p for p in mgr.get_providers()} assert "gpt-5.5" in provs["openai"]["suggested_models"] + assert "trustedrouter/auto" in provs["trustedrouter"]["suggested_models"] # ollama suggestions are bare names (no `ollama:` prefix); empty when unconfigured sugg = provs["ollama"]["suggested_models"] assert isinstance(sugg, list) diff --git a/tests/test_provider_verify.py b/tests/test_provider_verify.py index c24414e8..ccf9a7b1 100644 --- a/tests/test_provider_verify.py +++ b/tests/test_provider_verify.py @@ -16,6 +16,7 @@ [ ("sk-ant-api03-abc", "anthropic"), ("AIzaSyAbc123", "gemini"), + ("sk-tr-v1-abc", "trustedrouter"), ("sk-proj-abc", "openai"), ("sk_live_abc", "openai"), ("", None), @@ -58,6 +59,14 @@ def test_verify_openai_custom_endpoint(monkeypatch): assert cap["url"] == "https://gw.example/openai/v1/models" +def test_verify_trustedrouter_uses_attested_api(monkeypatch): + cap: dict = {} + _patch_get(monkeypatch, status=200, capture=cap) + assert verify_provider_key("trustedrouter", api_key="sk-tr-v1-test") == {"ok": True} + assert cap["url"] == "https://api.trustedrouter.com/v1/models" + assert cap["headers"]["Authorization"] == "Bearer sk-tr-v1-test" + + def test_verify_bad_key_is_invalid(monkeypatch): _patch_get(monkeypatch, status=401) assert verify_provider_key("openai", api_key="sk-bad") == {