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/coworker/providers/matrix.py b/coworker/providers/matrix.py
index bf6a878c..f48b6944 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", context_window=256_000
+ ),
+ "ark:dola-seed-2-1-turbo-260628": ModelEntry(
+ "Dola Seed 2.1 Turbo · BytePlus Ark", context_window=256_000
+ ),
+ "ark-agent-plan-cn:doubao-seed-evolving": ModelEntry(
+ "Doubao Seed Evolving · Volcengine Agent Plan", context_window=256_000
+ ),
+ "ark-agent-plan-cn:doubao-seed-2.1-turbo": ModelEntry(
+ "Doubao Seed 2.1 Turbo · Volcengine Agent Plan", context_window=256_000
+ ),
# -- 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/coworker/providers/openai_responses.py b/coworker/providers/openai_responses.py
index 8c673841..1f5a02d8 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
@@ -309,14 +310,20 @@ class OpenAIResponsesProvider(ProviderClient):
default_model: str = "gpt-5.6-sol",
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 →
- # 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
+ 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:
@@ -330,7 +337,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(
@@ -351,9 +361,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/coworker/providers/registry.py b/coworker/providers/registry.py
index 8f2b094b..6c1efb5c 100644
--- a/coworker/providers/registry.py
+++ b/coworker/providers/registry.py
@@ -210,6 +210,37 @@ 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,
+ *,
+ reasoning_summary: bool = True,
+):
+ """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,
+ reasoning_summary=reasoning_summary,
+ )
+
+ return build
+
+
def _compat(
name: str,
title: str,
@@ -248,6 +279,49 @@ def _compat(
)
+def _responses_compat(
+ name: str,
+ title: str,
+ *,
+ base_url: str,
+ 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(
+ 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,
+ 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.",
+ )
+
+
DESCRIPTORS: list[ProviderDescriptor] = [
ProviderDescriptor(
name="openai",
@@ -462,6 +536,26 @@ 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.",
+ reasoning_summary=False,
+ ),
+ _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`).
@@ -814,9 +908,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
@@ -843,6 +939,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/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 }) => {
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" },
diff --git a/surfaces/gui/src/providers/logos.ts b/surfaces/gui/src/providers/logos.ts
index 093c4fdd..f688315d 100644
--- a/surfaces/gui/src/providers/logos.ts
+++ b/surfaces/gui/src/providers/logos.ts
@@ -1,13 +1,15 @@
// 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";
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 @@
+
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 @@
+
diff --git a/tests/test_openai_responses.py b/tests/test_openai_responses.py
index 952232bc..a2e511ff 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 ------------------------------------------------------------------------
@@ -254,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(
@@ -304,6 +341,20 @@ def test_complete_usage_degrades_on_partial_or_missing_fields():
model="m", messages=[{"role": "user", "content": "hi"}]
)
assert turn2.usage is None
+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():
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")
diff --git a/tests/test_providers.py b/tests/test_providers.py
index 943c6e4c..064926db 100644
--- a/tests/test_providers.py
+++ b/tests/test_providers.py
@@ -338,6 +338,121 @@ 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",
+ "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,
+ },
+}
+
+
+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_builder_capabilities_PathsUnchanged(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"],
+ )
+ 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):
+ 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_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