fix(config): init_secrets no longer overwrites entire config (#726)

* fix(config): init_secrets no longer overwrites entire config

init_secrets() was calling Config::from_db_with_toml() to re-resolve
config after injecting credentials. This rebuilt the entire config from
env/DB/defaults, nuking all other config fields (agent, safety, tools,
etc.) even though only LlmConfig depends on injected credentials.

This caused 5 CI test failures: the test rig's carefully chosen config
values (max_tool_iterations, allow_local_tools, etc.) were silently
overwritten with production defaults after secret injection.

Fix: add Config::re_resolve_llm() that re-resolves only the LLM config
after credential injection, leaving all other config fields untouched.
Also fix TraceLlm::complete() to skip ToolCalls steps when called in
force_text mode (iteration limit).

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(test): update test to match TraceLlm::complete() skip-tool-calls behavior [skip-regression-check]

TraceLlm::complete() now skips ToolCalls steps (force_text mode) instead
of erroring. Update the test to verify it skips past a ToolCalls step and
returns the subsequent Text step.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Zaki <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-03-08 13:32:42 -07:00
committed by GitHub
co-authored by Claude Opus 4.6 Zaki
parent 1c5117eded
commit 461d7712e8
4 changed files with 99 additions and 56 deletions
+35 -26
View File
@@ -513,32 +513,41 @@ impl LlmProvider for TraceLlm {
}
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse, LlmError> {
let step = self.next_step(&request.messages)?;
match step.response {
TraceResponse::Text {
content,
input_tokens,
output_tokens,
} => Ok(CompletionResponse {
content,
input_tokens,
output_tokens,
finish_reason: FinishReason::Stop,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
}),
TraceResponse::ToolCalls { .. } => Err(LlmError::RequestFailed {
provider: self.model_name.clone(),
reason: "TraceLlm::complete() called but current step is a tool_calls response; \
use complete_with_tools() instead"
.to_string(),
}),
TraceResponse::UserInput { .. } => Err(LlmError::RequestFailed {
provider: self.model_name.clone(),
reason: "TraceLlm::complete() encountered a user_input step; \
these should have been filtered out during construction"
.to_string(),
}),
// complete() is called when Reasoning has force_text=true (no tools
// available). Skip any remaining ToolCalls steps in the trace and
// return the next Text step, since in real usage the LLM would
// produce text when no tools are offered.
loop {
let step = self.next_step(&request.messages)?;
match step.response {
TraceResponse::Text {
content,
input_tokens,
output_tokens,
} => {
return Ok(CompletionResponse {
content,
input_tokens,
output_tokens,
finish_reason: FinishReason::Stop,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
});
}
TraceResponse::ToolCalls { .. } => {
// Skip tool_calls steps — complete() is called in
// force_text mode so the LLM can't use tools anyway.
continue;
}
TraceResponse::UserInput { .. } => {
return Err(LlmError::RequestFailed {
provider: self.model_name.clone(),
reason: "TraceLlm::complete() encountered a user_input step; \
these should have been filtered out during construction"
.to_string(),
});
}
}
}
}
+16 -9
View File
@@ -571,22 +571,29 @@ mod trace_llm_tests {
}
#[tokio::test]
async fn complete_errors_on_tool_calls_step() {
async fn complete_skips_tool_calls_step() {
// complete() is called in force_text mode where tools aren't available.
// When the trace has a ToolCalls step followed by a Text step, complete()
// should skip the ToolCalls and return the Text response.
let trace = LlmTrace::single_turn(
"test-model",
"hi",
vec![tool_calls_step(vec![simple_tool_call("echo")], 10, 5)],
vec![
tool_calls_step(vec![simple_tool_call("echo")], 10, 5),
text_step("skipped past tools", 20, 8),
],
);
let llm = TraceLlm::from_trace(trace);
let result = llm.complete(make_completion_request("hi")).await;
let resp = llm
.complete(make_completion_request("hi"))
.await
.expect("complete() should skip ToolCalls and return the Text step");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("tool_calls"),
"Expected 'tool_calls' in error: {err_msg}"
);
assert_eq!(resp.content, "skipped past tools");
assert_eq!(resp.input_tokens, 20);
assert_eq!(resp.output_tokens, 8);
assert_eq!(resp.finish_reason, FinishReason::Stop);
}
#[tokio::test]