Skip to content

fix: pass AI Config model parameters through to every provider handler - #107

Open
apucacao wants to merge 45 commits into
mainfrom
alexis/forward-model-parameters
Open

apucacao wants to merge 45 commits into
mainfrom
alexis/forward-model-parameters

Conversation

@apucacao

@apucacao apucacao commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Every time I ask "how would I use the turnkey SDK here?", I hear about how model params aren't currently being passed to models. This PR fixes that.

Not entirely sure about the implementation though: we ended up definiting the list of params supported by each of the underlying SDKs, because I couldn't find a better export from those packages, and iterating on types felt harder than the explicit list. The downside is that if/when things change, we'd need to update the code. But presumably we already need to do that for our model parameter schemas in Gonfalon.


Model parameters set in an AI Config were silently ignored by most handlers. Every handler now passes them through to the provider.

  • Each handler forwards exactly the list for that handler in the cross-SDK spec (ai-sdks-monorepo feat: emit gen_ai.conversation.id #42, TESTING.md 1.12), the same lists the JS SDK uses. Any other key is dropped. A test per handler sets one key at a time and checks the forwarded set equals the spec list.
  • Never forwarded, by any handler: credentials, endpoints and connection settings (base URL, region, headers, timeouts, retries, HTTP clients), raw request bags (extra_*, model_kwargs), remote MCP servers, the Claude Agent SDK's host-process settings (cli_path, env, cwd, add_dirs, permission_mode, sessions, hooks, callbacks), and keys that set data location or retention, server-side state, identity, runtime wiring or API shape, extra prompt content, or safety configuration (for example store, truncation, metadata, user, include, system, guardrails).
  • invoke and stream forward the same keys, and so do each handler and its native graph.
  • A key the provider expects as an object (thinking, reasoning, text, ...) is dropped when the config sets it to anything else.
  • A test finds every handler module under packages/ and fails if one has no forwarded-keys list, so a new handler package must declare one.
  • Configs use snake_case (max_turns, top_p), the providers' own names and what the UI writes.
  • Every Python provider SDK takes snake_case, so all handlers pass keys through as-is.
  • The TypeScript SDK converts to camelCase for its agent and LangChain handlers only.

🤖 Generated with Claude Code


Note

Overview
AI Config model.parameters are now applied on provider calls instead of being ignored. A shared model_parameters reader and select_forwarded_parameters filter sit in the client package; each handler merges only its hand-maintained forwarded key set (cross-SDK TESTING.md 1.12) into ClaudeAgentOptions, Anthropic Messages, LangChain chat constructors, or LiteLLM ModelSettings / Runner.

Security and consistency: Credentials, endpoints, extra_* / model_kwargs, stream wiring, and Claude Agents host controls (cli_path, env, cwd, permissions, sessions, etc.) are explicitly excluded and dropped. Handler-owned fields (model, messages, system, tools) cannot be overridden from parameters. UI keys the SDK does not accept are dropped without TypeError; object-typed params with wrong shapes are dropped. Claude Messages maps top-level effort into output_config; LangChain applies alias renames (max_tokens_to_sample, stop); LiteLLM tightens reasoning / tool_choice and stops reading camelCase aliases.

Coverage: Native graph paths use the same forwarding as invoke/stream (e.g. root max_turns for LiteLLM graphs). Large drift/probe/never_forwarded test suites lock the allowlists to each SDK’s surface.

Reviewed by Cursor Bugbot for commit 172a7ac. Bugbot is set up for automated code reviews on this repo. Configure here.

model.parameters is a free-form dict the LaunchDarkly UI writes for
provider tuning values, including max_turns for agent turn caps. Every
Python handler except claude-messages ignored it entirely, and
claude-messages read only max_tokens. In particular there was no way
to cap an agent's turns: claude-agents never set
ClaudeAgentOptions.max_turns and openai-agents never passed max_turns
to Runner.run.

Adds a shared launchdarkly_ai_server.model_parameters(config) helper
that returns model.parameters as a fresh dict (or {} when absent/not
a mapping), and never reads model.custom. Applies it at every provider
call site across all six handler packages, including streaming and
native-graph paths. Keys are forwarded as-is (snake_case, matching
both the UI and every Python provider SDK here); no allowlisting and
no case conversion.

Handler-owned keys (model, messages/input, system/instructions, tools,
etc., decided per call site) are always removed from the forwarded
dict before merging with the handler's own kwargs, so a config value
can never override what the handler itself sets. claude-messages keeps
its max_tokens-defaults-to-1024 behaviour exactly.

LangChain's handlers already forwarded model.parameters through a
local _model_constructor_kwargs helper; swapped that to use the new
shared helper for consistency.

Verified: uv run pytest (1332 passed, 11 skipped), uv run mypy
packages/*/src, uv run ruff check ., uv run ruff format --check .

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@apucacao apucacao changed the title fix(client): forward model.parameters to every provider handler fix: pass AI Config model parameters through to every provider handler Sep 24, 2026
@apucacao

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread packages/claude-agents/src/launchdarkly_ai_claude_agents/handler.py
…ccepts

model.parameters is a free-form dict the LaunchDarkly UI writes, offering
keys that make sense across providers but that no single provider SDK
accepts in full. Forwarding it unfiltered (added in a prior commit on this
branch) raised TypeError before any request for several real configs:
ClaudeAgentOptions has no temperature/top_p/top_k/max_tokens/
stop_sequences/tool_choice/metadata; the OpenAI Responses API has no
max_tokens/frequency_penalty/presence_penalty/seed/n/stop/response_format/
logit_bias/logprobs/max_completion_tokens/audio/modalities/prediction;
Anthropic's Messages API has no top-level effort.

Adds launchdarkly_ai_server.parameter_forwarding, a shared filter used by
every handler: accept-sets are derived once from the live provider type
(inspect.signature for plain methods, dataclasses.fields for
ClaudeAgentOptions/ModelSettings, pydantic model_fields + aliases for the
LangChain chat models), never hand-listed, so an SDK's own drift is picked
up automatically. transport/escape-hatch keys (extra_headers, extra_query,
extra_body, timeout, extra_*) are stripped unconditionally regardless of
what a signature accepts, and each call site may additionally exclude keys
the API accepts but that would break the handler because the handler
itself already decides that behaviour: stream/stream_options/background/
conversation/prompt for the OpenAI Responses API, stream for Anthropic
Messages. Everything else the provider accepts is forwarded, including
keys that are not strictly generation settings (store, user,
safety_identifier, prompt_cache_key, prompt_cache_retention, include,
context_management, metadata, service_tier, instructions, moderation).

Two renames, applied before the accept filter: openai-messages maps
max_tokens/max_completion_tokens to the Responses API's max_output_tokens
(explicit max_output_tokens wins, then max_completion_tokens, then
max_tokens); claude-messages maps a top-level effort into
output_config.effort, unless the config already sets its own
output_config.effort, which wins.

Applied at every call site across all six handlers, including streaming
and native-graph paths. claude-agents/openai-agents dataclass accept-sets
are computed once at module load from the real SDK types, independent of
each package's per-call importlib.import_module mocking in tests.
LangChain's per-provider pydantic accept-sets are cached per class with
functools.cache, since the constructor class is only known once a provider
branch resolves langchain_openai/_anthropic/_aws.

Updated the existing tests whose old assertions relied on unfiltered
pass-through of a key the real SDK does not accept (tools forwarded to
ChatOpenAI's constructor in two langchain-agents/messages tests) to give
the mocked provider class an accurate model_fields shape and to reflect
the value now being dropped.

Verified: uv run pytest (1383 passed, 11 skipped), uv run mypy
packages/*/src, uv run ruff check ., uv run ruff format --check ..
uv.lock unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@apucacao

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

…warded-key lists

Every handler decided which model.parameters keys to forward by introspecting the
live provider SDK at runtime: inspect.signature for the two Anthropic/OpenAI methods,
dataclasses.fields for ClaudeAgentOptions/ModelSettings, pydantic model_fields plus
aliases for the three LangChain chat models. Every competitor SDK (LiteLLM, Vercel AI
SDK, Braintrust, LangChain) and this SDK's own TypeScript port use a written-down list
instead, so replaces the derivation with one: each handler now carries a literal
frozenset of the keys it forwards, generated once from the same introspection and
pasted in as a reviewable list, next to its existing handler-owned and excluded sets.

Deletes accepted_parameter_keys_from_signature/_from_dataclass/_from_pydantic_model,
_alias_strings, is_transport_parameter, strip_transport_parameters, and
filter_forwardable_parameters from packages/client's parameter_forwarding module now
that nothing calls them. In their place, select_forwarded_parameters(params, keys) is
the one shared piece left: keep the params keys that are on a handler's own forwarded
list, drop everything else.

Reproduces every handler's existing forwarded/owned/excluded classification exactly,
with one deliberate addition applied consistently: client/connection configuration
(API keys, base URLs, organization ids, HTTP clients, default headers/query, proxies,
timeouts, retry counts) is now always excluded, even when a provider's own accept-set
would otherwise let it through. This newly excludes, per handler:

- langchain-messages / langchain-agents (ChatOpenAI): api_key, openai_api_key,
  base_url, openai_api_base, organization, openai_organization, openai_proxy, client,
  async_client, root_client, root_async_client, http_client, http_async_client,
  http_socket_options, default_headers, default_query, max_retries, request_timeout
- langchain-messages / langchain-agents (ChatAnthropic): anthropic_api_key, api_key,
  anthropic_api_url, base_url, anthropic_proxy, default_request_timeout, max_retries,
  default_headers
- langchain-messages / langchain-agents (ChatBedrockConverse, opt-in dependency):
  bedrock_api_key, api_key, aws_access_key_id, aws_secret_access_key,
  aws_session_token, credentials_profile_name, endpoint_url, base_url, client,
  bedrock_client, config, default_headers, max_retries
- openai-agents (ModelSettings): retry

timeout/extra_headers/extra_query/extra_body/extra_args were already excluded by the
prior transport-prefix rule on every handler that has them; they are now named
explicitly in each handler's own excluded set instead of matched by a shared prefix
check, since nothing here is derived at runtime any more.

Adds one drift test per handler package (test_parameter_forwarding.py) that reads the
real provider SDK's signature/dataclass/model_fields and asserts every parameter it
accepts is classified in exactly one of forwarded, handler-owned, or excluded, so an
SDK addition nobody has classified fails loudly by name, and so does a stale list
entry. The LangChain ChatBedrockConverse test skips itself when langchain-aws (an
opt-in dependency) is not installed, matching this package's existing Bedrock test
pattern.

Updates langchain-messages' TestModelParametersReachTheWire test, which relied on
forwarding http_async_client/api_key through model.parameters to mock the outgoing
HTTP call: it now intercepts ChatOpenAI's own default httpx client builder instead, so
the mock transport is reached without any config value ever naming an HTTP client or a
key. Adds TestConnectionConfigIsNeverForwarded to both langchain-messages and
langchain-agents, asserting api_key/base_url from model.parameters never reach the
ChatOpenAI/ChatAnthropic constructor kwargs.

Verified: uv run --frozen pytest (1385 passed, 15 skipped), uv run --frozen mypy
packages/*/src, uv run --frozen ruff check ., uv run --frozen ruff format --check .,
uv.lock unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@apucacao

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread packages/langchain-agents/src/launchdarkly_ai_langchain_agents/handler.py Outdated
…eters

They are the same constructor field as `model`, under its field name or its
alias. The handler always sets `model` itself, so a config value for either
collided with the resolved model name or overrode it. Classify both as
handler-owned per model class, and have the drift tests read that set from
the handler instead of a test-local copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@apucacao

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@apucacao
apucacao marked this pull request as ready for review September 25, 2026 14:48

@jeffdupont jeffdupont 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.

Reviewed against the 1.0 GA plan. The fix is needed: parameters set in an AI Config were being ignored, and that should be fixed before 1.0. Tests pass at d51199d (make test: 1391 passed, 15 skipped, exit 0). The branch merges cleanly onto current main (fee904a), and the merged tree passes too: 1491 passed, make typecheck clean.

Four things I think need settling before merge, because each one changes the public interface and can't be changed after 1.0:

  1. Claude Agents forwards host-process settings, not just model settings. A config can set cli_path, env, cwd, add_dirs, permission_mode, settings, setting_sources, plugins and sandbox. I reproduced it: a config with cli_path: /tmp/attacker-binary makes the SDK launch that file as its agent process (claude_agent_sdk/_internal/transport/subprocess_cli.py:225), and env: {ANTHROPIC_BASE_URL: ...} points the customer's API key at another host (:434). That turns "can edit an AI Config" into "can run code on the customer's server." Inline comment below.
  2. Two LangChain keys get around the exclusion list. model_kwargs carries extra_headers / extra_query into the ChatOpenAI request payload (reproduced with _get_request_payload), though both are listed as excluded. mcp_servers (ChatAnthropic) would let a config attach a remote MCP server; I haven't tested that one.
  3. Two new public names. model_parameters and select_forwarded_parameters are in the root __all__, but only the provider packages use them. Same point as on #121 with make_graph_track_data: they belong in the internal group, or the 1.0 surface trim has to remove them again.
  4. Python and JS don't match (js #73). The two messages handlers agree. In the agent and LangChain handlers, JS forwards the whole camelCased bag with no allowlist, so api_key becomes apiKey, which ChatOpenAI and ChatAnthropic use. I'll leave a note there too. The monorepo has no spec for forwarding these parameters, and open monorepo #10 says "do not rename or filter parameter keys," which contradicts both PRs. One spec in TESTING.md would settle it for both languages.

Smaller points, not blocking:

  • output_format (Claude Messages) and response_id / starting_after / text_format (OpenAI) are forwarded on stream but not invoke, so one config can behave differently depending on the call.
  • OpenAI Responses forwards reasoning but not reasoning_effort. If the UI writes reasoning_effort, it's dropped silently, as effort was before your rename. I haven't checked what the UI writes.
  • The loops that remove handler-owned keys never find anything, since those keys aren't on the forwarded lists. They're harmless, just dead code.
  • In native graphs only the root node's max_turns is used.

Removing _HAS_ANTHROPIC is fine; nothing on main reads it.

Comment thread packages/claude-agents/src/launchdarkly_ai_claude_agents/handler.py
Comment thread packages/langchain-agents/src/launchdarkly_ai_langchain_agents/handler.py Outdated
Comment thread packages/langchain-agents/src/launchdarkly_ai_langchain_agents/handler.py Outdated
Comment thread packages/client/src/launchdarkly_ai_server/__init__.py Outdated
Comment thread packages/claude-messages/src/launchdarkly_ai_claude_messages/handler.py Outdated
@apucacao

apucacao commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread packages/claude-agents/src/launchdarkly_ai_claude_agents/handler.py
Comment thread packages/langchain-agents/src/launchdarkly_ai_langchain_agents/handler.py Outdated
Comment thread packages/langchain-agents/src/launchdarkly_ai_langchain_agents/handler.py Outdated
…d_parameters from the package root

Only the provider packages in this repo use these helpers, and anything in
__all__ becomes public API at 1.0. Callers now import them from the modules
that define them: launchdarkly_ai_server.utils and
launchdarkly_ai_server.parameter_forwarding.
@apucacao

apucacao commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

…rameters

ClaudeAgentOptions also configures the host process the SDK launches: which
binary runs, its environment, working directory, file access, permission
mode, settings, plugins, sandbox, and session state. Forwarding those from a
config let anyone who can edit an AI Config launch their own binary or point
the API key at another host.

The forwarded list is now max_turns, max_thinking_tokens, thinking, effort,
max_budget_usd, fallback_model, output_format and betas. Every other field is
excluded, with the reason next to it.

The loops that popped handler-owned keys are removed: none of those keys is
on the forwarded list, so the filter already drops them.
model_kwargs is merged straight into the request payload, so it carried
extra_headers and extra_query past the exclusions. mcp_servers let a config
attach a remote MCP server that then receives the conversation. region_name
and inference_geo move where data is processed. Bedrock's
additional_model_request_fields is another unfiltered request bag.

All of those are now excluded, along with LangChain's own runtime fields
(callbacks, cache, tags, metadata, streaming mode, message format, ...),
which configure how LangChain runs in this process rather than the request.

_model_constructor_kwargs now requires its forwarded list, and the dead pop
of tools for Bedrock is removed.
output_format is only accepted by messages.stream, so a config with it
behaved differently depending on the call. It is now dropped on both paths;
output_config, which both accept, carries the output format. One forwarded
list now serves both calls.

inference_geo is excluded too: it sets the region inference runs in.

The loops that popped handler-owned keys are removed, since none of those
keys is on the forwarded list.
…hropic

ChatAnthropic takes both spellings of one field, so a config that set both
left the winner to pydantic. The alias is now renamed to max_tokens before
forwarding, and a max_tokens the config also set wins.
@apucacao

apucacao commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

…sity win

A forwarded reasoning object becomes the SDK's own Reasoning type with only
effort and summary, the same two sub-keys the JS SDK keeps, and is dropped
when it has neither. A test runs the real Chat Completions model with it.

When a config sets both text.verbosity and a top-level verbosity, the
explicit text.verbosity now wins, matching the JS SDK.
ChatOpenAI has no prompt_cache_key field, so the key is cut from the
cross-SDK ChatOpenAI list rather than worked around. It is documented on the
excluded side with that reason, and a test checks a config value never
reaches the constructor.
@apucacao

apucacao commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@apucacao

apucacao commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@jeffdupont jeffdupont 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.

Re-reviewed at db839fe5. The credential and redirect work is done. No endpoint, key or header setting reaches a provider from any handler, and the tests prove it. Removing the filter fails 99 tests, and putting single keys back fails 5 to 10 each. The discovery test also fires: a trial merge with py #103 or py #102 goes red until they add allowlists. All seven Python lists match the monorepo #42 §1.12 tables. Suite: 1721 passed, 11 skipped, mypy and ruff clean; merged with main it's 2570 passed.

Three things before I'd approve:

  1. Two malformed settings now crash a run (inline). Spec §1.12 says a nested object without a type is dropped, and js #118 drops them (isTypedObject).

    • Claude Agents: thinking: {"budget_tokens": 1024} raises KeyError: 'type' in claude_agent_sdk 0.2.110's command builder, because it reads t["type"] directly. {"type": "enabled"} with no budget raises KeyError: 'budget_tokens'. Before this PR nothing was forwarded, so these configs ran.
    • OpenAI Agents: tool_choice: {"type": "auto"} makes ModelSettings(**...) raise a pydantic ValidationError (handler.py:332). JS drops a non-string toolChoice.
    • The same gap without a crash: Claude Messages forwards thinking, tool_choice and cache_control objects without a type unchanged, and openai-messages passes unknown reasoning sub-keys through. §1.12 keeps only effort and summary.

    The probe tests didn't catch any of this because probe_forwarded_keys counts an exception as "forwarded" (inline).

  2. The handlers need a lower bound on launchdarkly-ai-server. Six handler packages now import launchdarkly_ai_server.parameter_forwarding and utils.model_parameters, but declare a bare "launchdarkly-ai-server". PyPI's latest is 0.3.0, which has neither, so importing launchdarkly_ai_claude_messages against it fails with ModuleNotFoundError. A >= on the client release that ships these modules, in each handler's pyproject.toml, fixes it. (Not in this diff, so no inline.)

  3. Merge order. The 4 open threads (inference_geo / container, use_responses_api, Bedrock system, openai-agents parity) are all done in Python. They resolve once js #118 and monorepo #42 land with this PR. Both are drafts, and #42 has no human review yet.

Not blocking: with both stop and stop_sequences set, ChatAnthropic sends stop here and JS sends stop_sequences. The spec gives no precedence for the alias pairs, so that's a spec question for #42.


#: Forwarded keys whose value must be an object. A config value of any other type is malformed
#: and dropped rather than passed to ``ClaudeAgentOptions``.
_CLAUDE_AGENT_OPTIONS_MAPPING_KEYS = frozenset({"output_format", "thinking"})

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.

This checks that thinking is a mapping but not its shape. claude_agent_sdk 0.2.110 reads t["type"] and then t["budget_tokens"] straight away when it builds the CLI command. So {"budget_tokens": 1024} raises KeyError: 'type', and {"type": "enabled"} raises KeyError: 'budget_tokens'. Both configs ran before this PR. Could thinking be dropped unless it has a known type (and budget_tokens for enabled), as §1.12 and js #118 do?

name="assistant",
model=config.get("model", {}).get("name", "gpt-4o"),
**(
{"model_settings": agents_mod.ModelSettings(**model_settings_params)}

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.

tool_choice: {"type": "auto"} reaches here and ModelSettings(**model_settings_params) raises a pydantic ValidationError (4 errors, reproduced). JS drops a non-string toolChoice. Dropping it in _model_settings_parameters when it isn't a string would match.

#: Forwarded keys whose value must be an object. A config value of any other type is malformed
#: and dropped rather than passed to the API.
_MESSAGES_MAPPING_KEYS = frozenset(
{"cache_control", "output_config", "thinking", "tool_choice"}

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.

These are checked for being mappings only, so thinking: {"budget_tokens": 1}, tool_choice: {} and cache_control: {} are forwarded unchanged. §1.12 drops a nested object without a type, and js #118 does (isTypedObject). It doesn't crash here, but the Anthropic API will reject it.


#: Forwarded keys whose value must be an object. A config value of any other type is malformed
#: and dropped rather than passed to the API.
_RESPONSES_MAPPING_KEYS = frozenset({"reasoning"})

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.

reasoning is forwarded as-is, so {"effort": "low", "generate_summary": "auto", "x": 1} reaches the request. §1.12 keeps only effort and summary, and js #118's toReasoning does the same.

Comment thread tests/forwarding_spec.py
for key in sorted(set(candidates)):
try:
result = await call({key: sample(key)})
except Exception:

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.

Counting an exception as "forwarded" hides the crashes above. SAMPLE_VALUES["tool_choice"] is exactly the object that makes ModelSettings raise, so the probe reports it as forwarded and passes. Could an exception fail the probe instead, or be reported separately?

apucacao and others added 16 commits October 21, 2026 18:22
The forwarded-key lists supersede the tools pop from #133: no list
includes tools, so a tools key in model.parameters still never reaches
a LangChain constructor, and #133's tests pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some libraries take a different valid shape for the same key. Claude
Messages takes tool_choice as an object, while the OpenAI Agents model
settings over LiteLLM and the Python Vercel AI SDK take only a string.
The probe now accepts per-handler overrides, so each handler can be
probed with a value its own library accepts.

Also adds valid samples for max_tool_calls, reasoning_effort and seed,
which the Vercel handlers forward and type-check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orwarded bag

litellm.acompletion takes **kwargs and reads its own names for endpoints,
credentials, routing and retries from them: api_base, api_version,
deployment_id, custom_llm_provider, model_list, fallbacks, num_retries,
shared_session, vertex_credentials and aws_region_name. mock_response skips
the model call entirely. A config value under any of these names could
redirect the request or swap the credentials, so every handler's probe and
bag test now checks them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handler already kept an allowlist, but under its own name and with a
hand-written filter, so the never-forwarded check could not find it. The
same 11 keys are now _COMPLETION_FORWARDED_KEYS, applied with
select_forwarded_parameters, and registered with the cross-handler test.

Every named acompletion parameter is now classified as forwarded,
handler-owned or excluded, with the reason for each exclusion, and a drift
test fails when litellm adds one nobody has classified. A probe checks that
invoke and stream forward exactly this list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handler forwarded store, which is data retention and on the
never-forwarded list, and verbosity, which LitellmModel never reads and so
silently dropped. Both are gone. The list is now
_MODEL_SETTINGS_FORWARDED_KEYS, applied with select_forwarded_parameters
and registered with the cross-handler test: the eight ModelSettings fields
LitellmModel actually sends, plus max_turns for the run.

reasoning keeps only effort. LitellmModel sends it as reasoning_effort and
warns on every call that also sets summary. tool_choice is forwarded only
as a string, because ModelSettings rejects an object value while the Agent
is built, which turned a config edit into a failed run.

The camelCase aliases are gone too. Every other Python handler reads
snake_case only, which is what the LaunchDarkly UI writes.

A drift test classifies every ModelSettings field and checks each
forwarded one is read by LitellmModel, and a probe checks invoke and
stream forward exactly this list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aph run

The native graph built every node's ModelSettings from the handler's list
but never passed max_turns to Runner.run, so a graph always ran with the
SDK default whatever the root config said. The whole graph is one run and
the SDK counts turns once across every handoff, so the root's value applies,
as in the OpenAI Agents native graph.

A probe of the root node now checks the graph forwards exactly the LiteLLM
Agents list, and a bag test checks it forwards no never-forwarded key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handler copied model.parameters, removed a set of owned names, and
mapped what was left. That forwarded metadata and safety_identifier, which
are identity and attribution and on the never-forwarded list, and min_p and
repetition_penalty, which every built-in provider rejects, so setting one
failed every call. It also dropped frequency_penalty, presence_penalty,
max_output_tokens, max_completion_tokens and tool_choice.

The list is now _INFERENCE_PARAMS_FORWARDED_KEYS, applied with
select_forwarded_parameters after the renames: max_output_tokens wins over
max_completion_tokens, which wins over max_tokens, and reasoning wins over
reasoning_effort. Each key is rebuilt as the library's typed param.
reasoning is a string or an object read for its effort, and "none"
becomes effort None, the library's way to turn reasoning off. tool_choice
is forwarded only as auto, none or required, as ToolChoiceMode; an object
is dropped.

Each SDK forwards what its own library accepts, so this list also carries
parallel_tool_calls, max_tool_calls, service_tier and prompt_cache_key,
which the Python library takes as typed params and the JS AI SDK cannot
set outside providerOptions. stop_sequences stays off: the Python library
has no field for it.

The camelCase aliases and the owned-names set are gone, since an allowlist
already drops everything else. The test that asserted metadata and
safety_identifier reach the provider now asserts they do not. A drift test
classifies every InferenceRequestParams field, and a probe checks invoke
and stream forward exactly this list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handler and the native graph shared one params builder with the same
problems as vercel-messages: it forwarded metadata and safety_identifier
(identity and attribution), min_p and repetition_penalty (every built-in
provider rejects them), accepted camelCase aliases, and dropped
frequency_penalty, presence_penalty, max_output_tokens,
max_completion_tokens and tool_choice.

The builder now forwards _INFERENCE_PARAMS_FORWARDED_KEYS with the same
renames and typed rebuilds as vercel-messages, including the keys the
Python library takes natively and the JS AI SDK cannot: parallel_tool_calls,
max_tool_calls, service_tier and prompt_cache_key. The native graph calls
the same builder for every node, so it forwards the same list.

The owned-names set is gone, the leak test now asserts metadata and
safety_identifier never reach the provider, and probes check invoke,
stream and the native graph forward exactly this list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenCode's message route takes no sampling, length, stop, seed,
tool-choice or reasoning field. Those come from the OpenCode server's own
agent and provider config, which the handler does not control per call.
The fields the route does take are handler-owned (parts, model, system,
agent, tools), change where the answer lands (format), wire the run
(noReply), name session state (messageID), or name a server-side preset
whose meaning depends on the operator's config (variant).

So the handler forwards nothing, and now says so with an empty
_PROMPT_FORWARDED_KEYS applied with select_forwarded_parameters ahead of
the handler's own body fields. It is registered with the cross-handler
test, which no longer reports any handler module without a list, and a
probe checks no model.parameters key changes the posted body on invoke or
stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The imports this branch added for select_forwarded_parameters and
model_parameters sat out of order in nine modules, so make lint failed.
This is ruff's own fix, with no change in behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commit 8c1f9f7 cut max_tool_calls only because the JS openai SDK's
responses.create has no such parameter. The spec now keeps a key that one
language's library cannot take and marks it not supported there, so the
Python handler forwards it again.

Checked against openai 2.44.0 in the uv env: AsyncResponses.create and
AsyncResponses.stream (openai/resources/responses/responses.py) both take
max_tool_calls, so invoke and stream still forward the same keys. The key
goes back on _RESPONSES_FORWARDED_KEYS and OPENAI_MESSAGES, and off the
excluded list. uv run pytest -q packages/openai-messages: 133 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seed came off the ChatOpenAI list in 1c60bdf with no reason beyond not
being on the cross-SDK list, which had cut it because the JS ChatOpenAI
takes seed only as a per-call option. The spec now keeps a key that one
language's library cannot take where the handler sets it and marks it not
supported there, so both Python LangChain handlers forward it again.

Checked against langchain-openai 1.3.3 in the uv env: ChatOpenAI declares
seed as a constructor field (langchain_openai/chat_models/base.py) and
_default_params sends it on every Chat Completions request. It is not one
of the fields _use_responses_api switches on. The JS side was checked in
@langchain/openai 1.4.7: seed appears only in BaseChatOpenAICallOptions and
callKeys, not on the constructor fields.

uv run pytest -q packages/langchain-agents packages/langchain-messages:
366 passed, with the probes asserting the ChatOpenAI list exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reasoning_effort came off the ChatOpenAI list in 1c60bdf so a config could
not set reasoning by one spelling and not the other, and the spec held
reasoning control back until both SDKs could set it without switching
APIs. The JS ChatOpenAI takes reasoningEffort only as a per-call option, so
that was a parity cut. The spec now marks such a key not supported in the
language that cannot take it, so both Python LangChain handlers forward it
again. reasoning itself stays excluded, because it does switch APIs.

Checked against langchain-openai 1.3.3 in the uv env
(langchain_openai/chat_models/base.py): reasoning_effort is a constructor
field documented for the Chat Completions API, _default_params sends it,
and neither ChatOpenAI._use_responses_api nor the module-level
_use_responses_api reads it (they check reasoning, include, truncation,
context_management, text and previous_response_id). A new test in each
package builds a real ChatOpenAI from the forwarded kwargs and asserts the
payload is a Chat Completions one carrying reasoning_effort.

uv run pytest -q packages/langchain-agents packages/langchain-messages:
368 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…drockConverse again

stop, stop_sequences and reasoning_effort came off the ChatBedrockConverse
list in 1c60bdf because the spec cut them for both SDKs: the JS
ChatBedrockConverse takes stop only as a per-call option and has no
reasoning effort field. The spec now marks such a key not supported in the
language that cannot take it, so both Python LangChain handlers forward
them again.

Checked against langchain-aws 1.7.9 in the uv env
(langchain_aws/chat_models/bedrock_converse.py): stop_sequences is a
constructor field with the alias stop, sent as inferenceConfig.stopSequences,
and reasoning_effort is a typed constructor field that the class translates
per model family (langchain_aws/utils.py reasoning_effort_additional_fields),
checking the value against the model profile. The reasons the handler gave
for leaving them out do not hold for this handler: the stop merge it named
is in the prompt_prefill structured-output path, and the handler calls
with_structured_output with the default function_calling method.

The class rejects a call that passes both stop and stop_sequences, so stop
is renamed to stop_sequences before forwarding and stop_sequences wins, the
same way ChatAnthropic handles max_tokens_to_sample. New tests cover the
rename and build a real ChatBedrockConverse from the forwarded kwargs.

output_config stays excluded, for a reason other than parity: it only
carries Bedrock's JSON schema output format, which the handler owns through
outputFormat, and a constructor value would be sent alongside the forced
tool call the handler's structured-output turn makes.

uv run pytest -q packages/langchain-agents packages/langchain-messages:
374 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ChatOpenAI exclusion comment and its test still said the key was cut
from the cross-SDK list. The spec now keeps it and marks it not supported
in Python, since ChatOpenAI has no such constructor field, and the JS
handler forwards it. Comment and docstring only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each SDK now forwards what its own library accepts, so the JS SDK is no
longer held to the same lists. The docstring said it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 172a7ac. Configure here.


#: Forwarded keys whose value must be an object. A config value of any other type is malformed
#: and dropped rather than passed to ``ClaudeAgentOptions``.
_CLAUDE_AGENT_OPTIONS_MAPPING_KEYS = frozenset({"output_format", "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.

Incomplete thinking objects crash Claude Agents

High Severity

thinking is kept whenever the value is a mapping, so a value missing type, or budget_tokens when type is enabled, is passed into ClaudeAgentOptions. The Claude Agent SDK then raises KeyError while building the CLI command. Those configs were ignored before this change and now fail the run.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 172a7ac. Configure here.

name="assistant",
model=config.get("model", {}).get("name", "gpt-4o"),
**(
{"model_settings": agents_mod.ModelSettings(**model_settings_params)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Object tool_choice crashes OpenAI Agents

High Severity

_model_settings_parameters forwards tool_choice unchanged. An object value, the shape the UI and the shared sample use, is passed to ModelSettings and raises a pydantic ValidationError while the agent is built. LiteLLM Agents already drops a non-string tool_choice for this reason.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 172a7ac. Configure here.

#: and dropped rather than passed to the API.
_MESSAGES_MAPPING_KEYS = frozenset(
{"cache_control", "output_config", "thinking", "tool_choice"}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Untyped nested objects still forwarded

Medium Severity

thinking, tool_choice, and cache_control are kept whenever the value is a mapping. An object without a type still reaches the Anthropic API, which rejects the request. The shared filter only checks the top-level type, so previously ignored incomplete objects now fail the call.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 172a7ac. Configure here.

Comment thread tests/forwarding_spec.py
result = await call({key: sample(key, overrides)})
except Exception:
forwarded.add(key)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Probe treats crashes as forwarded

Medium Severity

probe_forwarded_keys counts any exception as evidence that a key was forwarded. SAMPLE_VALUES["tool_choice"] is the object that makes ModelSettings raise, so the OpenAI Agents probe reports the key as forwarded and passes while the handler crashes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 172a7ac. Configure here.

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.

2 participants