mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
Merge branch 'main' of https://github.com/andrewyng/openworker into feature/permission-modes
# Conflicts: # tests/test_openai_responses.py
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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), ""
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(
|
||||
<ProviderMark name="ark" title="BytePlus Ark" />,
|
||||
);
|
||||
expect(container.querySelector("img")).toBeTruthy();
|
||||
|
||||
rerender(
|
||||
<ProviderMark
|
||||
name="ark-agent-plan-cn"
|
||||
title="Volcengine Ark Agent Plan"
|
||||
/>,
|
||||
);
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ export const KEY_HELP: Record<string, { url: string; label: string }> = {
|
||||
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" },
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" shape-rendering="geometricPrecision" viewBox="0 0 27.9 22.01" xmlns="http://www.w3.org/2000/svg"><title>BytePlus</title><path fill="#0066FC" d="M17.196 9.442a.252.252 0 0 1-.419-.201V.612c0-.217-.263-.34-.418-.201L6.042 9.163a.252.252 0 0 1-.419-.201v-5.98a.31.31 0 0 0-.31-.31H.31a.31.31 0 0 0-.31.31v18.73c0 .216.263.34.418.2L10.72 13.16a.252.252 0 0 1 .418.202v8.644c0 .217.264.34.419.201l10.302-8.753a.252.252 0 0 1 .418.202v5.98c0 .17.14.31.31.31h5.003c.17 0 .31-.14.31-.31V.89c0-.217-.263-.341-.418-.202z"></path></svg>
|
||||
|
After Width: | Height: | Size: 543 B |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Volcengine</title><path d="M19.44 10.153l-2.936 11.586a.215.215 0 00.214.261h5.87a.215.215 0 00.214-.261l-2.95-11.586a.214.214 0 00-.412 0zM3.28 12.778l-2.275 8.96A.214.214 0 001.22 22h4.532a.212.212 0 00.214-.165.214.214 0 000-.097l-2.276-8.96a.214.214 0 00-.41 0z" fill="#00E5E5"></path><path d="M7.29 5.359L3.148 21.738a.215.215 0 00.203.261h8.29a.214.214 0 00.215-.261L7.7 5.358a.214.214 0 00-.41 0z" fill="#006EFF"></path><path d="M14.44.15a.214.214 0 00-.41 0L8.366 21.739a.214.214 0 00.214.261H19.9a.216.216 0 00.171-.078.214.214 0 00.044-.183L14.439.15z" fill="#006EFF"></path><path d="M10.278 7.741L6.685 21.736a.214.214 0 00.214.264h7.17a.215.215 0 00.214-.264L10.688 7.741a.214.214 0 00-.41 0z" fill="#00E5E5"></path></svg>
|
||||
|
After Width: | Height: | Size: 859 B |
@@ -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():
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user