Fix provider context limits and AWS authentication routing

This commit is contained in:
2026-09-11 11:15:48 -05:00
parent 73d6839ee9
commit 5d2a0ea2de
8 changed files with 270 additions and 41 deletions
+35 -4
View File
@@ -43,6 +43,8 @@ pub(crate) struct RuntimeResponseTranslator {
/// Usage for the most recent model call.
usage: Usage,
context_usage: Option<(u64, u64)>,
// Set before starting each model call, including failures before TurnStarted.
authentication_target: Option<(api::LlmProvider, String)>,
}
/// Projects a multi-turn provider run into one existing Galaxy response stream.
@@ -98,8 +100,16 @@ impl ProviderRunResponseProjector {
ProviderRunProjection::ModelRetry { .. } => {
Ok(self.translator.discard_failed_turn_output())
}
ProviderRunProjection::ModelTurnRequested { .. }
| ProviderRunProjection::ModelTurnFinished { .. } => Ok(Vec::new()),
ProviderRunProjection::ModelTurnRequested {
runtime_id,
model_id,
..
} => {
self.translator.authentication_target =
Some((authentication_provider_for_runtime(&runtime_id), model_id));
Ok(Vec::new())
}
ProviderRunProjection::ModelTurnFinished { .. } => Ok(Vec::new()),
ProviderRunProjection::ToolBatchReady { batch } => {
if !self.plan_tasks_projected {
if let Some(todos) = todos_from_plan_batch(&batch) {
@@ -149,6 +159,19 @@ impl ProviderRunResponseProjector {
}
}
// Runtime IDs identify the transport, unlike model IDs: a Bedrock-backed model
// behind LiteLLM still authenticates against the OpenAI-compatible endpoint.
fn authentication_provider_for_runtime(runtime_id: &str) -> api::LlmProvider {
match runtime_id.split_once(':').map(|(runtime, _)| runtime) {
Some("rig-bedrock") => api::LlmProvider::AwsBedrock,
Some("rig-openai-compatible" | "rig-chatgpt-subscription") => api::LlmProvider::Openai,
Some("rig-anthropic") => api::LlmProvider::Anthropic,
Some("rig-gemini" | "rig-vertex-ai") => api::LlmProvider::Google,
// Unrecognized runtimes must never trigger provider-specific login commands.
Some(_) | None => api::LlmProvider::Unknown,
}
}
// Only explicit multi-step checklists in a plan become tasks. Other document bullets
// (risks, examples, requirements) are not executable tasks.
fn todos_from_plan_batch(
@@ -251,6 +274,7 @@ impl RuntimeResponseTranslator {
has_visible_output: false,
usage: Usage::default(),
context_usage: None,
authentication_target: None,
}
}
@@ -528,8 +552,15 @@ impl RuntimeResponseTranslator {
.is_some_and(|error| error.kind == galaxy_agent_core::AgentErrorKind::Authentication)
{
stream_finished::Reason::InvalidApiKey(stream_finished::InvalidApiKey {
provider: warp_multi_agent_api::LlmProvider::AwsBedrock as i32,
model_name: self.config.model_id.clone(),
provider: self
.authentication_target
.as_ref()
.map_or(api::LlmProvider::Unknown, |(provider, _)| *provider)
as i32,
model_name: self
.authentication_target
.as_ref()
.map_or_else(|| self.config.model_id.clone(), |(_, model)| model.clone()),
})
} else {
stream_finished::Reason::InternalError(stream_finished::InternalError {
@@ -640,3 +640,63 @@ fn plan_tasks_require_multiple_explicit_checklist_items_in_a_task_section() {
assert_eq!(todos[1].title, "Test stale callbacks");
assert_ne!(todos[0].id, todos[1].id);
}
#[test]
fn authentication_failures_identify_the_requested_transport_before_stream_start() {
for (runtime, expected) in [
(
"rig-openai-compatible",
warp_multi_agent_api::LlmProvider::Openai,
),
(
"rig-chatgpt-subscription",
warp_multi_agent_api::LlmProvider::Openai,
),
("rig-bedrock", warp_multi_agent_api::LlmProvider::AwsBedrock),
(
"rig-anthropic",
warp_multi_agent_api::LlmProvider::Anthropic,
),
("rig-gemini", warp_multi_agent_api::LlmProvider::Google),
("rig-vertex-ai", warp_multi_agent_api::LlmProvider::Google),
("unrecognized", warp_multi_agent_api::LlmProvider::Unknown),
] {
let mut projector = ProviderRunResponseProjector::new(provider_translator().config);
// A previous Bedrock turn must not leak its identity into the next request.
for runtime_id in [
"rig-bedrock:old-model".to_owned(),
format!("{runtime}:new-model"),
] {
projector
.project(ProviderRunProjection::ModelTurnRequested {
work_id: galaxy_agent_core::ExternalWorkId {
run_id: galaxy_agent_core::ProviderRunId::new("run"),
epoch: galaxy_agent_core::RunEpoch::new(1),
},
profile: galaxy_agent_core::ProviderRequestProfile::new("cli"),
runtime_id,
model_id: "new-model".to_owned(),
retry_attempt: 0,
})
.expect("requested turn projection");
}
let error = galaxy_agent_core::AgentError::new(
galaxy_agent_core::AgentErrorKind::Authentication,
"Unauthorized",
);
let events =
projector
.translator
.provider_failure("Unauthorized", Some(&error), &Usage::default());
let Some(response_event::Type::Finished(finished)) = &events.last().unwrap().r#type else {
panic!("expected a finished event");
};
let Some(response_event::stream_finished::Reason::InvalidApiKey(details)) =
&finished.reason
else {
panic!("expected authentication failure");
};
assert_eq!(details.provider, expected as i32, "{runtime}");
assert_eq!(details.model_name, "new-model");
}
}