From d0103947c317765d883a23eb69ac286c6db19fec Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 16:53:50 +0800 Subject: [PATCH 01/10] feat: support custom Responses endpoints --- coworker/providers/openai_responses.py | 20 +++++++++----- tests/test_openai_responses.py | 37 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/coworker/providers/openai_responses.py b/coworker/providers/openai_responses.py index 566c620b..5b5ab317 100644 --- a/coworker/providers/openai_responses.py +++ b/coworker/providers/openai_responses.py @@ -1,4 +1,4 @@ -"""OpenAI Responses provider — native OpenAI models via `/v1/responses`. +"""OpenAI Responses provider — native and compatible models via `/responses`. Chat Completions rejects function tools combined with any `reasoning_effort` other than `none` on GPT-5.6+ ("use /v1/responses"), which had reasoning pinned OFF for native OpenAI @@ -8,9 +8,10 @@ reasoning + tools at real effort levels, streamed reasoning summaries (→ the s chain-of-thought continuity across tool round-trips via `store: false` + `include: ["reasoning.encrypted_content"]` — nothing retained server-side. -Routing: the `openai` provider entry with NO custom base_url builds this class; a custom -endpoint (Azure, vLLM, any OpenAI-compatible gateway) and every compat vendor keep the -Chat Completions `OpenAIProvider` (registry.py). +Routing: the `openai` provider entry with NO custom base_url builds this class. Most custom +endpoints (Azure, vLLM, and the existing compat vendors) keep the Chat Completions +`OpenAIProvider`; vendors that explicitly implement the Responses wire can opt into this +class with their own base URL (registry.py). Like the other native providers, this is mostly a pair of pure converters from the canonical OpenAI-chat-shaped history to Responses `input` items. What the converters @@ -289,14 +290,16 @@ class OpenAIResponsesProvider(ProviderClient): default_model: str = "gpt-5.6-sol", api_key: Optional[str] = None, secrets: Any = None, + base_url: Optional[str] = None, ): # Same deferred-client contract as OpenAIProvider: built lazily so an engine can be # assembled before any key exists; key resolves at call time (explicit → env → - # SecretStore). Tests inject a `client` directly. No base_url — a custom endpoint - # routes to the Chat Completions provider instead (registry.py). + # SecretStore). Tests inject a `client` directly. `base_url` is opt-in: stock OpenAI + # leaves it unset, while Responses-compatible vendors can supply their own endpoint. self._client = client self._api_key = api_key self._secrets = secrets + self._base_url = (base_url or "").strip().rstrip("/") or None self.default_model = default_model def _ensure_client(self) -> Any: @@ -310,7 +313,10 @@ class OpenAIResponsesProvider(ProviderClient): "No model API key configured. Set OPENAI_API_KEY in the environment, " "or add your key in Manage → Settings." ) - self._client = OpenAI(api_key=key) + kwargs = {"api_key": key} + if self._base_url: + kwargs["base_url"] = self._base_url + self._client = OpenAI(**kwargs) return self._client def _request_kwargs( diff --git a/tests/test_openai_responses.py b/tests/test_openai_responses.py index 9e871d57..1b375273 100644 --- a/tests/test_openai_responses.py +++ b/tests/test_openai_responses.py @@ -17,6 +17,43 @@ from coworker.providers.openai_responses import ( convert_tools, ) + +def test_responses_custom_base_url_reaches_sdk(monkeypatch): + captured: dict = {} + + def fake_openai(**kwargs): + captured.update(kwargs) + return SimpleNamespace() + + monkeypatch.setattr("openai.OpenAI", fake_openai) + provider = OpenAIResponsesProvider( + api_key="ark-key", + base_url="https://ark.example/api/v3/", + ) + + provider._ensure_client() + + assert captured == { + "api_key": "ark-key", + "base_url": "https://ark.example/api/v3", + } + + +def test_stock_openai_responses_path_unchanged(monkeypatch): + """Lockdown: stock OpenAI must not receive a vendor base URL.""" + captured: dict = {} + + def fake_openai(**kwargs): + captured.update(kwargs) + return SimpleNamespace() + + monkeypatch.setattr("openai.OpenAI", fake_openai) + provider = OpenAIResponsesProvider(api_key="openai-key") + + provider._ensure_client() + + assert captured == {"api_key": "openai-key"} + # -- fakes ------------------------------------------------------------------------ From 2cb6e36b24cccf652145d67f66ae23dc1fab314d Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 16:54:48 +0800 Subject: [PATCH 02/10] feat: register BytePlus and Volcengine Ark providers --- coworker/providers/registry.py | 79 ++++++++++++++++++++++++++++++++++ tests/test_providers.py | 68 +++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index 6c147705..01445564 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -210,6 +210,29 @@ def _openai_compat(vendor: str, default_base_url: str, env_key: Optional[str] = return build +def _openai_responses_compat( + vendor: str, default_base_url: str, env_key: Optional[str] = None +): + """Builder factory for vendors that explicitly implement the OpenAI Responses API. + + Credentials stay isolated to the vendor's own profile/environment variable, matching the + Chat Completions compat path above. In particular, an OpenAI key is never sent to Ark. + """ + + def build(profile: dict[str, Any], secrets: Any) -> ProviderClient: + base_url = ((profile or {}).get("base_url") or "").strip() or default_base_url + api_key = ((profile or {}).get("api_key") or "").strip() or ( + os.environ.get(env_key, "").strip() if env_key else "" + ) + if not api_key: + raise RuntimeError( + f"No {vendor} API key configured — add it in Settings ▸ Models." + ) + return OpenAIResponsesProvider(api_key=api_key, base_url=base_url) + + return build + + def _compat( name: str, title: str, @@ -248,6 +271,43 @@ def _compat( ) +def _responses_compat( + name: str, + title: str, + *, + base_url: str, + recommended_model: str, + env_key: str, + endpoint_help: str = "", +) -> ProviderDescriptor: + """Descriptor for a vendor exposing the OpenAI Responses API.""" + return ProviderDescriptor( + name=name, + title=title, + needs_key=True, + fields=[ + ProviderField( + "api_key", + f"{title} API key", + secret=True, + ), + ProviderField( + "base_url", + "Endpoint", + required=False, + default=base_url, + placeholder=base_url, + help=endpoint_help + or f"Prefilled with {title}'s official Responses endpoint.", + ), + ], + build=_openai_responses_compat(title, base_url, env_key), + recommended_model=recommended_model, + env_key=env_key, + blurb=f"Uses {title}'s OpenAI-compatible Responses API — the endpoint is prefilled, just add your key.", + ) + + DESCRIPTORS: list[ProviderDescriptor] = [ ProviderDescriptor( name="openai", @@ -462,6 +522,25 @@ DESCRIPTORS: list[ProviderDescriptor] = [ blurb="Runs models inside your own Google Cloud project. Gemini and Claude use " "their native APIs; open-weight models go through the Vertex MaaS endpoint.", ), + # Ark has two intentionally separate provider identities. BytePlus pay-as-you-go and + # Volcengine Agent Plan use different regions, endpoints, credentials, and model catalogs; + # combining them would let one provider profile route a model to the wrong service. + _responses_compat( + "ark", + "BytePlus Ark", + base_url="https://ark.ap-southeast.bytepluses.com/api/v3", + recommended_model="dola-seed-evolving-latest-version", + env_key="ARK_API_KEY", + endpoint_help="BytePlus Ark's Asia Pacific endpoint. This provider is separate from Volcengine Ark Agent Plan.", + ), + _responses_compat( + "ark-agent-plan-cn", + "Volcengine Ark Agent Plan", + base_url="https://ark.cn-beijing.volces.com/api/plan/v3", + recommended_model="doubao-seed-evolving", + env_key="ARK_AGENT_PLAN_CN_API_KEY", + endpoint_help="Volcengine Ark Agent Plan's China (Beijing) endpoint. It requires an Agent Plan API key.", + ), # OpenAI-compatible vendors, listed as first-class providers so users don't need to know the # "point the OpenAI slot at a different endpoint" trick (owner call, 2026-07-04). Each keeps # its own key profile; the endpoint is prefilled and editable (regional variants in `help`). diff --git a/tests/test_providers.py b/tests/test_providers.py index 943c6e4c..229d6dbf 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -338,6 +338,74 @@ def test_compat_builder_never_leaks_the_openai_key(monkeypatch): build_provider_client("kimi", {}, None) +ARK_RESPONSES_VENDORS = { + "ark": { + "base_url": "https://ark.ap-southeast.bytepluses.com/api/v3", + "env_key": "ARK_API_KEY", + "recommended_model": "dola-seed-evolving-latest-version", + }, + "ark-agent-plan-cn": { + "base_url": "https://ark.cn-beijing.volces.com/api/plan/v3", + "env_key": "ARK_AGENT_PLAN_CN_API_KEY", + "recommended_model": "doubao-seed-evolving", + }, +} + + +def test_ark_responses_descriptors_are_separate(): + from coworker.providers.registry import get_descriptor + + for name, expected in ARK_RESPONSES_VENDORS.items(): + d = get_descriptor(name) + assert d is not None and d.needs_key, name + assert d.env_key == expected["env_key"] + assert d.recommended_model == expected["recommended_model"] + assert "Responses API" in d.blurb + base = next(f for f in d.fields if f.key == "base_url") + assert base.default == expected["base_url"] + assert not base.required + + +def test_ark_responses_builders_use_their_own_keys_and_endpoints(monkeypatch): + from coworker.providers.openai_responses import OpenAIResponsesProvider + from coworker.providers.registry import build_provider_client + + monkeypatch.setenv("ARK_AGENT_PLAN_CN_API_KEY", "plan-key") + bp = build_provider_client("ark", {"api_key": "bp-key"}, None) + plan = build_provider_client("ark-agent-plan-cn", {}, None) + + assert isinstance(bp, OpenAIResponsesProvider) + assert (bp._api_key, bp._base_url) == ( + "bp-key", + ARK_RESPONSES_VENDORS["ark"]["base_url"], + ) + assert isinstance(plan, OpenAIResponsesProvider) + assert (plan._api_key, plan._base_url) == ( + "plan-key", + ARK_RESPONSES_VENDORS["ark-agent-plan-cn"]["base_url"], + ) + + +def test_ark_responses_never_leak_the_openai_key(monkeypatch): + import pytest + + from coworker.providers.registry import build_provider_client + + monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-real") + monkeypatch.delenv("ARK_API_KEY", raising=False) + with pytest.raises(RuntimeError, match="BytePlus Ark"): + build_provider_client("ark", {}, None) + + +def test_existing_chat_compat_paths_unchanged(): + """Lockdown: adding Responses vendors must not migrate existing compat providers.""" + from coworker.providers.registry import build_provider_client + + provider = build_provider_client("deepseek", {"api_key": "ds-key"}, None) + assert isinstance(provider, OpenAIProvider) + assert provider._base_url == COMPAT_VENDORS["deepseek"] + + def test_compat_models_route_and_get_tool_capabilities(): from coworker.providers.router import ProviderRouter From 3094d37627c2d4221a9c6b12227b3621dbf48382 Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 16:55:29 +0800 Subject: [PATCH 03/10] feat: curate Ark model catalogs --- coworker/providers/matrix.py | 15 +++++++++++++ tests/test_providers.py | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/coworker/providers/matrix.py b/coworker/providers/matrix.py index bf6a878c..8322f3b8 100644 --- a/coworker/providers/matrix.py +++ b/coworker/providers/matrix.py @@ -85,6 +85,21 @@ MATRIX: dict[str, ModelEntry] = { "gemini:gemini-2.5-flash": ModelEntry( "Gemini 2.5 Flash · Google", _AGENTIC_VISION, 1_048_576 ), + # Ark Responses API providers (verified 2026-08-14). BytePlus pay-as-you-go and + # Volcengine Agent Plan intentionally use separate provider prefixes because their + # endpoints, credentials, regions, and model catalogs are not interchangeable. + "ark:dola-seed-evolving-latest-version": ModelEntry( + "Dola Seed Evolving · BytePlus Ark" + ), + "ark:dola-seed-2-1-turbo-260628": ModelEntry( + "Dola Seed 2.1 Turbo · BytePlus Ark" + ), + "ark-agent-plan-cn:doubao-seed-evolving": ModelEntry( + "Doubao Seed Evolving · Volcengine Agent Plan" + ), + "ark-agent-plan-cn:doubao-seed-2.1-turbo": ModelEntry( + "Doubao Seed 2.1 Turbo · Volcengine Agent Plan" + ), # -- direct OpenAI-compatible vendors ---------------------------------------- # Muse Spark (Meta Model API, public preview 2026-07-09): multimodal + tools via # their OpenAI-compat surface. Vision yes; PDFs unverified over compat — falls diff --git a/tests/test_providers.py b/tests/test_providers.py index 229d6dbf..ad886429 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -406,6 +406,47 @@ def test_existing_chat_compat_paths_unchanged(): assert provider._base_url == COMPAT_VENDORS["deepseek"] +def test_ark_curated_models_are_strict_allowlists(): + from coworker.providers.matrix import models_for_provider + + assert models_for_provider("ark") == [ + "dola-seed-evolving-latest-version", + "dola-seed-2-1-turbo-260628", + ] + assert models_for_provider("ark-agent-plan-cn") == [ + "doubao-seed-evolving", + "doubao-seed-2.1-turbo", + ] + + +def test_ark_models_route_and_get_verified_agent_capabilities(): + from coworker.providers.router import ProviderRouter + + models = ( + "ark:dola-seed-evolving-latest-version", + "ark:dola-seed-2-1-turbo-260628", + "ark-agent-plan-cn:doubao-seed-evolving", + "ark-agent-plan-cn:doubao-seed-2.1-turbo", + ) + router = ProviderRouter.__new__(ProviderRouter) + for model in models: + prefix, bare = model.split(":", 1) + assert router._provider_name(model) == prefix + assert ProviderRouter._bare(model) == bare + caps = capabilities_for(model) + assert caps.tools and caps.parallel_tool_calls and caps.streaming + assert not caps.vision + + +def test_ark_recommended_models_are_curated(): + from coworker.providers.matrix import models_for_provider + from coworker.providers.registry import get_descriptor + + for name in ARK_RESPONSES_VENDORS: + d = get_descriptor(name) + assert d.recommended_model in models_for_provider(name) + + def test_compat_models_route_and_get_tool_capabilities(): from coworker.providers.router import ProviderRouter From e9c8aec43e6d970cbacb32160b3ea8a0171e187a Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 16:56:16 +0800 Subject: [PATCH 04/10] feat: verify Ark credentials via Responses --- coworker/providers/registry.py | 24 ++++++++++++-- tests/test_provider_verify.py | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index 01445564..04ed994e 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -882,9 +882,11 @@ def verify_provider_key( fields: Optional[dict[str, Any]] = None, timeout: float = 10.0, ) -> dict[str, Any]: - """Validate a provider's credentials with one cheap, read-only call (list models) — the same - pattern connectors use to validate tokens. Transient: callers pass the key directly so a user - can Test before saving. Never raises; returns {ok, error?}. Multi-field cloud providers + """Validate a provider's credentials with one cheap call — usually list models. + + Ark's Responses-compatible data plane does not document a `/models` probe, so its Test button + sends a non-persisted one-token Responses request instead. Callers pass the key directly so a + user can Test before saving. Never raises; returns {ok, error?}. Multi-field cloud providers (Bedrock, Vertex) take their whole form via `fields`; everyone else uses api_key/base_url. """ import httpx @@ -911,6 +913,22 @@ def verify_provider_key( elif name == "ollama": base = _normalize_ollama_url(base_url) resp = httpx.get(base.rstrip("/") + "/models", timeout=timeout) + elif name in ("ark", "ark-agent-plan-cn"): + default_base = next( + (f.default for f in d.fields if f.key == "base_url" and f.default), "" + ) + base = (base_url or "").strip().rstrip("/") or default_base.rstrip("/") + resp = httpx.post( + base + "/responses", + headers={"Authorization": f"Bearer {key}"}, + json={ + "model": d.recommended_model, + "input": "Reply with OK.", + "max_output_tokens": 1, + "store": False, + }, + timeout=timeout, + ) else: # openai + any OpenAI-compatible endpoint (Azure, OpenRouter, vendors, vLLM…) default_base = next( (f.default for f in d.fields if f.key == "base_url" and f.default), "" diff --git a/tests/test_provider_verify.py b/tests/test_provider_verify.py index 8da7fe76..a6e553af 100644 --- a/tests/test_provider_verify.py +++ b/tests/test_provider_verify.py @@ -41,6 +41,18 @@ def _patch_get(monkeypatch, status=200, capture=None, raise_exc=None): monkeypatch.setattr("httpx.get", fake_get) +def _patch_post(monkeypatch, status=200, capture=None, raise_exc=None): + def fake_post(url, **kwargs): + if capture is not None: + capture["url"] = url + capture.update(kwargs) + if raise_exc is not None: + raise raise_exc + return SimpleNamespace(status_code=status) + + monkeypatch.setattr("httpx.post", fake_post) + + def test_verify_openai_ok(monkeypatch): cap: dict = {} _patch_get(monkeypatch, status=200, capture=cap) @@ -91,6 +103,52 @@ def test_verify_ollama_uses_v1_models_no_key(monkeypatch): assert "headers" not in cap # keyless +@pytest.mark.parametrize( + "name,base_url,model", + [ + ( + "ark", + "https://ark.ap-southeast.bytepluses.com/api/v3", + "dola-seed-evolving-latest-version", + ), + ( + "ark-agent-plan-cn", + "https://ark.cn-beijing.volces.com/api/plan/v3", + "doubao-seed-evolving", + ), + ], +) +def test_verify_ark_uses_non_persisted_responses_probe( + monkeypatch, name, base_url, model +): + """Reverse-verified probe: the captured fixture must be non-empty and provider-specific.""" + cap: dict = {} + _patch_post(monkeypatch, status=200, capture=cap) + + assert verify_provider_key(name, api_key="ark-key") == {"ok": True} + assert cap["url"] == base_url + "/responses" + assert cap["headers"]["Authorization"] == "Bearer ark-key" + assert cap["json"] == { + "model": model, + "input": "Reply with OK.", + "max_output_tokens": 1, + "store": False, + } + + +def test_verify_ark_profile_endpoint_override(monkeypatch): + cap: dict = {} + _patch_post(monkeypatch, status=200, capture=cap) + + verify_provider_key( + "ark", + api_key="ark-key", + base_url="https://gateway.example/ark/v3/", + ) + + assert cap["url"] == "https://gateway.example/ark/v3/responses" + + def test_verify_network_error_is_clean(monkeypatch): _patch_get(monkeypatch, raise_exc=ConnectionError("boom")) res = verify_provider_key("openai", api_key="sk-x") From e74bbf1b1ada75850831f0b5a43d810e8659e13c Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 16:57:34 +0800 Subject: [PATCH 05/10] feat: add Ark provider branding --- surfaces/gui/src/providers/logos.ts | 6 ++++++ surfaces/gui/src/providers/logos/byteplus.svg | 1 + surfaces/gui/src/providers/logos/volcengine.svg | 1 + 3 files changed, 8 insertions(+) create mode 100644 surfaces/gui/src/providers/logos/byteplus.svg create mode 100644 surfaces/gui/src/providers/logos/volcengine.svg diff --git a/surfaces/gui/src/providers/logos.ts b/surfaces/gui/src/providers/logos.ts index 093c4fdd..028e0b3b 100644 --- a/surfaces/gui/src/providers/logos.ts +++ b/surfaces/gui/src/providers/logos.ts @@ -8,6 +8,8 @@ import anthropic from "./logos/anthropic.svg"; import openai from "./logos/openai.svg"; import gemini from "./logos/gemini.svg"; +import byteplus from "./logos/byteplus.svg"; +import volcengine from "./logos/volcengine.svg"; import ollama from "./logos/ollama.svg"; import bedrock from "./logos/bedrock.svg"; import vertex from "./logos/vertex.svg"; @@ -27,6 +29,8 @@ export const PROVIDER_LOGOS: Record = { anthropic, openai, gemini, + ark: byteplus, + "ark-agent-plan-cn": volcengine, meta, ollama, bedrock, @@ -47,6 +51,8 @@ export const PROVIDER_ORDER = [ "anthropic", "openai", "gemini", + "ark", + "ark-agent-plan-cn", "meta", "ollama", "bedrock", diff --git a/surfaces/gui/src/providers/logos/byteplus.svg b/surfaces/gui/src/providers/logos/byteplus.svg new file mode 100644 index 00000000..b2d64e33 --- /dev/null +++ b/surfaces/gui/src/providers/logos/byteplus.svg @@ -0,0 +1 @@ +BytePlus diff --git a/surfaces/gui/src/providers/logos/volcengine.svg b/surfaces/gui/src/providers/logos/volcengine.svg new file mode 100644 index 00000000..1c944722 --- /dev/null +++ b/surfaces/gui/src/providers/logos/volcengine.svg @@ -0,0 +1 @@ +Volcengine From 4bfd75a4df9a8a3b1adc3932867a034495e1791c Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 16:58:53 +0800 Subject: [PATCH 06/10] feat: add Ark provider setup links --- .../gui/src/providers/ProviderSetup.test.tsx | 28 ++++++++++++++++++- surfaces/gui/src/providers/ProviderSetup.tsx | 2 ++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/surfaces/gui/src/providers/ProviderSetup.test.tsx b/surfaces/gui/src/providers/ProviderSetup.test.tsx index 870aa7a0..89d0e8d2 100644 --- a/surfaces/gui/src/providers/ProviderSetup.test.tsx +++ b/surfaces/gui/src/providers/ProviderSetup.test.tsx @@ -2,7 +2,7 @@ // only the selected method's fields render, and clicking a segment switches them. import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { ProviderForm, type ProviderSetupState } from "./ProviderSetup"; +import { KEY_HELP, ProviderForm, ProviderMark, type ProviderSetupState } from "./ProviderSetup"; import type { ProviderInfo } from "../api"; vi.mock("../tauri", () => ({ openExternal: vi.fn() })); @@ -94,3 +94,29 @@ describe("ProviderForm auth-method choice", () => { expect(screen.queryByTestId("t-field-bedrock_api_key")).toBeNull(); }); }); + +describe("Ark provider presentation", () => { + it("uses separate BytePlus and Volcengine brand marks", () => { + const { container, rerender } = render( + , + ); + expect(container.querySelector("img")).toBeTruthy(); + + rerender( + , + ); + expect(container.querySelector("img")).toBeTruthy(); + }); + + it("links each provider to its own API key console", () => { + expect(KEY_HELP.ark.url).toBe( + "https://console.byteplus.com/ark/region:ark+ap-southeast-1/apiKey", + ); + expect(KEY_HELP["ark-agent-plan-cn"].url).toContain( + "advancedActiveKey=agentPlan", + ); + }); +}); diff --git a/surfaces/gui/src/providers/ProviderSetup.tsx b/surfaces/gui/src/providers/ProviderSetup.tsx index 1d8dc881..99aeed53 100644 --- a/surfaces/gui/src/providers/ProviderSetup.tsx +++ b/surfaces/gui/src/providers/ProviderSetup.tsx @@ -21,6 +21,8 @@ export const KEY_HELP: Record = { anthropic: { url: "https://console.anthropic.com/settings/keys", label: "console.anthropic.com" }, openai: { url: "https://platform.openai.com/api-keys", label: "platform.openai.com" }, gemini: { url: "https://aistudio.google.com/apikey", label: "aistudio.google.com" }, + ark: { url: "https://console.byteplus.com/ark/region:ark+ap-southeast-1/apiKey", label: "console.byteplus.com" }, + "ark-agent-plan-cn": { url: "https://console.volcengine.com/ark/region:cn-beijing/openManagement?LLM=%7B%7D&advancedActiveKey=agentPlan", label: "console.volcengine.com" }, openrouter: { url: "https://openrouter.ai/keys", label: "openrouter.ai" }, bedrock: { url: "https://console.aws.amazon.com/bedrock/home#/api-keys", label: "the AWS Bedrock console" }, fireworks: { url: "https://fireworks.ai/account/api-keys", label: "fireworks.ai" }, From 68c7914b80eae0b25fb1aadcb24e276c8df744d2 Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 17:01:25 +0800 Subject: [PATCH 07/10] test: cover Ark provider setup end to end --- surfaces/gui/e2e/fixtures.ts | 8 +++++++ surfaces/gui/e2e/settings.spec.ts | 38 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index e6dfa409..ec4deba7 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -45,6 +45,10 @@ const SETTINGS = { model_labels: { "anthropic:claude-opus-4-8": "Claude Opus 4.8 · Anthropic", "zai:glm-5.2": "GLM-5.2 · Z AI", + "ark:dola-seed-evolving-latest-version": "Dola Seed Evolving · BytePlus Ark", + "ark:dola-seed-2-1-turbo-260628": "Dola Seed 2.1 Turbo · BytePlus Ark", + "ark-agent-plan-cn:doubao-seed-evolving": "Doubao Seed Evolving · Volcengine Agent Plan", + "ark-agent-plan-cn:doubao-seed-2.1-turbo": "Doubao Seed 2.1 Turbo · Volcengine Agent Plan", }, // Context windows (subset — mirrors /v1/settings.model_context_windows); drives the // composer usage chip's context-fill meter. @@ -336,6 +340,10 @@ const PROVIDERS = [ { 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 }, + // Ark uses two provider identities: BytePlus pay-as-you-go and Volcengine Agent Plan CN + // have independent credentials, endpoints, and strict curated model lists. + { name: "ark", title: "BytePlus Ark", needs_key: true, blurb: "Uses BytePlus Ark's OpenAI-compatible Responses API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "BytePlus Ark API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "BytePlus Ark's Asia Pacific endpoint.", placeholder: "https://ark.ap-southeast.bytepluses.com/api/v3", default: "https://ark.ap-southeast.bytepluses.com/api/v3" }], configured: false, values: {}, suggested_models: ["dola-seed-evolving-latest-version", "dola-seed-2-1-turbo-260628"], key_set_at: null, last_used_at: null }, + { name: "ark-agent-plan-cn", title: "Volcengine Ark Agent Plan", needs_key: true, blurb: "Uses Volcengine Ark Agent Plan's OpenAI-compatible Responses API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "Volcengine Ark Agent Plan API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "Volcengine Ark Agent Plan's China (Beijing) endpoint.", placeholder: "https://ark.cn-beijing.volces.com/api/plan/v3", default: "https://ark.cn-beijing.volces.com/api/plan/v3" }], configured: false, values: {}, suggested_models: ["doubao-seed-evolving", "doubao-seed-2.1-turbo"], key_set_at: null, last_used_at: null }, // ollama: keyless local provider — "configured" without proving anything runs; the // onboarding gallery shows "No key needed" and its form is endpoint + Detect (§39). { name: "ollama", title: "Ollama (local models)", needs_key: false, fields: [{ key: "base_url", label: "Endpoint", secret: false, required: false, help: "", placeholder: "http://127.0.0.1:11434", default: "http://127.0.0.1:11434" }], configured: true, values: {}, suggested_models: ["qwen3-coder:30b"], key_set_at: null, last_used_at: null }, diff --git a/surfaces/gui/e2e/settings.spec.ts b/surfaces/gui/e2e/settings.spec.ts index 1bee81fa..1d1b6a5e 100644 --- a/surfaces/gui/e2e/settings.spec.ts +++ b/surfaces/gui/e2e/settings.spec.ts @@ -71,6 +71,44 @@ test("Models: provider gallery states; vendor form previews models", async ({ pa await expect(page.getByTestId("set-provider-openai")).toBeVisible(); }); +test("Models: BytePlus and Volcengine Ark stay visually and operationally separate", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("account-row").click(); + await page.getByRole("button", { name: "Settings", exact: true }).click(); + await page.getByRole("button", { name: "Models", exact: true }).click(); + + const byteplusCard = page.getByTestId("set-provider-ark"); + const volcengineCard = page.getByTestId("set-provider-ark-agent-plan-cn"); + await expect(byteplusCard).toContainText("BytePlus Ark"); + await expect(volcengineCard).toContainText("Volcengine Ark Agent Plan"); + const byteplusLogo = await byteplusCard.locator("img").getAttribute("src"); + const volcengineLogo = await volcengineCard.locator("img").getAttribute("src"); + expect(byteplusLogo).toBeTruthy(); + expect(volcengineLogo).toBeTruthy(); + expect(byteplusLogo).not.toBe(volcengineLogo); + + await byteplusCard.click(); + await page.getByTestId("set-endpoint-link").click(); + await expect(page.getByTestId("set-field-base_url")).toHaveValue( + "https://ark.ap-southeast.bytepluses.com/api/v3", + ); + let preview = page.getByTestId("model-preview"); + await expect(preview).toContainText("Dola Seed Evolving · BytePlus Ark"); + await expect(preview).toContainText("Dola Seed 2.1 Turbo · BytePlus Ark"); + await expect(preview).not.toContainText("Doubao Seed"); + + await page.getByTestId("set-back").click(); + await volcengineCard.click(); + await page.getByTestId("set-endpoint-link").click(); + await expect(page.getByTestId("set-field-base_url")).toHaveValue( + "https://ark.cn-beijing.volces.com/api/plan/v3", + ); + preview = page.getByTestId("model-preview"); + await expect(preview).toContainText("Doubao Seed Evolving · Volcengine Agent Plan"); + await expect(preview).toContainText("Doubao Seed 2.1 Turbo · Volcengine Agent Plan"); + await expect(preview).not.toContainText("Dola Seed"); +}); + // UX-021: a configured provider's form shows the in-field saved state and the Remove key… // affordance; removing reverts the card to "Not set up". test("Models: Remove key reverts a configured provider", async ({ page }) => { From 158b45ee50db6cf9a8625d414fd5f073827cce6d Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 17:01:50 +0800 Subject: [PATCH 08/10] docs: document Ark provider support --- README.md | 2 +- surfaces/gui/src/providers/logos.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b32a299d..72547b42 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,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 · BytePlus Ark · Volcengine Ark Agent Plan · 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**. 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/surfaces/gui/src/providers/logos.ts b/surfaces/gui/src/providers/logos.ts index 028e0b3b..f688315d 100644 --- a/surfaces/gui/src/providers/logos.ts +++ b/surfaces/gui/src/providers/logos.ts @@ -1,9 +1,9 @@ // Provider logo registry (UX-DECISIONS §39): official brand marks for the onboarding -// provider gallery, vendored from the MIT-licensed lobe-icons set (same -// bundled-asset posture as the connector registry — no CDN at runtime). Keys are -// /v1/providers names; unknown names get no mark (the gallery falls back to a -// neutral monogram). PROVIDER_ORDER is the gallery order — recognition first, -// long tail behind the scroll fold. +// provider gallery. Most are vendored from the MIT-licensed lobe-icons set; BytePlus is +// its official website mark, used with permission. All stay bundled like connector assets +// (no CDN at runtime). Keys are /v1/providers names; unknown names get no mark (the gallery +// falls back to a neutral monogram). PROVIDER_ORDER is the gallery order — recognition +// first, long tail behind the scroll fold. import anthropic from "./logos/anthropic.svg"; import openai from "./logos/openai.svg"; From 557723bf64eccf019027bccbe61f07a31dde8138 Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 17:56:51 +0800 Subject: [PATCH 09/10] fix: make Responses reasoning summaries configurable --- coworker/providers/openai_responses.py | 7 ++++++- tests/test_openai_responses.py | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/coworker/providers/openai_responses.py b/coworker/providers/openai_responses.py index 5b5ab317..9d34f84a 100644 --- a/coworker/providers/openai_responses.py +++ b/coworker/providers/openai_responses.py @@ -291,6 +291,7 @@ class OpenAIResponsesProvider(ProviderClient): api_key: Optional[str] = None, secrets: Any = None, base_url: Optional[str] = None, + reasoning_summary: bool = True, ): # Same deferred-client contract as OpenAIProvider: built lazily so an engine can be # assembled before any key exists; key resolves at call time (explicit → env → @@ -300,6 +301,9 @@ class OpenAIResponsesProvider(ProviderClient): self._api_key = api_key self._secrets = secrets self._base_url = (base_url or "").strip().rstrip("/") or None + if not isinstance(reasoning_summary, bool): + raise TypeError("reasoning_summary must be a bool") + self._reasoning_summary = reasoning_summary self.default_model = default_model def _ensure_client(self) -> Any: @@ -337,9 +341,10 @@ class OpenAIResponsesProvider(ProviderClient): # `_openai` sidecar instead, and summaries feed the GUI's thinking display. "store": False, "include": ["reasoning.encrypted_content"], - "reasoning": {"summary": "auto"}, **{k: v for k, v in settings.items() if k in _SETTINGS_WHITELIST}, } + if self._reasoning_summary: + kwargs["reasoning"] = {"summary": "auto"} if instructions: kwargs["instructions"] = instructions if tools: diff --git a/tests/test_openai_responses.py b/tests/test_openai_responses.py index 1b375273..64edfc90 100644 --- a/tests/test_openai_responses.py +++ b/tests/test_openai_responses.py @@ -291,7 +291,7 @@ def test_convert_tools_flattens_function_schemas(): # -- complete() ---------------------------------------------------------------------- -def test_complete_text_turn_and_request_shape(): +def test_complete_default_request_shape_PathsUnchanged(): fake = _FakeClient(response=_response([_message_item("hello")])) provider = OpenAIResponsesProvider(client=fake) turn = provider.complete( @@ -310,6 +310,22 @@ def test_complete_text_turn_and_request_shape(): assert fake.kwargs["reasoning"] == {"summary": "auto"} +def test_complete_can_omit_reasoning_summary_but_keep_encrypted_content(): + """BytePlus accepts encrypted reasoning output but rejects reasoning.summary.""" + fake = _FakeClient(response=_response([_message_item("hello")])) + provider = OpenAIResponsesProvider(client=fake, reasoning_summary=False) + + provider.complete(model="m", messages=[{"role": "user", "content": "hi"}]) + + assert "reasoning" not in fake.kwargs + assert fake.kwargs["include"] == ["reasoning.encrypted_content"] + + +def test_reasoning_summary_capability_rejects_unknown_mode(): + with pytest.raises(TypeError, match="reasoning_summary must be a bool"): + OpenAIResponsesProvider(client=SimpleNamespace(), reasoning_summary="auto") + + def test_complete_parses_function_calls_with_call_ids(): fake = _FakeClient( response=_response( From 19bbbbd7ad3ef06825a0ea74206e48bc7aaffa1c Mon Sep 17 00:00:00 2001 From: fanziqing Date: Fri, 14 Aug 2026 17:58:38 +0800 Subject: [PATCH 10/10] fix: omit reasoning summaries for BytePlus Ark --- coworker/providers/registry.py | 21 ++++++++++++++++++--- tests/test_providers.py | 8 +++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/coworker/providers/registry.py b/coworker/providers/registry.py index 04ed994e..1f7aea8e 100644 --- a/coworker/providers/registry.py +++ b/coworker/providers/registry.py @@ -211,7 +211,11 @@ def _openai_compat(vendor: str, default_base_url: str, env_key: Optional[str] = def _openai_responses_compat( - vendor: str, default_base_url: str, env_key: Optional[str] = None + vendor: str, + default_base_url: str, + env_key: Optional[str] = None, + *, + reasoning_summary: bool = True, ): """Builder factory for vendors that explicitly implement the OpenAI Responses API. @@ -228,7 +232,11 @@ def _openai_responses_compat( raise RuntimeError( f"No {vendor} API key configured — add it in Settings ▸ Models." ) - return OpenAIResponsesProvider(api_key=api_key, base_url=base_url) + return OpenAIResponsesProvider( + api_key=api_key, + base_url=base_url, + reasoning_summary=reasoning_summary, + ) return build @@ -279,6 +287,7 @@ def _responses_compat( recommended_model: str, env_key: str, endpoint_help: str = "", + reasoning_summary: bool = True, ) -> ProviderDescriptor: """Descriptor for a vendor exposing the OpenAI Responses API.""" return ProviderDescriptor( @@ -301,7 +310,12 @@ def _responses_compat( or f"Prefilled with {title}'s official Responses endpoint.", ), ], - build=_openai_responses_compat(title, base_url, env_key), + build=_openai_responses_compat( + title, + base_url, + env_key, + reasoning_summary=reasoning_summary, + ), recommended_model=recommended_model, env_key=env_key, blurb=f"Uses {title}'s OpenAI-compatible Responses API — the endpoint is prefilled, just add your key.", @@ -532,6 +546,7 @@ DESCRIPTORS: list[ProviderDescriptor] = [ recommended_model="dola-seed-evolving-latest-version", env_key="ARK_API_KEY", endpoint_help="BytePlus Ark's Asia Pacific endpoint. This provider is separate from Volcengine Ark Agent Plan.", + reasoning_summary=False, ), _responses_compat( "ark-agent-plan-cn", diff --git a/tests/test_providers.py b/tests/test_providers.py index ad886429..064926db 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -343,11 +343,13 @@ ARK_RESPONSES_VENDORS = { "base_url": "https://ark.ap-southeast.bytepluses.com/api/v3", "env_key": "ARK_API_KEY", "recommended_model": "dola-seed-evolving-latest-version", + "reasoning_summary": False, }, "ark-agent-plan-cn": { "base_url": "https://ark.cn-beijing.volces.com/api/plan/v3", "env_key": "ARK_AGENT_PLAN_CN_API_KEY", "recommended_model": "doubao-seed-evolving", + "reasoning_summary": True, }, } @@ -366,7 +368,7 @@ def test_ark_responses_descriptors_are_separate(): assert not base.required -def test_ark_responses_builders_use_their_own_keys_and_endpoints(monkeypatch): +def test_ark_responses_builder_capabilities_PathsUnchanged(monkeypatch): from coworker.providers.openai_responses import OpenAIResponsesProvider from coworker.providers.registry import build_provider_client @@ -384,6 +386,10 @@ def test_ark_responses_builders_use_their_own_keys_and_endpoints(monkeypatch): "plan-key", ARK_RESPONSES_VENDORS["ark-agent-plan-cn"]["base_url"], ) + assert bp._reasoning_summary is ARK_RESPONSES_VENDORS["ark"]["reasoning_summary"] + assert plan._reasoning_summary is ARK_RESPONSES_VENDORS["ark-agent-plan-cn"][ + "reasoning_summary" + ] def test_ark_responses_never_leak_the_openai_key(monkeypatch):