Skip to content

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

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

apucacao wants to merge 28 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 now reach provider SDKs through a shared filter (model_parameters + select_forwarded_parameters) instead of being ignored or passed through wholesale.

Each Python handler declares hand-maintained allowlists (cross-SDK spec) for what may be forwarded into ClaudeAgentOptions, Anthropic messages.create/.stream, LangChain chat model constructors, or OpenAI Agents ModelSettings / Runner.run. Everything else—including credentials, endpoints, extra_* / model_kwargs, Claude Agent host-process settings (cli_path, env, permission_mode, …), and UI-only keys the SDK does not accept—is dropped so configs cannot override handler-owned fields like model or system_prompt. Native graph paths reuse the same forwarding helpers as their handlers.

Provider-specific shaping includes moving effort → output_config (Claude Messages), text.verbosity → verbosity and rebuilt reasoning (OpenAI Agents), max_turns on Runner.run (not ModelSettings), and Anthropic max_tokens_to_sample → max_tokens. LangChain handlers no longer pass tools into chat model constructors.

Drift and probe tests assert each SDK field is classified exactly once and that invoke/stream forward the same keys; regression tests cover wire-level forwarding and blocked “never forwarded” bags.

Reviewed by Cursor Bugbot for commit c76acd5. 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.
max_turns is not a ModelSettings field, so the forwarded-keys filter already
drops it. The native graph now says why only the root node's max_turns
applies: the whole graph is one Runner.run, and the Agents SDK has no
per-agent turn limit.
…ed keys

One test over all six packages asserts that no forwarded list holds a
credential, endpoint, request-injection, remote-tool or host-process key,
and that every forwarded list in each handler module is covered. The client
docstrings now say what a forwarded list may hold.
container (Claude Messages) and reuse_last_container (ChatAnthropic) carry
server-side container state over from another request, and user_profile_id
attributes the request to another party. None of them is a model setting,
so they move to the excluded lists.
…arded bag

extra_body is not a ClaudeAgentOptions field, so asserting it was dropped
proved nothing. The handler test now sends every never-forwarded key through
query and checks none reaches the options. provider_data joins the shared
list, since in newer Agents SDK releases it carries raw request overrides.
…parameters

# Conflicts:
#	packages/openai-agents/tests/test_native_graph.py
@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.

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

Every point from my 2 Oct review is fixed at 175050b. I couldn't find a way for a config to change credentials, endpoints or headers in any of the six handlers. At this head I get 1547 passed and 11 skipped (uv run pytest, exit 0). mypy, ruff check and ruff format --check are clean. There are no replies on the threads, so here is where each one stands:

  • Claude Agents host settings (thread 1): fixed. _build_query_options now drops cli_path, env, permission_mode, add_dirs, cwd, settings, extra_args and plugins, and keeps max_turns, thinking and output_format (claude-agents handler.py:85-96, :565). The native graph uses the same list (native_graph.py:155).
  • model_kwargs (thread 2): fixed. It's excluded for ChatOpenAI and ChatAnthropic in both LangChain packages (langchain-agents handler.py:145, :200).
  • mcp_servers and region_name (thread 3): fixed. Both are excluded (:201, :258).
  • Root exports (thread 4): fixed in c25d33bb. Neither name is in the root __init__.py.
  • output_format on stream only (thread 5): fixed. Invoke and stream now read the same list, and output_format is excluded (claude-messages handler.py:55, :89). The OpenAI stream-only keys are excluded the same way (openai-messages handler.py:115-117).
  • Smaller points from the review body: the dead pop loops are gone (d6813999). Root-only max_turns in the OpenAI native graph is now documented (native_graph.py:283-287). reasoning_effort for Responses is still dropped, on the basis of the comment that the UI writes reasoning: {effort} (openai-messages handler.py:62-63). I haven't checked what the UI writes.

Wire probe. I ran each handler, invoke and stream, against a local server, with the customer's keys in env. The config tried to override api_key, base_url, headers, extra_headers, default_headers, extra_query, extra_body, model_kwargs and every key in tests/never_forwarded.py. All 14 requests (openai-messages, claude-messages, openai-agents, and LangChain messages and agents for OpenAI and Anthropic) reached the local server. Each one used the customer's key and carried no config header, query or body value, and temperature and max_tokens still came through. To check the probe can fail, I added extra_headers to _RESPONSES_FORWARDED_KEYS. The request then went out with Authorization: Bearer ATTACKER. Bedrock was not probed on the wire.

Mutations. All 18 get caught by at least one test. They were: cli_path/env, model_kwargs, mcp_servers (both packages), region_name (both), inference_geo and container on each allowlist, output_format on both paths or on stream only, text_format, extra_headers (Responses and ModelSettings), the raw bag in each of the three native graphs, and dropping the max_tokens rename. That's a better result than any other forwarding PR I've reviewed.

What's left:

  1. The cross-handler test only checks the six modules it names (tests/test_never_forwarded_parameters.py:25-41, :52-58). A new handler package with no *_FORWARDED_KEYS list isn't checked at all. py #103 and py #102 are both approved and can merge as they are, and neither uses these helpers. Vercel forwards extra_headers and extra_query from model.parameters and puts every other key in extra_body (#103 vercel-messages handler.py:102-106). LiteLLM agents builds its model from parameters["base_url"] and parameters["api_key"] (#102 litellm-agents handler.py:117-121). I trial-merged #103 into this branch, and this test still reports 17 passed, the same count as without it. Could the test find every packages/*/src/launchdarkly_ai_* that registers a handler, and fail any that isn't in FORWARDED_LISTS? Then whichever of #102/#103 lands after this one goes red instead of quietly shipping the leak.

  2. Python and js #73 now follow the same approach but disagree on the lists. Both use per-handler allowlists, both rename effort and max_tokens the same way, and the Claude Agents lists match exactly. OpenAI Messages differs only by max_tool_calls. The rest don't match (snake_case compared, js at 532a0eef):

    • Claude Messages: JS forwards inference_geo and container. Python excludes both: inference_geo because it "decides where data is processed", and container because it's server-side state (handler.py:76-78). tests/never_forwarded.py:37 lists inference_geo as never forwarded.
    • LangChain ChatAnthropic: JS forwards inferenceGeo as well.
    • LangChain ChatOpenAI: JS also forwards use_responses_api, streaming, stream_usage, disable_streaming, output_version, tags, metadata, verbose, user, zdr_enabled, audio, modalities, prompt_cache_key and prompt_cache_retention. Python treats the first eight as handler or runtime settings it excludes (_LANGCHAIN_RUNTIME_KEYS, langchain-agents handler.py:60-75). verbose matters most: in JS it lets a config turn on LangChain's verbose logging, which prints prompts to stdout (reported from the js #73 review; I haven't run it). Python also forwards seed, include, store, truncation, context_management and reasoning_effort, and JS doesn't.
    • LangChain ChatBedrockConverse: Python forwards system, guardrails, guard_last_turn_only, request_metadata, output_config, reasoning_effort, stop and stop_sequences. JS forwards supports_tool_choice_values, which Python excludes as tool behaviour the handler depends on.
    • OpenAI Agents: Python forwards include_usage, metadata, response_include, top_logprobs and a top-level verbosity. JS forwards text and maps verbosity into it.
    • Nested shapes: JS rebuilds thinking, outputFormat, reasoning and context_management and drops unknown sub-keys. Python passes nested values through as written. That's probably fine for Python's snake_case SDKs, but the two will differ when a nested value is malformed.
    • JS exports pickForwardedModelParameters, normalizeModelParameters and camelizeModelParameters from @launchdarkly/ai-server. Python dropped its equivalents (c25d33bb). One of the two should change.
    • The two say different things. JS's messages handlers say "exclude a key only if setting it would BREAK the handler; forward everything else". Python says "only model and run settings". inference_geo and container fall right in that gap.

    Python is the stricter side on the keys that matter for data location. I'd fix the inference_geo/container items in JS and settle the rest in the spec. A key forwarded at 1.0 can't be dropped later without breaking someone, so this needs settling before 1.0.

  3. The spec is monorepo #42 (apucacao, draft), and it covers most of this. §1.12 requires allowlists, never-forward categories in any spelling, handler-owned keys winning, a bag test with every category, and recommends an exhaustive classification test. #107 already does all of that. #42 doesn't yet:

    • say that the same handler in two languages forwards the same keys. It only says handlers for different providers may differ.
    • say that invoke and stream forward the same keys, which the PR body promises.
    • place inference_geo and container. Its "region / region_name" example suggests inference_geo is never forwarded, which would make JS wrong.
    • close the conflict with monorepo #10, which is still open and says "Do not rename or filter parameter keys" (#10 diff line 44).

    #42 also mixes this with the graph identity spec and renames span attributes (ld.ai.graph.key to launchdarkly.graph.key). Splitting them would let §1.12 land first. I'd merge #107, js #73 and #42 together.

  4. Minor: ChatOpenAI excludes use_responses_api because it changes "which API and response shape the handler gets back" (langchain-agents handler.py:120-122). But include, reasoning, truncation and context_management are forwarded, and setting any one of them makes langchain_openai switch to the Responses API anyway (ChatOpenAI._use_responses_api, langchain-openai 1.3.3 base.py:1708-1717). Either the reason in the comment is wrong, or those four have the same problem. I haven't tested whether the handler reads a Responses-shaped reply correctly.

  5. Minor: Claude Messages excludes user_profile_id because it "attributes the request to a party other than the caller" (handler.py:79-80). OpenAI Messages forwards user and safety_identifier, which do the same job (handler.py:80, :88). Either is defensible, but the two handlers should agree.

Comment thread tests/test_never_forwarded_parameters.py
{
"stream",
"output_format",
"inference_geo",

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.

Agree with excluding inference_geo and container. js #73 forwards both, though (claude-messages handler.ts FORWARDED_MODEL_PARAMETER_KEYS), and LangChain JS forwards inferenceGeo on ChatAnthropic. So a config that sets inference_geo changes where data is processed in JS and has no effect in Python. That needs settling in #42 and in JS before 1.0.

"include_response_headers",
"disabled_params",
"tiktoken_model_name",
"use_responses_api",

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.

use_responses_api is excluded because it changes the API the handler talks to. But include, reasoning, truncation and context_management are forwarded above, and any one of them makes ChatOpenAI switch to the Responses API anyway (_use_responses_api, langchain-openai 1.3.3 base.py:1708-1717). Either the reason here is wrong or those four have the same problem. JS forwards useResponsesApi, streaming, streamUsage, tags, metadata and verbose, all of which this list excludes. Whichever way this goes, the two SDKs should match.

"service_tier",
"stop",
"stop_sequences",
"system",

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.

system lets a config add Bedrock system prompt blocks on top of instructions. That's not a security issue, since a config already controls the instructions, but JS doesn't forward it, and JS's ChatOpenAI list leaves out prefixMessages as "prompt content". Is it meant to be on this list? guardrails, guard_last_turn_only, request_metadata, output_config, reasoning_effort and stop(_sequences) are also Python-only for Bedrock.

#: Excluded, and why: all client/connection configuration, never a config-controlled setting.
#: * ``retry``: an HTTP retry count.
#: * ``extra_headers``, ``extra_query``, ``extra_body``, ``extra_args``: raw HTTP/request overrides.
_MODEL_SETTINGS_FORWARDED_KEYS = frozenset(

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.

Parity with js #73: Python forwards include_usage, metadata, response_include, top_logprobs and a top-level verbosity. JS forwards text instead and maps verbosity into text.verbosity. JS also rebuilds reasoning and context_management and drops unknown sub-keys, while this passes them through as written. Could the two lists match, or could the difference be written down in #42?

Comment thread packages/openai-messages/src/launchdarkly_ai_openai_messages/handler.py Outdated
Scan packages/*/src/launchdarkly_ai_* source for modules that build a
ProviderHandler and fail when one has no entry in FORWARDED_LISTS, so a
new handler package cannot skip the never-forwarded check. The package
that defines ProviderHandler is the client core and is skipped.
@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.

…arameters

select_forwarded_parameters takes an optional mapping_keys set: forwarded
keys whose value the provider expects to be an object. A value of any other
type for one of them is dropped like an unlisted key, instead of reaching the
provider call as written.
tests/forwarding_spec.py holds each handler's forwarded list from the
cross-SDK spec (TESTING.md 1.12), in the config's own spelling, plus
probe_forwarded_keys: it sets one key at a time and reports which ones
change what the handler hands its provider. Package tests use it to assert
each call site forwards exactly its list.
metadata leaves the forwarded list and joins the excluded side as identity
and attribution, next to user_profile_id. Every excluded key now names its
category. cache_control, output_config, thinking and tool_choice are dropped
when they are not objects. invoke and stream read one helper, and a per-key
probe of each path asserts both forward exactly the spec list.
Drops context_management, include, instructions, metadata, moderation,
prompt_cache_retention, safety_identifier, store, truncation and user from
the Responses allowlist. Each is excluded with its reason: data retention,
server-side state, identity and attribution, API-shape switch, prompt
content beyond instructions, or safety configuration. A reasoning value that
is not an object is dropped. A per-key probe of invoke and stream asserts
both forward exactly the spec list.
ModelSettings no longer takes context_management, include_usage, metadata,
prompt_cache_retention, response_include, store, top_logprobs or truncation
from a config; each is excluded with its reason. text is read only for
text.verbosity, which becomes verbosity unless the config set verbosity
itself, and a text or reasoning value that is not an object is dropped. The
handler and the native graph share one helper, and per-key probes of
invoke, stream and the native graph assert they forward exactly the spec
list, with max_turns going to the run.
…ot objects

The forwarded list is unchanged and already matches the cross-SDK list. The
handler and the native graph now share one helper that also drops malformed
object values, and per-key probes of invoke, stream and the native graph
assert they forward exactly the spec list.
ChatOpenAI drops context_management, include, reasoning, reasoning_effort,
seed, store and truncation. ChatAnthropic drops context_management.
ChatBedrockConverse drops guardrails, guardrail_config,
guard_last_turn_only, request_metadata, system, output_config,
reasoning_effort, stop and stop_sequences. Each removed key is excluded with
its reason.

The use_responses_api reason now holds: include, reasoning, truncation and
context_management also switch ChatOpenAI to the Responses API, and all of
them are excluded together. prompt_cache_key is on the spec list but
ChatOpenAI has no such field, so it is left out, with a test that fails once
the field exists.

Object-valued keys (logit_bias, thinking, output_config,
performance_config) are dropped when malformed. Both packages probe each
constructor one key at a time against the spec list, check that invoke and
stream hand the builder the same config, and probe the LangGraph native
graph's ChatOpenAI too.
The bag now also holds data retention (store, prompt_cache_retention),
server-side state (context_management, truncation), identity and
attribution (metadata, user, safety_identifier, request_metadata), API-shape
switches (include, include_usage, response_include), prompt content
(instructions, system) and safety configuration (moderation, guardrails,
guardrail_config, guard_last_turn_only). No handler forwards any of them.
Claude Messages sets system itself, so its bag test allows the key and still
checks no config value reaches it.
@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.

The JS openai SDK's responses.create has no max_tool_calls parameter, so the
key is cut from the cross-SDK OpenAI Messages list rather than worked around
in one language. It moves to the excluded side with that reason, and the
exact-set test follows the shorter 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c76acd5. 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