Add backends/strategy configuration and round-robin routing for virtual provider LLM agents #155
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
Depends on
#154 Epic: Virtual Provider — Load-Balanced LLM Backend Routing
cleveragents/cleveractors-core
#156 Add random backend-selection strategy for virtual provider LLM agents
cleveragents/cleveractors-core
#157 feat(agents): add virtual provider backends config and round-robin routing
cleveragents/cleveractors-core
Reference
cleveragents/cleveractors-core#155
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Metadata
Commit Message: feat(agents): add virtual provider backends config and round-robin routing
Branch: feature/m1-virtual-provider-round-robin
Background and context
Part of the virtual-provider Epic #154. Design is recorded in
docs/adr/ADR-2038-virtual-provider-load-balanced-backend-routing.md. Thisissue delivers the full first increment: a
type: llmagent can declarebackends(a hard-configured list of backend credential sets, each with itsown optional
provider/model, defaulting to the agent's own) and thedefault
round_robinselection strategy, so that a single logical agent canforward requests across multiple backend credential sets — e.g. multiple
OAuth subscriptions for the same provider — to increase effective capacity.
Current behavior
An
agents.<name>entry oftype: llmalways constructs exactly one chatmodel from its own
provider/model/credentials(
cleveractors.agents.llm_client.build_chat_model). There is no field orcode path to configure more than one backend credential set for a single
logical agent, and the shared, provider-keyed
credentialsdict cannot holdtwo distinct credential sets for the same provider name.
Expected behavior
agents.<name>.configfortype: llmaccepts an optionalbackendslist(minimum 2 entries) and an optional
strategyfield (default"round_robin"; currently the only accepted value).backends[i]entry accepts optionalprovider/model(defaulting tothe agent's own
provider/model) and a REQUIREDcredentialsmapping,validated independently per backend using the existing per-provider
validation logic (native-provider env-var resolution;
_validate_base_urlSSRF checks for non-native
base_url).backendsis present, invoking the agent forwards the request toexactly one backend — selected by round-robin order — and returns that
backend's response unmodified. No retry, failover, or health checking is
performed (see ADR-2038 Non-goals).
backendsis absent, agent construction and behavior are unchangedfrom today.
Acceptance criteria
backendslist with >= 2 entries constructs successfully; invoking theresulting agent N times (N = number of backends) selects each configured
backend exactly once, in list order, and the sequence wraps correctly
after the last backend.
backendslist with 0 or 1 entries raises aConfigurationErroridentifying the minimum-count requirement.
credentialsraises aConfigurationErroridentifying which backend index is missing credentials.
provider/modeluses the agent's ownprovider/model.providerbut differentcredentialsare both constructed correctly and used independently (nevermerged into, or looked up from, the shared provider-keyed
credentialsdict).
strategyabsent, or explicitly set to"round_robin", selectsround-robin order as described above.
strategyset to any other value raises aConfigurationErrornaming theunsupported value (until a follow-up issue adds
"random").underlying chat-model call is retried against that same backend, using
the agent's existing
max_retries/max_retry_timeconfiguration(ADR-2032) — exactly as retries already worked before the virtual provider
existed. The backend is never re-selected mid-retry-sequence. Once that
backend's retry budget is exhausted, the resulting
ExecutionErrorpropagates to the caller unmodified; it is not retried against a different
backend, and the outer, unmodified
LLMAgent._retry_ainvokewrapper doesnot re-trigger a further retry (
ExecutionErroris not a transient error).BadRequestError,AuthenticationError) fromthe selected backend propagates immediately, unmodified, with no retry —
identical to today's single-provider behavior.
memory_enabled: true, a multi-turn conversation that is round-robinedacross different backends on successive turns still receives the complete
conversation history (system prompt + prior turns + new message) on every
turn, regardless of which backend is selected — no context is lost when the
selected backend changes between turns (see ADR-2038, "Conversation state
is unaffected by backend switching").
reasoning_feedback: trueand two reasoning-aware (non-native)backends, a multi-turn tool-call loop that round-robins between them
still round-trips
reasoning_contentcorrectly on the turn that followsa backend switch (see ADR-2038, "Compatibility with reasoning-aware
backends (ADR-2036)").
reasoning_feedbackis read once, at the virtualprovider level, and applied identically to every backend — there is no
per-backend override.
agents.<name>configurations that omitbackendsbehavebyte-for-byte as before this change (non-regression).
Supporting information
docs/adr/ADR-2038-virtual-provider-load-balanced-backend-routing.mdSubtasks
backends/strategyfields to the LLM agent configuration schemaand validation (
src/cleveractors/validation/_agents.py,_vocabulary.py)native/non-native provider construction logic unchanged, with isolated
per-backend credentials
SelectionStrategyinterface andRoundRobinStrategyimplementation
VirtualChatModelas a genuineBaseChatModelsubclass(same precedent as
ReasoningChatModel, ADR-2036) overriding only_generate/_agenerate/_stream/_astream/_llm_type, thatselects a backend once per call and forwards unmodified, with a
concurrency-safe round-robin counter
invocation by calling the existing, unmodified
cleveractors.agents.retry.call_with_retryinternally (samemax_retries/max_retry_timethe agent already reads from config) —do not rely on
LLMAgent._retry_ainvoke's outer wrapping to retry,since that would re-select a backend on every retry attempt; no
changes to
LLMAgent._retry_ainvoke,_run_pruning_pass, or_start_streamare needed or permittedbackends/strategyintocleveractors.agents.llm_client.build_chat_modeldocs/index.md§4.4 with the new fields and a new §4.4.7describing virtual-provider routing semantics; bump the Actor
Configuration Standard version and add a §21.1 revision-history entry
examples/demonstrating apooled, same-provider virtual agent
per-backend credentials, provider/model defaulting), round-robin
ordering across repeated invocations, non-regression for agents
without
backends, amemory_enabled: truescenario proving fullconversation history is still sent on every turn when successive turns
are round-robined across different backends, and a retry-pinning
scenario proving a transient failure on the selected backend is
retried against that same backend (not a different one) and that an
ExecutionErrorfrom exhausted retries is not retried a second timeby the outer agent-level wrapper, and a
reasoning_feedback: truescenario proving
reasoning_contentround-trips correctly across abackend switch between two reasoning-aware backends
>= 2 backends through the library API
nox -s coverage_reportnox(all default sessions), fix any errorsDefinition of Done
This issue is complete when:
in Metadata exactly.
exactly.