Skip to content
Merged
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
11 changes: 11 additions & 0 deletions docs/content/docs/framework/agent/responses-api.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ root_agent = Agent(
)
```

**Model fallbacks**

The Responses API also accepts a `model_name` list for resilience. If a request fails, VeADK tries each subsequent model in order until one succeeds or all models fail.

```python
root_agent = Agent(
enable_responses=True,
model_name=["doubao-seed-1-8-251228", "deepseek-r1-250528"],
)
```

**Result**

![responses-api](/assets/images/agents/responses_api.png)
Expand Down
11 changes: 11 additions & 0 deletions docs/content/docs/framework/agent/responses-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ root_agent = Agent(
)
```

**多模型容错**

Responses API 同样支持通过 `model_name` 列表配置容错模型。请求失败时会按顺序尝试后续模型,直到成功或所有模型均失败。

```python
root_agent = Agent(
enable_responses=True,
model_name=["doubao-seed-1-8-251228", "deepseek-r1-250528"],
)
```

**效果展示**

![responses-api](/assets/images/agents/responses_api.png)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies = [
"opentelemetry-exporter-otlp==1.37.0",
"opentelemetry-instrumentation-logging>=0.56b0",
"wrapt==1.17.2", # For patching built-in functions
"volcengine-python-sdk>=5.0.1", # For Volcengine API
"volcengine-python-sdk>=5.0.36", # For Volcengine API and Ark Responses context management
"volcengine>=1.0.193", # For Volcengine sign and AgentKit Runtime API access
"agent-pilot-sdk==0.1.2", # Prompt optimization by Volcengine AgentPilot/PromptPilot toolkits
"fastmcp>=2.12.3", # For running MCP
Expand Down
41 changes: 41 additions & 0 deletions tests/models/test_ark_llm_context_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from veadk.models.ark_llm import request_reorganization_by_ark


def test_request_reorganization_preserves_context_management():
context_management = {
"edits": [
{
"type": "clear_thinking",
"keep": {"type": "thinking_turns", "value": 1},
},
{
"type": "clear_tool_uses",
"trigger": {"type": "tool_uses", "value": 30},
"keep": {"type": "tool_uses", "value": 20},
},
]
}

request = request_reorganization_by_ark(
{
"model": "openai/test-model",
"input": [],
"context_management": context_management,
}
)

assert request["context_management"] == context_management
138 changes: 138 additions & 0 deletions tests/models/test_ark_llm_fallbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import httpx
import pytest
from google.adk.models import LlmResponse
from volcenginesdkarkruntime._exceptions import ArkBadRequestError

from veadk.models.ark_llm import ArkLlm


@pytest.mark.asyncio
async def test_ark_llm_uses_next_model_when_primary_fails(monkeypatch):
model = ArkLlm(
model="openai/primary-model",
fallbacks=["openai/fallback-model"],
)
calls = []

async def fake_generate(self, responses_args, stream=False):
calls.append((responses_args["model"], stream))
if responses_args["model"] == "openai/primary-model":
raise RuntimeError("primary failed")
yield LlmResponse(model_version=responses_args["model"])

monkeypatch.setattr(ArkLlm, "generate_content_via_responses", fake_generate)

responses = [
response
async for response in model._generate_content_with_fallbacks(
{"input": []}, stream=True
)
]

assert calls == [
("openai/primary-model", True),
("openai/fallback-model", True),
]
assert [response.model_version for response in responses] == [
"openai/fallback-model"
]


@pytest.mark.asyncio
async def test_ark_llm_raises_last_error_when_all_models_fail(monkeypatch):
model = ArkLlm(
model="openai/primary-model",
fallbacks=["openai/fallback-model"],
)

async def fake_generate(self, responses_args, stream=False):
if False:
yield
raise RuntimeError(f"{responses_args['model']} failed")

monkeypatch.setattr(ArkLlm, "generate_content_via_responses", fake_generate)

with pytest.raises(RuntimeError, match="openai/fallback-model failed"):
async for _ in model._generate_content_with_fallbacks({"input": []}):
pass


@pytest.mark.asyncio
async def test_ark_llm_does_not_fallback_after_stream_output(monkeypatch):
model = ArkLlm(
model="openai/primary-model",
fallbacks=["openai/fallback-model"],
)
calls = []

async def fake_generate(self, responses_args, stream=False):
calls.append(responses_args["model"])
yield LlmResponse(model_version=responses_args["model"], partial=True)
raise RuntimeError("stream interrupted")

monkeypatch.setattr(ArkLlm, "generate_content_via_responses", fake_generate)

generator = model._generate_content_with_fallbacks({"input": []}, stream=True)
first_response = await anext(generator)
assert first_response.model_version == "openai/primary-model"

with pytest.raises(RuntimeError, match="stream interrupted"):
await anext(generator)

assert calls == ["openai/primary-model"]


@pytest.mark.asyncio
async def test_ark_llm_retries_expired_response_before_fallback(monkeypatch):
model = ArkLlm(
model="openai/primary-model",
fallbacks=["openai/fallback-model"],
)
calls = []
request = httpx.Request("POST", "https://ark.example/v1/responses")
expired_error = ArkBadRequestError(
"previous response expired",
response=httpx.Response(400, request=request),
body={"code": "InvalidParameter.PreviousResponseNotFound"},
request_id="request-id",
)

async def fake_generate(self, responses_args, stream=False):
calls.append(
(responses_args["model"], responses_args.get("previous_response_id"))
)
if len(calls) == 1:
raise expired_error
if responses_args["model"] == "openai/primary-model":
raise RuntimeError("primary failed without cache")
yield LlmResponse(model_version=responses_args["model"])

monkeypatch.setattr(ArkLlm, "generate_content_via_responses", fake_generate)

responses = [
response
async for response in model._generate_content_with_fallbacks(
{"input": [], "previous_response_id": "expired-id"}
)
]

assert calls == [
("openai/primary-model", "expired-id"),
("openai/primary-model", None),
("openai/fallback-model", None),
]
assert responses[0].model_version == "openai/fallback-model"
40 changes: 40 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,46 @@ def test_agent_model_creation(mock_lite_llm):
assert agent.model == mock_model


@patch("veadk.models.ark_llm.ArkLlm")
def test_agent_passes_context_management_to_responses_model(mock_ark_llm):
context_management = {
"edits": [
{
"type": "clear_thinking",
"keep": {"type": "thinking_turns", "value": 1},
}
]
}

Agent(
model_name="test_model",
model_provider="ark",
model_api_key="test_key",
model_api_base="test_base",
enable_responses=True,
model_extra_config={"context_management": context_management},
)

assert mock_ark_llm.call_args.kwargs["context_management"] == context_management


@patch("veadk.models.ark_llm.ArkLlm")
def test_agent_configures_responses_model_fallbacks(mock_ark_llm):
Agent(
model_name=["primary-model", "fallback-model-1", "fallback-model-2"],
model_provider="openai",
model_api_key="test_key",
model_api_base="test_base",
enable_responses=True,
)

assert mock_ark_llm.call_args.kwargs["model"] == "openai/primary-model"
assert mock_ark_llm.call_args.kwargs["fallbacks"] == [
"openai/fallback-model-1",
"openai/fallback-model-2",
]


@patch.dict("os.environ", {"MODEL_AGENT_API_KEY": "mock_api_key"})
def test_agent_with_existing_model():
existing_model = LiteLlm(model="test_model")
Expand Down
Loading
Loading