Skip to content

fix: don't bleed provider-specific request_params into delegate() sessions - #9906

Merged
DOsinga merged 4 commits into
aaif-goose:mainfrom
kyledef:kdefreitas/fix-delegate-request-params
Jul 3, 2026
Merged

fix: don't bleed provider-specific request_params into delegate() sessions#9906
DOsinga merged 4 commits into
aaif-goose:mainfrom
kyledef:kdefreitas/fix-delegate-request-params

Conversation

@kyledef

@kyledef kyledef commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Problem

When delegate() (the summon extension) overrides the model to a different model family — e.g. the parent session is a Claude model (Anthropic) and the child is goose-gemini-3-5-flash (Google) — two bugs cause the Databricks Gemini endpoint to reject the request with 400 INVALID_ARGUMENT.

Bug 1 — anthropic_beta bleeds into the child session (summon.rs)

resolve_model_config() unconditionally merged all of the parent ModelConfig.request_params into the child config when a model override was specified. The parent session's model (e.g. goose-claude-4-6-sonnet) has anthropic_beta in its request_params (populated from GOOSE_PREDEFINED_MODELS in the app environment). That key was blindly forwarded to the Gemini request body, which the endpoint rejected.

Fix: Only inherit the model-family-agnostic key thinking_effort from parent request_params. Provider-specific keys like anthropic_beta are derived from GOOSE_PREDEFINED_MODELS at ModelConfig::new() time for the specific target model and must not be carried over from a different model family.

Bug 2 — parametersJsonSchema used for Gemini on Databricks (formats/databricks.rs)

format_tools() detected "gemini" in the model name and emitted parametersJsonSchema (the Google-native Vertex/Gemini API field) instead of parameters. However, Databricks serving endpoints — including those backed by Gemini — use the OpenAI-compatible chat completions format, which expects parameters. The branch was incorrect and caused every tool definition to be rejected.

Fix: Remove the model-name branch entirely. Databricks tools always use parameters.

Changes

  • crates/goose/src/agents/platform_extensions/summon.rs: Only inherit thinking_effort from parent request_params on model override. Updated tests: renamed existing test to assert the correct (fixed) behaviour, added a new test asserting thinking_effort is still inherited.
  • crates/goose/src/providers/formats/databricks.rs: Removed is_gemini branch in format_tools(); always use parameters. Updated test assertions accordingly.

Testing

  • cargo test -p goose --lib providers::formats::databricks::tests::test_format_tools — passes
  • cargo clippy -p goose --all-targets -- -D warnings — clean (pre-existing errors in unrelated files only)
  • cargo fmt — clean

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e8e28fb0e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

for k in ["thinking_effort"] {
if let Some(v) = parent_params.get(k).cloned() {
let merged = cfg.request_params.get_or_insert_with(Default::default);
merged.entry(k.to_string()).or_insert(v);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve parent thinking effort over default config

When a global/default GOOSE_THINKING_EFFORT is set and the parent session has changed its own thinking effort, model_config_from_user_config() has already seeded cfg.request_params with the default before this merge. Because this uses or_insert, delegate(..., model: ...) ignores the parent session's thinking_effort and runs the subagent with the stale/default effort instead; the normal model-switch path builds without defaults first so the session value wins. For this whitelisted inherited key, overwrite the existing default rather than preserving it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might actually be a useful side effect rather than a regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by routing the override through model_config_from_user_config_with_session_settings, which inherits thinking_effort from the parent with correct precedence (explicit child param > parent session > global default) rather than the old or_insert. A parent that raised its effort above the global default now wins; added test_resolve_model_config_inherits_thinking_effort_on_override to cover it.

@kimnamu kimnamu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this fix, @kdefreitas — splitting model-family-agnostic keys from provider-specific ones is the right model, and the Databricks parameters correction looks solid. I'm not a maintainer, just another Bedrock/provider user who reviewed this carefully and ran the branch locally. I wanted to add concrete evidence to the open thread between the Codex bot's P2 and @kyledef's reply, because I think it tips the decision.

The Codex P2 is a real regression — reproduced locally. With a global GOOSE_THINKING_EFFORT=low set and a parent session that explicitly raised its own effort to high, the override path now runs the child at low:

REPRO resolved thinking_effort = Some(String("low"))   # left  = PR behavior
expected                        Some(String("high"))   # right = parent's value
assertion failed: child should inherit parent's raised thinking_effort

Root cause: this override path builds cfg via model_config_from_user_config(provider, &model) -> materialize_model_config(include_default_thinking_effort = true), so when GOOSE_THINKING_EFFORT is set, with_default_thinking_effort() has already seeded cfg.request_params["thinking_effort"] with the global default. The .or_insert(v) then sees the key present and drops the parent's explicit value. The non-delegate model-switch path (model_config_from_user_config_with_session_settings, built with include_default_thinking_effort = false) inherits before applying the default, so the session value wins there — the two paths diverge.

Re @kyledef's "useful side effect": when the parent has no explicit effort, overwriting changes nothing (there's no key to copy, and the default is preserved — verified below). The only case affected is when a user deliberately raised effort on the parent, which is exactly the case where their intent should carry into the subagent. So switching to overwrite preserves the default-when-absent behavior and only fixes the lost-explicit-value case.

The minimal fix (Codex's suggestion) — overwrite for the whitelisted key, at summon.rs:1628:

                            merged.insert(k.to_string(), v);

With that one-line change both cases pass locally, and the existing test_resolve_model_config_* tests stay green:

repro_thinking_effort_default_shadows_parent        ... high (parent wins)
repro_thinking_effort_default_kept_when_parent_absent ... low  (default kept)
test_resolve_model_config_inherits_thinking_effort_on_override ... ok
test_resolve_model_config_does_not_inherit_provider_specific_request_params ... ok

If it's useful, here's a regression test you could drop next to the existing ones (fails on the current or_insert, passes with insert):

#[tokio::test]
#[serial]
async fn test_resolve_model_config_parent_thinking_effort_overrides_global_default() {
    let _env = env_lock::lock_env([
        ("GOOSE_CONTEXT_LIMIT", None::<&str>),
        ("GOOSE_MAX_TOKENS", None::<&str>),
        ("GOOSE_SUBAGENT_MODEL", None::<&str>),
        ("GOOSE_THINKING_EFFORT", Some("low")),
    ]);

    let mut parent = parent_config();
    parent.request_params = Some(HashMap::from([(
        "thinking_effort".to_string(),
        serde_json::json!("high"),
    )]));

    let resolved = resolve_with_override(Some(OVERRIDE_MODEL), parent);

    assert_eq!(
        resolved.request_params.as_ref().and_then(|p| p.get("thinking_effort")),
        Some(&serde_json::json!("high")),
        "parent's explicit thinking_effort should win over the global default on override"
    );
}

One thing I checked and is not a problem: the ["thinking_effort"] allowlist isn't too narrow. thinking_effort is the only key with cross-family inheritance semantics elsewhere (ModelConfig::with_inherited_session_settings_from inherits exactly this one key across models), so limiting the allowlist to it matches the existing convention — nice and consistent.

Both fixes are clean and well-scoped; with the insert tweak I'd love to see this land.

— Reviewed with the help of an AI agent (Claude Code); I ran the branch and the reproduction above locally and verified the results myself before posting.

for k in ["thinking_effort"] {
if let Some(v) = parent_params.get(k).cloned() {
let merged = cfg.request_params.get_or_insert_with(Default::default);
merged.entry(k.to_string()).or_insert(v);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified locally: with a global GOOSE_THINKING_EFFORT default set, cfg is already seeded here, so .or_insert drops the parent's explicit value (the child ran low instead of the parent's high). Overwriting for this whitelisted key fixes it and keeps the default-when-parent-absent case intact (confirmed both directions pass).

Suggested change
merged.entry(k.to_string()).or_insert(v);
merged.insert(k.to_string(), v);

@DOsinga

DOsinga commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for chasing this down, Kyle — the analysis is solid and the two bugs are real.

The databricks.rs fix (always use parameters, drop the is_gemini branch) is clearly correct and self-contained. I'd suggest we split that out into its own PR so it can land immediately, and take it out of this one — there's no reason for the easy, unambiguous fix to wait on the trickier summon.rs discussion.

On the summon.rs side, I don't think the whitelist-loop approach is the right shape, and I think it's the source of the brittleness codex/@kimnamu are circling around:

  • The override path builds a ModelConfig from scratch and then hand-copies model-agnostic state back from the parent (toolshim, toolshim_model, fast_model_config, temperature, and now individual request_params keys). Every time we add a model-agnostic field we have to remember to copy it here — that's fragile by construction, and it's why the thinking_effort precedence bug exists.
  • This logic already exists as ModelConfig::with_inherited_session_settings_from / model_config_from_user_config_with_session_settings. That helper inherits thinking_effort from the previous config only if the new params don't specify it, then applies the global default after — i.e. the correct precedence (explicit > parent > default). Routing the override through it would fix the or_insert bug as a natural consequence and stop anthropic_beta leaking, without a bespoke whitelist.

More fundamentally: delegate() is an LLM-driven tool. Right now the agent picks model/provider/temperature but has no say over reasoning, so we're forced to infer thinking effort by copying from the parent — which is exactly why "the model didn't change but request_params jumped back to defaults" feels surprising. I'd rather make the delegating agent express intent explicitly: add a thinking_effort (or more general request-param) field to DelegateParams, then feed model + those params through model_config_from_user_config_with_session_settings(provider, &model, Some(&parent), <delegate params>, context_limit). Inheritance, precedence, and default-effort all fall out of the one canonical path, and the codex/@kimnamu precedence debate resolves itself.

So my suggestion: pull the databricks fix into its own PR, and rework the summon side to go through the existing session-settings helper (ideally with an explicit thinking_effort on DelegateParams) rather than the manual copy-back loop. Happy to talk it through if any of that is unclear.

…n-settings helper

delegate() with a model override previously built the child ModelConfig from
scratch and hand-copied model-agnostic state back from the parent, then merged
parent request_params with a whitelist loop. That was fragile (every new
model-agnostic field had to be remembered here) and had a precedence bug: with a
global GOOSE_THINKING_EFFORT default set, model_config_from_user_config() seeded
request_params before the merge, so the or_insert kept the default and dropped a
parent session's explicitly-raised thinking_effort.

Route the override through model_config_from_user_config_with_session_settings
instead. It materializes model-specific fields for the new model and inherits
thinking_effort from the parent with the correct precedence (explicit child
param > parent session > global default), while *not* carrying provider-specific
request_params (e.g. anthropic_beta) into a child targeting a different model
family — which would otherwise trigger a 400 INVALID_ARGUMENT.

Addresses review feedback (@DOsinga, @kimnamu, codex). The self-contained
Databricks tool-format fix that was previously bundled here is split into its
own PR.
@kyledef
kyledef force-pushed the kdefreitas/fix-delegate-request-params branch from 4e8e28f to 2f8b474 Compare June 30, 2026 04:05
@kyledef

kyledef commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Done on both counts. Split the self-contained Databricks parameters fix into its own PR (#10101), and reworked the summon side to route the override through model_config_from_user_config_with_session_settings as you suggested — the manual copy-back whitelist loop is gone, and the helper handles thinking_effort inheritance/precedence and provider-param filtering as a natural consequence, which also resolves the codex/@kimnamu or_insert precedence thread. This PR is now summon-only; anthropic_beta-exclusion and thinking_effort-inheritance are both covered by tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f8b474763

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

provider_name,
&model,
Some(&parent),
None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve compatible request params on overrides

When delegate() overrides the model but stays on a compatible provider/model family (for example, an ACP/predefined Anthropic/Databricks Claude session carrying supported params such as budget_tokens or preserve_thinking_context and delegating to another Claude model), passing None here makes with_inherited_session_settings_from keep only thinking_effort and drop the rest of the parent request_params. Those keys are consumed by the provider formatters to shape the request, so subagents silently lose required session/request settings even though no cross-family Gemini bleed is happening; please filter only incompatible params or rebuild the target model's own predefined params instead of discarding all non-thinking_effort params.

Useful? React with 👍 / 👎.

Douwe M Osinga added 2 commits June 30, 2026 15:39
Generalize with_inherited_session_settings_from to carry the set of
model-family-agnostic reasoning params (thinking_effort, budget_tokens,
preserve_thinking_context, etc.) across a model switch or subagent
delegation, while still dropping provider-specific keys like
anthropic_beta. Addresses the codex review note that overriding to a
same-family model previously dropped budget_tokens and related settings.

@DOsinga DOsinga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a follow-up commit (fb2fff0) to address the codex P2 note about same-family overrides dropping reasoning params.

The override path now inherits a defined allowlist of model-family-agnostic reasoning controls — thinking_effort, thinking_budget, budget_tokens, enable_thinking, preserve_thinking_context, preserve_unsigned_thinking — across a model switch / subagent delegation, while still dropping provider-specific keys like anthropic_beta so they can't bleed into a different model family. These are exactly the keys the provider formatters read out of request_params. The change lives in with_inherited_session_settings_from, so the two ACP model-switch call sites benefit too. Added test coverage in both model.rs and summon.rs.

Thanks for the clean rework of the summon path through the canonical helper — LGTM.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb2fff04dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +17 to +21
"thinking_budget",
"budget_tokens",
"enable_thinking",
"preserve_thinking_context",
"preserve_unsigned_thinking",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop inheriting provider-scoped params globally

When a parent session carries provider-specific reasoning params such as budget_tokens, thinking_budget, or preserve_thinking_context and delegate()/ACP switches to an incompatible target, this whitelist now copies those keys into the child config. Several target formatters then forward unknown request params (for example OpenAI chat and Databricks forward everything except thinking_effort), so a Claude parent with a custom budget can still make a Gemini/OpenAI child fail with invalid top-level fields—the same class of cross-family bleed this change is trying to avoid. Please filter these params by the resolved target provider/model instead of treating the whole list as globally safe.

Useful? React with 👍 / 👎.

@DOsinga
DOsinga added this pull request to the merge queue Jul 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fcc006a030

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const INHERITED_SESSION_PARAM_KEYS: &[&str] = &[
"thinking_effort",
"thinking_budget",
"budget_tokens",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter inherited params by target provider

Fresh evidence in this revision is that budget_tokens is now explicitly included in the global inherited whitelist. When a Claude/Anthropic parent with budget_tokens delegates or switches to a non-Claude target such as Databricks Gemini or OpenAI chat, with_inherited_session_settings_from copies that key into the child config, and those formatters forward unknown request params while only skipping thinking_effort, producing unsupported top-level fields and the same cross-family request failure this change is trying to prevent; only inherit this key when the resolved target formatter consumes it.

Useful? React with 👍 / 👎.

@DOsinga
DOsinga added this pull request to the merge queue Jul 3, 2026
Merged via the queue into aaif-goose:main with commit d873c91 Jul 3, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants