model-provider: a configured `anthropic_beta` list in `model.params` is replaced instead of merged for Azure Claude models
Description
addAzureClaudeParamsIfNeeded() is documented as adding the Azure Claude beta flag, but it spreads the automatic
value over whatever anthropic_beta list the caller already put in model.params, so the caller's flags are
discarded rather than added to. This is the model.params twin of the model.headers bug in #2015, and the follow-up
I pointed at in the scope note of #2016 (and its correction comment): the same replace-instead-of-merge shape, in the
other file that has one.
On main (c2ad42e3), multimodal/tarko/model-provider/src/azure-claude-params.ts:15-29:
export function addAzureClaudeParamsIfNeeded(
modelId: string,
provider: string,
existingParams?: Record<string, any>,
): Record<string, any> | undefined {
// Only apply to azure-openai provider with gcp-claude4-sonnet model
if (provider === 'azure-openai' && modelId === 'gcp-claude4-sonnet') {
return {
...existingParams,
anthropic_beta: ['fine-grained-tool-streaming-2025-05-14'],
};
}
return existingParams;
}...existingParams at :23 copies the caller's object first and :24 then overwrites the anthropic_beta key with a
one-element list of its own. Both sides write the same key, so whenever a configuration supplies anthropic_beta,
exactly one of the two values survives — the automatic one.
Why this is a bug and not the intended precedence
- The function's own contract says "adds". Its JSDoc reads "Automatically adds anthropic_beta parameters for
Azure OpenAI provider with Claude models" (
:7) — the verb is add, not override — the name isaddAzureClaudeParamsIfNeeded, and the parameter is documented as "Existing experimental parameters" that the result is supposed to update (:12-13). Replacing one entry of the object it was handed is not adding. - The value is a list, so there is nothing to choose between.
anthropic_betais a list of beta flags — that is how this very line builds it (['fine-grained-tool-streaming-2025-05-14']). A caller's['context-management-…']is another element of the same list, not a competing value for one slot, so the merge costs nothing. model.paramsis a supported, user-facing passthrough.multimodal/tarko/model-provider/src/types.ts:67-71documents it as "Experimental parameters passed directly through request body / @warning Use with caution - these parameters bypass validation" on theparams?: Record<string, any>field ofAgentModel, i.e. the option exists precisely so callers can write body fields this package knows nothing about — including this one.
Nothing pins the current behaviour either: the package's own merge test
(multimodal/tarko/model-provider/tests/azure-claude-params.test.ts:17-24) feeds in an unrelated key
({ customParam: 'value' }), and tests/model-resolver.test.ts:145-166 resolves an AgentModel with no params
at all, so neither of them ever has two writers for anthropic_beta.
Reachability, hop by hop
multimodal/tarko/agent-interface/src/agent-options.ts:9importsAgentModelfrom@tarko/model-provider/typesand:69exposes it asmodel?: AgentModelon the publicAgentModelOptions— somodel.paramsis a documented agent option, not an internal field.multimodal/tarko/agent/src/agent/agent.ts:94(this.currentModel = resolveModel(options.model)) and:513pass that object intomodel-provider.multimodal/tarko/model-provider/src/model-resolver.ts:78(const params = addAzureClaudeParamsIfNeeded(model, provider, agentModel?.params)) calls the function under report and:87puts its return value on the resolved model.multimodal/tarko/model-provider/src/llm-client.ts:31destructuresparamsoff the resolved model,:69-70spreads it intorequestPayloadwith the comment "Merge experimental params directly into request body", and:77-80hands that payload toclient.chat.completions.create(...).
So a flag a user writes in model.params.anthropic_beta reaches this function, is dropped there, and never appears in
the HTTP body — the one place it was headed.
Reproduction
import { addAzureClaudeParamsIfNeeded } from '../src/azure-claude-params';
const params = addAzureClaudeParamsIfNeeded('gcp-claude4-sonnet', 'azure-openai', {
anthropic_beta: ['context-management-2025-06-27'],
});
console.log(params?.anthropic_beta);
// expected: ['fine-grained-tool-streaming-2025-05-14', 'context-management-2025-06-27']
// actual on main: ['fine-grained-tool-streaming-2025-05-14']Measured by importing the main copy and a candidate copy of the module into one Node process
(node --experimental-strip-types) and diffing their results over 9 inputs: 4 rows change, 5 are byte-identical.
Every changed row is a caller-supplied flag surviving instead of vanishing; the 5 identical rows are the controls
(no params, unrelated param, caller passing only the automatic flag, caller passing '', and the non-matching-model
path).
Scope
The trigger itself is deliberately not part of this: it matches one exact model-id literal at :21
(modelId === 'gcp-claude4-sonnet'), while the repo's own azure-openai examples deploy Claude under different ids
(multimodal/tarko/agent/examples/model-providers/azure-openai.ts:15-17 uses aws_sdk_claude37_sonnet, and
multimodal/pdk.config.ts:14-15 pairs provider: 'azure-openai' with model: 'aws_sdk_claude37_sonnet'), so for
those the function returns existingParams untouched and the beta is not added at all. Deciding which Azure-hosted
Claude deployments need the automatic flag is a product call about the proxy, not a defect fix, and widening the match
would start sending a beta to deployments that may not accept it. This issue is only about the case where the function
already fires: it should add, not replace.
I would like to work on this.
Source: bytedance/UI-TARS-desktop