Plan: Target system prompts follow routed answer candidates
Linear: SWITCH-1253
Goal
A client sends a request to a Switchyard route and does not know which upstream target will answer.
Different targets may require different standing system instructions. Today, only Stage Router
supports this through capable_system_prompt and efficient_system_prompt. Other routers cannot
configure the same behavior.
The selected target may also fail after routing completes:
route selects fast
-> fast fails
-> libsy-llm-client falls back to capable
Preparing the request once for fast would cause the capable fallback to inherit the wrong prompt.
Switchyard must prepare the request independently for each answer candidate.
The resulting boundary is:
Native server
read targets.*.system_prompt
|
v
libsy Algorithm
select target and ordered fallbacks
return RoutingOutcome
|
v
libsy-llm-client or custom host
outcome.request_for(candidate)
|
v
switchyard-translation
stamp candidate model
prepend candidate prompt
encode provider request
Existing TOML without target prompts remains unchanged. Legacy Stage prompt fields remain
supported.
Configuration interface
Move answer prompt configuration from Stage-specific route roles:
[targets.fast]
id = "meta/llama-small"
llm_client = "together"
[targets.capable]
id = "meta/llama-large"
llm_client = "together"
[routes.agent]
id = "switchyard/agent"
type = "stage_router"
efficient_target = "fast"
capable_target = "capable"
efficient_system_prompt = "Be concise."
capable_system_prompt = "Reason carefully before answering."
To the targets themselves:
[targets.fast]
id = "meta/llama-small"
llm_client = "together"
system_prompt = "Be concise."
[targets.capable]
id = "meta/llama-large"
llm_client = "together"
system_prompt = "Reason carefully before answering."
[routes.agent]
id = "switchyard/agent"
type = "stage_router"
efficient_target = "fast"
capable_target = "capable"
The target-level prompt follows that target through every built-in client-visible answer path,
including fallback. If both forms configure the same Stage target,
targets.*.system_prompt takes precedence.
Request preparation ownership
switchyard-server owns configuration:
- Read
targets.*.system_prompt.
- Build the effective answer prompt map for each route.
- Exclude judge-only targets.
- Preserve legacy Stage configuration.
libsy owns candidate policy:
- Keep the selected target and ordered fallbacks in
RoutingOutcome.
- Retain an unprompted request snapshot when prompted fallback requires one.
- Prepare the request separately for each attempted candidate.
- Expose the same behavior to native and custom hosts.
switchyard-translation owns request mutation:
- Stamp the upstream model ID.
- Prepend the target system prompt.
- Invalidate exact request replay when it becomes stale.
Retry and fallback policy remain in libsy-llm-client. This work does not change which errors
permit fallback or how backend retries work.
Call boundary
Target prompts are answer policy, not general instructions for every call made through a target.
| Call purpose |
Target prompt |
| Passthrough, Random, Stage, or LLM Classifier final answer |
Applied |
| Selected or fallback answer candidate |
Applied |
| Escalation provisional answer |
Applied |
| Advisor executor answer |
Applied |
| Classifier or Stage judge |
Not applied |
| Advisor reviewer |
Not applied |
| Noop |
No model call |
| Anthropic count-tokens |
Applied explicitly |
Algorithms use Driver::call_model(...) for classifier, judge, and reviewer work. An algorithm
making a call whose response may become client-visible uses Driver::call_answer_model(...).
Candidate request API
Add candidate-aware preparation to terminal outcomes:
impl RoutingOutcome {
pub fn request_for(&self, target: &ModelId) -> Result<Request>;
}
Add the same operation for routing-time candidate calls:
impl CallModel {
pub fn request_for(&self, target: &ModelId) -> Result<Request>;
}
The Python bindings expose equivalent methods. A host uses
outcome.request_for(target) rather than copying outcome.request and replacing only its model.
RoutingOutcome gains private preparation state and becomes #[non_exhaustive]. Rust code
tracking unreleased main that constructs it with a struct literal must use
RoutingOutcome::route_to(...) or RoutingOutcome::answered(...).
Success criteria
targets.*.system_prompt is optional.
- Existing TOML without the field behaves as before.
- Every built-in client-visible answer path applies the selected target's prompt.
- A fallback receives its own prompt and never inherits the selected target's prompt.
- Caller-provided system instructions remain after the target prompt.
- Classifier, judge, and reviewer calls remain unchanged.
- Legacy Stage prompt fields remain supported.
- Target-level configuration takes precedence over legacy Stage configuration.
- Native Rust and Python hosts prepare candidates through
request_for().
- Two answer aliases that collapse to one model ID with different prompts fail during configuration.
- Retry order, fallback eligibility, affinity, and routing decisions remain unchanged.
Step-by-step
Step 1 - add target request preparation
In switchyard-translation:
- Add
prepare_request_for_target(...).
- Stamp the selected upstream model.
- Prepend an optional target prompt.
- Clear preserved exact requests when prompt insertion makes them stale.
- Test prompted and unprompted preparation.
Implemented in #455.
Step 2 - prepare every candidate in libsy
In switchyard-libsy and switchyard-llm-client:
- Add immutable
TargetPrompts policy.
- Add
RoutingOutcome::request_for(...) and CallModel::request_for(...).
- Prepare selected and fallback candidates independently.
- Distinguish routing-only calls from provisional answer calls.
- Add Rust and Python host support.
- Preserve Stage's existing tier-prompt behavior.
Implemented in #463.
Step 3 - expose native target configuration
In switchyard-server:
- Add
system_prompt to TargetConfig.
- Build effective prompt policy from each route's answer targets.
- Preserve legacy Stage fields and define precedence.
- Reject ambiguous target aliases.
- Apply the prompt to count-tokens requests.
- Update documentation and server tests.
Implemented in #464.
Tests to add or adapt
| Test |
Asserts |
| selected candidate |
Selected target receives its configured prompt |
| fallback candidate |
Fallback receives only its own prompt |
| unconfigured target |
Request remains unprompted |
| caller instructions |
Existing system instructions remain after the target prompt |
| Stage compatibility |
Legacy fields continue to work |
| precedence |
Target-level prompt wins over legacy Stage prompt |
| judge isolation |
Classifier and judge calls receive no answer prompt |
| Advisor isolation |
Executor receives the prompt; reviewer does not |
| alias conflict |
Different prompts for one resolved model fail at startup |
| Python host |
request_for() prepares selected and fallback candidates |
| count-tokens |
Token counting includes the effective target prompt |
Verification
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
uv run pytest tests/ -v
uv run ruff check .
uv run mypy switchyard
Out of scope
- Changing how routers select targets.
- Changing retry budgets, retryable errors, or fallback ordering.
- Applying answer prompts to classifier, judge, or reviewer calls.
- Introducing a first-class
TargetId separate from ModelId.
- Supporting different prompts for the same target in different routes.
- Preserving unknown provider-only request fields after prompt insertion.
Plan: Target system prompts follow routed answer candidates
Linear: SWITCH-1253
Goal
A client sends a request to a Switchyard route and does not know which upstream target will answer.
Different targets may require different standing system instructions. Today, only Stage Router
supports this through
capable_system_promptandefficient_system_prompt. Other routers cannotconfigure the same behavior.
The selected target may also fail after routing completes:
Preparing the request once for
fastwould cause the capable fallback to inherit the wrong prompt.Switchyard must prepare the request independently for each answer candidate.
The resulting boundary is:
Existing TOML without target prompts remains unchanged. Legacy Stage prompt fields remain
supported.
Configuration interface
Move answer prompt configuration from Stage-specific route roles:
To the targets themselves:
The target-level prompt follows that target through every built-in client-visible answer path,
including fallback. If both forms configure the same Stage target,
targets.*.system_prompttakes precedence.Request preparation ownership
switchyard-serverowns configuration:targets.*.system_prompt.libsyowns candidate policy:RoutingOutcome.switchyard-translationowns request mutation:Retry and fallback policy remain in
libsy-llm-client. This work does not change which errorspermit fallback or how backend retries work.
Call boundary
Target prompts are answer policy, not general instructions for every call made through a target.
Algorithms use
Driver::call_model(...)for classifier, judge, and reviewer work. An algorithmmaking a call whose response may become client-visible uses
Driver::call_answer_model(...).Candidate request API
Add candidate-aware preparation to terminal outcomes:
Add the same operation for routing-time candidate calls:
The Python bindings expose equivalent methods. A host uses
outcome.request_for(target)rather than copyingoutcome.requestand replacing only its model.RoutingOutcomegains private preparation state and becomes#[non_exhaustive]. Rust codetracking unreleased
mainthat constructs it with a struct literal must useRoutingOutcome::route_to(...)orRoutingOutcome::answered(...).Success criteria
targets.*.system_promptis optional.request_for().Step-by-step
Step 1 - add target request preparation
In
switchyard-translation:prepare_request_for_target(...).Implemented in #455.
Step 2 - prepare every candidate in libsy
In
switchyard-libsyandswitchyard-llm-client:TargetPromptspolicy.RoutingOutcome::request_for(...)andCallModel::request_for(...).Implemented in #463.
Step 3 - expose native target configuration
In
switchyard-server:system_prompttoTargetConfig.Implemented in #464.
Tests to add or adapt
request_for()prepares selected and fallback candidatesVerification
Out of scope
TargetIdseparate fromModelId.