mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-20 03:35:40 +02:00
134 lines
5.8 KiB
Python
134 lines
5.8 KiB
Python
"""Provider API-key sync into 9Router.
|
|
|
|
Mirrors the user's stored Gemini / OpenAI / OpenRouter keys into 9Router
|
|
as OpenSwarm-managed apikey connections. Talks to the already-running
|
|
9Router over HTTP; never spawns the subprocess (that's process.py's job).
|
|
"""
|
|
|
|
import logging
|
|
|
|
from backend.apps.nine_router.process import NINE_ROUTER_API, cli_auth_headers
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def nr():
|
|
"""The package module. is_running / get_providers / httpx are read off it
|
|
at call time so tests that patch `backend.apps.nine_router.<name>` still
|
|
take effect after the split (they used to all live on one module)."""
|
|
from backend.apps import nine_router
|
|
return nine_router
|
|
|
|
# API-key auth (provider="gemini", authType="apikey") and OAuth hit different Google quotas: OAuth uses the Code Assist free tier (aggressively rate-limited; 429s on Gemini 3 Pro/Flash even for paid users), while an AI Studio API key uses generativelanguage.googleapis.com (independent and far higher). We mirror google_api_key into 9Router so the API-key path is preferred when a key is set.
|
|
|
|
NINE_ROUTER_KEYED_NAME = "AI Studio (OpenSwarm-managed)"
|
|
NINE_ROUTER_OPENAI_KEYED_NAME = "OpenAI (OpenSwarm-managed)"
|
|
NINE_ROUTER_OPENROUTER_KEYED_NAME = "OpenRouter (OpenSwarm-managed)"
|
|
NINE_ROUTER_CLAUDE_PRO_NAME = "OpenSwarm Pro (OpenSwarm-managed)"
|
|
|
|
# Reserved prefix that registry.py's gpt-5.*-api router_model_ids depend on. Changing this breaks model resolution for OpenAI own-key users.
|
|
NINE_ROUTER_OPENAI_KEYED_PREFIX = "cp-openai"
|
|
|
|
|
|
async def find_keyed_connection(provider: str, name: str) -> dict | None:
|
|
"""Return the 9Router connection we manage for this provider, if any."""
|
|
conns = await nr().get_providers()
|
|
if not isinstance(conns, list):
|
|
return None
|
|
for c in conns:
|
|
if (
|
|
isinstance(c, dict)
|
|
and c.get("provider") == provider
|
|
and c.get("authType") == "apikey"
|
|
and c.get("name") == name
|
|
):
|
|
return c
|
|
return None
|
|
|
|
|
|
async def p_sync_apikey_provider(
|
|
provider: str,
|
|
api_key: str | None,
|
|
name: str,
|
|
*,
|
|
label: str,
|
|
) -> None:
|
|
"""Create/update/delete an OpenSwarm-managed apikey connection. Silent if 9Router is down."""
|
|
if not nr().is_running():
|
|
return
|
|
|
|
existing = await find_keyed_connection(provider, name)
|
|
try:
|
|
async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client:
|
|
if api_key:
|
|
payload = {
|
|
"provider": provider,
|
|
"authType": "apikey",
|
|
"name": name,
|
|
"apiKey": api_key,
|
|
# Priority 0 = highest. OAuth connections default to 1, so keyed connections are preferred when both exist.
|
|
"priority": 0,
|
|
}
|
|
if existing:
|
|
await client.patch(
|
|
f"{NINE_ROUTER_API}/providers/{existing['id']}",
|
|
json=payload,
|
|
)
|
|
logger.info(f"9Router: updated {label} API-key connection")
|
|
else:
|
|
r = await client.post(f"{NINE_ROUTER_API}/providers", json=payload)
|
|
if r.status_code < 300:
|
|
logger.info(f"9Router: created {label} API-key connection")
|
|
else:
|
|
logger.warning(
|
|
f"9Router: failed to create {label} API-key connection: "
|
|
f"{r.status_code} {r.text[:200]}"
|
|
)
|
|
else:
|
|
if existing:
|
|
await client.delete(f"{NINE_ROUTER_API}/providers/{existing['id']}")
|
|
logger.info(f"9Router: removed {label} API-key connection")
|
|
except Exception as e:
|
|
logger.warning(f"9Router {label} API-key sync failed: {e}")
|
|
|
|
|
|
async def sync_gemini_api_key(api_key: str | None) -> None:
|
|
"""Mirror google_api_key into 9Router; bypasses Code Assist's tight quota."""
|
|
await p_sync_apikey_provider(
|
|
"gemini", api_key, NINE_ROUTER_KEYED_NAME, label="Gemini"
|
|
)
|
|
|
|
|
|
async def sync_openai_api_key(api_key: str | None) -> None:
|
|
"""Mirror openai_api_key into 9Router as an `openai-compatible` provider
|
|
node pointed at our local /api/openai-passthrough proxy.
|
|
|
|
Why not the built-in `openai` provider type: 9Router 0.3.60 hardcodes
|
|
`https://api.openai.com/v1` for the `openai` provider and ignores any
|
|
`baseUrl` field on the connection. Only the `openai-compatible-*`
|
|
provider-node type honors `baseUrl` (verified statically against
|
|
9Router's compiled bundle). So we register our OpenAI lane AS an
|
|
openai-compatible node; same upstream protocol, different routing.
|
|
|
|
Why we route through openai-passthrough at all: OpenAI's GPT-5 family
|
|
rejects the legacy `max_tokens` parameter with HTTP 400, but every
|
|
9Router version (including 0.4.20) emits `max_tokens` in its
|
|
Anthropic→OpenAI translator. The passthrough renames it to
|
|
`max_completion_tokens` for `gpt-5*` models before forwarding to
|
|
api.openai.com. Pre-fix: every gpt-5.* own-key session 400'd silently.
|
|
|
|
Companion change: the registry entries `gpt-5.*-api` are routed via
|
|
the `cp-openai/<model>` prefix (set by NINE_ROUTER_OPENAI_KEYED_PREFIX
|
|
above) so 9Router's translator dispatches to this provider-node
|
|
instead of the built-in `openai` provider.
|
|
"""
|
|
from backend.apps.nine_router.sync_custom import sync_openai_compat_node
|
|
await sync_openai_compat_node(api_key)
|
|
|
|
|
|
async def sync_openrouter_api_key(api_key: str | None) -> None:
|
|
"""Mirror openrouter_api_key into 9Router; supplies bearer for openrouter/ routes."""
|
|
await p_sync_apikey_provider(
|
|
"openrouter", api_key, NINE_ROUTER_OPENROUTER_KEYED_NAME, label="OpenRouter"
|
|
)
|