Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,23 @@ def anthropic_prompt_cache_policy(
if is_minimax_provider or is_minimax_host:
return True, True

# Volcengine Ark (火山引擎) on its Anthropic-compatible /api/coding
# endpoint serves non-Claude model families (deepseek-v4, glm-5.2,
# doubao-seed, kimi-k2, minimax-m3), so the blanket is_claude gate above
# excludes them. Ark honors Anthropic cache_control on /api/coding and
# reports hits via cache_read_input_tokens. Opt these in explicitly via
# provider id or host so users get the same cost reduction as Claude
# traffic; without this branch Ark serves 0% cache hits and re-bills the
# full prompt + history on every turn, burning the subscription quota.
# Caching is per-model (empirically ~8/11 models, prefix-size thresholds
# ~4k–48k) and probabilistic — emitting markers is always safe; models
# that don't cache simply ignore them.
if is_anthropic_wire:
is_ark_provider = provider_lower in {"volcengine-ark", "ark", "volcengine"}
is_ark_host = base_url_host_matches(eff_base_url, "ark.cn-beijing.volces.com")
if is_ark_provider or is_ark_host:
return True, True

# Qwen/Alibaba on OpenCode (Zen/Go) and native DashScope: OpenAI-wire
# transport that accepts Anthropic-style cache_control markers and
# rewards them with real cache hits. Without this branch
Expand Down
102 changes: 102 additions & 0 deletions plugins/model-providers/volcengine-ark/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Volcengine Ark (火山引擎) — ByteDance unified model platform.

Ark is ByteDance's API gateway that gives access to models from multiple
vendors (doubao-seed, deepseek-v4, glm-5.2, kimi-k2, minimax-m3) through
a single API key.

The platform provides two subscription plans:
- Agent Plan: General-purpose AI agent usage, AFP billing, multi-modal
- Coding Plan: Coding-focused, token quotas, AI coding tools only

Both plans share the same Anthropic-compatible /api/coding endpoint.

IMPORTANT — model list maintenance:
Ark's /models endpoint returns 124+ stale model IDs from the full
platform catalog, not the user's actual subscription. This provider
hardcodes the current Agent Plan text-generation model list and must
be updated when Ark adds/removes models.

Last updated: 2026-06-26 (11 models)
"""

import logging
from typing import Any

from providers import register_provider
from providers.base import ProviderProfile

logger = logging.getLogger(__name__)

# ── Hardcoded model list (Agent Plan text generation, as of 2026-06-26) ──
# Ark's auto-discovery returns 124 stale models. We hardcode the actual
# working list to avoid confusion. Update when the provider's lineup changes.
#
# Prompt-caching on Ark's Anthropic /api/coding endpoint is per-model AND
# prefix-size-dependent (the hit shows up as usage.cache_read_input_tokens).
# Measured 2026-06-26 by sending an identical cached prefix twice per model
# at sizes 4k..64k tokens. Two caveats worth knowing:
# - Each model has its own minimum cacheable prefix (4k..48k below).
# - Hits are probabilistic, not guaranteed (Ark routes implicitly; a
# supported model can miss on any given call). This diverges from
# Anthropic's deterministic cache_control guarantee.
# So this is empirical and does NOT match Ark's /api/v3 implicit-cache docs.
_ARK_MODELS = [
"deepseek-v4-flash", # no cache (0 across 4k..64k)
"deepseek-v4-pro", # caches >= ~4k
"glm-5.2", # caches >= ~4k
"doubao-seed-2.0-pro", # caches only >= ~48k (high threshold)
"doubao-seed-2.0-lite", # caches >= ~4k (flaky hit rate)
"doubao-seed-2.0-mini", # caches >= ~8k
"doubao-seed-2.0-code", # caches >= ~8k
"kimi-k2.7-code", # caches >= ~4k
"kimi-k2.6", # no cache (0 across 4k..64k)
"minimax-m3", # caches >= ~4k
"minimax-m2.7", # no cache (0 across 4k..64k)
]


class VolcengineArkProfile(ProviderProfile):
"""Volcengine Ark — hardcoded model list to avoid stale auto-discovery."""

def fetch_models(
self,
*,
api_key: str | None = None,
base_url: str | None = None,
timeout: float = 8.0,
) -> list[str] | None:
"""Return hardcoded model list.

We intentionally skip the API call because Ark's /models endpoint
returns 124+ stale model IDs from the full platform catalog rather
than the user's actual subscription.
"""
return list(_ARK_MODELS)


volcengine_ark = VolcengineArkProfile(
name="volcengine-ark",
aliases=(
"ark",
"volcengine",
"volcano",
"bytedance",
),
display_name="Volcengine Ark (火山引擎)",
description="ByteDance model platform — doubao-seed, deepseek-v4, glm-5.2, kimi-k2, minimax",
signup_url="https://console.volcengine.com/ark/region:ark+cn-beijing/",
api_mode="anthropic_messages",
env_vars=("ARK_API_KEY",),
base_url="https://ark.cn-beijing.volces.com/api/coding",
auth_type="api_key",
# Aux model runs high-frequency background tasks (titles, compression,
# etc.) where a cached system/context prefix pays off. deepseek-v4-flash
# is cheap but never caches on /api/coding (see _ARK_MODELS notes), so
# every aux call re-bills the full prefix. doubao-seed-2.0-lite is the
# cheapest model that DOES cache, and at the lowest prefix threshold
# (~4k) — the best cost pick for aux. Swap back to a non-caching model
# only if you specifically want flash's latency over cache savings.
default_aux_model="doubao-seed-2.0-lite",
)

register_provider(volcengine_ark)
5 changes: 5 additions & 0 deletions plugins/model-providers/volcengine-ark/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: volcengine-ark-provider
kind: model-provider
version: 1.0.0
description: Volcengine Ark (火山引擎) — ByteDance unified model platform with doubao-seed, deepseek-v4, glm-5.2, kimi-k2, minimax-m3
author: hermes-agent contributors
50 changes: 50 additions & 0 deletions tests/run_agent/test_anthropic_prompt_cache_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,56 @@ def test_minimax_provider_on_openai_wire_does_not_cache(self):
assert agent._anthropic_prompt_cache_policy() == (False, False)


class TestVolcengineArkAnthropicWire:
"""Volcengine Ark (火山引擎) on its Anthropic-compatible /api/coding endpoint.

Ark serves non-Claude model families (deepseek-v4, glm-5.2, doubao-seed,
kimi-k2, minimax-m3), so the blanket ``is_claude`` gate on the
third-party-gateway branch excludes them — same shape as the MiniMax gap.
Ark honors cache_control on /api/coding (hits via
cache_read_input_tokens); allowlist it explicitly via provider id or host.
"""

def test_ark_deepseek_on_provider_caches_native_layout(self):
agent = _make_agent(
provider="volcengine-ark",
base_url="https://ark.cn-beijing.volces.com/api/coding",
api_mode="anthropic_messages",
model="deepseek-v4-pro",
)
assert agent._anthropic_prompt_cache_policy() == (True, True)

def test_ark_glm_on_short_alias_caches(self):
agent = _make_agent(
provider="ark",
base_url="https://ark.cn-beijing.volces.com/api/coding",
api_mode="anthropic_messages",
model="glm-5.2",
)
assert agent._anthropic_prompt_cache_policy() == (True, True)

def test_custom_provider_pointed_at_ark_host_caches(self):
# Host match alone should be sufficient (e.g. custom:ark wiring).
agent = _make_agent(
provider="custom",
base_url="https://ark.cn-beijing.volces.com/api/coding",
api_mode="anthropic_messages",
model="doubao-seed-2.0-lite",
)
assert agent._anthropic_prompt_cache_policy() == (True, True)

def test_ark_provider_on_openai_wire_does_not_cache(self):
# chat_completions transport — Ark's cache_control caching applies to
# the Anthropic /api/coding route. Stay off for OpenAI-wire.
agent = _make_agent(
provider="volcengine-ark",
base_url="https://ark.cn-beijing.volces.com/api/v3",
api_mode="chat_completions",
model="deepseek-v4-pro",
)
assert agent._anthropic_prompt_cache_policy() == (False, False)


class TestOpenAIWireFormatOnCustomProvider:
"""A custom provider using chat_completions (OpenAI wire) should NOT get caching."""

Expand Down