* fix(cli): overwrite stale poetry.lock backup on windows
os.rename raises FileExistsError on Windows when poetry-old.lock already
exists from a previous run, so a second `crewai update` crashed there
while POSIX silently replaced the file. Use os.replace, which overwrites
on every platform, and add a regression test.
* test(cli): add docstrings to update_crew tests
---------
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
`Memory.read_only` was enforced only on `remember()` and `remember_many()`.
Two other paths still mutated the backing store:
- `update()` re-embedded the supplied content and wrote the record back.
- `recall()` refreshed `last_accessed` through `touch_records()`, so simply
reading a read-only memory left a persistent trace.
Both now respect the flag, so a read-only Memory leaves stored records
unchanged. `update()` returns the existing record untouched rather than
raising, matching the silent no-op behaviour of `remember()`. Explicit
deletion through `forget()`/`reset()` is deliberately unaffected.
Claude-Session: https://claude.ai/code/session_01HkDjVYVzHFEj5re9B8JH9p
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(llms): send reasoning_effort to every openai reasoning model
The completions path gated the parameter behind
is_o1_model = "o1" in model.lower(), a literal substring test. gpt-5, o3 and
o4-mini contain no "o1", so an explicitly configured effort was dropped and the
model thought at the server default. The request still succeeded, so nothing
surfaced -- one measured extraction ran 6.2s with the setting applied against
149.7s with it dropped.
The gate could not be widened: is_o1_model also drives
supports_function_calling, supports_stop_words and the system->user message
rewrite, so marking gpt-5 as an o1 model would report that it cannot call
tools. The parameter is forwarded unconditionally instead, matching the
responses path, and a model that genuinely does not support it says so in a 400
that is retried once without the key.
Also adds "minimal" to LLM.reasoning_effort, which gpt-5 accepts and the
Literal omitted, so the cheapest setting was unreachable on the typed surface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(llms): gate reasoning_effort on model shape, not every model
Forwarding to every model made a non-reasoning model pay a rejected request
and a retry on every call. `_supports_reasoning_effort` matches on shape
instead -- the o-series, and GPT generation 5 onwards -- so gpt-4o and gpt-4.1
never send the parameter at all.
Matched by shape rather than by a list of names so a new member of an existing
family works without a release here; gpt-6 and o5 already classify correctly.
The unsupported-parameter retry stays as a safety net for the case the shape
match is wrong for a future family, where it costs nothing when the match is
right.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(llms): honour reasoning_effort on compatible servers and fine-tunes
Review follow-ups. A fine-tune (ft:<base>:...) is judged by its base model.
On an OpenAI-compatible server -- anything whose effective base URL is not
api.openai.com, whether set explicitly, via env, or by a provider subclass --
the model name is the server's namespace and says nothing about support, so an
explicit setting is sent as configured; a 400 naming the parameter, in whatever
words the server uses, is recovered by retrying without it, unless it reads as
a complaint about the value. A model that rejected the parameter is remembered
per (endpoint, model) for the process so the rejected call is paid once, and
the drop is logged as a warning since a configured setting is not being
applied. The Literal also gains "xhigh", the remaining value the SDK accepts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(llms): recover reasoning_effort only on evidence the parameter is unknown
Two review follow-ups. The endpoint check now uses the same base URL precedence
as the client itself, so a `client_params` override selects the server. And a
rejection is recovered only when the message says the field is not one the
server knows -- OpenAI's two shapes plus the common compatible-server wordings
("unknown field", "Extra inputs are not permitted") -- rather than any 400 that
lacks a value-sounding word. A pydantic enum complaint such as "Input should be
'low', 'medium' or 'high'" names the parameter but is about its value, and
surfaces instead of being dropped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(llms): remember a reasoning_effort rejection only after the retry succeeds
Two review points on the reasoning_effort fallback. The rejection was recorded
before the retry ran, so a retry that died for an unrelated reason (a dropped
connection, say) silently stopped sending the configured effort for the rest of
the process even though a call without it had never succeeded; the (endpoint,
model) is now remembered only once the retry returns. And _effective_base_url
accepted only a str override in client_params while the SDK, and
_get_client_params, accept httpx.URL too, so a compatible deployment configured
with a URL object was detected as OpenAI and its rejection keyed under the wrong
endpoint; both forms are normalised to the string the client calls.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(cli): reject non-serializable literal_eval results in TUI JSON formatting
_try_parse_structured() accepted any dict/list coming out of
ast.literal_eval(), including values json.dumps() cannot encode such as
[Ellipsis] from a literal [...]. _format_json_in_text() then raised
TypeError: Object of type ellipsis is not JSON serializable, which
propagated through _tick and cancelled the whole crew run.
Validate the parsed object with json.dumps() inside
_try_parse_structured() so only JSON-serializable dict/list values are
returned; anything else falls back to the original text. Fixes#7434.
* fix(cli): contain RecursionError in the TUI JSON formatting boundary
A streamed structure nested deeper than the JSON backend can walk could
raise RecursionError out of _try_parse_structured (from json.loads) or
out of the render-path dumps, escaping _tick and losing the TUI update.
Catch RecursionError when loading, and validate literal_eval results
with the exact kwargs the render path uses, so any structure the render
cannot encode is rejected at the boundary and the raw text renders
instead.
---------
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
mypy does not narrow on `platform.system()`, so Windows-based contributors
get 8 spurious attr-defined/unused-ignore errors from the termios and
resource imports. Switch to `sys.platform` comparisons, which mypy
understands natively, and drop the now-unneeded type-ignore comments.
Fixes#7400
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(agents): request the forced final answer as a user turn
When an agent reaches max_iter, handle_max_iterations_exceeded appended
the "give your best final answer" instruction as an assistant message and
relied on assistant prefill to make the model continue it. Current Claude
models (Opus 5, Sonnet 5, Fable 5.x, the 4.6+ family) reject a request
that ends on an assistant turn with a 400, after the whole iteration
budget has already been spent.
The instruction is now appended as a user turn, which every provider
accepts. The handler's formatted_answer parameter is dropped: every
caller had already appended that text as the last assistant message, so
prefixing it again only duplicated history.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(agents): stop the lite agent loop after the forced final answer
LiteAgent._invoke_loop fell through after handle_max_iterations_exceeded
and issued a regular LLM call on the same history, discarding the forced
answer. Break out of the loop the way CrewAgentExecutor already does, and
assert a single LLM call in the test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(agents): keep null in the task output schema embedded in the prompt
build_task_prompt_with_schema embeds the task output schema into the prompt
via generate_model_description, whose strip_null_types defaults to True.
Combined with ensure_all_properties_required, an Optional[str] = None field
reaches the model as a required, non-nullable string, contradicting the
provider-side response schema generated from the same model.
That sanitizer targets OpenAI strict function-calling schemas. This call site
produces prompt prose, where those constraints do not apply.
Pass strip_null_types=False, matching the existing call for tool schemas in
utilities/agent_utils.py.
Fixes#6774
* test(agents): cover the output_json branch of the prompt schema
build_task_prompt_with_schema embeds a schema on both the output_json and
the output_pydantic branch, and this PR changes both. The regression test
only built a Task with output_pydantic, so the output_json branch shipped
unpinned.
Parameterize over both output attributes. Checked on this branch with the
fix reverted: both cases fail on the missing anyOf, and both pass with it.
Also compare the anyOf members as a set, so member order is not part of the
test contract.
---------
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* feat(embeddings): add openrouter provider type definitions
* feat(embeddings): implement OpenRouterProvider
* feat(embeddings): export openrouter provider package symbols
* feat(embeddings): register openrouter in allowed embedding providers
* feat(embeddings): register openrouter provider and overloads in factory
* feat(tools): add openrouter embedding service support
* test(embeddings): add comprehensive openrouter provider and factory tests
* test(embeddings): add openrouter build test in embedding factory
* test(embeddings): add openrouter model key alias and env tests
* test(tools): add openrouter tests for embedding service
* docs: add openrouter embedder configuration example
* docs(ar): sync openrouter embedder translation
* docs(ko): sync openrouter embedder translation
* docs(pt-BR): sync openrouter embedder translation
* feat(embeddings): allow model alias and None fields in OpenRouterProviderConfig
* fix(tools): resolve EMBEDDINGS_OPENROUTER_API_KEY before OPENROUTER_API_KEY
* test(tools): add regression tests for openrouter env var precedence and fallback
* feat(embeddings): drop organization_id and resolve api_key via OPENROUTER_API_KEY only
* fix(tools): use OPENROUTER_API_KEY in embedding service and update tests
* docs: switch openrouter knowledge example to model_name and document OPENROUTER_API_KEY
* fix(tools): default openrouter model to namespaced openai/text-embedding-3-small
---------
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(openai): surface gateway errors reported inside an HTTP 200
OpenAI-compatible gateways commit `200 OK` as soon as the upstream provider
accepts a request, so a later provider failure arrives in the body as an
`error` object with no `choices`. That reached the SDK's parse helper and
surfaced as `TypeError: 'NoneType' object is not iterable`, naming neither the
provider, the status, nor the fact that a timeout happened.
The four non-streaming paths now inspect the raw body before parsing and raise
the exception the upstream code maps to, so a masked 504 is catchable exactly
like an honest one. Streaming already had this guard inside the SDK.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(openai): teach the tool-cache fake about with_raw_response
The provider now reads the raw body before parsing, so a client double that
only implements `create` no longer satisfies it. Same shape as the fixes to the
reasoning-effort retry and Snowflake doubles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(tracing): reset the TraceCollectionListener singleton between tests
TraceCollectionListener caches a TraceBatchManager on the class and
`_initialized` short-circuits `__init__`, so batch state survives for the whole
xdist worker. `test_nested_agent_executor_flow_does_not_finalize_parent_batch`
left `trace_batch_id="debug-trace-batch"` behind, which moved every later trace
POST from /tracing/ephemeral/batches to /tracing/batches/<id>/events. The
recorded cassette then stopped matching, the agent retried, and the second call
found the cassette consumed -- surfacing as ConnectionError in an unrelated
test hundreds of tests later.
Reproduced deterministically by running the leaking test followed by
tests/tracing/test_trace_enable_disable.py::test_trace_calls_when_enabled_via_env;
fails on a68b5e903 too, so this predates the gateway fix it was blocking.
An autouse fixture now clears the cached instance after each test. Two canaries
pin the invariant and fail without the fixture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(tracing): drop the unwritable _listeners_setup canary
Both review bots flagged that the canary read `_listeners_setup` off the class,
where it is always False, so it could never fail. Correct, and the suggested fix
does not work either: `BaseEventListener.__init__` calls `setup_listeners`
(base_event_listener.py:16), which sets the flag on the instance
(trace_listener.py:229), so reading it back through `TraceCollectionListener()`
is always True. Neither read observes a leak, so the canary is deleted rather
than replaced, with the reasoning recorded so it is not re-added.
The same finding showed the fixture was resetting two class attributes that are
never assigned at class level. Only dropping `_instance` is load-bearing, so the
fixture is now one line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(tracing): correct why the _listeners_setup canary is unwritable
setup_listeners returns early when tracing is off and no override applies
(trace_listener.py:213-220), assigning the flag at :229 only when it actually
registers. Construction therefore does not always set it, as the previous note
claimed: with tracing disabled the flag never even reaches the instance dict.
The instance read reports ambient tracing state rather than isolation, which is
a better reason not to assert on it than the one recorded before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(tests): clear trace batch state in place instead of dropping the singleton
Dropping `TraceCollectionListener._instance` made the next construction re-run
`setup_listeners`, re-registering its handlers on the event bus. That broke
tests/telemetry/test_task_failure_instrumentation.py, which requires exactly one
handler per event: the re-registered `on_task_failed` made two. Verified against
a53ecc17f, where the same sequence passes -- the regression was mine.
The leak that needed fixing was batch state, not registration, so the fixture
now clears the manager's batch fields in place. Handler cleanup already belongs
to `cleanup_event_handlers`, and `first_time_handler` keeps its reference to the
same manager object.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(tests): clear tracing context vars so the listener can re-register
Bugbot flagged that keeping the singleton leaves `_listeners_setup` set, so after
`cleanup_event_handlers` wipes the bus `setup_listeners` returns early
(trace_listener.py:208) and tracing silently registers nothing for the rest of
the worker. Confirmed: after a tracing-enabled run, re-running setup restores 0
of 119 handler entries.
Dropping the singleton fixes that but previously broke
test_task_failure_instrumentation. The real cause was a third leak: the
`_tracing_enabled` context var stayed set, so the replacement listener still
believed tracing was on and re-registered `on_task_failed` next to telemetry's.
Clearing the context vars is what makes replacing the listener safe, so the
fixture now does both, and a canary pins it (fails with `assert True is False`
without the drop).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Push was choosing ZIP vs git from a local origin remote, so adding origin later rebuilt the last ZIP with no files. Prefer AMP zip_deployment from status, and fall back to the old origin heuristic when that field is missing.
`oxylabs` was pinned to exactly 2.0.0, so consumers could not take 3.0.0, out
since March. 3.x keeps the `RealtimeClient` surface these tools use, and all
four tools plus their failure paths were verified against the live API on both
2.0.0 and 3.0.0.
The lockfile keeps oxylabs at 2.0.0, so this permits the upgrade rather than
forcing it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(oxylabs): report scrape failures instead of raising IndexError
The oxylabs SDK logs HTTP errors and returns an empty response rather than
raising, so the unchecked `response.results[0]` in every Oxylabs tool turned a
rejected request into `IndexError: list index out of range`. Invalid credentials
-- the most likely first-run mistake -- gave no indication of the cause. A
result carrying a non-2xx `status_code` had the same problem one level down: the
job ran, the page did not come back, and the tool returned its empty content as
though the scrape had succeeded, handing the agent "[]".
Both are now reported as a `ToolFailure` naming what went wrong, so the agent
gets something it can act on and the framework records the call as failed:
401 Unauthorized
400 Bad Request - Parameter `parsing_instructions` can be used just with
`parse` parameter set to `true`.
Because the SDK keeps the cause only in its own log, the failing call is run
with a handler attached to the `oxylabs` logger and the status, the API's
explanation and timeouts are read back off it. `code` and `retryable` are set
from the status, so 429 and 5xx are marked worth retrying. Nothing about the
caller's logging configuration is changed; an application that has silenced the
SDK still gets the generic failure.
Content that is neither a string nor a dict is also serialized properly:
`parsing_instructions` commonly yields a list, and the previous `str()`
fallback produced a Python repr with single quotes instead of JSON.
The client construction and response handling these four tools duplicated
verbatim now live in a shared `OxylabsBaseTool`, following the existing
`SerpApiBaseTool` pattern, so the handling above exists in one place. The
generated tool specs change only by the new `locale` field, confirming the
tools' public surface is otherwise untouched.
Also add the `locale` option to the Google Search config, which the docs
already documented but the config model silently dropped, and correct two
copy-paste errors in the docs across all four locales.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(oxylabs): keep concurrent scrape diagnoses apart
The error capture attached a fresh handler to the shared `oxylabs` logger for
each scrape, so two scrapes in flight at once each saw both errors. `_diagnose`
reads the first HTTP status it finds, so a timeout could be reported as the
other request's 400 -- `retryable=False` on a failure that was worth retrying.
One handler now serves every scrape and routes each record to the capture of
the call that caused it via a `ContextVar`, which isolates threads and asyncio
tasks alike. Serializing the captures would have fixed the cross-talk too, but
at the cost of running every scrape one at a time. The handler stays attached
once installed: it is inert outside a capture, and detaching it would race with
concurrent scrapes.
The regression test forces the interleaving -- one capture is held open while
the other call logs -- and fails against the previous implementation.
Also drive `config` through the public constructor in the tests instead of
assigning `__dict__["config"]`, so they would catch `__init__` dropping a
supplied config.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_search_url interpolated ${query} inside an f-string, producing
URLs like https://www.bing.com/search?q=$test. The URL is passed
straight to the SERP request, so every search carried the malformed
query string.
Fixes#7325
The Args block for ConsoleFormatter.handle_llm_stream_chunk listed
'chunk' and 'crew_tree' parameters that no longer exist on the method.
The signature was refactored to (accumulated_text, call_type) but the
docstring was not updated. Callers in event_listener.py pass only the
two real params.
Refs #7312.
* docs(streaming): fix streaming output docstring examples
CrewStreamingOutput's example called crew.kickoff() without setting
stream=True on the Crew, so the snippet returned a CrewOutput and did
not stream anything.
FlowStreamingOutput's example called flow.kickoff_streaming() and
flow.kickoff_streaming_async(); neither method exists. Flow-level
streaming is exposed through Flow.kickoff with stream=True and
returns a StreamSession, not a FlowStreamingOutput.
Refs #7285
* docs(streaming): clarify Flow.kickoff does not take stream param
Flow.kickoff() has no stream parameter; the runtime returns a
StreamSession when self.stream is True. Reword the FlowStreamingOutput
note so callers know to configure the Flow with stream=True before
calling kickoff().
Addresses CodeRabbit review on #7286.
* docs(streaming): restore FlowStreamingOutput example
Add back an Example block showing valid usage of FlowStreamingOutput.
The class is only ever constructed directly with a chunk-producing
iterator (see lib/crewai/tests/test_streaming.py), so the example
mirrors that pattern instead of the original snippet that referenced
non-existent Flow.kickoff_streaming methods.
Addresses review feedback on #7286.
* docs(streaming): swap FlowStreamingOutput example for public Flow streaming path
Replace the test-only FlowStreamingOutput(sync_iterator=...) example
with the actual public flow-streaming path: Flow.stream=True followed
by kickoff() / kickoff_async(), which return StreamSession /
AsyncStreamSession. The example is labeled explicitly to make clear
that Flow.kickoff() does not return a FlowStreamingOutput, and points
readers at the streaming-flow-execution guide.
Addresses review feedback on #7286.
---------
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(schema): support list-form "type" arrays in JSON schema conversion
_json_schema_to_pydantic_type already handles anyOf/oneOf for nullable
unions -- the form Pydantic's own schema generation produces for
Optional[T] fields -- but had no handling for the other, equally valid
JSON Schema way of expressing the same thing: a list-form type array,
e.g. {"type": ["string", "null"]}. This is what .NET/System.Text.Json
-based schema generators produce instead, so any MCP tool schema from
a non-Python server using this form crashed create_model_from_schema
outright with "Unsupported JSON schema type: ['string', 'null']" --
taking down the entire MCPServerAdapter connection, not just the one
affected tool.
Confirmed against a real self-hosted MCP server (Equibles,
github.com/daniel3303/Equibles): several of its tools (e.g.
ListCompanyDocuments's startDate/endDate filters) use exactly this
pattern, and MCPServerAdapter couldn't connect to it at all as a
result -- reproduced identically on both Windows and macOS.
Fix mirrors the existing anyOf/oneOf handling: treat each entry in a
list-form type the same way an anyOf member is handled, building a
Union of the corresponding Python types. A single-element list
collapses to that one type via typing.Union's own behavior, and
"null" entries resolve to None (matching how the type == "null"
branch already behaves), producing the same Optional[T] shape as the
anyOf case would.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(schema): preserve union members when applying FORMAT_TYPE_MAP
CodeRabbit flagged this reviewing #7058: the format override in
_json_schema_to_pydantic_field replaced the whole resolved type with
FORMAT_TYPE_MAP[format_], even when that type was a Union built from a
list-form `type` (or anyOf/oneOf) rather than a plain `str`. For a
schema like {"type": ["string", "null"], "format": "date-time"}, this
collapsed Union[str, None] down to plain datetime, silently dropping
the null option -- masked for non-required fields by the
Optional-rewrap at the end of the same function, but not for a
required-but-nullable field (a valid, if unusual, JSON Schema shape).
The same override also drops any non-string members of a multi-type
array (e.g. ["string", "integer", "null"]) regardless of required
status, since nothing rewraps those.
Narrow the override to the `str` member specifically: replace `type_`
outright when it's already plain `str`, or substitute only the `str`
element inside a Union via get_origin/get_args, leaving null and
other type-array members untouched.
Added two tests covering the previously-broken cases: a required
nullable formatted field, and a multi-type array (string/integer/null)
with a format.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
gpt-4o-mini's official context window is 128,000 tokens (OpenAI
announcement and API docs; litellm's model database agrees), but the
shared LLM_CONTEXT_WINDOW_SIZES table and the OpenAI/Azure provider-local
tables listed 200000 - apparently copied from the neighboring o3-mini /
o4-mini entries. With CONTEXT_WINDOW_USAGE_RATIO = 0.85, crews resolved
the usable window to 170000 instead of 108800, letting history grow past
the model's real 128k limit and failing with API 400s on long runs.
Fixes#7293
Organization names are not unique, so the documented `@org/name` form can
resolve to the wrong organization and fail to find the skill. Document the
`@org-uuid/name` form instead, and add a note pointing at `crewai org list`
for the UUID.
Applies to the agent-side registry refs too: they resolve through the same
`/skills/:org/:name` endpoint and the same `~/.crewai/skills/{org}/{name}/`
cache path, so leaving them as `@acme` would contradict the install command.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
* fix(bedrock): preserve streaming tool call arguments at contentBlockStop
Streaming Converse handlers accumulate tool input as JSON string deltas in
accumulated_tool_input but never fold it back into current_tool_use["input"],
so function_args reads an empty {} at contentBlockStop. Parse the accumulated
input into the tool-use block (with a {} fallback) in both the sync and async
streaming handlers. This is the streaming counterpart of the non-streaming fix
in #5415 (issue #4972).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bedrock): coerce non-dict streaming tool input to empty dict
json.loads on the accumulated tool input can return a valid-but-non-object
JSON value (e.g. a string or list), which would fail at fn(**function_args)
with a TypeError. Enforce a dict shape before use in both the sync and async
streaming handlers, and add a regression test for the non-dict case.
Addresses CodeRabbit review feedback on #6150.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
LegacyClient filtered the server response with exact app and action
names. The platform returns canonical app names and provider action
names, so valid aliases produced no tools.