Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 256 additions & 90 deletions crates/forge_app/src/dto/anthropic/request.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ pub struct ReasoningTransform;
impl Transformer for ReasoningTransform {
type Value = Context;
fn transform(&mut self, mut context: Self::Value) -> Self::Value {
if let Some(reasoning) = context.reasoning.as_ref()
&& reasoning.enabled.unwrap_or(false)
{
// if reasoning is enabled then we've to drop top_k and top_p
// Must stay in lockstep with the Anthropic request builder, which gates
// on the same predicate — otherwise `thinking`/`output_config` ship
// alongside sampling params that Anthropic rejects.
if context.is_reasoning_supported() {
context.top_k = None;
context.top_p = None;
}
Expand Down Expand Up @@ -85,4 +85,51 @@ mod tests {

assert_eq!(actual, expected);
}

#[test]
fn test_enabled_none_with_effort_still_strips_top_k_and_top_p() {
// `enabled: None` + effort is treated as reasoning-on (domain rule).
let fixture = create_context_fixture().reasoning(ReasoningConfig {
enabled: None,
max_tokens: None,
effort: Some(forge_domain::Effort::High),
exclude: None,
});
let mut transformer = ReasoningTransform;
let actual = transformer.transform(fixture);

assert_eq!(actual.top_k, None);
assert_eq!(actual.top_p, None);
}

#[test]
fn test_enabled_none_with_positive_max_tokens_still_strips_top_k_and_top_p() {
let fixture = create_context_fixture().reasoning(ReasoningConfig {
enabled: None,
max_tokens: Some(8000),
effort: None,
exclude: None,
});
let mut transformer = ReasoningTransform;
let actual = transformer.transform(fixture);

assert_eq!(actual.top_k, None);
assert_eq!(actual.top_p, None);
}

#[test]
fn test_enabled_none_with_zero_max_tokens_preserves_top_k_and_top_p() {
// Matches `is_reasoning_supported`: max_tokens == 0 is treated as off.
let fixture = create_context_fixture().reasoning(ReasoningConfig {
enabled: None,
max_tokens: Some(0),
effort: None,
exclude: None,
});
let mut transformer = ReasoningTransform;
let actual = transformer.transform(fixture.clone());

assert_eq!(actual.top_k, fixture.top_k);
assert_eq!(actual.top_p, fixture.top_p);
}
}
8 changes: 7 additions & 1 deletion crates/forge_app/src/orch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tokio::sync::Notify;
use tracing::warn;

use crate::agent::AgentService;
use crate::transformers::ModelSpecificReasoning;
use crate::{EnvironmentInfra, TemplateEngine};

#[derive(Clone, Setters)]
Expand Down Expand Up @@ -208,7 +209,12 @@ impl<S: AgentService + EnvironmentInfra<Config = forge_config::ForgeConfig>> Orc
.pipe(DropReasoningDetails.when(|_| !reasoning_supported))
// Strip all reasoning from messages when the model has changed (signatures are
// model-specific and invalid across models). No-op when model is unchanged.
.pipe(ReasoningNormalizer::new(model_id.clone()));
.pipe(ReasoningNormalizer::new(model_id.clone()))
// Normalize Anthropic reasoning knobs per model family before provider conversion.
.pipe(
ModelSpecificReasoning::new(model_id.as_str())
.when(|_| model_id.as_str().to_lowercase().contains("claude")),
);
let response = self
.services
.chat_agent(
Expand Down
2 changes: 2 additions & 0 deletions crates/forge_app/src/transformers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod compaction;
mod dedupe_role;
mod drop_role;
mod model_specific_reasoning;
mod strip_working_dir;
mod trim_context_summary;

pub use compaction::SummaryTransformer;
pub(crate) use model_specific_reasoning::ModelSpecificReasoning;
Loading
Loading