From 5816b16909ca178052d924b47041fec364c9a98f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Aug 2026 00:51:54 -0400 Subject: [PATCH 1/3] Don't mutate underlying message dicts unnecessarily --- cecli/helpers/requests.py | 52 +++++++++++++++++++++++- tests/test_requests.py | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/cecli/helpers/requests.py b/cecli/helpers/requests.py index 84634906f95..cc2899554aa 100644 --- a/cecli/helpers/requests.py +++ b/cecli/helpers/requests.py @@ -172,7 +172,7 @@ def add_continue_for_no_prefill(model, messages, tools): if not model.info.get("supports_assistant_prefill", False): # Only add "Continue" if the last message is not a user message - if not messages or messages[-1].get("role") != "user": + if not messages or messages[-1].get("role") not in ("user", "tool"): # Add a user message with content "Continue" to the messages list append_message = True @@ -214,6 +214,7 @@ def prevent_consecutive_assistant_messages(messages): def model_request_parser(model, messages, tools): + messages = _copy_messages_for_request(messages) messages = thought_signature(model, messages) messages = remove_empty_tool_calls(messages) messages = concatenate_user_messages(messages) @@ -222,3 +223,52 @@ def model_request_parser(model, messages, tools): messages = add_continue_for_no_prefill(model, messages, tools) messages = prevent_consecutive_assistant_messages(messages) return messages + + +def _copy_psf_container(container): + """Shallow-copy a container dict and give its provider_specific_fields a fresh dict.""" + container = dict(container) + + psf = container.get("provider_specific_fields") + if isinstance(psf, dict): + container["provider_specific_fields"] = dict(psf) + + return container + + +def _copy_messages_for_request(messages): + """Return a shallow copy of the message list that request formatting can safely mutate. + + The formatters in this module (``thought_signature``, ``add_reasoning_content``, + ``add_continue_for_no_prefill``, ``ensure_alternating_roles``, ...) mutate message + dicts in place. The messages passed in come straight from the conversation store via + ``BaseMessage.to_dict()``, which shares the nested ``provider_specific_fields`` dicts + with the stored history, so a bare top-level copy would corrupt the stored messages — + e.g. ``reasoning_items`` (relied on for exact-prefix prompt caching) would be popped + out of ``provider_specific_fields`` and lost from the store. + + Shallow-copy the top-level message dict plus the nested ``provider_specific_fields`` + dicts (message-level and per tool-call) — the only structures the formatters mutate — + instead of deep-copying the whole message. + """ + copied = [] + for msg in messages: + msg = dict(msg) + + psf = msg.get("provider_specific_fields") + if isinstance(psf, dict): + msg["provider_specific_fields"] = dict(psf) + + tool_calls = msg.get("tool_calls") + if isinstance(tool_calls, list): + msg["tool_calls"] = [ + _copy_psf_container(call) if isinstance(call, dict) else call for call in tool_calls + ] + + function_call = msg.get("function_call") + if isinstance(function_call, dict): + msg["function_call"] = _copy_psf_container(function_call) + + copied.append(msg) + + return copied diff --git a/tests/test_requests.py b/tests/test_requests.py index 19dbee001eb..4e40b99570f 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -630,6 +630,90 @@ def test_empty_messages(self): result = model_request_parser(model, [], None) assert result == [] + def test_does_not_mutate_stored_provider_specific_fields(self): + """The parser must not mutate the stored messages' nested provider_specific_fields.""" + model = _MockModel(name="gpt-4", supports_assistant_prefill=True) + reasoning_items = [ + {"id": "item-1", "type": "reasoning", "encrypted_content": "abc", "summary": []} + ] + stored = { + "role": "assistant", + "content": "Let me check that.", + "reasoning_content": "thinking...", + "provider_specific_fields": { + "reasoning_items": reasoning_items, + "reasoning_content": "thinking...", + }, + } + + result = model_request_parser(model, [stored], None) + + # The returned request promotes reasoning_items to the top level... + assert result[0]["reasoning_items"] == reasoning_items + assert "reasoning_items" not in result[0]["provider_specific_fields"] + + # ...while the stored message keeps its provider_specific_fields intact. + assert stored["provider_specific_fields"] == { + "reasoning_items": reasoning_items, + "reasoning_content": "thinking...", + } + assert "reasoning_items" not in stored + + def test_preserves_base_message_provider_specific_fields(self): + """Stored BaseMessage psf survives a send (to_dict shares psf with the store).""" + from cecli.helpers.conversation.base_message import BaseMessage + + model = _MockModel(name="gpt-4", supports_assistant_prefill=True) + reasoning_items = [ + {"id": "item-1", "type": "reasoning", "encrypted_content": "abc", "summary": []} + ] + message = BaseMessage( + message_dict={ + "role": "assistant", + "content": "Let me check that.", + "reasoning_content": "thinking...", + "provider_specific_fields": {"reasoning_items": reasoning_items}, + }, + tag="cur", + ) + + result = model_request_parser(model, [message.to_dict()], None) + + assert result[0]["reasoning_items"] == reasoning_items + assert ( + message.message_dict["provider_specific_fields"]["reasoning_items"] == reasoning_items + ) + assert "reasoning_items" not in message.message_dict + + def test_preserves_tool_call_provider_specific_fields(self): + """Gemini thought-signature processing must not mutate stored tool_calls.""" + model = _MockModel(name="gemini/gemini-2.0-flash", supports_assistant_prefill=True) + reasoning_items = [ + {"id": "item-1", "type": "reasoning", "encrypted_content": "abc", "summary": []} + ] + stored_call = { + "id": "call-1", + "type": "function", + "function": {"name": "Local--Grep", "arguments": "{}"}, + "provider_specific_fields": {"reasoning_items": reasoning_items}, + } + stored = [ + {"role": "assistant", "content": None, "tool_calls": [stored_call]}, + {"role": "tool", "tool_call_id": "call-1", "content": "result"}, + ] + + result = model_request_parser(model, stored, None) + + # Thought signatures are added to the copied tool call only... + call_psf = result[0]["tool_calls"][0]["provider_specific_fields"] + assert call_psf["thought_signature"] == "skip_thought_signature_validator" + + # ...while the stored tool call keeps its original psf. + assert stored[0]["tool_calls"][0]["provider_specific_fields"] == { + "reasoning_items": reasoning_items + } + assert "thought_signature" not in stored[0]["tool_calls"][0]["provider_specific_fields"] + # --------------------------------------------------------------------------- # ensure_alternating_roles (from sendchat) From 7fe604953a2ea4d0e1999f72eb58400a8f4176db Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Aug 2026 01:52:05 -0400 Subject: [PATCH 2/3] Fix parallel tool calling index iteration and TUI output --- cecli/coders/base_coder.py | 185 ++++++++++++----- cecli/helpers/io_proxy.py | 6 +- cecli/tools/ls.py | 4 +- cecli/tools/utils/output.py | 3 +- cecli/tui/io.py | 34 +-- tests/coders/test_tool_call_consolidation.py | 206 +++++++++++++++++++ tests/tools/test_tool_arguments.py | 4 +- 7 files changed, 365 insertions(+), 77 deletions(-) create mode 100644 tests/coders/test_tool_call_consolidation.py diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 679cba8f3e5..462d78f4812 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -3440,11 +3440,17 @@ async def add_assistant_reply_to_cur_messages(self): to be `None` when `tool_calls` are present. """ msg = dict(role="assistant") - response = ( - self.partial_response_chunks[0] - if not self.stream - else litellm.stream_chunk_builder(self.partial_response_chunks) - ) + + # Prefer the response already produced by consolidate_chunks(): it carries + # the provider-specific fields (e.g. reasoning_items) that we preserved + # across all chunks, which a fresh litellm.stream_chunk_builder() pass + # alone would drop or truncate. + if self.partial_response_consolidated: + response = self.partial_response_consolidated[0] + elif not self.stream: + response = self.partial_response_chunks[0] + else: + response = litellm.stream_chunk_builder(self.partial_response_chunks) try: # Use response_dict as a regular dictionary @@ -3963,63 +3969,51 @@ def consolidate_chunks(self): if getattr(last_chunk, "usage", None): response.usage = last_chunk.usage - # Collect provider-specific fields from chunks to preserve them - # We need to track both by ID (primary) and index (fallback) since - # early chunks might not have IDs established yet - provider_specific_fields_by_id = {} - provider_specific_fields_by_index = {} - + # Collect message-level provider-specific fields (e.g. `reasoning_items` + # for reasoning models) from ALL chunks. litellm's stream_chunk_builder() + # merges these with last-wins semantics for list fields, silently dropping + # every reasoning item except the final one. Reasoning models depend on the + # full ordered item list being present in the assistant message so that + # exact-prefix prompt caching keeps working across turns, so we collect the + # fields ourselves and concatenate list-valued entries. + message_provider_specific_fields = {} for chunk in self.partial_response_chunks: try: - if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.tool_calls: - for tool_call in chunk.choices[0].delta.tool_calls: - if ( - hasattr(tool_call, "provider_specific_fields") - and tool_call.provider_specific_fields - ): - # Ensure provider_specific_fields is a dictionary - psf = tool_call.provider_specific_fields - if not isinstance(psf, dict): - continue - - # Try to use ID first - if hasattr(tool_call, "id") and tool_call.id: - tool_id = tool_call.id - if tool_id not in provider_specific_fields_by_id: - provider_specific_fields_by_id[tool_id] = {} - # Merge provider-specific fields for this tool ID - provider_specific_fields_by_id[tool_id].update(psf) - # Also track by index as fallback - elif hasattr(tool_call, "index"): - tool_index = tool_call.index - if tool_index not in provider_specific_fields_by_index: - provider_specific_fields_by_index[tool_index] = {} - provider_specific_fields_by_index[tool_index].update(psf) + if chunk.choices and chunk.choices[0].delta: + psf = getattr(chunk.choices[0].delta, "provider_specific_fields", None) + if psf and isinstance(psf, dict): + for key, value in psf.items(): + if isinstance(value, list): + message_provider_specific_fields.setdefault(key, []).extend(value) + elif value is not None: + message_provider_specific_fields[key] = value except (AttributeError, IndexError): continue + if message_provider_specific_fields: + message_psf = getattr(response.choices[0].message, "provider_specific_fields", None) + if not isinstance(message_psf, dict): + message_psf = {} + message_psf.update(message_provider_specific_fields) + response.choices[0].message.provider_specific_fields = message_psf + try: - if response.choices[0].message.tool_calls: - for i, tool_call in enumerate(response.choices[0].message.tool_calls): - # Add provider-specific fields if we collected any for this tool - tool_id = tool_call.id - - # Try ID first - if tool_id in provider_specific_fields_by_id: - # Add provider-specific fields directly to the tool call object - tool_call.provider_specific_fields = provider_specific_fields_by_id[tool_id] - # Fall back to index - elif i in provider_specific_fields_by_index: - # Add provider-specific fields directly to the tool call object - tool_call.provider_specific_fields = provider_specific_fields_by_index[i] - - # Only append to partial_response_tool_calls if it's empty - if len(self.partial_response_tool_calls) == 0: - self.partial_response_tool_calls.append(tool_call) - - self.partial_response_function_call = ( - response.choices[0].message.tool_calls[0].function - ) + message_tool_calls = response.choices[0].message.tool_calls + if message_tool_calls and len(message_tool_calls): + if self.stream: + built_tool_calls = self._build_tool_calls_from_chunks() + if built_tool_calls: + response.choices[0].message.tool_calls = built_tool_calls + self.partial_response_tool_calls = built_tool_calls + else: + # Fall back to litellm's merged list, keeping every call + self.partial_response_tool_calls = list(message_tool_calls) + else: + # Non-streaming: the single response chunk already carries the + # full tool_calls list + self.partial_response_tool_calls = list(message_tool_calls) + + self.partial_response_function_call = self.partial_response_tool_calls[0].function except AttributeError as e: func_err = e @@ -4075,6 +4069,85 @@ def consolidate_chunks(self): self.partial_response_consolidated = (response, func_err, content_err) return response, func_err, content_err + def _build_tool_calls_from_chunks(self): + """Rebuild tool calls from the raw streaming chunks, keyed by delta index. + + Streaming deltas for parallel tool calls arrive interleaved and may start + at any index (not necessarily 0). Indexing into a dict by the delta's + tool-call ``index`` before converting it back to a list ensures every + parallel call is preserved, correctly ordered, and keeps its + provider-specific fields (e.g. thought signatures) attached. + """ + from litellm.types.utils import ChatCompletionMessageToolCall, Function + + tool_calls_dict = {} + + for chunk in self.partial_response_chunks: + try: + if not (chunk.choices and chunk.choices[0].delta): + continue + + delta = chunk.choices[0].delta + for tool_call in delta.tool_calls or []: + if tool_call is None: + continue + + if nested.getter(tool_call, "function") is None: + continue + + index = nested.getter(tool_call, "index") + if index is None: + index = len(tool_calls_dict) + + entry = tool_calls_dict.setdefault( + index, + { + "id": None, + "name": None, + "type": "function", + "arguments": [], + "provider_specific_fields": {}, + }, + ) + + entry["id"] = nested.getter(tool_call, "id") or entry["id"] + entry["type"] = nested.getter(tool_call, "type") or entry["type"] + entry["name"] = nested.getter(tool_call, "function.name") or entry["name"] + + arguments = nested.getter(tool_call, "function.arguments") + if arguments: + entry["arguments"].append(arguments) + + psf = nested.getter(tool_call, "provider_specific_fields") + if not psf: + psf = nested.getter(tool_call, "function.provider_specific_fields") + if psf and isinstance(psf, dict): + entry["provider_specific_fields"].update(psf) + except (AttributeError, IndexError): + continue + + tool_calls = [] + for index in sorted(tool_calls_dict.keys()): + data = tool_calls_dict[index] + if not (data["id"] and data["name"]): + continue + + function = Function( + arguments="".join(data["arguments"]) or "{}", + name=data["name"], + ) + params = { + "id": data["id"], + "function": function, + "type": data["type"] or "function", + } + if data["provider_specific_fields"]: + params["provider_specific_fields"] = data["provider_specific_fields"] + + tool_calls.append(ChatCompletionMessageToolCall(**params)) + + return tool_calls + def stream_wrapper(self, content, final): if not hasattr(self, "_streaming_buffer_length"): self._streaming_buffer_length = 0 diff --git a/cecli/helpers/io_proxy.py b/cecli/helpers/io_proxy.py index bc5754f4e1a..0dfb87154b4 100644 --- a/cecli/helpers/io_proxy.py +++ b/cecli/helpers/io_proxy.py @@ -51,6 +51,9 @@ def __init__(self, target: T, coder: Any) -> None: super().__setattr__("_coder", weakref.ref(coder)) # Per-coder task storage: {coder_uuid: {attr_name: asyncio.Task}} super().__setattr__("_per_coder", {coder_uuid: {}}) + # Last tool `type` emitted via tool_output — lives on the proxy, + # never on the shared target (like coder_uuid) + super().__setattr__("_last_type", None) # Register a per-coder input queue (TUI mode only) # Allows the TUI to push input directly to this coder's queue, @@ -76,6 +79,7 @@ def tool_output(self, *messages: Any, **kwargs: Any) -> Any: """Forward tool_output with coder_uuid injected.""" if "coder_uuid" not in kwargs: kwargs["coder_uuid"] = self._coder_uuid + self._last_type = kwargs.get("type") return self._target.tool_output(*messages, **kwargs) def tool_error(self, message: str = "", strip: bool = True, **kwargs: Any) -> Any: @@ -265,7 +269,7 @@ def __getattr__(self, name: str) -> Any: def __setattr__(self, name: str, value: Any) -> None: # Proxy-internal attributes — store on proxy instance only - if name in ("_target", "_coder_uuid", "_coder", "_per_coder"): + if name in ("_target", "_coder_uuid", "_coder", "_per_coder", "_last_type"): super().__setattr__(name, value) # Per-coder task attributes — isolate per-coder so coders don't # compete for the same promise on the shared InputOutput instance diff --git a/cecli/tools/ls.py b/cecli/tools/ls.py index 2054a261ef5..b5eca2b5ead 100644 --- a/cecli/tools/ls.py +++ b/cecli/tools/ls.py @@ -82,7 +82,7 @@ def execute(cls, coder, path=None, **kwargs): if contents: coder.io.tool_output( - f"🗐 Listed {len(contents)} file(s) in '{dir_path}'", type="tool-result" + f"🗐 Listed {len(contents)} file(s) in '{dir_path}'", type="tool-result" ) sorted_contents = sorted(contents) if len(sorted_contents) > 500: @@ -98,7 +98,7 @@ def execute(cls, coder, path=None, **kwargs): ) return response else: - coder.io.tool_output(f"🗐 No files found in '{dir_path}'", type="tool-result") + coder.io.tool_output(f"🗐 No files found in '{dir_path}'", type="tool-result") response.append_result("No files found in directory") return response except Exception as e: diff --git a/cecli/tools/utils/output.py b/cecli/tools/utils/output.py index 67d51011466..1aa9ede0384 100644 --- a/cecli/tools/utils/output.py +++ b/cecli/tools/utils/output.py @@ -28,9 +28,10 @@ def tool_header(coder, mcp_server, tool_response, params=None): tool_response: a tool_response dictionary """ color_start, color_end = color_markers(coder) + nl = "\n" if coder.io._last_type == "tool-footer" else "" coder.io.tool_output( - f"{color_start}Tool Call:{color_end} {mcp_server.name} • {tool_response.function.name}", + f"{nl}{color_start}Tool Call:{color_end} {mcp_server.name} • {tool_response.function.name}", type="Tool Call", ) diff --git a/cecli/tui/io.py b/cecli/tui/io.py index 1848d39f76a..9db636bbb9c 100644 --- a/cecli/tui/io.py +++ b/cecli/tui/io.py @@ -53,10 +53,11 @@ def __init__(self, output_queue, input_queue, **kwargs): ("Removing", "file_op"), ] - # Tool call buffering for styled panel rendering - self._tool_call_buffer = [] - self._in_tool_call = False - self._expect_tool_result = False + # Tool call buffering for styled panel rendering — per-coder tracking + # Dicts keyed by coder_uuid to support simultaneous multi-coder streaming + self._tool_call_buffers: dict[str, list] = {} + self._in_tool_call: dict[str, bool] = {} + self._expect_tool_result: dict[str, bool] = {} def rule(self): pass @@ -283,44 +284,45 @@ def tool_output(self, *messages, **kwargs): def _reroute_output(self, text, msg_type, **kwargs): # Handle tool call buffering for styled panel rendering coder_uuid = kwargs.get("coder_uuid", None) + key = coder_uuid if coder_uuid else "default" if msg_type == "Tool Call": # Start buffering a new tool call - self._in_tool_call = True - self._tool_call_buffer = [text] + self._in_tool_call[key] = True + self._tool_call_buffers[key] = [text] # Log to history self.append_chat_history(text, linebreak=True, blockquote=True) return True elif msg_type == "tool-footer": # End of tool call - flush buffer as styled panel - if self._in_tool_call and self._tool_call_buffer: + if self._in_tool_call.get(key, False) and self._tool_call_buffers.get(key): msg = { "type": "tool_call", - "lines": self._tool_call_buffer, + "lines": self._tool_call_buffers[key], } if coder_uuid: msg["coder_uuid"] = coder_uuid self.output_queue.put(msg) server_signals.send_tool_call( - self, lines=self._tool_call_buffer, coder_uuid=coder_uuid + self, lines=self._tool_call_buffers[key], coder_uuid=coder_uuid ) # Expect a tool result next - self._expect_tool_result = True - self._in_tool_call = False - self._tool_call_buffer = [] + self._expect_tool_result[key] = True + self._in_tool_call[key] = False + self._tool_call_buffers[key] = [] return True - elif self._in_tool_call: + elif self._in_tool_call.get(key, False): # Add to tool call buffer if text.strip(): - self._tool_call_buffer.append(text) + self._tool_call_buffers[key].append(text) # Log to history self.append_chat_history(text, linebreak=True, blockquote=True) return True # Check if this is a tool result (comes right after tool call) - if self._expect_tool_result and text.strip(): + if self._expect_tool_result.get(key, False) and text.strip(): if msg_type != "tool-result": - self._expect_tool_result = False + self._expect_tool_result[key] = False msg = { "type": "tool_result", "text": text, diff --git a/tests/coders/test_tool_call_consolidation.py b/tests/coders/test_tool_call_consolidation.py new file mode 100644 index 00000000000..5fedb4feac1 --- /dev/null +++ b/tests/coders/test_tool_call_consolidation.py @@ -0,0 +1,206 @@ +"""Regression tests for streaming chunk consolidation bugs. + +These cover three issues found when comparing raw stream chunks against the +constructed conversation history: + +1. Parallel tool calls were dropped (only the first call was saved). +2. Provider reasoning items (``provider_specific_fields.reasoning_items``) + streamed across multiple chunks were lost (only the last item survived). +3. Tool-call deltas that start at a non-zero index were mishandled. +""" + +from litellm.types.utils import ( + ChatCompletionDeltaToolCall, + Delta, + Function, + ModelResponseStream, + StreamingChoices, +) + +from cecli.coders.base_coder import Coder + + +def mk_chunk(delta_kwargs, finish_reason=None, usage=None, cid="cmpl-1", created=1000): + delta = Delta(**delta_kwargs) + choices = [StreamingChoices(finish_reason=finish_reason, index=0, delta=delta, logprobs=None)] + return ModelResponseStream( + id=cid, + created=created, + model="gpt-test", + object="chat.completion.chunk", + system_fingerprint=None, + choices=choices, + usage=usage, + ) + + +def tc(index, id=None, name=None, arguments=None): + return ChatCompletionDeltaToolCall( + id=id, + function=Function(arguments=arguments or "", name=name), + type="function", + index=index, + ) + + +def make_coder(chunks, stream=True): + coder = Coder.__new__(Coder) + coder.stream = stream + coder.partial_response_chunks = chunks + coder.partial_response_tool_calls = [] + coder.partial_response_function_call = dict() + coder.partial_response_consolidated = None + coder.partial_response_reasoning_content = "" + coder.partial_response_content = "" + coder.tool_reflection = False + return coder + + +def test_parallel_tool_calls_are_all_preserved(): + """Bug 1: parallel tool calls streamed in one turn must all survive.""" + chunks = [ + mk_chunk( + { + "role": "assistant", + "content": None, + "tool_calls": [tc(0, "call_ls", "Local--ls", "")], + } + ), + mk_chunk( + {"role": None, "content": None, "tool_calls": [tc(1, "call_grep", "Local--Grep", "")]} + ), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(0, None, None, '{"pa')]}), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(1, None, None, '{"sea')]}), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(0, None, None, 'th":"."}')]}), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(1, None, None, 'rches":[]}')]}), + mk_chunk({}, finish_reason="tool_calls"), + ] + coder = make_coder(chunks) + response, func_err, content_err = coder.consolidate_chunks() + + assert func_err is None + names = [t.function.name for t in coder.partial_response_tool_calls] + assert names == ["Local--ls", "Local--Grep"], f"dropped tool calls: {names}" + + msg_tool_calls = response.choices[0].message.tool_calls + assert [t.function.name for t in msg_tool_calls] == ["Local--ls", "Local--Grep"] + assert msg_tool_calls[0].function.arguments == '{"path":"."}' + assert msg_tool_calls[1].function.arguments == '{"searches":[]}' + + +def test_non_zero_starting_tool_call_index(): + """Bug 3: tool-call deltas may start at index 1 (not 0) and must still be kept.""" + chunks = [ + mk_chunk( + {"role": "assistant", "content": None, "tool_calls": [tc(1, "call_a", "Local--A", "")]} + ), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(1, None, None, '{"a":1}')]}), + mk_chunk({}, finish_reason="tool_calls"), + ] + coder = make_coder(chunks) + response, func_err, content_err = coder.consolidate_chunks() + + assert func_err is None + assert [t.function.name for t in coder.partial_response_tool_calls] == ["Local--A"] + assert coder.partial_response_tool_calls[0].function.arguments == '{"a":1}' + + +def test_reasoning_items_preserved_across_chunks(): + """Bug 2: reasoning_items streamed across chunks must all be kept, in order.""" + chunks = [ + mk_chunk( + { + "role": "assistant", + "content": None, + "provider_specific_fields": { + "reasoning_items": [ + {"id": "item-1", "type": "reasoning", "encrypted_content": "AAA"} + ] + }, + }, + ), + mk_chunk( + { + "role": None, + "content": None, + "provider_specific_fields": { + "reasoning_items": [ + {"id": "item-2", "type": "reasoning", "encrypted_content": "BBB"} + ] + }, + }, + ), + mk_chunk( + {"role": None, "content": None, "tool_calls": [tc(0, "call_x", "Local--X", "{}")]} + ), + mk_chunk({}, finish_reason="tool_calls"), + ] + coder = make_coder(chunks) + response, func_err, content_err = coder.consolidate_chunks() + + assert func_err is None + psf = response.choices[0].message.provider_specific_fields + items = psf.get("reasoning_items", []) if psf else [] + ids = [i["id"] for i in items] + assert ids == ["item-1", "item-2"], f"reasoning items dropped: {ids}" + + # The dumped message (what add_assistant_reply_to_cur_messages stores) + # must carry the full provider_specific_fields. + dumped = response.model_dump() + dumped_psf = dumped["choices"][0]["message"]["provider_specific_fields"] + assert [i["id"] for i in dumped_psf["reasoning_items"]] == ["item-1", "item-2"] + + +def test_reasoning_items_combined_with_tool_calls(): + """Reasoning items + parallel tool calls in the same turn all survive.""" + chunks = [ + mk_chunk( + { + "role": "assistant", + "content": None, + "provider_specific_fields": { + "reasoning_items": [ + {"id": "item-1", "type": "reasoning", "encrypted_content": "AAA"} + ] + }, + }, + ), + mk_chunk( + { + "role": None, + "content": None, + "provider_specific_fields": { + "reasoning_items": [ + {"id": "item-2", "type": "reasoning", "encrypted_content": "BBB"} + ] + }, + }, + ), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(0, "call_a", "Local--A", "")]}), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(1, "call_b", "Local--B", "")]}), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(0, None, None, '{"a":1}')]}), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(1, None, None, '{"b":2}')]}), + mk_chunk({}, finish_reason="tool_calls"), + ] + coder = make_coder(chunks) + response, func_err, content_err = coder.consolidate_chunks() + + assert func_err is None + assert [t.function.name for t in coder.partial_response_tool_calls] == ["Local--A", "Local--B"] + psf = response.choices[0].message.provider_specific_fields + assert [i["id"] for i in psf["reasoning_items"]] == ["item-1", "item-2"] + + +def test_build_tool_calls_from_chunks_handles_missing_index(): + """Delta tool calls without an index fall back to append order.""" + coder = make_coder([]) + coder.partial_response_chunks = [ + mk_chunk( + {"role": "assistant", "content": None, "tool_calls": [tc(0, "call_a", "Local--A", "")]} + ), + mk_chunk({"role": None, "content": None, "tool_calls": [tc(0, None, None, '{"a":1}')]}), + mk_chunk({}, finish_reason="tool_calls"), + ] + built = coder._build_tool_calls_from_chunks() + assert [t.function.name for t in built] == ["Local--A"] + assert built[0].function.arguments == '{"a":1}' diff --git a/tests/tools/test_tool_arguments.py b/tests/tools/test_tool_arguments.py index c63a84135a7..ab734be40ec 100644 --- a/tests/tools/test_tool_arguments.py +++ b/tests/tools/test_tool_arguments.py @@ -82,7 +82,9 @@ def __init__(self): def test_grep_format_output_empty_searches_does_not_crash_tool_footer(): coder = SimpleNamespace( - io=SimpleNamespace(tool_error=Mock(), tool_output=Mock(), tool_warning=Mock()), + io=SimpleNamespace( + tool_error=Mock(), tool_output=Mock(), tool_warning=Mock(), _last_type=False + ), verbose=False, pretty=False, tui=lambda: None, From 50ae7d861e710386bcd0f6d316bd4771154ee30f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Aug 2026 01:59:13 -0400 Subject: [PATCH 3/3] Update model-metadata,json --- cecli/resources/model-metadata.json | 460 +++++++++++++++++++--------- 1 file changed, 308 insertions(+), 152 deletions(-) diff --git a/cecli/resources/model-metadata.json b/cecli/resources/model-metadata.json index 6e1d71f5e43..38281944f96 100644 --- a/cecli/resources/model-metadata.json +++ b/cecli/resources/model-metadata.json @@ -785,6 +785,7 @@ "prompt_cache_min_tokens": 1024 }, "anthropic.claude-sonnet-5": { + "bedrock_converse_supports_strict_tools": false, "cache_creation_input_token_cost": 0.0000025, "cache_creation_input_token_cost_above_1hr": 0.000004, "cache_read_input_token_cost": 2e-7, @@ -1388,6 +1389,7 @@ "prompt_cache_min_tokens": 1024 }, "au.anthropic.claude-sonnet-5": { + "bedrock_converse_supports_strict_tools": false, "cache_creation_input_token_cost": 0.00000275, "cache_creation_input_token_cost_above_1hr": 0.0000044, "cache_read_input_token_cost": 2.2e-7, @@ -1872,20 +1874,20 @@ "supports_minimal_reasoning_effort": false }, "azure/eu/gpt-5.6-luna": { - "cache_read_input_token_cost": 1.1e-7, - "cache_read_input_token_cost_above_272k_tokens": 2.2e-7, - "cache_read_input_token_cost_priority": 2.75e-7, - "input_cost_per_token": 0.0000011, - "input_cost_per_token_above_272k_tokens": 0.0000022, - "input_cost_per_token_priority": 0.00000275, + "cache_read_input_token_cost": 2.2e-8, + "cache_read_input_token_cost_above_272k_tokens": 4.4e-8, + "cache_read_input_token_cost_priority": 5.5e-8, + "input_cost_per_token": 2.2e-7, + "input_cost_per_token_above_272k_tokens": 4.4e-7, + "input_cost_per_token_priority": 5.5e-7, "litellm_provider": "azure", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.0000066, - "output_cost_per_token_above_272k_tokens": 0.0000099, - "output_cost_per_token_priority": 0.0000165, + "output_cost_per_token": 0.00000132, + "output_cost_per_token_above_272k_tokens": 0.00000198, + "output_cost_per_token_priority": 0.0000033, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -1956,20 +1958,20 @@ "supports_minimal_reasoning_effort": false }, "azure/eu/gpt-5.6-terra": { - "cache_read_input_token_cost": 2.75e-7, - "cache_read_input_token_cost_above_272k_tokens": 5.5e-7, - "cache_read_input_token_cost_priority": 6.875e-7, - "input_cost_per_token": 0.00000275, - "input_cost_per_token_above_272k_tokens": 0.0000055, - "input_cost_per_token_priority": 0.000006875, + "cache_read_input_token_cost": 2.2e-7, + "cache_read_input_token_cost_above_272k_tokens": 4.4e-7, + "cache_read_input_token_cost_priority": 5.5e-7, + "input_cost_per_token": 0.0000022, + "input_cost_per_token_above_272k_tokens": 0.0000044, + "input_cost_per_token_priority": 0.0000055, "litellm_provider": "azure", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.0000165, - "output_cost_per_token_above_272k_tokens": 0.00002475, - "output_cost_per_token_priority": 0.00004125, + "output_cost_per_token": 0.0000132, + "output_cost_per_token_above_272k_tokens": 0.0000198, + "output_cost_per_token_priority": 0.000033, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -3422,7 +3424,7 @@ "cache_read_input_token_cost": 7.5e-8, "input_cost_per_token": 7.5e-7, "litellm_provider": "azure", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -3457,7 +3459,7 @@ "cache_read_input_token_cost": 7.5e-8, "input_cost_per_token": 7.5e-7, "litellm_provider": "azure", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -3492,7 +3494,7 @@ "cache_read_input_token_cost": 2e-8, "input_cost_per_token": 2e-7, "litellm_provider": "azure", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -3527,7 +3529,7 @@ "cache_read_input_token_cost": 2e-8, "input_cost_per_token": 2e-7, "litellm_provider": "azure", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -3691,23 +3693,23 @@ "supports_minimal_reasoning_effort": false }, "azure/gpt-5.6-luna": { - "cache_read_input_token_cost": 1e-7, - "cache_read_input_token_cost_above_272k_tokens": 2e-7, - "cache_read_input_token_cost_priority": 2e-7, - "cache_read_input_token_cost_above_272k_tokens_priority": 4e-7, - "input_cost_per_token": 0.000001, - "input_cost_per_token_above_272k_tokens": 0.000002, - "input_cost_per_token_priority": 0.000002, - "input_cost_per_token_above_272k_tokens_priority": 0.000004, + "cache_read_input_token_cost": 2e-8, + "cache_read_input_token_cost_above_272k_tokens": 4e-8, + "cache_read_input_token_cost_priority": 4e-8, + "cache_read_input_token_cost_above_272k_tokens_priority": 8e-8, + "input_cost_per_token": 2e-7, + "input_cost_per_token_above_272k_tokens": 4e-7, + "input_cost_per_token_priority": 4e-7, + "input_cost_per_token_above_272k_tokens_priority": 8e-7, "litellm_provider": "azure", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.000006, - "output_cost_per_token_above_272k_tokens": 0.000009, - "output_cost_per_token_priority": 0.000012, - "output_cost_per_token_above_272k_tokens_priority": 0.000018, + "output_cost_per_token": 0.0000012, + "output_cost_per_token_above_272k_tokens": 0.0000018, + "output_cost_per_token_priority": 0.0000024, + "output_cost_per_token_above_272k_tokens_priority": 0.0000036, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -3781,23 +3783,23 @@ "supports_minimal_reasoning_effort": false }, "azure/gpt-5.6-terra": { - "cache_read_input_token_cost": 2.5e-7, - "cache_read_input_token_cost_above_272k_tokens": 5e-7, - "cache_read_input_token_cost_priority": 5e-7, - "cache_read_input_token_cost_above_272k_tokens_priority": 0.000001, - "input_cost_per_token": 0.0000025, - "input_cost_per_token_above_272k_tokens": 0.000005, - "input_cost_per_token_priority": 0.000005, - "input_cost_per_token_above_272k_tokens_priority": 0.00001, + "cache_read_input_token_cost": 2e-7, + "cache_read_input_token_cost_above_272k_tokens": 4e-7, + "cache_read_input_token_cost_priority": 4e-7, + "cache_read_input_token_cost_above_272k_tokens_priority": 8e-7, + "input_cost_per_token": 0.000002, + "input_cost_per_token_above_272k_tokens": 0.000004, + "input_cost_per_token_priority": 0.000004, + "input_cost_per_token_above_272k_tokens_priority": 0.000008, "litellm_provider": "azure", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.000015, - "output_cost_per_token_above_272k_tokens": 0.0000225, - "output_cost_per_token_priority": 0.00003, - "output_cost_per_token_above_272k_tokens_priority": 0.000045, + "output_cost_per_token": 0.000012, + "output_cost_per_token_above_272k_tokens": 0.000018, + "output_cost_per_token_priority": 0.000024, + "output_cost_per_token_above_272k_tokens_priority": 0.000036, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -4763,20 +4765,20 @@ "supports_minimal_reasoning_effort": false }, "azure/us/gpt-5.6-luna": { - "cache_read_input_token_cost": 1.1e-7, - "cache_read_input_token_cost_above_272k_tokens": 2.2e-7, - "cache_read_input_token_cost_priority": 2.75e-7, - "input_cost_per_token": 0.0000011, - "input_cost_per_token_above_272k_tokens": 0.0000022, - "input_cost_per_token_priority": 0.00000275, + "cache_read_input_token_cost": 2.2e-8, + "cache_read_input_token_cost_above_272k_tokens": 4.4e-8, + "cache_read_input_token_cost_priority": 5.5e-8, + "input_cost_per_token": 2.2e-7, + "input_cost_per_token_above_272k_tokens": 4.4e-7, + "input_cost_per_token_priority": 5.5e-7, "litellm_provider": "azure", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.0000066, - "output_cost_per_token_above_272k_tokens": 0.0000099, - "output_cost_per_token_priority": 0.0000165, + "output_cost_per_token": 0.00000132, + "output_cost_per_token_above_272k_tokens": 0.00000198, + "output_cost_per_token_priority": 0.0000033, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -4847,20 +4849,20 @@ "supports_minimal_reasoning_effort": false }, "azure/us/gpt-5.6-terra": { - "cache_read_input_token_cost": 2.75e-7, - "cache_read_input_token_cost_above_272k_tokens": 5.5e-7, - "cache_read_input_token_cost_priority": 6.875e-7, - "input_cost_per_token": 0.00000275, - "input_cost_per_token_above_272k_tokens": 0.0000055, - "input_cost_per_token_priority": 0.000006875, + "cache_read_input_token_cost": 2.2e-7, + "cache_read_input_token_cost_above_272k_tokens": 4.4e-7, + "cache_read_input_token_cost_priority": 5.5e-7, + "input_cost_per_token": 0.0000022, + "input_cost_per_token_above_272k_tokens": 0.0000044, + "input_cost_per_token_priority": 0.0000055, "litellm_provider": "azure", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.0000165, - "output_cost_per_token_above_272k_tokens": 0.00002475, - "output_cost_per_token_priority": 0.00004125, + "output_cost_per_token": 0.0000132, + "output_cost_per_token_above_272k_tokens": 0.0000198, + "output_cost_per_token_priority": 0.000033, "supported_endpoints": [ "/v1/chat/completions", "/v1/batch", @@ -5805,22 +5807,16 @@ }, "azure_ai/gpt-5.4-mini": { "cache_read_input_token_cost": 7.5e-8, - "cache_read_input_token_cost_above_272k_tokens": 1.5e-7, "cache_read_input_token_cost_priority": 1.5e-7, - "cache_read_input_token_cost_above_272k_tokens_priority": 3e-7, "input_cost_per_token": 7.5e-7, - "input_cost_per_token_above_272k_tokens": 0.0000015, "input_cost_per_token_priority": 0.0000015, - "input_cost_per_token_above_272k_tokens_priority": 0.000003, "litellm_provider": "azure_ai", - "max_input_tokens": 400000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 0.0000045, - "output_cost_per_token_above_272k_tokens": 0.00000675, "output_cost_per_token_priority": 0.000009, - "output_cost_per_token_above_272k_tokens_priority": 0.0000135, "source": "https://ai.azure.com/catalog/models/gpt-5.4-mini", "supported_endpoints": [ "/v1/chat/completions", @@ -5851,22 +5847,16 @@ }, "azure_ai/gpt-5.4-mini-2026-03-17": { "cache_read_input_token_cost": 7.5e-8, - "cache_read_input_token_cost_above_272k_tokens": 1.5e-7, "cache_read_input_token_cost_priority": 1.5e-7, - "cache_read_input_token_cost_above_272k_tokens_priority": 3e-7, "input_cost_per_token": 7.5e-7, - "input_cost_per_token_above_272k_tokens": 0.0000015, "input_cost_per_token_priority": 0.0000015, - "input_cost_per_token_above_272k_tokens_priority": 0.000003, "litellm_provider": "azure_ai", - "max_input_tokens": 400000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 0.0000045, - "output_cost_per_token_above_272k_tokens": 0.00000675, "output_cost_per_token_priority": 0.000009, - "output_cost_per_token_above_272k_tokens_priority": 0.0000135, "source": "https://ai.azure.com/catalog/models/gpt-5.4-mini", "supported_endpoints": [ "/v1/chat/completions", @@ -5897,22 +5887,16 @@ }, "azure_ai/gpt-5.4-nano": { "cache_read_input_token_cost": 2e-8, - "cache_read_input_token_cost_above_272k_tokens": 4e-8, "cache_read_input_token_cost_priority": 4e-8, - "cache_read_input_token_cost_above_272k_tokens_priority": 8e-8, "input_cost_per_token": 2e-7, - "input_cost_per_token_above_272k_tokens": 4e-7, "input_cost_per_token_priority": 4e-7, - "input_cost_per_token_above_272k_tokens_priority": 8e-7, "litellm_provider": "azure_ai", - "max_input_tokens": 400000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 0.00000125, - "output_cost_per_token_above_272k_tokens": 0.000001875, "output_cost_per_token_priority": 0.0000025, - "output_cost_per_token_above_272k_tokens_priority": 0.00000375, "source": "https://ai.azure.com/catalog/models/gpt-5.4-nano", "supported_endpoints": [ "/v1/chat/completions", @@ -5943,22 +5927,16 @@ }, "azure_ai/gpt-5.4-nano-2026-03-17": { "cache_read_input_token_cost": 2e-8, - "cache_read_input_token_cost_above_272k_tokens": 4e-8, "cache_read_input_token_cost_priority": 4e-8, - "cache_read_input_token_cost_above_272k_tokens_priority": 8e-8, "input_cost_per_token": 2e-7, - "input_cost_per_token_above_272k_tokens": 4e-7, "input_cost_per_token_priority": 4e-7, - "input_cost_per_token_above_272k_tokens_priority": 8e-7, "litellm_provider": "azure_ai", - "max_input_tokens": 400000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 0.00000125, - "output_cost_per_token_above_272k_tokens": 0.000001875, "output_cost_per_token_priority": 0.0000025, - "output_cost_per_token_above_272k_tokens_priority": 0.00000375, "source": "https://ai.azure.com/catalog/models/gpt-5.4-nano", "supported_endpoints": [ "/v1/chat/completions", @@ -10667,6 +10645,56 @@ } ] }, + "dashscope/qwen3.7-max": { + "cache_read_input_token_cost": 5e-7, + "input_cost_per_token": 0.0000025, + "litellm_provider": "dashscope", + "max_input_tokens": 991808, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 0.0000075, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "dashscope/qwen3.7-plus": { + "litellm_provider": "dashscope", + "max_input_tokens": 991808, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tiered_pricing": [ + { + "cache_read_input_token_cost": 8e-8, + "input_cost_per_token": 4e-7, + "output_cost_per_token": 0.0000016, + "range": [ + 0, + 256000 + ] + }, + { + "cache_read_input_token_cost": 2.4e-7, + "input_cost_per_token": 0.0000012, + "output_cost_per_token": 0.0000048, + "range": [ + 256000, + 1000000 + ] + } + ] + }, "dashscope/qwq-plus": { "input_cost_per_token": 8e-7, "litellm_provider": "dashscope", @@ -12760,6 +12788,7 @@ "prompt_cache_min_tokens": 1024 }, "eu.anthropic.claude-sonnet-5": { + "bedrock_converse_supports_strict_tools": false, "cache_creation_input_token_cost": 0.00000275, "cache_creation_input_token_cost_above_1hr": 0.0000044, "cache_read_input_token_cost": 2.2e-7, @@ -13807,8 +13836,8 @@ "input_cost_per_token": 6e-7, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000003, "source": "https://fireworks.ai/pricing", @@ -13821,8 +13850,8 @@ "input_cost_per_token": 9.5e-7, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000004, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -13837,8 +13866,8 @@ "input_cost_per_token": 9.5e-7, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000004, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -15340,8 +15369,8 @@ "input_cost_per_token": 0.000002, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000008, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -15356,8 +15385,8 @@ "input_cost_per_token": 0.0000019, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000008, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -15499,8 +15528,8 @@ "input_cost_per_token": 6e-7, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000003, "source": "https://fireworks.ai/pricing", @@ -15513,8 +15542,8 @@ "input_cost_per_token": 9.5e-7, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000004, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -15529,8 +15558,8 @@ "input_cost_per_token": 0.000002, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000008, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -15545,8 +15574,8 @@ "input_cost_per_token": 9.5e-7, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000004, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -15561,8 +15590,8 @@ "input_cost_per_token": 0.0000019, "litellm_provider": "fireworks_ai", "max_input_tokens": 262144, - "max_output_tokens": 262144, - "max_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 0.000008, "source": "https://docs.fireworks.ai/serverless/pricing", @@ -19026,6 +19055,99 @@ "search_context_size_high": 0.035 } }, + "gemini/gemini-robotics-er-1.6-preview": { + "input_cost_per_audio_token": 0.000002, + "input_cost_per_token": 0.000001, + "litellm_provider": "gemini", + "max_input_tokens": 131072, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_reasoning_token": 0.000005, + "output_cost_per_token": 0.000005, + "search_context_cost_per_query": { + "search_context_size_low": 0.014, + "search_context_size_medium": 0.014, + "search_context_size_high": 0.014 + }, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-robotics-er", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true, + "web_search_billing_unit": "per_query" + }, + "gemini/gemini-robotics-er-2-preview": { + "cache_read_input_token_cost": 2e-7, + "input_cost_per_audio_token": 0.000002, + "input_cost_per_token": 0.000002, + "litellm_provider": "gemini", + "max_input_tokens": 131072, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_reasoning_token": 0.00001, + "output_cost_per_token": 0.00001, + "search_context_cost_per_query": { + "search_context_size_low": 0.014, + "search_context_size_medium": 0.014, + "search_context_size_high": 0.014 + }, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-robotics-er-2", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true, + "web_search_billing_unit": "per_query" + }, "gemini/gemma-3-27b-it": { "input_cost_per_audio_per_second": 0, "input_cost_per_audio_per_second_above_128k_tokens": 0, @@ -19835,6 +19957,7 @@ "prompt_cache_min_tokens": 1024 }, "global.anthropic.claude-sonnet-5": { + "bedrock_converse_supports_strict_tools": false, "cache_creation_input_token_cost": 0.0000025, "cache_creation_input_token_cost_above_1hr": 0.000004, "cache_read_input_token_cost": 2e-7, @@ -21503,7 +21626,7 @@ "input_cost_per_token_batches": 3.75e-7, "input_cost_per_token_priority": 0.0000015, "litellm_provider": "openai", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -21549,7 +21672,7 @@ "input_cost_per_token_batches": 3.75e-7, "input_cost_per_token_priority": 0.0000015, "litellm_provider": "openai", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -21593,7 +21716,7 @@ "input_cost_per_token_flex": 1e-7, "input_cost_per_token_batches": 1e-7, "litellm_provider": "openai", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -21636,7 +21759,7 @@ "input_cost_per_token_flex": 1e-7, "input_cost_per_token_batches": 1e-7, "litellm_provider": "openai", - "max_input_tokens": 1050000, + "max_input_tokens": 272000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", @@ -21773,14 +21896,17 @@ "gpt-5.6": { "cache_creation_input_token_cost": 0.00000625, "cache_creation_input_token_cost_above_272k_tokens": 0.0000125, + "cache_creation_input_token_cost_above_272k_tokens_flex": 0.00000625, "cache_creation_input_token_cost_flex": 0.000003125, "cache_creation_input_token_cost_priority": 0.0000125, "cache_read_input_token_cost": 5e-7, "cache_read_input_token_cost_above_272k_tokens": 0.000001, + "cache_read_input_token_cost_above_272k_tokens_flex": 5e-7, "cache_read_input_token_cost_flex": 2.5e-7, "cache_read_input_token_cost_priority": 0.000001, "input_cost_per_token": 0.000005, "input_cost_per_token_above_272k_tokens": 0.00001, + "input_cost_per_token_above_272k_tokens_flex": 0.000005, "input_cost_per_token_batches": 0.0000025, "input_cost_per_token_flex": 0.0000025, "input_cost_per_token_priority": 0.00001, @@ -21791,6 +21917,7 @@ "mode": "chat", "output_cost_per_token": 0.00003, "output_cost_per_token_above_272k_tokens": 0.000045, + "output_cost_per_token_above_272k_tokens_flex": 0.0000225, "output_cost_per_token_batches": 0.000015, "output_cost_per_token_flex": 0.000015, "output_cost_per_token_priority": 0.00006, @@ -21824,29 +21951,33 @@ "supports_xhigh_reasoning_effort": true }, "gpt-5.6-luna": { - "cache_creation_input_token_cost": 0.00000125, - "cache_creation_input_token_cost_above_272k_tokens": 0.0000025, - "cache_creation_input_token_cost_flex": 6.25e-7, - "cache_creation_input_token_cost_priority": 0.0000025, - "cache_read_input_token_cost": 1e-7, - "cache_read_input_token_cost_above_272k_tokens": 2e-7, - "cache_read_input_token_cost_flex": 5e-8, - "cache_read_input_token_cost_priority": 2e-7, - "input_cost_per_token": 0.000001, - "input_cost_per_token_above_272k_tokens": 0.000002, - "input_cost_per_token_batches": 5e-7, - "input_cost_per_token_flex": 5e-7, - "input_cost_per_token_priority": 0.000002, + "cache_creation_input_token_cost": 2.5e-7, + "cache_creation_input_token_cost_above_272k_tokens": 5e-7, + "cache_creation_input_token_cost_above_272k_tokens_flex": 2.5e-7, + "cache_creation_input_token_cost_flex": 1.25e-7, + "cache_creation_input_token_cost_priority": 5e-7, + "cache_read_input_token_cost": 2e-8, + "cache_read_input_token_cost_above_272k_tokens": 4e-8, + "cache_read_input_token_cost_above_272k_tokens_flex": 2e-8, + "cache_read_input_token_cost_flex": 1e-8, + "cache_read_input_token_cost_priority": 4e-8, + "input_cost_per_token": 2e-7, + "input_cost_per_token_above_272k_tokens": 4e-7, + "input_cost_per_token_above_272k_tokens_flex": 2e-7, + "input_cost_per_token_batches": 1e-7, + "input_cost_per_token_flex": 1e-7, + "input_cost_per_token_priority": 4e-7, "litellm_provider": "openai", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.000006, - "output_cost_per_token_above_272k_tokens": 0.000009, - "output_cost_per_token_batches": 0.000003, - "output_cost_per_token_flex": 0.000003, - "output_cost_per_token_priority": 0.000012, + "output_cost_per_token": 0.0000012, + "output_cost_per_token_above_272k_tokens": 0.0000018, + "output_cost_per_token_above_272k_tokens_flex": 9e-7, + "output_cost_per_token_batches": 6e-7, + "output_cost_per_token_flex": 6e-7, + "output_cost_per_token_priority": 0.0000024, "regional_processing_uplift_multiplier_eu": 1.1, "regional_processing_uplift_multiplier_us": 1.1, "supported_endpoints": [ @@ -21879,14 +22010,17 @@ "gpt-5.6-sol": { "cache_creation_input_token_cost": 0.00000625, "cache_creation_input_token_cost_above_272k_tokens": 0.0000125, + "cache_creation_input_token_cost_above_272k_tokens_flex": 0.00000625, "cache_creation_input_token_cost_flex": 0.000003125, "cache_creation_input_token_cost_priority": 0.0000125, "cache_read_input_token_cost": 5e-7, "cache_read_input_token_cost_above_272k_tokens": 0.000001, + "cache_read_input_token_cost_above_272k_tokens_flex": 5e-7, "cache_read_input_token_cost_flex": 2.5e-7, "cache_read_input_token_cost_priority": 0.000001, "input_cost_per_token": 0.000005, "input_cost_per_token_above_272k_tokens": 0.00001, + "input_cost_per_token_above_272k_tokens_flex": 0.000005, "input_cost_per_token_batches": 0.0000025, "input_cost_per_token_flex": 0.0000025, "input_cost_per_token_priority": 0.00001, @@ -21897,6 +22031,7 @@ "mode": "chat", "output_cost_per_token": 0.00003, "output_cost_per_token_above_272k_tokens": 0.000045, + "output_cost_per_token_above_272k_tokens_flex": 0.0000225, "output_cost_per_token_batches": 0.000015, "output_cost_per_token_flex": 0.000015, "output_cost_per_token_priority": 0.00006, @@ -21930,29 +22065,33 @@ "supports_xhigh_reasoning_effort": true }, "gpt-5.6-terra": { - "cache_creation_input_token_cost": 0.000003125, - "cache_creation_input_token_cost_above_272k_tokens": 0.00000625, - "cache_creation_input_token_cost_flex": 0.0000015625, - "cache_creation_input_token_cost_priority": 0.00000625, - "cache_read_input_token_cost": 2.5e-7, - "cache_read_input_token_cost_above_272k_tokens": 5e-7, - "cache_read_input_token_cost_flex": 1.25e-7, - "cache_read_input_token_cost_priority": 5e-7, - "input_cost_per_token": 0.0000025, - "input_cost_per_token_above_272k_tokens": 0.000005, - "input_cost_per_token_batches": 0.00000125, - "input_cost_per_token_flex": 0.00000125, - "input_cost_per_token_priority": 0.000005, + "cache_creation_input_token_cost": 0.0000025, + "cache_creation_input_token_cost_above_272k_tokens": 0.000005, + "cache_creation_input_token_cost_above_272k_tokens_flex": 0.0000025, + "cache_creation_input_token_cost_flex": 0.00000125, + "cache_creation_input_token_cost_priority": 0.000005, + "cache_read_input_token_cost": 2e-7, + "cache_read_input_token_cost_above_272k_tokens": 4e-7, + "cache_read_input_token_cost_above_272k_tokens_flex": 2e-7, + "cache_read_input_token_cost_flex": 1e-7, + "cache_read_input_token_cost_priority": 4e-7, + "input_cost_per_token": 0.000002, + "input_cost_per_token_above_272k_tokens": 0.000004, + "input_cost_per_token_above_272k_tokens_flex": 0.000002, + "input_cost_per_token_batches": 0.000001, + "input_cost_per_token_flex": 0.000001, + "input_cost_per_token_priority": 0.000004, "litellm_provider": "openai", "max_input_tokens": 1050000, "max_output_tokens": 128000, "max_tokens": 128000, "mode": "chat", - "output_cost_per_token": 0.000015, - "output_cost_per_token_above_272k_tokens": 0.0000225, - "output_cost_per_token_batches": 0.0000075, - "output_cost_per_token_flex": 0.0000075, - "output_cost_per_token_priority": 0.00003, + "output_cost_per_token": 0.000012, + "output_cost_per_token_above_272k_tokens": 0.000018, + "output_cost_per_token_above_272k_tokens_flex": 0.000009, + "output_cost_per_token_batches": 0.000006, + "output_cost_per_token_flex": 0.000006, + "output_cost_per_token_priority": 0.000024, "regional_processing_uplift_multiplier_eu": 1.1, "regional_processing_uplift_multiplier_us": 1.1, "supported_endpoints": [ @@ -22490,6 +22629,11 @@ "max_tokens": 32766, "mode": "chat", "output_cost_per_token": 6e-7, + "search_context_cost_per_query": { + "search_context_size_high": 0.005, + "search_context_size_low": 0.005, + "search_context_size_medium": 0.005 + }, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -22506,6 +22650,11 @@ "max_tokens": 32768, "mode": "chat", "output_cost_per_token": 3e-7, + "search_context_cost_per_query": { + "search_context_size_high": 0.005, + "search_context_size_low": 0.005, + "search_context_size_medium": 0.005 + }, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -22522,6 +22671,11 @@ "max_tokens": 65536, "mode": "chat", "output_cost_per_token": 3e-7, + "search_context_cost_per_query": { + "search_context_size_high": 0.005, + "search_context_size_low": 0.005, + "search_context_size_medium": 0.005 + }, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -23093,6 +23247,7 @@ "prompt_cache_min_tokens": 1024 }, "jp.anthropic.claude-sonnet-5": { + "bedrock_converse_supports_strict_tools": false, "cache_creation_input_token_cost": 0.00000275, "cache_creation_input_token_cost_above_1hr": 0.0000044, "cache_read_input_token_cost": 2.2e-7, @@ -32767,6 +32922,7 @@ "prompt_cache_min_tokens": 1024 }, "us.anthropic.claude-sonnet-5": { + "bedrock_converse_supports_strict_tools": false, "cache_creation_input_token_cost": 0.00000275, "cache_creation_input_token_cost_above_1hr": 0.0000044, "cache_read_input_token_cost": 2.2e-7,