From b967e4f6849b0b237acc898260f2200088b40cf5 Mon Sep 17 00:00:00 2001 From: Mick Huang Date: Mon, 27 Jul 2026 09:55:12 +0800 Subject: [PATCH 1/3] providers: make Anthropic endpoint configurable via base_url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the OpenAIProvider pattern: the constructor accepts an optional `base_url` and the lazy SDK client forwards it to `Anthropic(**kwargs)`. When unset, behavior is identical to stock Anthropic (api.anthropic.com). This unlocks Anthropic-protocol-compatible gateways (third-party Claude or MiniMax endpoints) without forking the provider class. The change is strictly additive — no model or call-payload semantics shift. --- coworker/providers/anthropic_provider.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coworker/providers/anthropic_provider.py b/coworker/providers/anthropic_provider.py index 579be5bb..84965bd9 100644 --- a/coworker/providers/anthropic_provider.py +++ b/coworker/providers/anthropic_provider.py @@ -345,6 +345,7 @@ def __init__( *, default_model: str = "claude-sonnet-4-6", api_key: Optional[str] = None, + base_url: Optional[str] = None, secrets: Any = None, thinking_budget: Optional[int] = None, ): @@ -352,8 +353,12 @@ def __init__( # before any key exists; the key resolves at call time (explicit → env → SecretStore). # Tests inject a `client` directly. `thinking_budget` (tokens, from the provider # profile's optional field) opts every request into extended thinking. + # `base_url` points the same SDK at any Anthropic-protocol-compatible gateway (e.g. + # a third-party Claude/MiniMax endpoint at https://api./anthropic). When None, + # behavior is identical to stock Anthropic (api.anthropic.com). self._client = client self._api_key = api_key + self._base_url = base_url self._secrets = secrets self.default_model = default_model self.thinking_budget = thinking_budget or 0 @@ -369,7 +374,10 @@ def _ensure_client(self) -> Any: "No Anthropic API key configured. Set ANTHROPIC_API_KEY in the environment, " "or add your key in Manage → Configure Models." ) - self._client = Anthropic(api_key=key) + kwargs: dict[str, Any] = {"api_key": key} + if self._base_url: + kwargs["base_url"] = self._base_url + self._client = Anthropic(**kwargs) return self._client def _request_kwargs( From d0eaa811460267b64226a443b27c48b50a6256d1 Mon Sep 17 00:00:00 2001 From: Mick Huang Date: Mon, 27 Jul 2026 09:55:20 +0800 Subject: [PATCH 2/3] providers: plumb anthropic base_url through descriptor, _build, and verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three coordinated changes so the new AnthropicProvider.base_url is actually reachable end-to-end from the GUI: 1. `anthropic` ProviderDescriptor gains an optional `base_url` field with a stock-api.anthropic.com placeholder, so Settings renders the input. 2. `_build_anthropic` reads `base_url` from the SecretStore profile and forwards it to AnthropicProvider, matching the OpenAIProvider pattern. 3. `verify_provider_key`'s anthropic branch stops hardcoding api.anthropic.com and instead probes the user-configured endpoint so Settings → Test can validate a third-party gateway. Untouched: gemini, ollama, openai branches (already configurable) and the OpenAI-compatible vendor slots (already honor user-edited base_url). --- coworker/providers/registry.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index b5c628c6..8428fff0 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -108,15 +108,21 @@ def _build_anthropic(profile: dict[str, Any], secrets: Any) -> ProviderClient: # deferred to first call so the provider can be built before a key exists. # thinking_budget: hidden profile override — absent/invalid → the default (ON), # explicit 0 → off (see DEFAULT_THINKING_BUDGET). + # base_url (optional): points the same Anthropic SDK at any Anthropic-protocol gateway + # (e.g. a third-party Claude/MiniMax endpoint). None → stock api.anthropic.com. from .anthropic_provider import DEFAULT_THINKING_BUDGET api_key = ((profile or {}).get("api_key") or "").strip() or None + base_url = ((profile or {}).get("base_url") or "").strip() or None try: thinking_budget = int(str((profile or {}).get("thinking_budget") or "").strip()) except ValueError: thinking_budget = DEFAULT_THINKING_BUDGET return AnthropicProvider( - api_key=api_key, secrets=secrets, thinking_budget=thinking_budget + api_key=api_key, + base_url=base_url, + secrets=secrets, + thinking_budget=thinking_budget, ) @@ -229,6 +235,14 @@ def _compat( secret=True, placeholder="sk-ant-…", ), + ProviderField( + "base_url", + "Endpoint (optional)", + secret=False, + required=False, + placeholder="https://api.anthropic.com", + help="Leave blank for api.anthropic.com, or point at any Anthropic-protocol-compatible gateway (e.g. a third-party Claude/MiniMax endpoint).", + ), # No thinking_budget field (owner call 2026-07-23): extended thinking is # on by default; the profile key stays a hidden override (0 = off). ], @@ -410,8 +424,13 @@ def verify_provider_key( key = (api_key or "").strip() try: if name == "anthropic": + # Honor a custom `base_url` so Settings → Test can probe third-party + # Anthropic-protocol gateways (e.g. a MiniMax-Claude endpoint). Strip a + # trailing slash and rely on the SDK's /v1 path default; fall back to + # stock api.anthropic.com when no override is set. + base = ((base_url or "").strip().rstrip("/") or "https://api.anthropic.com") resp = httpx.get( - "https://api.anthropic.com/v1/models", + base + "/v1/models", headers={"x-api-key": key, "anthropic-version": "2023-06-01"}, timeout=timeout, ) From c4ad00a9d75957f904965c924bfe675a67481143 Mon Sep 17 00:00:00 2001 From: Mick Huang Date: Mon, 27 Jul 2026 09:55:26 +0800 Subject: [PATCH 3/3] providers: surface MiniMax-M3 in the curated model matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'add model' suggestion list (COMPAT_MODELS in server/manager.py) already lists `MiniMax-M3`, but the picker wouldn't show it because the curated matrix was the source of truth for label + capability and had no row for it. Add `minimax:MiniMax-M3` next to `MiniMax-M2.5` with the same agentic capabilities (tools + parallel + streaming; vision left off, matching M2.5). Capabilities stay symmetric with M2.5 on purpose — until M3's vision/PDF support is probed via a real probe, the matrix should not advertise features that would degrade gracefully into pdf_support fallback at runtime. --- coworker/providers/matrix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coworker/providers/matrix.py b/coworker/providers/matrix.py index 9b40cacf..548c74bf 100644 --- a/coworker/providers/matrix.py +++ b/coworker/providers/matrix.py @@ -85,6 +85,7 @@ class ModelEntry: "deepseek:deepseek-v4-pro": ModelEntry("DeepSeek V4 Pro · DeepSeek"), "kimi:kimi-k2.6": ModelEntry("Kimi K2.6 · Moonshot"), "minimax:MiniMax-M2.5": ModelEntry("MiniMax M2.5 · MiniMax"), + "minimax:MiniMax-M3": ModelEntry("MiniMax M3 · MiniMax"), "qwen:qwen3-max": ModelEntry("Qwen3 Max · Alibaba"), "xai:grok-4.3": ModelEntry("Grok 4.3 · xAI"), "mistral:mistral-large-latest": ModelEntry("Mistral Large · Mistral"),