Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e2966ab
feat(bedrock): bridge gpt-5.6+ function tools with reasoning to nativ…
6matt Oct 10, 2026
30c6687
fix(bedrock): keep Converse for tool names OpenAI rejects, prefix the…
mateo-berri Oct 10, 2026
982825a
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 16, 2026
cd175d0
fix(bedrock): keep Converse for params the Responses bridge drops, st…
mateo-berri Oct 16, 2026
99118c3
fix(bedrock): keep sync completion() and object reasoning_effort on C…
mateo-berri Oct 16, 2026
6b9823a
fix(bedrock): replay reasoning only to the model that minted it, keep…
mateo-berri Oct 16, 2026
ad25280
fix(bedrock): share replayed reasoning across inference profiles of o…
mateo-berri Oct 16, 2026
88355aa
test(bedrock): cover reasoning replay across inference profiles, mode…
mateo-berri Oct 16, 2026
45219c2
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 16, 2026
7808c84
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 17, 2026
811317c
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 19, 2026
511f7a6
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 20, 2026
71983d8
test(bedrock): cover a declaring deployment's rounded effort on the /…
mateo-berri Oct 20, 2026
b66028f
fix(bedrock): keep previous_response_id, instructions, prompt and a n…
mateo-berri Oct 21, 2026
222a9bf
fix(bedrock): keep provider headers, tool search results, and encrypt…
mateo-berri Oct 21, 2026
29f49eb
test(bedrock): expect the converse deployment's own key in the tool s…
mateo-berri Oct 21, 2026
229dbaf
fix(bedrock): keep streams bridged back from Responses working and ke…
mateo-berri Oct 21, 2026
3b95b1b
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 28, 2026
bde4179
refactor(bedrock): move the responses bridge choice out of completion…
mateo-berri Oct 28, 2026
bc4ac51
style(ui): drop the comment that repeats the Responses API tool call …
mateo-berri Oct 28, 2026
f6873b9
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 28, 2026
ed30e41
Merge remote-tracking branch 'origin/main' into litellm_bedrock_gpt_t…
mateo-berri Oct 28, 2026
9556fc1
Merge main into litellm_bedrock_gpt_tools_reasoning_responses_bridge
mateo-berri Oct 29, 2026
2fb185e
Merge main 4786dd861b into litellm_bedrock_gpt_tools_reasoning_respon…
mateo-berri Oct 30, 2026
bf3c6f2
Merge main c8cef1a95b into litellm_bedrock_gpt_tools_reasoning_respon…
mateo-berri Oct 31, 2026
240ed79
Merge main ff182c194e into litellm_bedrock_gpt_tools_reasoning_respon…
mateo-berri Nov 3, 2026
3613392
Merge main e60e6422904 into litellm_bedrock_gpt_tools_reasoning_respo…
mateo-berri Nov 6, 2026
e4fe5d6
fix(bedrock): ask for encrypted reasoning whenever the Responses brid…
mateo-berri Nov 7, 2026
826af5a
Merge main 478d9bf5d2 into litellm_bedrock_gpt_tools_reasoning_respon…
mateo-berri Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import AsyncIterable, Coroutine, Iterable
from typing import TYPE_CHECKING, Any, Final, Union

from typing_extensions import TypedDict
from typing_extensions import ReadOnly, TypedDict

from litellm.types.llms.openai import ResponsesAPIResponse

Expand All @@ -23,6 +23,7 @@ class ResponsesToCompletionBridgeHandlerInputKwargs(TypedDict):
logging_obj: "LiteLLMLoggingObj"
custom_llm_provider: str
encoding: object
client: ReadOnly[object]


def _restore_routing_prefix(model: str, custom_llm_provider: str) -> str:
Expand Down Expand Up @@ -152,6 +153,7 @@ def validate_input_kwargs(self, kwargs: dict) -> ResponsesToCompletionBridgeHand
logging_obj=logging_obj,
custom_llm_provider=custom_llm_provider,
encoding=typed_kwargs.get("encoding"),
client=typed_kwargs.get("client"),
)

def completion(
Expand Down Expand Up @@ -186,7 +188,7 @@ def completion(
litellm_params=litellm_params,
headers=headers,
litellm_logging_obj=logging_obj,
client=kwargs.get("client"),
client=validated_kwargs["client"],
)

# Set on request_data rather than passed as explicit kwargs: the spread of
Expand Down Expand Up @@ -282,6 +284,7 @@ async def acompletion(self, *args, **kwargs) -> Union["ModelResponse", "CustomSt
litellm_params=litellm_params,
headers=headers,
litellm_logging_obj=logging_obj,
client=validated_kwargs["client"],
)
except Exception as e:
raise e
Expand Down
73 changes: 67 additions & 6 deletions litellm/llms/bedrock/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"openai",
"mantle",
"chat_completions",
"responses",
]


Expand Down Expand Up @@ -997,7 +998,8 @@ def bedrock_request_needs_converse(model: str, request_params: Mapping[str, obje
written onto the Converse body,
function tools (``tools`` or legacy ``functions``) on a model without
``supports_bedrock_runtime_chat_completions_tools_with_reasoning`` are rejected there unless
``reasoning_effort`` is exactly ``"none"``, and a ``response_format`` goes native only as
``reasoning_effort`` is exactly ``"none"`` (function-only ``tools`` on a model listing ``/v1/responses`` are
bridged to bedrock-runtime's native Responses API instead), and a ``response_format`` goes native only as
``{"type": "json_schema", "json_schema": ...}`` (a pydantic model is converted to that) on a model with
``supports_bedrock_runtime_chat_completions_response_format``: a schema on any other model is only
honored by Converse, and every ``json_object`` form (``response_schema`` included) keeps Converse's
Expand All @@ -1015,6 +1017,12 @@ def bedrock_request_needs_converse(model: str, request_params: Mapping[str, obje
return True
if _response_format_needs_converse(model, request_params.get("response_format")):
return True
return _chat_completions_rejects_function_tools(model, request_params) and not _bridgeable_to_responses(
model, request_params
)


def _chat_completions_rejects_function_tools(model: str, request_params: Mapping[str, object]) -> bool:
if not (request_params.get("tools") or request_params.get("functions")):
return False
return (
Expand All @@ -1023,12 +1031,64 @@ def bedrock_request_needs_converse(model: str, request_params: Mapping[str, obje
)


class _NamedFunction(TypedDict):
name: ReadOnly[str]


class _FunctionTool(TypedDict):
type: ReadOnly[Literal["function"]]
function: ReadOnly[_NamedFunction]


_FUNCTION_TOOLS_ADAPTER: Final = TypeAdapter(tuple[_FunctionTool, ...])
_OPENAI_FUNCTION_NAME: Final = re.compile(r"[a-zA-Z0-9_-]{1,64}")


def _function_tools_only(tools: object) -> bool:
if not isinstance(tools, (list, tuple)):
return False
try:
validated: Final = _FUNCTION_TOOLS_ADAPTER.validate_python(tools)
except ValidationError:
return False
return all(_OPENAI_FUNCTION_NAME.fullmatch(tool["function"]["name"]) for tool in validated)


_EXTRA_BODY_ADAPTER: Final = TypeAdapter(dict[str, object])


def _extra_body_has_tools(extra_body: object) -> bool:
return isinstance(extra_body, dict) and bool(_EXTRA_BODY_ADAPTER.validate_python(extra_body).get("tools"))


def _bridgeable_to_responses(model: str, request_params: Mapping[str, object]) -> bool:
return (
not request_params.get("functions")
and request_params.get("aws_bedrock_client") is None
and _function_tools_only(request_params.get("tools"))
and not _extra_body_has_tools(request_params.get("extra_body"))
and _bedrock_serves_responses(model)
Comment on lines +1535 to +1546

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Live e2e not proven at this tip

The claim “Async calls take the shared chat-to-Responses bridge” relies on live QA from 222a9bf827, before the stream fix and main merge. The description says current verification uses unit tests. Repository rules require live proof at this tip before merging. Start the current proxy and repeat real Bedrock requests through /v1/chat/completions, /v1/messages, and /v1/responses, including streaming and a tool-result follow-up

Rule Used: Apply this rule to every PR, including PRs that change only tests, docs, or catalog data. It does not require new live tests for test-only PRs; it only checks that each runtime claim the PR makes is backed by evidence at this PR's tip A runtime clai... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, live QA at this tip waits on a Bedrock credential, and it runs again on all three endpoints before this PR merges

)


def _bedrock_serves_responses(model: str) -> bool:
region, model_id = split_bedrock_region_path(model)
return region is None and any(
_price_map_entry_lists_endpoint(_model_cost_entry(key), "/v1/responses")
for key in (model_id, f"bedrock/{model_id}")
)


def _chat_completions_unless_converse_needed(
model: str, request_params: Mapping[str, object] | None
) -> Literal["converse", "chat_completions"]:
if request_params is not None and bedrock_request_needs_converse(model, request_params):
) -> Literal["converse", "chat_completions", "responses"]:
if request_params is None:
return "chat_completions"
if bedrock_request_needs_converse(model, request_params):
return "converse"
return "chat_completions"
if not _chat_completions_rejects_function_tools(model, request_params):
return "chat_completions"
return "converse" if request_params.get("_litellm_responses_api_bridge_allowed") is False else "responses"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved


def bedrock_route_for_request(
Expand Down Expand Up @@ -1474,8 +1534,9 @@ def get_bedrock_route(
Grok and GPT 5.6 and newer go to bedrock-runtime's native OpenAI Chat Completions by
default (``bedrock_runtime_chat_completions_is_default``) and ``chat_completions/`` opts any other model in;
``request_params`` (the caller's chat params) sends such a request to Converse when it needs a
feature only Converse serves, and ``converse/`` pins a model to Converse. Every other OpenAI-family
model stays on Converse without the prefix.
feature only Converse serves, to ``responses`` (the chat-to-Responses bridge onto bedrock-runtime's
native ``/openai/v1/responses``) when it carries function tools with reasoning on, and ``converse/``
pins a model to Converse. Every other OpenAI-family model stays on Converse without the prefix.
"""
route_mappings: dict[
str,
Expand Down
16 changes: 6 additions & 10 deletions litellm/llms/bedrock/responses/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,13 @@ def get_complete_url(
api_base: str | None,
litellm_params: dict, # mutable-ok: signature fixed by the BaseResponsesAPIConfig override contract
) -> str:
region: Final = self._get_aws_region_name(optional_params=litellm_params, model=None)
override: Final = (
api_base
or litellm_params.get("aws_bedrock_runtime_endpoint")
or get_secret_str("AWS_BEDROCK_RUNTIME_ENDPOINT")
runtime_endpoint: Final = litellm_params.get("aws_bedrock_runtime_endpoint")
_, destination = self.get_runtime_endpoint(
api_base=api_base or None,
aws_bedrock_runtime_endpoint=runtime_endpoint if isinstance(runtime_endpoint, str) else None,
aws_region_name=self._get_aws_region_name(optional_params=litellm_params, model=None),
)
# Partition-aware: bedrock-runtime is amazonaws.com.cn in China, and other
# suffixes in GovCloud/ISO, so defer to the shared endpoint builder.
host: Final = (
override or self._select_default_endpoint_url(endpoint_type="runtime", aws_region_name=region)
).rstrip("/")
host: Final = destination.rstrip("/")
base: Final = next(
(host[: -len(suffix)] for suffix in BEDROCK_RUNTIME_OPENAI_BASE_SUFFIXES if host.endswith(suffix)),
host,
Expand Down
33 changes: 28 additions & 5 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5723,7 +5723,11 @@ def completion(
"allowed_openai_params": allowed_openai_params,
"base_model": base_model,
}
optional_params = get_optional_params(**optional_param_args, **non_default_params)
optional_params = get_optional_params(
**optional_param_args,
**non_default_params,
_litellm_responses_api_bridge_allowed=not skip_responses_api_bridge,
)
processed_non_default_params: Final = pre_process_non_default_params(
model=model,
passed_params=optional_param_args,
Expand Down Expand Up @@ -5878,11 +5882,32 @@ def completion(
api_base=api_base,
)

request_params: Final = MappingProxyType(
_OPTIONAL_PARAMS_ADAPTER.validate_python(
{
**optional_param_args,
**non_default_params,
"_litellm_responses_api_bridge_allowed": not skip_responses_api_bridge,
}
)
)
bedrock_bridges_to_responses: Final = (
custom_llm_provider == "bedrock"
and bedrock_route_for_request(
model,
request_params,
_ADDITIONAL_DROP_PARAMS_ADAPTER.validate_python(kwargs.get("additional_drop_params") or ()),
)
== "responses"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
)

# Use base_model (the true underlying model) for Azure model-type
# detection when the deployment name differs from the model name.
_azure_detection_model: Final = base_model or model

if responses_api_model_info.get("mode") == "responses" and not skip_responses_api_bridge:
if (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium · Bridged Bedrock chat calls can't be found in spend logs by the returned id

  1. Setup: a proxy deployment such as bedrock/global.openai.gpt-6.1-sol (GPT-5.6+), where the cost map lists /v1/responses.
  2. Action: a client sends /v1/chat/completions with a function tool and reasoning_effort: "low". On base, Converse served this call. The response id was a chatcmpl-... value, and the LiteLLM_SpendLogs row used the same request_id.
  3. Change: bedrock_bridges_to_responses now sends the call through responses_api_bridge, which calls aresponses. Success logging records the Responses result, so get_spend_logs_id takes the encoded id (resp_bGl0ZWxsbTpjdXN0b21f...) and sets call_type to responses/aresponses.
  4. Result: the bridge transformation decodes the id before returning it, so the caller gets the plain upstream resp_abc123. A /spend/logs?request_id=<returned id> lookup or a join of response ids against LiteLLM_SpendLogs finds no row. Before this PR, the same Bedrock request matched its row.

The PR caveats acknowledge this as "same as OpenAI's bridge". The PR still brings the mismatch to Bedrock deployments that had working lookups, and the integration tests avoid it by querying _only_spend_row_of(model) instead of the returned id. Smallest fix: log bridged chat calls under the chat-facing id, or return the same id the spend row stores. Until then, require a test that looks up the spend row by completion.id.

Evidence

SDK probe (/tmp/probe4.py, MockTransport returning a Responses body with id: resp_abc123 for acompletion(model="bedrock/global.openai.gpt-6-sol", tools=[...], reasoning_effort="low")):

returned id resp_abc123
('responses', 'resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOmJlZHJvY2s7bW9kZWxfaWQ6Tm9uZTtyZXNwb25zZV9pZDpyZXNwX2FiYzEyMw==', <standard_logging_object.id same>, 'responses', 7e-05)

litellm/proxy/spend_tracking/spend_tracking_utils.py:312-326: get_spend_logs_id prefers response_obj.id, then standard_logging_object.id; both hold the encoded id.

litellm/completion_extras/litellm_responses_transformation/transformation.py:1457: model_response.id = _upstream_response_id(raw_response.id) decodes the id the caller receives.

tests/integration/providers/test_bedrock_runtime_chat_completions_wire.py:435,459,479,494: bridged tests assert _only_spend_row_of(model), while Converse and native tests assert _spend_row(completion.id).

The PR description's Medium caveat acknowledges this: "Bridged chat calls log as Responses calls, so /spend/logs?request_id=&lt;id> finds no row".

This conclusion rests on code inspection plus the SDK logging probe. I did not query a proxy DB row by the returned id.

Latest summary by code-review · View this review · Reviewed 99118c3720ab

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAI and Azure bridged chat log the same encoded id on main, so the fix belongs in the shared bridge as a separate follow-up

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding still applies

The reply correctly says the id mismatch lives in the shared chat-to-Responses bridge, which OpenAI and Azure already use on main. Fixing it there in a follow-up is reasonable. That fact does not refute this finding, which covers a regression for Bedrock deployments.

  1. On base, Converse served a Bedrock GPT-5.6+ chat call with tools and reasoning. The spend row's request_id matched the returned chatcmpl-... id, and the tests in this PR still check those Converse paths with _spend_row(completion.id).
  2. This PR now routes those calls through the bridge. The bridge logs the encoded resp_bGl0... id but returns the decoded upstream resp_... id to the caller.
  3. As a result, Bedrock users who look up /spend/logs by the id they received now find no row.

The unchanged code in head still shows this mismatch. The bridged Bedrock tests also avoid it by using _only_spend_row_of(model). Until the shared bridge fix lands, or this PR keeps logging and response ids consistent for Bedrock, the finding still applies.

Evidence

litellm/completion_extras/litellm_responses_transformation/transformation.py:1457 (not changed by this PR): model_response.id = _upstream_response_id(raw_response.id) or raw_response.id. Lines 210-213 decode with ResponsesAPIRequestUtils.decode_previous_response_id_to_original_previous_response_id.

litellm/proxy/spend_tracking/spend_tracking_utils.py:312-326: get_spend_logs_id uses response_obj.id first, then standard_logging_object.id. For a bridged call, the logged Responses object holds the encoded id.

tests/integration/providers/test_bedrock_runtime_chat_completions_wire.py: the Converse and native paths assert _spend_row(completion.id) / _spend_row(payload["id"]) (lines 254, 329, 344, 403, 507, 525). The bridged paths assert only _only_spend_row_of(model) (lines 435, 459, 479, 494).

Files changed by this PR: handler.py, bedrock/common_utils.py, main.py, utils.py, and tests. transformation.py is unchanged, so the shared defect existed before this PR. Bedrock exposure to it is new.

The PR description lists this as a known Medium caveat. Acknowledging a limitation does not resolve it.

The finding's original SDK probe showed a returned id of resp_abc123 and a logged id of resp_bGl0ZWxsbTpj.... I did not rerun it for this reply because the cited code has not changed.

Reviewed 99118c3720ab · View follow-up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now a listed Decision. Changing the shared bridge's logged or returned id also changes OpenAI and Azure, so a maintainer decides

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding still applies

Thanks for adding this to the Decisions list. The new entry documents the tradeoff and leaves the choice to a maintainer, but it doesn't change the code or show that the finding is wrong.

The defect is still in head 6b9823ab27:

  1. Before this PR, Converse served Bedrock GPT-5.6+ chat calls with tools and reasoning. Their spend rows used the same chatcmpl-... id the caller got back.
  2. This PR sends those calls through the shared bridge instead.
  3. The bridge stores the encoded resp_bGl0... id in the spend row but returns the decoded upstream resp_... id to the caller.
  4. A /spend/logs?request_id=&lt;returned id> lookup that found the row on base now finds nothing.

I agree the right fix may belong in the shared bridge and may need a maintainer's call because it affects OpenAI and Azure. Until that fix lands, or this PR keeps Bedrock's logged and returned ids consistent, the finding stays open. A maintainer can still accept it as a known regression.

Evidence

The head diff touches litellm/completion_extras/litellm_responses_transformation/transformation.py. None of its added or removed lines mention id, response_id, _upstream_response_id or spend logging.

transformation.py:211-214: _upstream_response_id decodes the id with decode_previous_response_id_to_original_previous_response_id. At line 1522, model_response.id = _upstream_response_id(raw_response.id) or raw_response.id sets the id the caller receives to the decoded value.

litellm/proxy/spend_tracking/spend_tracking_utils.py:312-326: get_spend_logs_id uses response_obj.id, then standard_logging_object.id. For a bridged call, the logged Responses object holds the encoded id.

tests/integration/providers/test_bedrock_runtime_chat_completions_wire.py: the bridged tests at lines 435, 459, 479 and 494 still assert _only_spend_row_of(model). The Converse and native tests look up rows by the returned id, for example _spend_row(completion.id) at line 254 and _spend_row(str(payload["id"])) at line 329.

The PR description lists this under Decisions ("Bridged chat calls log as Responses calls ... with ... an encoded resp_ request id") and as a Medium caveat ("/spend/logs?request_id=&lt;id> finds no row"). Both acknowledge the behavior; neither resolves it.

I did not rerun the earlier SDK probe for this reply because head leaves the cited id paths unchanged.

Reviewed 6b9823ab27ca · View follow-up

responses_api_model_info.get("mode") == "responses" or bedrock_bridges_to_responses
Comment thread
veria-ai[bot] marked this conversation as resolved.
Resolved
) and not skip_responses_api_bridge:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
from litellm.completion_extras import responses_api_bridge

optional_params, rs_val = strip_reasoning_summary_aliases_from_optional_params(optional_params)
Expand Down Expand Up @@ -5950,9 +5975,7 @@ def completion(
optional_params=optional_params,
organization=organization,
provider_config=provider_config,
request_params=MappingProxyType(
_OPTIONAL_PARAMS_ADAPTER.validate_python({**optional_param_args, **non_default_params})
),
request_params=request_params,
shared_session=shared_session,
stream=stream,
temperature=temperature,
Expand Down
8 changes: 7 additions & 1 deletion litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4522,6 +4522,7 @@ def get_optional_params(
store: bool | None = None,
prompt_cache_key: str | None = None,
base_model: str | None = None,
_litellm_responses_api_bridge_allowed: bool = True,
**kwargs: object,
):
drop_params = normalize_drop_params(drop_params) # rebind-ok: config and DB deployments pass "true" as a string
Expand All @@ -4532,6 +4533,7 @@ def get_optional_params(
# non_default_params / _check_valid_arg — it's a routing hint, not an
# OpenAI param.
passed_params.pop("base_model", None)
passed_params.pop("_litellm_responses_api_bridge_allowed")
provider_config: BaseConfig | None = None
if custom_llm_provider is not None and custom_llm_provider in [provider.value for provider in LlmProviders]:
provider_config = ProviderConfigManager.get_provider_chat_config(
Expand Down Expand Up @@ -4583,7 +4585,11 @@ def _check_valid_arg(supported_params: list[str]):
)

bedrock_route: Final = (
_bedrock_route_for_request(model, passed_params, additional_drop_params)
_bedrock_route_for_request(
model,
{**passed_params, "_litellm_responses_api_bridge_allowed": _litellm_responses_api_bridge_allowed},
additional_drop_params,
)
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
if custom_llm_provider == "bedrock"
else None
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import httpx
import openai
import pytest
from integration._support.bedrock_runtime_peer import answer, respond, target_of
from integration._support.bedrock_runtime_peer import NATIVE_RESPONSES, answer, respond, target_of
from integration._support.client import Gateway, Scenario, eventually
from integration._support.database import read_rows
from integration._support.sigv4 import signature
Expand Down Expand Up @@ -159,6 +159,19 @@ def _spend_row(identity: str) -> dict[str, JsonValue]:
return rows[0]


def _only_spend_row_of(model: str) -> dict[str, JsonValue]:
rows: Final = eventually(
lambda: read_rows(
'SELECT model_group, status, prompt_tokens, completion_tokens, api_base FROM "LiteLLM_SpendLogs"'
" WHERE model_group=%s",
(model,),
),
lambda found: len(found) == 1,
seconds=70,
)
return rows[0]


def _success_row(model: str, api_base: str) -> dict[str, JsonValue]:
return {"model_group": model, "status": "success", "prompt_tokens": 9, "completion_tokens": 5, "api_base": api_base}

Expand Down Expand Up @@ -387,17 +400,44 @@ def test_json_schema_response_format_is_forwarded_natively(gateway: Gateway) ->
assert _spend_row(f"chatcmpl-{marker}") == _success_row(model, f"{wire.url}{NATIVE_TARGET}")


def test_tools_while_reasoning_keep_converse(gateway: Gateway) -> None:
def test_tools_while_reasoning_are_bridged_to_native_responses(gateway: Gateway) -> None:
marker: Final = uuid.uuid4().hex
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire)
response: Final = _chat(gateway, model, marker, tools=[dict(TOOL)], reasoning_effort="high")
payload: Final = _payload(response)
assert payload["id"] == f"resp_upstream_{marker}", response.text
choices: Final = payload["choices"]
assert isinstance(choices, list) and len(choices) == 1, response.text
assert choices[0]["finish_reason"] == "stop", response.text
assert choices[0]["message"]["content"] == answer(marker), response.text
body: Final = _body(_converse_request(wire, NATIVE_RESPONSES))
assert body["model"] == GPT, body
assert body["reasoning"] == {"effort": "high"}, body
assert [(tool["type"], tool["name"], tool["parameters"]) for tool in body["tools"]] == [
("function", "lookup_invoice", dict(TOOL_PARAMETERS))
], body
assert _upstream_requests_mentioning(gateway, marker) == [], response.text
assert _only_spend_row_of(model) == {
**_success_row(model, f"{wire.url}{NATIVE_RESPONSES}"),
"prompt_tokens": 30,
}


def test_tools_while_reasoning_with_a_name_openai_rejects_keep_converse(gateway: Gateway) -> None:
marker: Final = uuid.uuid4().hex
tool: Final = {**TOOL, "function": {**TOOL["function"], "name": "lookup invoice!"}}
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire)
response: Final = _chat(gateway, model, marker, tools=[tool], reasoning_effort="high")
payload: Final = _payload(response)
assert payload["choices"] == [
{"finish_reason": "stop", "index": 0, "message": {"content": answer(marker), "role": "assistant"}}
], response.text
body: Final = _body(_converse_request(wire))
assert body["toolConfig"] == {"tools": [CONVERSE_TOOL]}, body
assert body["toolConfig"] == {
"tools": [{"toolSpec": {**CONVERSE_TOOL["toolSpec"], "name": "lookup_invoice_"}}]
}, body
assert body["additionalModelRequestFields"] == {"reasoning": {"effort": "high"}}, body
assert _spend_row(str(payload["id"])) == _success_row(model, f"{wire.url}{CONVERSE_TARGET}")

Expand Down
Loading
Loading