diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 7e409eb8e3..0f163f38ef 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -2213,7 +2213,12 @@ class _BedrockCompletionsAdapter: model=model, messages=messages, tools=kwargs.get("tools"), - max_tokens=int(max_tokens) if max_tokens else 4096, + # Omitted/None caller cap → None: build_converse_kwargs then omits + # inferenceConfig.maxTokens so Bedrock uses the model's maximum + # allowed output, matching the no-cap-by-default policy every + # other aux wire already follows (#10809: vision descriptions + # stayed capped at the shim's old hardcoded 4096 on Bedrock). + max_tokens=int(max_tokens) if max_tokens else None, temperature=kwargs.get("temperature"), top_p=kwargs.get("top_p"), stop_sequences=stop, diff --git a/agent/bedrock_adapter.py b/agent/bedrock_adapter.py index c399081619..8d63323fd2 100644 --- a/agent/bedrock_adapter.py +++ b/agent/bedrock_adapter.py @@ -1019,7 +1019,7 @@ def build_converse_kwargs( model: str, messages: List[Dict], tools: Optional[List[Dict]] = None, - max_tokens: int = 4096, + max_tokens: Optional[int] = 4096, temperature: Optional[float] = None, top_p: Optional[float] = None, stop_sequences: Optional[List[str]] = None, @@ -1028,16 +1028,24 @@ def build_converse_kwargs( """Build kwargs for ``bedrock-runtime.converse()`` or ``converse_stream()``. Converts OpenAI-format inputs to Converse API parameters. + + ``max_tokens=None`` omits ``inferenceConfig.maxTokens`` entirely, in which + case Bedrock defaults to the model's maximum allowed output — the Converse + field is optional per the AWS API reference. The default stays 4096 so + existing callers are unaffected; callers that want the model's full output + budget (e.g. uncapped auxiliary vision calls) pass ``None`` explicitly. """ system_prompt, converse_messages = convert_messages_to_converse(messages) cache_enabled = _model_supports_prompt_cache(model) + inference_config: Dict[str, Any] = {} + if max_tokens is not None: + inference_config["maxTokens"] = max_tokens + kwargs: Dict[str, Any] = { "modelId": model, "messages": converse_messages, - "inferenceConfig": { - "maxTokens": max_tokens, - }, + "inferenceConfig": inference_config, } if system_prompt: @@ -1086,6 +1094,10 @@ def build_converse_kwargs( if guardrail_config: kwargs["guardrailConfig"] = guardrail_config + if not kwargs["inferenceConfig"]: + # inferenceConfig is optional on the wire; don't send an empty object. + del kwargs["inferenceConfig"] + return kwargs @@ -1094,7 +1106,7 @@ def call_converse( model: str, messages: List[Dict], tools: Optional[List[Dict]] = None, - max_tokens: int = 4096, + max_tokens: Optional[int] = 4096, temperature: Optional[float] = None, top_p: Optional[float] = None, stop_sequences: Optional[List[str]] = None, @@ -1135,7 +1147,7 @@ def call_converse_stream( model: str, messages: List[Dict], tools: Optional[List[Dict]] = None, - max_tokens: int = 4096, + max_tokens: Optional[int] = 4096, temperature: Optional[float] = None, top_p: Optional[float] = None, stop_sequences: Optional[List[str]] = None, diff --git a/tests/agent/test_bedrock_adapter.py b/tests/agent/test_bedrock_adapter.py index 8994688e0f..8d6acf2ede 100644 --- a/tests/agent/test_bedrock_adapter.py +++ b/tests/agent/test_bedrock_adapter.py @@ -389,6 +389,40 @@ class TestBuildConverseKwargs: assert "toolConfig" in kwargs assert len(kwargs["toolConfig"]["tools"]) == 1 + def test_default_max_tokens_stays_4096(self): + """Callers that don't pass max_tokens keep the historical 4096 cap — + the None-omission behavior is strictly opt-in.""" + from agent.bedrock_adapter import build_converse_kwargs + kwargs = build_converse_kwargs( + model="test-model", messages=[{"role": "user", "content": "Hi"}], + ) + assert kwargs["inferenceConfig"]["maxTokens"] == 4096 + + def test_max_tokens_none_omits_cap(self): + """max_tokens=None omits inferenceConfig.maxTokens so Bedrock uses the + model's maximum allowed output (the Converse field is optional).""" + from agent.bedrock_adapter import build_converse_kwargs + kwargs = build_converse_kwargs( + model="test-model", + messages=[{"role": "user", "content": "Hi"}], + max_tokens=None, + temperature=0.1, + ) + assert "maxTokens" not in kwargs["inferenceConfig"] + # Other inference params still flow through. + assert kwargs["inferenceConfig"]["temperature"] == 0.1 + + def test_max_tokens_none_and_no_sampling_drops_empty_inference_config(self): + """When every inference param is absent, don't send an empty + inferenceConfig object on the wire.""" + from agent.bedrock_adapter import build_converse_kwargs + kwargs = build_converse_kwargs( + model="test-model", + messages=[{"role": "user", "content": "Hi"}], + max_tokens=None, + ) + assert "inferenceConfig" not in kwargs + diff --git a/tests/agent/test_bedrock_integration.py b/tests/agent/test_bedrock_integration.py index 6f6fcbd8e0..32cd9056ee 100644 --- a/tests/agent/test_bedrock_integration.py +++ b/tests/agent/test_bedrock_integration.py @@ -437,3 +437,39 @@ class TestAuxiliaryClientBedrockResolution: # got-final-object downgrade path handles the rest. assert resp is sentinel assert mock_converse.call_count == 1 + + def test_bedrock_shim_uncapped_when_caller_omits_max_tokens(self, monkeypatch): + """No caller max_tokens → the shim passes None through and the wire + request carries no inferenceConfig.maxTokens, so Bedrock uses the + model's maximum allowed output (#10809 on the Bedrock wire). + + Guards against the shim's old hardcoded ``else 4096`` fallback, which + kept aux vision descriptions capped after the vision call sites + dropped their own caps.""" + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIO...MPLE") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") + + from agent.auxiliary_client import BedrockAuxiliaryClient + + client = BedrockAuxiliaryClient("us-east-1", "openai.gpt-oss-20b-1:0") + boto3_client = MagicMock() + with patch("agent.bedrock_adapter._get_bedrock_runtime_client", + return_value=boto3_client), \ + patch("agent.bedrock_adapter.normalize_converse_response"): + # Aux vision-style call: no max_tokens key at all. + client.chat.completions.create( + model="openai.gpt-oss-20b-1:0", + messages=[{"role": "user", "content": "describe"}], + temperature=0.1, + ) + wire_kwargs = boto3_client.converse.call_args.kwargs + assert "maxTokens" not in wire_kwargs.get("inferenceConfig", {}) + + # An explicit caller cap still lands on the wire unchanged. + client.chat.completions.create( + model="openai.gpt-oss-20b-1:0", + messages=[{"role": "user", "content": "describe"}], + max_tokens=1234, + ) + wire_kwargs = boto3_client.converse.call_args.kwargs + assert wire_kwargs["inferenceConfig"]["maxTokens"] == 1234