Skip to content

Commit 25a8ea6

Browse files
xgtcodeshanchunhua
andauthored
feat: support model fallback endpoints (#1083)
Co-authored-by: shanchunhua <shanchunhua@bytedance.com>
1 parent 7d2c15a commit 25a8ea6

6 files changed

Lines changed: 345 additions & 10 deletions

File tree

tests/runtime/differential/test_runtime_parity.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,3 +667,21 @@ def test_explicit_field_snapshot_survives_clone() -> None:
667667
assert explicit_fields(clone) == explicit_fields(agent)
668668
# The real contract: the clone still validates.
669669
check_agent_runtime_support(clone, "codex")
670+
671+
672+
def test_model_fallbacks_warn_for_external_runtime(caplog) -> None:
673+
from veadk import Agent
674+
from veadk.runtime.compat import reset_warning_state
675+
676+
reset_warning_state()
677+
agent = Agent(
678+
name="codex_agent_with_fallbacks",
679+
model_name="scripted-model",
680+
model_api_base="https://backend.invalid/v1",
681+
model_api_key="backend-key",
682+
model_fallbacks=["backup-model"],
683+
runtime="codex",
684+
)
685+
686+
assert agent.model_fallbacks == ["backup-model"]
687+
assert "drops Agent(model_fallbacks=...)" in caplog.text

tests/test_agent.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from google.adk.models.lite_llm import LiteLlm
2222
from google.adk.tools import load_memory
2323

24-
from veadk import Agent
24+
from veadk import Agent, ModelFallbackEndpoint
2525
from veadk.consts import (
2626
DEFAULT_AGENT_NAME,
2727
DEFAULT_MODEL_AGENT_API_BASE,
@@ -31,6 +31,7 @@
3131
)
3232
from veadk.knowledgebase import KnowledgeBase
3333
from veadk.memory.long_term_memory import LongTermMemory
34+
from veadk.models.retrying_lite_llm import RetryingLiteLlm
3435
from veadk.tools import load_knowledgebase_tool
3536
from veadk.tracing.telemetry.opentelemetry_tracer import OpentelemetryTracer
3637

@@ -190,6 +191,138 @@ def test_agent_configures_responses_model_fallbacks(mock_ark_llm):
190191
]
191192

192193

194+
@patch("veadk.agent.RetryingLiteLlm")
195+
def test_agent_configures_cross_provider_litellm_fallbacks(mock_lite_llm, monkeypatch):
196+
monkeypatch.setenv("BACKUP_MODEL_API_KEY", "backup-key")
197+
198+
Agent(
199+
model_name="primary-model",
200+
model_provider="ark",
201+
model_api_key="primary-key",
202+
model_api_base="https://ark.example.com/api/v3",
203+
model_fallbacks=[
204+
{
205+
"model_provider": "openai",
206+
"model_name": "gpt-4o-mini",
207+
"model_api_base": "https://api.openai.com/v1",
208+
"model_api_key_env": "BACKUP_MODEL_API_KEY",
209+
"model_extra_config": {
210+
"extra_headers": {"x-fallback": "1"},
211+
"temperature": 0.1,
212+
},
213+
}
214+
],
215+
)
216+
217+
assert mock_lite_llm.call_args.kwargs["model"] == "ark/primary-model"
218+
assert mock_lite_llm.call_args.kwargs["fallbacks"] == [
219+
{
220+
"model": "openai/gpt-4o-mini",
221+
"api_key": "backup-key",
222+
"api_base": "https://api.openai.com/v1",
223+
"extra_headers": {
224+
**DEFAULT_MODEL_EXTRA_CONFIG["extra_headers"],
225+
"x-fallback": "1",
226+
},
227+
"temperature": 0.1,
228+
}
229+
]
230+
231+
232+
@patch("veadk.agent.RetryingLiteLlm")
233+
def test_agent_combines_legacy_and_explicit_litellm_fallbacks(mock_lite_llm):
234+
Agent(
235+
model_name=["primary-model", "same-provider-a"],
236+
model_provider="ark",
237+
model_api_key="primary-key",
238+
model_api_base="https://ark.example.com/api/v3",
239+
model_fallbacks=[
240+
"same-provider-b",
241+
ModelFallbackEndpoint(
242+
model_provider="anthropic",
243+
model_name="claude-3-5-haiku-latest",
244+
model_api_key="anthropic-key",
245+
),
246+
],
247+
)
248+
249+
assert mock_lite_llm.call_args.kwargs["fallbacks"] == [
250+
"ark/same-provider-a",
251+
"ark/same-provider-b",
252+
{
253+
"model": "anthropic/claude-3-5-haiku-latest",
254+
"api_key": "anthropic-key",
255+
"api_base": None,
256+
},
257+
]
258+
259+
260+
@patch("veadk.agent.RetryingLiteLlm")
261+
def test_agent_accepts_litellm_style_fallback_dict(mock_lite_llm):
262+
Agent(
263+
model_name="primary-model",
264+
model_provider="ark",
265+
model_api_key="primary-key",
266+
model_api_base="https://ark.example.com/api/v3",
267+
model_fallbacks=[
268+
{
269+
"model": "openai/gpt-4o-mini",
270+
"api_key": "openai-key",
271+
"api_base": "https://api.openai.com/v1",
272+
}
273+
],
274+
)
275+
276+
assert mock_lite_llm.call_args.kwargs["fallbacks"] == [
277+
{
278+
"model": "openai/gpt-4o-mini",
279+
"api_key": "openai-key",
280+
"api_base": "https://api.openai.com/v1",
281+
}
282+
]
283+
284+
285+
def test_agent_rejects_endpoint_fallbacks_for_responses_model():
286+
with pytest.raises(ValueError, match="Endpoint model_fallbacks"):
287+
Agent(
288+
model_name="primary-model",
289+
model_provider="ark",
290+
model_api_key="primary-key",
291+
model_api_base="https://ark.example.com/api/v3",
292+
enable_responses=True,
293+
model_fallbacks=[
294+
{
295+
"model_provider": "openai",
296+
"model_name": "gpt-4o-mini",
297+
}
298+
],
299+
)
300+
301+
302+
def test_retrying_litellm_refreshes_mutable_fallbacks_between_calls():
303+
model = RetryingLiteLlm(
304+
model="ark/primary",
305+
fallbacks=[
306+
{
307+
"model": "openai/fallback",
308+
"api_key": "fallback-key",
309+
"api_base": "https://fallback.example.com/v1",
310+
}
311+
],
312+
)
313+
314+
model._additional_args["fallbacks"][0].pop("model")
315+
model._refresh_fallbacks()
316+
317+
assert model._additional_args["fallbacks"] == [
318+
{
319+
"model": "openai/fallback",
320+
"api_key": "fallback-key",
321+
"api_base": "https://fallback.example.com/v1",
322+
}
323+
]
324+
325+
193326
@patch.dict("os.environ", {"MODEL_AGENT_API_KEY": "mock_api_key"})
194327
def test_agent_with_existing_model():
195328
existing_model = LiteLlm(model="test_model")

veadk/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from veadk.version import VERSION
1818

1919
if TYPE_CHECKING:
20-
from veadk.agent import Agent
20+
from veadk.agent import Agent, ModelFallbackEndpoint
2121
from veadk.runner import Runner
2222

2323

@@ -27,11 +27,15 @@ def __getattr__(name):
2727
from veadk.agent import Agent
2828

2929
return Agent
30+
if name == "ModelFallbackEndpoint":
31+
from veadk.agent import ModelFallbackEndpoint
32+
33+
return ModelFallbackEndpoint
3034
if name == "Runner":
3135
from veadk.runner import Runner
3236

3337
return Runner
3438
raise AttributeError(f"module 'veadk' has no attribute '{name}'")
3539

3640

37-
__all__ = ["Agent", "Runner", "VERSION"]
41+
__all__ = ["Agent", "ModelFallbackEndpoint", "Runner", "VERSION"]

0 commit comments

Comments
 (0)