Conversation
|
bugbot run |
buildQueryOptions already accepted an unused extra options bag; the call site never filled it. An AI Config could set a turn cap or agent-run tuning knobs and this handler silently dropped them, so an agent driven through this SDK ran with no budget at all. Now config.model.parameters is checked against the Options keys the Claude Agent SDK actually accepts (maxTurns, maxThinkingTokens, maxBudgetUsd, effort, fallbackModel, thinking) and only those are forwarded. Anything else, including temperature and max_tokens which this SDK has no equivalent for, is ignored rather than passed through. No default is added, so a config that sets nothing behaves exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The handler built responses.create()/responses.stream() calls as only
{model, input, tools}, so a customer who set temperature, top_p, or
max_output_tokens in an AI Config had it silently dropped on every
call, blocking and streaming alike.
config.model.parameters is now checked against an explicit allowlist
of ResponseCreateParams keys the Responses API actually accepts
(temperature, top_p, max_output_tokens, top_logprobs,
parallel_tool_calls, reasoning, truncation, metadata, store,
service_tier, prompt_cache_key, safety_identifier), and only those are
forwarded, on every call site: the initial turn, the tool-loop
follow-up turn, and both branches of the streaming path. The allowlist
is spread first so model, input, tools, and previous_response_id -
computed by the handler itself - always win, so a parameters bag can
never smuggle those back in. A config that sets nothing produces {},
so the request sent is unchanged from before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The handler only ever read config.model.parameters.max_tokens; every other Anthropic Messages API knob a customer set in an AI Config (temperature, top_p, top_k, stop_sequences, thinking, and the rest of MessageCreateParamsBase) was silently dropped. Now an explicit allowlist, taken from the SDK's own MessageCreateParamsBase type, is forwarded from model.parameters at both call sites (the blocking tool loop and the streaming tool loop). The allowlist excludes model, messages, tools, system, max_tokens, and stream, so a config value can never override what the handler already decided for those. max_tokens keeps its existing behaviour and 1024 default unchanged. A config that sets no parameters, or an empty parameters object, produces the exact same call as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Runner.run already accepts maxTurns and the Agents SDK's ModelSettings tunes temperature, topP, and similar knobs, but the handler never read either from config.model.parameters. An agent driven through this SDK ran with no turn budget at all, and a customer's temperature/topP/etc. settings silently did nothing. Now config.model.parameters is checked against the ModelSettings keys the Agents SDK's Agent constructor actually accepts (temperature, topP, frequencyPenalty, presencePenalty, maxTokens, toolChoice, parallelToolCalls, truncation, store, promptCacheRetention) and only those are forwarded as modelSettings. maxTurns is read separately and passed to Runner.run's options, since it caps the agentic loop rather than tuning a model call. Both the blocking and streaming call paths are covered. Anything else in model.parameters is ignored rather than forwarded. No default is added, so a config that sets nothing behaves exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6a1c4cf to
1f14632
Compare
Extracts the duplicated `buildModelParameterOptions` / `buildModelSettings`
picker loop (claude-agents, claude-messages, openai-messages, openai-agents)
into `pickForwardedModelParameters`, and the repeated
`config.model?.parameters && typeof ... === 'object' ? ... : {}` guard
(langchain-messages, langchain-agents, langchain-agents/native-graph) into
`normalizeModelParameters`. Both live in packages/client and are exported
from its public index.
Each handler keeps its own FORWARDED_*_KEYS allowlist next to the request
shape it maps onto; the shared picker only owns the loop. openai-agents
keeps collapsing an empty result to `undefined` itself. The LangChain
handlers keep spreading the whole parameters object, unchanged, since their
chat models take an open-ended options bag.
No behaviour change: same keys picked, same spread order, same undefined vs
{} results. Adds unit tests for both new helpers in packages/client.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…bag through
The four allowlist handlers (claude-agents, claude-messages,
openai-messages, openai-agents) each maintained a hand-picked
FORWARDED_*_KEYS list and a pickForwardedModelParameters call to filter
config.model.parameters before forwarding it to the provider SDK. The
allowlists silently dropped valid settings and needed updating whenever
a provider added a new tunable. The LaunchDarkly UI already constrains
which keys a customer can save, so an unsupported key reaching the
provider is an acceptable error, not something this SDK needs to guard.
Replace every allowlist with normalizeModelParameters, the same
pass-through helper the LangChain handlers already use, and delete
pickForwardedModelParameters from @launchdarkly/ai-server now that no
caller is left.
While making every call site spread parameters first, found three
places where a handler-owned key was set with a conditional spread
(`...(cond ? { key } : {})`) instead of an explicit key: when the
handler's own condition was false, the key was never set, so a
customer's model.parameters could smuggle a value through unopposed.
Fixed by always setting the key explicitly, to undefined when absent:
claude-messages' `system`/`tools`, claude-agents' `systemPrompt`, and
openai-messages' `tools`/`previous_response_id`/`text` (structured
output).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
buildQueryOptions took a single `extra` bag that carried both the customer's model.parameters and this handler's own internal flags (includePartialMessages). Two different trust levels in one untyped argument, with the safe ordering only holding by accident of how the call site happened to build the literal. Split them: `modelParameters` is required, documented as untrusted, and spread first so every handler-owned key overrides it. `internalOptions` carries the handler's own flags and is spread last so it wins over everything. A reader of the signature can now see which is which. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dlers The LaunchDarkly UI saves model.parameters keys in snake_case, the wire name every provider SDK expects (max_turns, top_p, ...). claude-agents, openai-agents, and both langchain handlers wrap JavaScript frameworks that only read camelCase option names, so a snake_case key like max_turns silently vanished instead of reaching the framework: no error, the value just never took effect. Add camelizeModelParameters (packages/client/src/utils.ts), converting top-level snake_case keys to camelCase generated from the key itself. Nested values (e.g. thinking.budget_tokens) pass through untouched, and when both spellings of a key are set the snake_case one wins, deterministically regardless of key order. Apply it at every call site in the four affected handlers: claude-agents (query() options, both the blocking and streaming paths), openai-agents (ModelSettings and buildMaxTurns for Runner.run), and both langchain handlers' default model constructors, including langchain-agents' native-graph.ts. claude-messages and openai-messages wrap raw provider clients that already read snake_case themselves, so they are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… forward it verbatim Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
bugbot run |
…r request types The four framework handlers correctly forward the whole model.parameters bag via normalizeModelParameters, since their frameworks ignore keys they do not recognize. The two handlers that wrap a raw provider client (claude-messages, openai-messages) do not have that safety net: an unsupported key reaches the wire and the provider returns 400. Restores pickForwardedModelParameters (previously dropped in cf9a611) as a shared, exported helper: pick the subset of parameters whose keys appear in an explicit allowlist, drop the rest silently. The two raw-client handlers use it next to a compiler-checked allowlist derived from each SDK's own request type, so the list cannot silently drift. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ameters This handler wraps the raw OpenAI SDK's responses.create / responses.stream, which reject an unrecognized field with a 400. The LaunchDarkly UI offers the Chat Completions parameter set, so a config that sets max_tokens, frequency_penalty, presence_penalty, seed, n, stop, response_format, logit_bias, logprobs, max_completion_tokens, audio, modalities, or prediction used to reach the API verbatim and would now break the run. FORWARDED_MODEL_PARAMETER_KEYS lists every ResponseCreateParamsBase key this handler forwards. A compile-time exhaustiveness check classifies every other key on that type as handler-owned (input, model, previous_response_id, text, tools) or excluded as transport/plumbing, with a reason in the comment above the list; a key the SDK adds and this file does not classify fails to build, naming the key. buildModelParameterOptions also renames the two Chat Completions token-limit spellings the UI offers, max_tokens and max_completion_tokens, to the Responses API's max_output_tokens, before filtering. Precedence when a config sets more than one spelling: an explicit max_output_tokens wins, then max_completion_tokens, then max_tokens. Applied at every responses.create / responses.stream call site: the initial blocking turn, the blocking tool-loop turn, and both branches of the streaming turn. Behaviour is unchanged when model.parameters is absent or empty. Updates the handler.test.ts case that asserted an unrecognized key reached the request to assert the opposite, and adds coverage for the accepted-key path, the dropped-key path, both renames, and precedence. Adds wire.test.ts, which does not mock the openai package and instead stubs global fetch to assert on the literal JSON body responses.create sends. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…meters This handler wraps the raw Anthropic SDK's messages.create / messages.stream, which reject an unrecognized field with a 400. The LaunchDarkly UI offers a top-level effort setting that the Messages API only accepts nested under output_config, so a config that set it used to reach the API verbatim and would now break the run. FORWARDED_MODEL_PARAMETER_KEYS lists every MessageCreateParamsBase key this handler forwards. A compile-time exhaustiveness check classifies every other key on that type as handler-owned (model, messages, system, tools, max_tokens) or excluded as transport/plumbing (stream, since this handler selects streaming by choosing between messages.create and messages.stream, not by setting a field), with the reason in the comment above the list; a key the SDK adds and this file does not classify fails to build, naming the key. buildModelParameterOptions also moves a top-level effort into output_config.effort before filtering. An output_config.effort already present in the config wins over the renamed value. The handler does not set output_config itself anywhere (outputFormat is folded into the system prompt instead), so there is no handler-owned output_config to protect here. Applied at both call sites inside the tool loop shared by the blocking and streaming paths. Keeps max_tokens ?? 1024. Behaviour is unchanged when model.parameters is absent or empty. Updates the two handler.test.ts cases that asserted an unrecognized key reached the request to assert the opposite, and adds coverage for the effort rename, the explicit-output_config.effort-wins case, and the merge with an output_config that also sets other fields. Adds wire.test.ts, which does not mock @anthropic-ai/sdk and instead stubs global fetch to assert on the literal JSON body messages.create sends. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…reak the handler
Orchestrator follow-up: TypeScript and Python must apply the same exclusion
rule, and the previous excluded set went too far. Excluding stream_options,
store, safety_identifier, prompt_cache_key, prompt_cache_retention, include,
context_management, conversation, prompt, and user was based on "is this a
generation setting", not "would this break the handler" — the latter is the
actual contract: forward everything the API accepts unless setting it
produces a broken or missing result.
Moves store, user, safety_identifier, prompt_cache_key,
prompt_cache_retention, include, and context_management from excluded to
forwarded. The excluded set is now exactly five keys, matching the shared
rule:
- stream, stream_options: the handler chooses streaming itself by calling
responses.create() vs responses.stream().
- background: the call returns before the output exists, so the handler
gets no result.
- conversation, prompt: supply server-side state/templates that conflict
with the input the handler builds itself.
The compile-time exhaustiveness check against ResponseCreateParamsBase still
passes with the narrowed OpenAIExcludedKeys union (verified by removing
'user' from FORWARDED_MODEL_PARAMETER_KEYS and confirming tsc names it).
claude-messages needed no change: its excluded set was already exactly
{ stream }, which is what the shared rule requires for the Messages API.
Refreshed its doc comment to state the same "would break the handler" rule
explicitly, no behavior change.
Updates handler.test.ts to add coverage for the seven now-forwarded keys and
to keep asserting the five still-excluded keys are dropped. Extends
wire.test.ts with one case proving `store` reaches the literal wire request
while `background` is dropped from it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
bugbot run |
jeffdupont
left a comment
There was a problem hiding this comment.
Parity note alongside my review of python #107 (launchdarkly/python-ai-sdk#107). The two messages handlers match Python: allowlists, the effort → output_config and max_tokens → max_output_tokens renames, and unknown keys dropped. The agent and LangChain handlers don't match. Python allowlists every handler. Here the whole model.parameters bag is camelCased and spread in with no filtering, so:
- LangChain (
modelConstructorArgs):api_keybecomesapiKey, which ChatOpenAI and ChatAnthropic use.anthropic_api_url,configuration,client_options,timeout,max_retriesand Bedrockcredentials/region/endpoint_hostalso reach the constructors. Spreading the raw bag predates this PR, but camelCasing is what makes the snake_case spellings the UI writes take effect. - Claude Agents (
buildQueryOptions):cwd,env,permissionMode,extraArgs,pathToClaudeCodeExecutable,executableandallowDangerouslySkipPermissionscan all come from a config. The Python PR has the same problem with its allowlist; details are in my inline comment there. - Nested keys aren't converted:
thinking.budget_tokensstays snake_case, but the Claude Agent SDK readsbudgetTokens, so the thinking budget is probably ignored. The same goes forcontext_management[].compact_thresholdin openai-agents. I haven't confirmed either at runtime. - Native graphs: neither the claude-agents nor the openai-agents native graph forwards parameters or
maxTurns; Python's do. - New public names:
camelizeModelParameters,normalizeModelParametersandpickForwardedModelParametersare exported from the client root (index.ts:87,93,97). For the 1.0 surface they belong in the internal group.
The monorepo has no spec for forwarding these parameters, and open monorepo #10 says not to rename or filter keys. I'd suggest agreeing one rule in TESTING.md (allowlist per handler, connection and host keys never forwarded) and landing both PRs against it.
| return { | ||
| prompt, | ||
| options: { | ||
| ...modelParameters, |
There was a problem hiding this comment.
Owned keys win over this spread, but everything else in the bag reaches query() options, including cwd, env, permissionMode, pathToClaudeCodeExecutable and allowDangerouslySkipPermissions. That lets an AI Config decide which program runs on the customer's server and what environment it gets. Could this use an allowlist like claude-messages does?
| const parameters = { | ||
| ...(config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}), | ||
| }; | ||
| const parameters = { ...camelizeModelParameters(normalizeModelParameters(config.model?.parameters)) }; |
There was a problem hiding this comment.
With camelCasing, a config's api_key / anthropic_api_url / client_options becomes a working constructor option here. The same line is at langchain-agents/src/handler.ts:265. Python allowlists per model class and excludes credentials and connection settings; matching that would keep the two languages aligned.
The handler spread model.parameters into generateText/streamText as saved, but the AI SDK only reads its own camelCase call settings and drops everything else without an error. A config's max_tokens, top_p, stop_sequences and the like never reached the model; only single-word keys such as temperature did. The handler now renames max_tokens / max_completion_tokens / max_output_tokens to maxOutputTokens and reasoning_effort to reasoning, camelizes the rest of the top-level keys, and forwards only the AI SDK call settings (LanguageModelCallOptions and RequestOptions, minus abortSignal, plus toolChoice and providerOptions). The allowlist is checked at compile time against the AI SDK's own types, and replaces the old denylist of handler-owned names. vercelEvaluate also forwards the three request fields experimental_evaluate accepts (maxRetries, headers, providerOptions) from model.parameters; options passed to vercelEvaluate win. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings Same problem as vercel-messages: the ToolLoopAgent settings were built by spreading model.parameters as saved, so snake_case keys such as max_tokens and top_p were dropped by the AI SDK. The single-agent handler and every native-graph node now go through the same mapping and compile-time-checked allowlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ 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 4241d75. Configure here.
| ...(systemPrompt ? { systemPrompt } : {}), | ||
| ...extra, | ||
| systemPrompt, | ||
| ...internalOptions, |
There was a problem hiding this comment.
Unsandboxed Claude query option spread
High Severity
buildQueryOptions spreads the full model.parameters bag into Claude Agent SDK query() options. Handler-owned keys win, but the rest still reach the subprocess, including cwd, env, permissionMode, pathToClaudeCodeExecutable, and allowDangerouslySkipPermissions. A remotely delivered AI Config can choose the binary and environment that run on the customer's server.
Reviewed by Cursor Bugbot for commit 4241d75. Configure here.
| const parameters = { | ||
| ...(config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}), | ||
| }; | ||
| const parameters = { ...camelizeModelParameters(normalizeModelParameters(config.model?.parameters)) }; |
There was a problem hiding this comment.
Camelized params expose constructor secrets
High Severity
Camelizing the unsandboxed model.parameters bag turns UI snake_case keys such as api_key, anthropic_api_url, and client_options into working LangChain constructor options (apiKey, anthropicApiUrl, clientOptions). A config can now override credentials or redirect the client at a remote URL. The Python SDK allowlists per model class and excludes connection settings.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 4241d75. Configure here.
| function buildModelSettings(parameters: AiConfigRep['model']['parameters']): ModelSettings | undefined { | ||
| const settings = camelizeModelParameters(normalizeModelParameters(parameters)); | ||
| return Object.keys(settings).length > 0 ? (settings as ModelSettings) : undefined; | ||
| } |
There was a problem hiding this comment.
OpenAI agents forward untrusted settings
Medium Severity
buildModelSettings camelizes the entire model.parameters bag and passes it through as Agents SDK ModelSettings. That activates real fields such as extraHeaders, extraBody, and extraQuery, so a config can inject request headers or body fields on every model call. maxTurns is also left in the settings object after being copied to Runner.run.
Reviewed by Cursor Bugbot for commit 4241d75. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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 4241d75. Configure here.
| ...(systemPrompt ? { systemPrompt } : {}), | ||
| ...extra, | ||
| systemPrompt, | ||
| ...internalOptions, |
There was a problem hiding this comment.
Untrusted params reach Claude query()
High Severity
buildQueryOptions spreads the full camelized model.parameters bag into Claude Agent SDK query() options. Handler-owned keys win, but the rest of the bag is trusted as runtime options, including cwd, env, permissionMode, pathToClaudeCodeExecutable, and allowDangerouslySkipPermissions. A remotely served AI Config can therefore choose which program runs on the customer host and with what environment. The UI allowlist is not a security boundary; configs can be written via the API.
Reviewed by Cursor Bugbot for commit 4241d75. Configure here.
| const parameters = { | ||
| ...(config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}), | ||
| }; | ||
| const parameters = { ...camelizeModelParameters(normalizeModelParameters(config.model?.parameters)) }; |
There was a problem hiding this comment.
Camelize enables LangChain credentials
High Severity
camelizeModelParameters turns UI/API snake_case keys such as api_key, anthropic_api_url, and client_options into live LangChain constructor options (apiKey, anthropicApiUrl, clientOptions). Those values are then spread into ChatOpenAI / ChatAnthropic / ChatBedrockConverse. A served AI Config can now override credentials or redirect the client at a hostile base URL, leaking prompts. Python allowlists per model class and excludes connection settings.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 4241d75. Configure here.


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.
max_turns,top_p), the providers' own names and what the UI writes.vercel-messages,vercel-agents, andvercelEvaluate) map keys onto the AI SDK's own call settings, such asmax_tokenstomaxOutputTokensandtop_ptotopP. The AI SDK silently drops names it does not know.🤖 Generated with Claude Code
Note
Overview
AI Config
model.parametersnow reach provider calls instead of being ignored. Shared helpers in@launchdarkly/ai-server(normalizeModelParameters,camelizeModelParameters,pickForwardedModelParameters) centralize how each handler treats the parameter bag.Framework / agent SDK paths (Claude Agents, OpenAI Agents, LangChain agents/messages/graph) spread normalized parameters into SDK options, camelizing top-level snake_case keys (
max_turns→maxTurns) while handler-owned fields (model, tools, system prompt, streaming flags) still win when spread last. Claude Agents refactorsbuildQueryOptionsto separate untrustedmodelParametersfrom trustedinternalOptions.Direct Anthropic/OpenAI client handlers use compile-time-checked allowlists and keep snake_case on the wire; they drop or rename keys that would 400 or break the handler (e.g. Claude Messages moves UI
effortintooutput_config.effort; OpenAI Messages maps token limits tomax_output_tokens).Vercel messages/agents/evaluate replace ad-hoc parameter filtering with dedicated
model-parametersbuilders that map UI names to AI SDK call settings (maxOutputTokens,topP, etc.) and forward only recognized keys.Coverage adds handler unit tests for forwarding, casing, precedence, and override protection, plus wire tests that stub
fetchor use real LangChain/Vercel models to assert actual request bodies.Reviewed by Cursor Bugbot for commit 4241d75. Bugbot is set up for automated code reviews on this repo. Configure here.