Since #688, ChatBedrockConverse rewrites a missing tool_choice to any for deepseek.v3* models and omits auto from their capability table. If tools are in use, the model is always forced to call a tool and can never answer in text, and asking for auto explicitly is refused by lanchain-aws code before any request is made. Bedrock itself accepts auto (and no toolChoice) on both DeepSeek V3.1 and V3.2 today.
Versions: langchain-aws 1.7.5 (also reproduced on 1.6.0 and 1.7.4), langchain-core 1.6.1, boto3 1.43.82. Region us-east-1 unless noted.
The Converse API accepts tool selection as documented
With tools, no toolChoice (Bedrock's default is auto), asked for text:
import boto3
rt = boto3.client("bedrock-runtime", region_name="us-east-1")
tool = {"toolSpec": {"name": "get_weather", "description": "Get the weather for a city.",
"inputSchema": {"json": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}}
r = rt.converse(modelId="deepseek.v3.2",
messages=[{"role": "user", "content": [{"text": "Reply with only the text OK. Do not use any tools."}]}],
inferenceConfig={"maxTokens": 100}, toolConfig={"tools": [tool]})
print(r["stopReason"], r["output"]["message"]["content"])
# end_turn [{'text': 'OK'}]
Asked for the weather instead, the same request returns a valid toolUse block.
Through langchain-aws auto is incorrectly refused
from langchain_aws import ChatBedrockConverse
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny in {city}"
llm = ChatBedrockConverse(model="deepseek.v3.2", provider="deepseek", region_name="us-east-1", max_tokens=100)
print(llm.supports_tool_choice_values) # ('any', 'tool') <- no 'auto'
msg = llm.bind_tools([get_weather]).invoke("Reply with only the text OK. Do not use any tools.")
print(msg.tool_calls) # [{'name': 'get_weather', ...}] <- forced tool call
# request body sent: toolConfig.toolChoice == {"any": {}}
llm.bind_tools([get_weather], tool_choice="auto")
# ValueError: Model deepseek.v3.2 does not currently support tool_choice of type auto.
root cause:
Both come from `_resolve_tool_choice` in `bedrock_converse.py`:
if not tool_choice:
if "deepseek.v3" in self._get_base_model():
return _format_tool_choice("any") # None -> any
return None
...
if tool_choice_type in supported: # 'auto' not in ('any', 'tool') -> raise
Other providers are unaffected
Same code, model="us.anthropic.claude-haiku-4-5-20251001-v1:0":
llm.bind_tools([get_weather]).invoke("Reply with only the text OK. Do not use any tools.").content
# 'OK' (no toolChoice sent; caps ('auto', 'any', 'tool'))
Not a problem for some other model families: Raw Converse behaves the same for Claude, Grok 4.6, GPT-5.6 and Qwen3, and when toolChoice not provided as a parameter, we get text when asked and a tool call when appropriate.
Impact
On DeepSeek v3 with tools configured, all model calls go out with tool_choice='any', preventing text responses.
Proposed fix
- Remove the
deepseek.v3 branch in _resolve_tool_choice, and we get Bedrock defaults.
- Add
"auto" to the DeepSeek entries in the supports_tool_choice_values table.
Further details: measurements and origin
#927 doesn't resolve DeepSeek. Workaround on current releases: pass supports_tool_choice_values=("auto", "any") at construction and set tool_choice="auto" explicitly (we used a custom middleware to do this).
#688 said: "DeepSeek-V3.1 does not work with the auto and named tool choice values. While these options will not be rejected on Converse API side, only any will produce a valid tool call output." and "DeepSeek-V3.2-Exp supports tool choice in addition to any, but auto is still not supported." I cannot say whether that held in October 2025; it does not hold now.
Raw Converse, prompt "What's the weather in Boston? Use the tool.", valid toolUse (correct name, non-empty city) per 3 trials:
| model |
region |
no toolChoice |
auto |
tool |
| deepseek.v3-v1:0 (V3.1) |
us-west-2 |
3/3 |
3/3 |
3/3 |
| deepseek.v3.2 |
us-west-2 |
3/3 |
3/3 |
3/3 |
| deepseek.v3.2 |
us-east-1 |
3/3 |
3/3 |
3/3 |
Prompt "Reply with only the text OK.": end_turn with a text block under no toolChoice and under auto; tool_use under any, as expected.
Two-turn loop through ChatBedrockConverse.bind_tools (tool call, tool result, second call): the second call is sent with toolChoice: {"any": {}} and returns another tool call rather than the sentence asked for.
Full reproducer with an outbound-request tap, showing the toolChoice value actually sent for each case:
"""Demo: langchain-aws forces `toolChoice: any` on DeepSeek V3 whenever tools are bound."""
import json
import boto3
from langchain_aws import ChatBedrockConverse
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool
MODEL, REGION = "deepseek.v3.2", "us-east-1"
sent: list[dict] = []
_orig_client = boto3.Session.client
def _tapped_client(self, *args, **kwargs):
client = _orig_client(self, *args, **kwargs)
if (args[0] if args else kwargs.get("service_name")) == "bedrock-runtime":
client.meta.events.register("before-call.bedrock-runtime.Converse",
lambda params, **_: sent.append(json.loads(params["body"])))
return client
boto3.Session.client = _tapped_client
@tool
def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny, 22C in {city}"
def tool_choice_sent():
return sent[-1].get("toolConfig", {}).get("toolChoice") if sent else None
def describe(msg):
return f"TOOL CALL: {msg.tool_calls[0]['name']}" if msg.tool_calls else f"TEXT: {msg.content!r:.40}"
llm = ChatBedrockConverse(model=MODEL, provider="deepseek", region_name=REGION, max_tokens=200)
ASK = "Reply with only the text OK. Do not use any tools."
sent.clear(); r = llm.invoke(ASK)
print("1. no tools: ", tool_choice_sent(), describe(r))
bound = llm.bind_tools([get_weather])
sent.clear(); r = bound.invoke(ASK)
print("2. tools, no choice: ", tool_choice_sent(), describe(r))
try:
llm.bind_tools([get_weather], tool_choice="auto").invoke(ASK)
except ValueError as exc:
print("3. tools, auto: REFUSED:", str(exc)[:80])
msgs = [HumanMessage("What's the weather in Boston? Use the tool, then answer in one sentence.")]
first = bound.invoke(msgs); msgs.append(first)
for c in first.tool_calls:
msgs.append(ToolMessage(content=get_weather.invoke(c["args"]), tool_call_id=c["id"]))
sent.clear(); final = bound.invoke(msgs)
print("4. after tool result: ", tool_choice_sent(), describe(final))
Output:
1. no tools: None TEXT: 'OK'
2. tools, no choice: {'any': {}} TOOL CALL: get_weather
3. tools, auto: REFUSED: Model deepseek.v3.2 does not currently support tool_choice of type auto. ...
4. after tool result: {'any': {}} TOOL CALL: get_weather
Since #688,
ChatBedrockConverserewrites a missingtool_choicetoanyfordeepseek.v3*models and omitsautofrom their capability table. If tools are in use, the model is always forced to call a tool and can never answer in text, and asking forautoexplicitly is refused by lanchain-aws code before any request is made. Bedrock itself acceptsauto(and notoolChoice) on both DeepSeek V3.1 and V3.2 today.Versions: langchain-aws 1.7.5 (also reproduced on 1.6.0 and 1.7.4), langchain-core 1.6.1, boto3 1.43.82. Region us-east-1 unless noted.
The Converse API accepts tool selection as documented
With tools, no
toolChoice(Bedrock's default isauto), asked for text:Asked for the weather instead, the same request returns a valid
toolUseblock.Through langchain-aws
autois incorrectly refusedroot cause:
Both come from `_resolve_tool_choice` in `bedrock_converse.py`:Other providers are unaffected
Same code,
model="us.anthropic.claude-haiku-4-5-20251001-v1:0":Not a problem for some other model families: Raw Converse behaves the same for Claude, Grok 4.6, GPT-5.6 and Qwen3, and when
toolChoicenot provided as a parameter, we get text when asked and a tool call when appropriate.Impact
On DeepSeek v3 with tools configured, all model calls go out with
tool_choice='any', preventing text responses.Proposed fix
deepseek.v3branch in_resolve_tool_choice, and we get Bedrock defaults."auto"to the DeepSeek entries in thesupports_tool_choice_valuestable.Further details: measurements and origin
#927 doesn't resolve DeepSeek. Workaround on current releases: pass
supports_tool_choice_values=("auto", "any")at construction and settool_choice="auto"explicitly (we used a custom middleware to do this).#688 said: "DeepSeek-V3.1 does not work with the
autoand namedtoolchoice values. While these options will not be rejected on Converse API side, onlyanywill produce a valid tool call output." and "DeepSeek-V3.2-Exp supportstoolchoice in addition toany, butautois still not supported." I cannot say whether that held in October 2025; it does not hold now.Raw
Converse, prompt "What's the weather in Boston? Use the tool.", validtoolUse(correct name, non-emptycity) per 3 trials:Prompt "Reply with only the text OK.":
end_turnwith a text block under no toolChoice and underauto;tool_useunderany, as expected.Two-turn loop through
ChatBedrockConverse.bind_tools(tool call, tool result, second call): the second call is sent withtoolChoice: {"any": {}}and returns another tool call rather than the sentence asked for.Full reproducer with an outbound-request tap, showing the
toolChoicevalue actually sent for each case:Output: