diff --git a/coworker/providers/anthropic_provider.py b/coworker/providers/anthropic_provider.py index e01dd33d..0a4ede9c 100644 --- a/coworker/providers/anthropic_provider.py +++ b/coworker/providers/anthropic_provider.py @@ -35,6 +35,12 @@ from .capabilities import capabilities_for # Required by the Messages API; a ceiling, not a spend target. DEFAULT_MAX_TOKENS = 16000 +# Extended thinking is ON by default (owner call 2026-07-23: no user-facing setting — +# most users wouldn't know what a budget is; a per-turn composer control is future work). +# The provider profile's `thinking_budget` remains a hidden override: a number replaces +# the default, 0 disables thinking entirely. +DEFAULT_THINKING_BUDGET = 8192 + # Anthropic stop_reason → the engine's OpenAI-shaped finish_reason vocabulary. _STOP_REASON_MAP = { "end_turn": "stop", diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index ccf13d2a..58aee92b 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -106,11 +106,15 @@ def _build_openai(profile: dict[str, Any], secrets: Any) -> ProviderClient: def _build_anthropic(profile: dict[str, Any], secrets: Any) -> ProviderClient: # Key resolution stays in AnthropicProvider/resolve_api_key (explicit → env → SecretStore), # 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). + from .anthropic_provider import DEFAULT_THINKING_BUDGET + api_key = ((profile or {}).get("api_key") or "").strip() or None try: thinking_budget = int(str((profile or {}).get("thinking_budget") or "").strip()) except ValueError: - thinking_budget = 0 + thinking_budget = DEFAULT_THINKING_BUDGET return AnthropicProvider( api_key=api_key, secrets=secrets, thinking_budget=thinking_budget ) @@ -225,13 +229,8 @@ DESCRIPTORS: list[ProviderDescriptor] = [ secret=True, placeholder="sk-ant-…", ), - ProviderField( - "thinking_budget", - "Extended thinking budget (tokens, optional)", - required=False, - placeholder="e.g. 8192 — blank = off", - help="Turns on Claude's extended thinking for every request, with this token budget. The thought process shows in the transcript.", - ), + # No thinking_budget field (owner call 2026-07-23): extended thinking is + # on by default; the profile key stays a hidden override (0 = off). ], build=_build_anthropic, recommended_model="claude-fable-5", diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index 3aa62025..b8f53743 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -328,7 +328,7 @@ const PROVIDERS = [ // openai: configured + used (drives the "Last used" sub-line and the status dot). { 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-…" }, { key: "thinking_budget", label: "Extended thinking budget (tokens, optional)", secret: false, required: false, help: "Turns on Claude's extended thinking for every request.", placeholder: "e.g. 8192 — blank = off" }], configured: true, values: {}, suggested_models: ["claude-opus-4-8"], key_set_at: null, last_used_at: null }, + { 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 }, // 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/provider-keys.spec.ts b/surfaces/gui/e2e/provider-keys.spec.ts index 934e4d2a..55dbda9a 100644 --- a/surfaces/gui/e2e/provider-keys.spec.ts +++ b/surfaces/gui/e2e/provider-keys.spec.ts @@ -53,20 +53,21 @@ test("a configured provider's form opens with the saved state, no plaintext key" await expect(page.getByTestId("set-field-api_key")).toHaveAttribute("placeholder", "••••••••"); }); -test("non-secret extras blur-save on a configured provider (thinking budget)", async ({ +test("non-secret fields blur-save on a configured provider (ollama endpoint)", async ({ page, }) => { - // Owner-hit 2026-07-23: typed a thinking budget, left Settings, value silently never - // saved — the Test button was the form's only save path. Blur now saves extras. + // Owner-hit 2026-07-23 (as the thinking-budget field, since folded into a default): + // the Test button was the form's only save path — typing into a non-secret field and + // leaving Settings silently discarded it. Blur now saves. await openModels(page); - await page.getByTestId("set-provider-anthropic").click(); - const budget = page.getByTestId("set-field-thinking_budget"); - await budget.fill("8192"); - await budget.blur(); - await expect(page.getByTestId("set-field-saved-thinking_budget")).toBeVisible(); + await page.getByTestId("set-provider-ollama").click(); + const endpoint = page.getByTestId("set-field-base_url"); + await endpoint.fill("http://127.0.0.1:9999"); + await endpoint.blur(); + await expect(page.getByTestId("set-field-saved-base_url")).toBeVisible(); // Leave and come back: the value survived (served from the provider's stored values). await page.getByTestId("set-back").click(); - await page.getByTestId("set-provider-anthropic").click(); - await expect(page.getByTestId("set-field-thinking_budget")).toHaveValue("8192"); + await page.getByTestId("set-provider-ollama").click(); + await expect(page.getByTestId("set-field-base_url")).toHaveValue("http://127.0.0.1:9999"); }); diff --git a/tests/test_anthropic_provider.py b/tests/test_anthropic_provider.py index c6a5d81d..394032fd 100644 --- a/tests/test_anthropic_provider.py +++ b/tests/test_anthropic_provider.py @@ -623,3 +623,14 @@ def test_convert_replays_thinking_blocks_ahead_of_tool_use(): assistant = msgs[1]["content"] assert assistant[0] == {"type": "thinking", "thinking": "plan", "signature": "S"} assert assistant[1]["type"] == "tool_use" + + +def test_thinking_defaults_on_with_hidden_profile_override(): + """No user-facing setting (owner call 2026-07-23): thinking is ON by default; the + profile's thinking_budget stays a hidden override, 0 = off.""" + from coworker.providers.anthropic_provider import DEFAULT_THINKING_BUDGET + from coworker.providers.registry import build_provider_client + + assert build_provider_client("anthropic", {}, None).thinking_budget == DEFAULT_THINKING_BUDGET + assert build_provider_client("anthropic", {"thinking_budget": "2048"}, None).thinking_budget == 2048 + assert build_provider_client("anthropic", {"thinking_budget": "0"}, None).thinking_budget == 0 diff --git a/tests/test_server.py b/tests/test_server.py index 97d5e986..b88c558e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -769,22 +769,13 @@ def test_google_one_click_paused_but_manual_alive(tmp_path): def test_set_provider_persists_extra_fields(tmp_path): - """Non-secret descriptor extras (anthropic thinking_budget) round-trip: saved into the + """Non-secret descriptor extras (ollama's endpoint) round-trip: saved into the profile, echoed by get_providers for form prefill, cleared by an empty save.""" manager = SessionManager(workspace=tmp_path, provider=ScriptedProvider([])) - assert manager.set_provider( - "anthropic", {"api_key": "sk-ant-test", "thinking_budget": "8192"} - )["ok"] + assert manager.set_provider("ollama", {"base_url": "http://127.0.0.1:9999"})["ok"] providers = {p["name"]: p for p in manager.get_providers()} - assert providers["anthropic"]["values"]["thinking_budget"] == "8192" + assert providers["ollama"]["values"]["base_url"] == "http://127.0.0.1:9999" - from coworker.providers.registry import build_provider_client - - built = build_provider_client( - "anthropic", manager.secrets.get("provider:anthropic"), manager.secrets - ) - assert built.thinking_budget == 8192 - - manager.set_provider("anthropic", {"thinking_budget": ""}) + manager.set_provider("ollama", {"base_url": ""}) providers = {p["name"]: p for p in manager.get_providers()} - assert "thinking_budget" not in providers["anthropic"]["values"] + assert "base_url" not in providers["ollama"]["values"]