mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] conventions: one-line-comment + no-gratuitous-blank rules in CLAUDE.md; apply to configure_provider_env (271->223)
This commit is contained in:
@@ -42,6 +42,18 @@ Personal / per-machine notes go in `CLAUDE.local.md` (gitignored), **not** here.
|
|||||||
are dynamic-key maps (a registry keyed by a runtime id) and external protocol shapes (the Claude
|
are dynamic-key maps (a registry keyed by a runtime id) and external protocol shapes (the Claude
|
||||||
Agent SDK hook returns, `model_dump` output).
|
Agent SDK hook returns, `model_dump` output).
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
- **Every comment is ONE physical line. No exceptions.** Never wrap a comment across multiple `#`
|
||||||
|
lines — collapse it into a single line (long is fine; multi-line is not).
|
||||||
|
- **Delete comments that aren't pulling weight.** Keep only the WHY (a non-obvious reason, gotcha, or
|
||||||
|
ambiguity); delete anything that restates the code, and delete dead/commented-out code outright.
|
||||||
|
- A module/function docstring is exempt (it's a docstring, not a `#` comment).
|
||||||
|
|
||||||
|
## Whitespace
|
||||||
|
- **No gratuitous blank lines.** One blank line separates logical units; never stack 2+ blank lines.
|
||||||
|
- **Tight imports** — no blank lines inside an import block beyond the single separator between
|
||||||
|
stdlib / third-party / local groups. Delete any blank line that isn't doing real readability work.
|
||||||
|
|
||||||
When you add or change code, the files you touch must be clean under these rules. Pre-existing debt
|
When you add or change code, the files you touch must be clean under these rules. Pre-existing debt
|
||||||
in files you are not otherwise editing is grandfathered via the linter's exception lists — do not
|
in files you are not otherwise editing is grandfathered via the linter's exception lists — do not
|
||||||
mass-migrate untouched files.
|
mass-migrate untouched files.
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ All conventions in the root `CLAUDE.md` apply here. Python-specific emphasis:
|
|||||||
structured data — model it. Legitimate dicts: dynamic-key registries and external protocol shapes
|
structured data — model it. Legitimate dicts: dynamic-key registries and external protocol shapes
|
||||||
(SDK hook returns, `model_dump` output).
|
(SDK hook returns, `model_dump` output).
|
||||||
- **Single-purpose file naming** — a one-export file is named after its export.
|
- **Single-purpose file naming** — a one-export file is named after its export.
|
||||||
|
- **Comments are ONE line each, no exceptions** — never wrap across multiple `#` lines; keep only
|
||||||
|
WHY/gotcha comments, delete restating or dead-code comments. Docstrings are exempt.
|
||||||
|
- **No gratuitous blank lines** — never stack 2+ blanks; keep imports tight (only the single
|
||||||
|
stdlib/third-party/local group separators).
|
||||||
|
|
||||||
Touch a file → it must be clean under these rules. Pre-existing debt is grandfathered in
|
Touch a file → it must be clean under these rules. Pre-existing debt is grandfathered in
|
||||||
`linter/config/config.json`; don't mass-migrate untouched files.
|
`linter/config/config.json`; don't mass-migrate untouched files.
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"""Configure the SDK environment for the run's provider route: set ANTHROPIC/OPENAI/GOOGLE
|
"""Configure the SDK environment for the run's provider route: set ANTHROPIC/OPENAI/GOOGLE
|
||||||
auth env vars (direct key, OpenSwarm Pro proxy, OpenRouter, or 9Router) and pin subagent models,
|
auth env vars (direct key, OpenSwarm Pro proxy, OpenRouter, or 9Router) and pin subagent models,
|
||||||
ensuring 9Router is up where the route needs it. Lifted out of the agent loop; mutates
|
ensuring 9Router is up where the route needs it. sub_conns is the active-connection list for
|
||||||
options_kwargs[\"env\"] in place exactly as inline. sub_conns is the active-connection list used
|
subagent-model fallback (empty today)."""
|
||||||
for subagent-model fallback (empty today)."""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
@@ -41,25 +40,14 @@ async def configure_provider_env(
|
|||||||
options_kwargs["env"] = {
|
options_kwargs["env"] = {
|
||||||
"ANTHROPIC_API_KEY": global_settings.anthropic_api_key,
|
"ANTHROPIC_API_KEY": global_settings.anthropic_api_key,
|
||||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||||
# Pin subagent envs so they don't drift back to the proxy.
|
# Pin subagents so they don't drift back to the proxy.
|
||||||
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6",
|
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6",
|
||||||
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5",
|
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5",
|
||||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5",
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5",
|
||||||
}
|
}
|
||||||
logger.info(f"[MCP-DEBUG] Using direct Anthropic API key (route=api) for {session.model}")
|
logger.info(f"[MCP-DEBUG] Using direct Anthropic API key (route=api) for {session.model}")
|
||||||
elif is_pinned_api_route and api_route_provider == "openai" and getattr(global_settings, "openai_api_key", None):
|
elif is_pinned_api_route and api_route_provider == "openai" and getattr(global_settings, "openai_api_key", None):
|
||||||
# Goes through 9Router's Anthropic→OpenAI translator like
|
# openai-passthrough renames max_tokens->max_completion_tokens before relaying (GPT-5 400s on max_tokens; 9Router 0.3.60, pinned for WebSearch, emits the legacy field).
|
||||||
# other own-key routes, but we point OPENAI_BASE_URL at a
|
|
||||||
# tiny local pass-through (/api/openai-passthrough/v1) that
|
|
||||||
# renames max_tokens → max_completion_tokens before relaying
|
|
||||||
# to api.openai.com. OpenAI's GPT-5 family rejects max_tokens
|
|
||||||
# with HTTP 400, and 9Router 0.3.60 doesn't know about
|
|
||||||
# max_completion_tokens yet (its CLI<->OpenAI translator
|
|
||||||
# emits the legacy field). The pin on 0.3.60 is intentional
|
|
||||||
# (newer 9Router versions regress WebSearch, see
|
|
||||||
# nine_router.py comment) so we patch the boundary instead
|
|
||||||
# of bumping. Pre-fix: every gpt-5.* / gpt-5.* own-key
|
|
||||||
# session 400'd silently.
|
|
||||||
passthrough_url = f"http://127.0.0.1:{os.environ.get('OPENSWARM_PORT', '8324')}/api/openai-passthrough/v1"
|
passthrough_url = f"http://127.0.0.1:{os.environ.get('OPENSWARM_PORT', '8324')}/api/openai-passthrough/v1"
|
||||||
options_kwargs["env"] = {
|
options_kwargs["env"] = {
|
||||||
"OPENAI_API_KEY": global_settings.openai_api_key,
|
"OPENAI_API_KEY": global_settings.openai_api_key,
|
||||||
@@ -69,9 +57,7 @@ async def configure_provider_env(
|
|||||||
}
|
}
|
||||||
logger.info(f"[MCP-DEBUG] Using direct OpenAI API key (route=api) for {session.model} via openai-passthrough")
|
logger.info(f"[MCP-DEBUG] Using direct OpenAI API key (route=api) for {session.model} via openai-passthrough")
|
||||||
elif is_pinned_api_route and api_route_provider == "custom":
|
elif is_pinned_api_route and api_route_provider == "custom":
|
||||||
# User-configured OpenAI-compatible endpoint (Ollama Cloud,
|
# User OpenAI-compatible endpoint (Ollama/Together/LM Studio) via 9Router's synced provider node.
|
||||||
# Together, local Ollama, etc.). Routes through 9Router's
|
|
||||||
# openai-compatible provider node we synced from settings.
|
|
||||||
from backend.apps.nine_router import ensure_running as p_9r_ensure_c
|
from backend.apps.nine_router import ensure_running as p_9r_ensure_c
|
||||||
if not nine_router_running():
|
if not nine_router_running():
|
||||||
logger.info(f"[MCP-DEBUG] custom provider selected but 9Router not running; waiting for startup")
|
logger.info(f"[MCP-DEBUG] custom provider selected but 9Router not running; waiting for startup")
|
||||||
@@ -90,34 +76,24 @@ async def configure_provider_env(
|
|||||||
"ENABLE_TOOL_SEARCH": "auto",
|
"ENABLE_TOOL_SEARCH": "auto",
|
||||||
}
|
}
|
||||||
if cp:
|
if cp:
|
||||||
# Local OpenAI-compatible servers (LM Studio, Ollama, ...)
|
# Local servers often run auth-disabled; placeholder key since the OpenAI SDK requires non-empty.
|
||||||
# often run with auth disabled, the user leaves api_key
|
|
||||||
# blank in Settings. The OpenAI-style SDK insists on a
|
|
||||||
# non-empty key; substitute a harmless placeholder so the
|
|
||||||
# CLI can issue requests. Servers that DO check auth always
|
|
||||||
# have a real key configured.
|
|
||||||
env["OPENAI_API_KEY"] = (cp.api_key or "").strip() or "no-auth-required"
|
env["OPENAI_API_KEY"] = (cp.api_key or "").strip() or "no-auth-required"
|
||||||
from backend.apps.nine_router import normalize_openai_compat_base_url as norm_cp_url
|
from backend.apps.nine_router import normalize_openai_compat_base_url as norm_cp_url
|
||||||
env["OPENAI_BASE_URL"] = norm_cp_url(cp.base_url or "")
|
env["OPENAI_BASE_URL"] = norm_cp_url(cp.base_url or "")
|
||||||
# Pin subagent ids, without these, CLI's default Haiku 4.5
|
# Pin subagents or CLI's default Haiku 4.5 404s on the custom provider.
|
||||||
# gets sent to the custom provider and 404s.
|
|
||||||
if global_settings.anthropic_api_key:
|
if global_settings.anthropic_api_key:
|
||||||
env["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-sonnet-4-6"
|
env["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-sonnet-4-6"
|
||||||
env["ANTHROPIC_SMALL_FAST_MODEL"] = "claude-haiku-4-5-20251001"
|
env["ANTHROPIC_SMALL_FAST_MODEL"] = "claude-haiku-4-5-20251001"
|
||||||
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = "claude-haiku-4-5-20251001"
|
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = "claude-haiku-4-5-20251001"
|
||||||
else:
|
else:
|
||||||
# Pin to the same custom-provider model so subagents stay
|
# No Anthropic key: pin subagents to the custom model so they stay on the user's endpoint.
|
||||||
# within the user's configured endpoint instead of hitting
|
|
||||||
# an unconfigured Anthropic lane.
|
|
||||||
env["CLAUDE_CODE_SUBAGENT_MODEL"] = resolved_model
|
env["CLAUDE_CODE_SUBAGENT_MODEL"] = resolved_model
|
||||||
env["ANTHROPIC_SMALL_FAST_MODEL"] = resolved_model
|
env["ANTHROPIC_SMALL_FAST_MODEL"] = resolved_model
|
||||||
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = resolved_model
|
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = resolved_model
|
||||||
options_kwargs["env"] = env
|
options_kwargs["env"] = env
|
||||||
logger.info(f"[MCP-DEBUG] Using custom provider for {session.model} → {resolved_model}")
|
logger.info(f"[MCP-DEBUG] Using custom provider for {session.model} → {resolved_model}")
|
||||||
elif is_pinned_api_route and api_route_provider == "gemini" and getattr(global_settings, "google_api_key", None):
|
elif is_pinned_api_route and api_route_provider == "gemini" and getattr(global_settings, "google_api_key", None):
|
||||||
# Routed through the local anthropic-proxy so it can scrub the
|
# Local anthropic-proxy scrubs JSON-Schema fields Gemini rejects ($schema, additionalProperties, propertyNames, exclusiveMinimum, nested const) that 9Router 0.3.60 misses.
|
||||||
# JSON-Schema fields Gemini's API rejects ($schema, additionalProperties,
|
|
||||||
# propertyNames, exclusiveMinimum, nested const) that 9Router 0.3.60 misses.
|
|
||||||
proxy_url = f"http://127.0.0.1:{os.environ.get('OPENSWARM_PORT', '8324')}/api/anthropic-proxy"
|
proxy_url = f"http://127.0.0.1:{os.environ.get('OPENSWARM_PORT', '8324')}/api/anthropic-proxy"
|
||||||
options_kwargs["env"] = {
|
options_kwargs["env"] = {
|
||||||
"GEMINI_API_KEY": global_settings.google_api_key,
|
"GEMINI_API_KEY": global_settings.google_api_key,
|
||||||
@@ -127,12 +103,7 @@ async def configure_provider_env(
|
|||||||
}
|
}
|
||||||
logger.info(f"[MCP-DEBUG] Using direct Google API key (route=api) for {session.model} via local proxy")
|
logger.info(f"[MCP-DEBUG] Using direct Google API key (route=api) for {session.model} via local proxy")
|
||||||
elif api_type == "openrouter" and getattr(global_settings, "openrouter_api_key", None):
|
elif api_type == "openrouter" and getattr(global_settings, "openrouter_api_key", None):
|
||||||
# OpenRouter primary. The route="openrouter" entry's
|
# OpenRouter via 9Router; with no Anthropic key/sub, fall back to OR's resold Claude for subagents (incl. WebSearch delegation) so they stay on the same OR billing.
|
||||||
# router_model_id is `openrouter/<vendor>/<model>` so
|
|
||||||
# 9Router routes via the apikey connection synced from
|
|
||||||
# CLI's WebSearch delegation needs an Anthropic-shaped lane;
|
|
||||||
# if the user has no Anthropic key/sub/Pro, fall back to OR's
|
|
||||||
# resold Claude so subagents stay on the same OR billing.
|
|
||||||
if not nine_router_running():
|
if not nine_router_running():
|
||||||
from backend.apps.nine_router import ensure_running as nine_router_ensure
|
from backend.apps.nine_router import ensure_running as nine_router_ensure
|
||||||
logger.info(f"[MCP-DEBUG] OpenRouter selected but 9Router not running; waiting for startup")
|
logger.info(f"[MCP-DEBUG] OpenRouter selected but 9Router not running; waiting for startup")
|
||||||
@@ -166,33 +137,24 @@ async def configure_provider_env(
|
|||||||
options_kwargs["env"] = {
|
options_kwargs["env"] = {
|
||||||
"ANTHROPIC_AUTH_TOKEN": bearer,
|
"ANTHROPIC_AUTH_TOKEN": bearer,
|
||||||
"ANTHROPIC_BASE_URL": proxy_url,
|
"ANTHROPIC_BASE_URL": proxy_url,
|
||||||
# Pin subagent ids; CLI default 'claude-haiku-4-5-20251001'
|
# Pin subagents; Pro rejects CLI's default haiku as "No credentials for provider: anthropic".
|
||||||
# gets rejected by Pro's surface as "No credentials for provider: anthropic".
|
|
||||||
# (Free-trial clamps to its allowed Claude set + weights credits server-side.)
|
|
||||||
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6",
|
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6",
|
||||||
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5-20251001",
|
"ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-5-20251001",
|
||||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
|
||||||
# auto, never the bare default: tengu_defer_all_bn4 marks every tool
|
# auto: tengu_defer_all_bn4 defers every tool, which collides with cache_control and 400s the first tool-laden request.
|
||||||
# defer_loading=true, which collides with our cache_control and 400s the
|
|
||||||
# first tool-laden request (the other anthropic branches all set this).
|
|
||||||
"ENABLE_TOOL_SEARCH": "auto",
|
"ENABLE_TOOL_SEARCH": "auto",
|
||||||
}
|
}
|
||||||
# Free lane meters one run per agent task: tag every call of this task (and its
|
# Tag every call (subagents inherit env) + aux calls with the task id so a query plus its title-gen meter as ONE free run; base goes straight to cloud (no 9Router) so the header rides.
|
||||||
# subagents, which inherit the env) AND its aux calls (title-gen, see generate_title)
|
|
||||||
# with the session id, so a query plus its title generation is ONE run, not two.
|
|
||||||
# The base goes straight to the cloud (no 9Router), so the header rides through.
|
|
||||||
if getattr(global_settings, "connection_mode", "own_key") == "free-trial":
|
if getattr(global_settings, "connection_mode", "own_key") == "free-trial":
|
||||||
options_kwargs["env"]["ANTHROPIC_CUSTOM_HEADERS"] = f"X-Openswarm-Task-Id: {session.id}"
|
options_kwargs["env"]["ANTHROPIC_CUSTOM_HEADERS"] = f"X-Openswarm-Task-Id: {session.id}"
|
||||||
# The cloud serves every free run as Haiku, so keep the subagent on Haiku too:
|
# Cloud serves every free run as Haiku; keep the subagent on Haiku too (sonnet adds `effort`, Haiku 400s).
|
||||||
# a sonnet subagent makes the CLI attach `effort`, which Haiku 400s on.
|
|
||||||
options_kwargs["env"]["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-haiku-4-5-20251001"
|
options_kwargs["env"]["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-haiku-4-5-20251001"
|
||||||
logger.info(f"[MCP-DEBUG] Using OpenSwarm cloud proxy at {proxy_url}")
|
logger.info(f"[MCP-DEBUG] Using OpenSwarm cloud proxy at {proxy_url}")
|
||||||
elif api_type == "anthropic" and not resolved_is_9router and global_settings.anthropic_api_key:
|
elif api_type == "anthropic" and not resolved_is_9router and global_settings.anthropic_api_key:
|
||||||
options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key}
|
options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key}
|
||||||
logger.info("[MCP-DEBUG] Using direct Anthropic API key")
|
logger.info("[MCP-DEBUG] Using direct Anthropic API key")
|
||||||
elif nine_router_running():
|
elif nine_router_running():
|
||||||
# Gemini-bound ids go through the local proxy for schema scrubbing;
|
# Gemini-bound ids go through the local proxy for schema scrubbing; everything else hits 9Router directly.
|
||||||
# everything else hits 9Router directly.
|
|
||||||
is_gemini_bound = (
|
is_gemini_bound = (
|
||||||
isinstance(resolved_model, str)
|
isinstance(resolved_model, str)
|
||||||
and resolved_model.startswith(("gemini/", "gc/", "ag/"))
|
and resolved_model.startswith(("gemini/", "gc/", "ag/"))
|
||||||
@@ -208,12 +170,7 @@ async def configure_provider_env(
|
|||||||
"ANTHROPIC_API_KEY": "9router",
|
"ANTHROPIC_API_KEY": "9router",
|
||||||
"ANTHROPIC_BASE_URL": "http://localhost:20128",
|
"ANTHROPIC_BASE_URL": "http://localhost:20128",
|
||||||
}
|
}
|
||||||
# Pin subagent ids to whichever lane the user has, else CLI's
|
# Pin subagents to whichever lane the user has, else CLI's default Haiku 4.5 hits 9Router with no Claude route and 401s. NOTE: callers pass sub_conns=[] today so this is inert (latent regression from the run/ split; pyright caught the dangling _conns ref).
|
||||||
# default Haiku 4.5 hits 9Router with no Claude route and 401s.
|
|
||||||
# NOTE: sub_conns is the connection list passed in. Callers currently pass []
|
|
||||||
# (see RunOptions), so `active` is empty and this pinning is inert until the
|
|
||||||
# real connection list is wired through — a latent regression from the run/ split,
|
|
||||||
# surfaced by pyright (the old inline `_conns` reference was left dangling here).
|
|
||||||
active = {c.get("provider") for c in sub_conns
|
active = {c.get("provider") for c in sub_conns
|
||||||
if isinstance(c, dict) and c.get("isActive")}
|
if isinstance(c, dict) and c.get("isActive")}
|
||||||
sub_model = None
|
sub_model = None
|
||||||
@@ -241,11 +198,7 @@ async def configure_provider_env(
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"[MCP-DEBUG] 9Router direct, subagent_model={sub_model}, small_fast={small_model}"
|
f"[MCP-DEBUG] 9Router direct, subagent_model={sub_model}, small_fast={small_model}"
|
||||||
)
|
)
|
||||||
# ENABLE_TOOL_SEARCH=auto: without it, CLI's tengu_defer_all_bn4
|
# auto eagerly loads tools when the schema budget fits; without it tengu_defer_all_bn4 defers 16 tools unloadable off Anthropic networks. Don't use --bare (strips system prompt).
|
||||||
# Statsig flag defers 16 tools with no way to load them on non-
|
|
||||||
# Anthropic networks. "auto" eagerly loads tools when schema
|
|
||||||
# budget fits in ~10% of context. Don't pass --bare, sets
|
|
||||||
# CLAUDE_CODE_SIMPLE=1 which strips the system prompt scaffolding.
|
|
||||||
env["ENABLE_TOOL_SEARCH"] = "auto"
|
env["ENABLE_TOOL_SEARCH"] = "auto"
|
||||||
options_kwargs["env"] = env
|
options_kwargs["env"] = env
|
||||||
logger.info(f"[MCP-DEBUG] Using 9Router (api_type={api_type})")
|
logger.info(f"[MCP-DEBUG] Using 9Router (api_type={api_type})")
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ Python-only rules do not. Specifically:
|
|||||||
- **Single-purpose file naming** — a one-export file is named after its export.
|
- **Single-purpose file naming** — a one-export file is named after its export.
|
||||||
- **Type everything** under strict `tsconfig`; model data with typed interfaces, not untyped
|
- **Type everything** under strict `tsconfig`; model data with typed interfaces, not untyped
|
||||||
objects (the TS equivalent of "no bare dicts").
|
objects (the TS equivalent of "no bare dicts").
|
||||||
|
- **Comments are ONE line each, no exceptions** (`//` — never stack into a multi-line block); keep
|
||||||
|
only WHY/gotcha, delete restating or dead-code comments. JSDoc blocks are exempt.
|
||||||
|
- **No gratuitous blank lines** — never stack 2+ blanks; keep imports tight.
|
||||||
- **Does not apply (Python-only):** `@typechecked`, pydantic `BaseModel`, the no-relative-imports
|
- **Does not apply (Python-only):** `@typechecked`, pydantic `BaseModel`, the no-relative-imports
|
||||||
rule (the frontend uses the `@/` path alias and local relative imports), and `p-private`.
|
rule (the frontend uses the `@/` path alias and local relative imports), and `p-private`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user