taskiq-llm-queue 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. taskiq_llm_queue-0.1.0a1/.gitignore +35 -0
  2. taskiq_llm_queue-0.1.0a1/CHANGELOG.md +85 -0
  3. taskiq_llm_queue-0.1.0a1/LICENSE +21 -0
  4. taskiq_llm_queue-0.1.0a1/NOTICE +15 -0
  5. taskiq_llm_queue-0.1.0a1/PKG-INFO +296 -0
  6. taskiq_llm_queue-0.1.0a1/README.md +247 -0
  7. taskiq_llm_queue-0.1.0a1/STABILITY.md +14 -0
  8. taskiq_llm_queue-0.1.0a1/docs/DESIGN.md +89 -0
  9. taskiq_llm_queue-0.1.0a1/docs/EXAMPLES.md +20 -0
  10. taskiq_llm_queue-0.1.0a1/docs/RELEASING.md +47 -0
  11. taskiq_llm_queue-0.1.0a1/docs/TESTING.md +293 -0
  12. taskiq_llm_queue-0.1.0a1/pyproject.toml +141 -0
  13. taskiq_llm_queue-0.1.0a1/samples/README.md +175 -0
  14. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/__init__.py +138 -0
  15. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/_logging.py +40 -0
  16. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/_version.py +5 -0
  17. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/__init__.py +86 -0
  18. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/acker.py +239 -0
  19. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/batching.py +188 -0
  20. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/broker.py +937 -0
  21. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/delivery.py +50 -0
  22. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/errors.py +178 -0
  23. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/events.py +51 -0
  24. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/filters.py +32 -0
  25. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/heartbeat.py +157 -0
  26. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/queue.py +141 -0
  27. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/s3_offload.py +346 -0
  28. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/scheduler.py +113 -0
  29. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/broker/serializer.py +32 -0
  30. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/cli.py +488 -0
  31. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/client.py +1409 -0
  32. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/config.py +592 -0
  33. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/context.py +62 -0
  34. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/core.py +832 -0
  35. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/envelope.py +136 -0
  36. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/errors.py +71 -0
  37. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/failover.py +286 -0
  38. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/http_transport.py +220 -0
  39. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/keys.py +87 -0
  40. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/langchain/__init__.py +64 -0
  41. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/langchain/_client.py +142 -0
  42. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/langchain/bedrock.py +291 -0
  43. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/langchain/openai.py +103 -0
  44. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/lifecycle.py +512 -0
  45. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/metrics/__init__.py +45 -0
  46. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/metrics/emf.py +100 -0
  47. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/metrics/prometheus.py +66 -0
  48. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/metrics_names.py +19 -0
  49. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/protocols.py +140 -0
  50. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/__init__.py +62 -0
  51. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/bedrock.py +188 -0
  52. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/classify.py +251 -0
  53. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/embeddings.py +87 -0
  54. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/hosts.py +34 -0
  55. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/openai_compat.py +142 -0
  56. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/providers/registry.py +173 -0
  57. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/py.typed +0 -0
  58. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/__init__.py +59 -0
  59. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/_lua.py +461 -0
  60. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/breaker.py +86 -0
  61. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/claims.py +226 -0
  62. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/config_store.py +148 -0
  63. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/fallback.py +111 -0
  64. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/limiter.py +482 -0
  65. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/reliability/local_limiter.py +153 -0
  66. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/results/__init__.py +25 -0
  67. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/results/backend.py +226 -0
  68. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/results/dispatcher.py +366 -0
  69. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/results/s3_backend.py +105 -0
  70. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/routing.py +173 -0
  71. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/routing_hooks.py +90 -0
  72. taskiq_llm_queue-0.1.0a1/src/taskiq_llm_queue/tasks.py +453 -0
@@ -0,0 +1,35 @@
1
+ # Environments and secrets (never commit tokens: GitHub, PyPI, AWS)
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ .pypirc
6
+ *.pem
7
+ *.key
8
+ .venv/
9
+ venv/
10
+
11
+ # Python build and cache
12
+ __pycache__/
13
+ *.py[cod]
14
+ *.egg-info/
15
+ build/
16
+ dist/
17
+ .pytest_cache/
18
+ .ruff_cache/
19
+ .mypy_cache/
20
+ .coverage
21
+ coverage.xml
22
+ htmlcov/
23
+
24
+ # Test and tool output
25
+ .hypothesis/
26
+ .locust/
27
+ *.log
28
+ reports/
29
+
30
+ # Editors and OS
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ .DS_Store
35
+ Thumbs.db
@@ -0,0 +1,85 @@
1
+ # Changelog
2
+
3
+ All notable changes are listed here. The project uses semantic versioning; while it is 0.x,
4
+ a minor release may break the public API, with a deprecation announced one minor release ahead
5
+ (see STABILITY.md).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0a1] - 2026-10-29
10
+
11
+ First pre-release. Everything listed below is new.
12
+
13
+ ### Changed before 0.1 (breaking against the earlier dev snapshots)
14
+ - Every call returns a result object with `.meta`: `ConverseResult`, `ChatResult`, `EmbedResult`, `InvokeResult`.
15
+ - `timeout=` / `deadline=` became `timeout_secs=` / `deadline_at=`; `request_id=` is accepted on every call.
16
+ - `invoke_model(model, body, *, content_type=, accept=)` replaces guessing from a `"body"` key.
17
+ - Cancellation raises `LLMQueueCancelled`.
18
+ - `__all__` lists only the client surface; wire and plumbing names import from their submodules.
19
+ - `aws_region` defaults to `None`; `redis_password` is a `SecretStr`; `_SECS` env names (old `_S` names warn once).
20
+ - Redis keys are hash-tagged for cluster mode (key schema 2); ids are hashed with keyed BLAKE2b (`LLMQ_ID_HASH_SALT`), and xxhash is no longer a dependency.
21
+ - `QueuedChatBedrockConverse` takes `model_alias=`; an alias passed as `model=` raises.
22
+ - The empty `s3` extra is gone.
23
+
24
+ ### Added
25
+ - Package skeleton (src layout, hatchling, uv, ruff, mypy strict, pytest).
26
+ - Frozen contracts for 0.x:
27
+ - `QueueConfig` / `ModelConfig` / `Target`, with `alias[@target]` resolution and `LLMQ_*` env loading
28
+ - envelope v1 and `LLMReply`, with the version rule and hashed-id labels
29
+ - `request_context`
30
+ - `KeySpace` (Redis key schema)
31
+ - error tree and `ErrorInfo`
32
+ - protocols for providers, limiter, claims, results and receive gates
33
+ - metric names
34
+ - Contract test suite.
35
+ - Docker test stack (Valkey 8.1 with a password, ElasticMQ 1.7.1 with all queues and DLQs).
36
+ - SQS capability check script: ElasticMQ 1.7.1 passes 20/20, moto 5.2.5 server passes 19/20 (no `StartMessageMoveTask`).
37
+ - `HybridSQSBroker`: strict priority across queues with reserved slots, capacity-aware receive, receive gates, real long polls, batched acks and heartbeats, release on shutdown, byte-aware batched send, delays, FIFO, fair queues, expiry, message metadata; unknown tasks and newer envelopes are released, never dropped.
38
+ - Reliability:
39
+ - adaptive concurrency limiter in Lua (baseline start, ramp, AIMD, pause gate, lane share, leases), with a sync twin and a local fallback limiter
40
+ - Lua claim store for exactly-once execution, circuit breaker and fallback resolver
41
+ - live config store with an audit stream
42
+ - Results: JSON result backend (no pickle; `GETDEL`; shared-owner safe) and a per-pod dispatcher. Measured: 1,000 ready results take 1 `MGET`.
43
+ - Providers:
44
+ - Bedrock with SDK retries off (`total_max_attempts=1`), and OpenAI-compatible with `max_retries=0`
45
+ - error-family classification and `Retry-After` parsing
46
+ - provider registry for built-in, import-path and entry-point providers, with allowed and denied hosts
47
+ - `fakellm`: a fake Bedrock + OpenAI server with real error shapes, SigV4 region detection, scenarios, timed phases and an admin API, plus a Docker service.
48
+ - `docs/DESIGN.md`: the delivery-safety rules (DS1–DS19) and broker features (P1–P17, A1–A9) that the code refers to.
49
+ - Makefile and `tools/llmq_dev.py` task runner.
50
+ - S3 offload for large messages (P9) and an S3 result backend (P10).
51
+ - LangChain adapters for Bedrock and OpenAI-compatible chat and embeddings; LangChain 0.3 and 1.x; LangGraph compatible.
52
+ - Ops: canary router, failover to the direct path, `llm-queue` CLI (routing, fallback, limiter, breaker, queues, jobs), metrics hooks.
53
+ - `LLMQueue` client with jobs, cancellation, sync calls and an ordered drain on `stop()`.
54
+ - Performance: orjson for JSON and the SQS wire format, one parse per received message, results encoded once, 6 Valkey round trips per worker call, `speedups` extra (hiredis).
55
+ - Logging: structlog key-value events over stdlib `logging`; the library never configures logging.
56
+ - Reliability fixes from a multi-pod review: infrastructure errors release messages instead of deleting them, owner-checked claims, a pod heartbeat for direct and sync paths, the limiter grows only when busy, throttles never trip the breaker, S3 payloads deleted only after ack.
57
+ - Runnable `samples/` (13), including a replay of a real app's flows against real Bedrock.
58
+ - 619 tests; CI on Python 3.11–3.14, oldest-dependency and LangChain 1.x jobs, integration on the Docker stack; trusted publishing to PyPI.
59
+
60
+ ### Known gaps
61
+ - Valkey restarted without persistence wipes all claims; in-flight calls are then treated as lost and may run a second time (S50: 0–16 re-runs). Keep Valkey persistence on.
62
+ - A reply that can't be stored is cached per pod; a redelivery to a different pod calls the provider again.
63
+ - Exhausted jobs reach a dead-letter queue only if their queue has one configured.
64
+ - Cluster-mode Valkey is covered by key-slot tests, not yet by a real cluster run.
65
+ - A call queued during a Valkey outage longer than `valkey_wait_failover_secs` can run twice if a worker takes it before its cancel flag is replayed.
66
+
67
+ ### Fixed before release (found by the scenario harness, `tools/scenarios`)
68
+ - Limiter collapse under sporadic throttles: cuts only on clustered throttles (≥2 and ≥5 % in a 1 s window), at most one cut per window, pause only when half the window throttles, growth scales with the limit. 10K burst with 2 % throttles: 22.5 → 76.1 req/s, 6,721 → 10,000 ok (S35).
69
+ - Limit overshoot: growth slows near the last cut point; settles at 57–63 instead of swinging 44–64 (S07).
70
+ - Spurious claim takeovers under load: the heartbeat treated the worker's own just-finished claim as lost and cancelled the result publish (S35c, S27: 19 and 4 losses → 0). A real claim loss now ends the task quietly with a warning, no ERROR traceback.
71
+ - Valkey pools are sized to worker slots + 16; a failed lease release is retried on the next limiter call instead of holding the slot for 240 s.
72
+ - Throttle storms: a cut that does not lower the throttle ratio stops at the start value; a throttled step gets `throttle_retry_attempts` (new, default 6) tries; an rt call walks the fallback chain up to 3 times before `no_capacity`; leases live 60 s (was 240 s, renewed every 10 s) so a killed pod's slots return within a minute; a send queued behind a burst no longer counts as SQS down while other sends complete. 10K burst with 30 % random throttles and a worker kill: 16.3 → 49.7-59.3 req/s, 17.5 % → 99.2 % ok (S35b); S35 76.1 → 90.4 req/s.
73
+ - SQS down: the send is bounded by `sqs_send_timeout_secs` and half the remaining budget, then the call runs direct; first call served in 5.7 s instead of timing out (S20).
74
+ - Valkey down on a send-only pod: result-read errors now trip failover; waiting callers run direct; cancels are replayed on recovery (S21).
75
+ - Target-specific errors (ResourceNotFound, AccessDenied, model not in region, 404) move to the next fallback target instead of ending the call (S38).
76
+ - Per-attempt timeout `attempt_timeout_secs`; configurable provider read/connect timeouts (S39).
77
+ - A worker missing a model alias releases the message unclaimed (jittered delay) instead of acking an `internal_error`; it answers `model_not_configured` only near the delivery budget or deadline (S48).
78
+
79
+ - Valkey blip: a worker releasing work after a Valkey error is no longer cancelled by the claim heartbeat mid-release; the SQS message is released before the claim, so no call is lost (S50: 32/40 → 40/40 ok).
80
+ - Result dispatcher: a new same-lane waiter no longer forces a full read of every pending result (float rounding woke the loop); 1K waiters 43 → 26 MGET/s (S31).
81
+
82
+ ### Added before release
83
+ - Scenario harness: 54 scenarios (work, retry, pause, cancel, purge, region change, timeouts, chaos, scale, 10K burst) on an isolated compose profile; `make scenarios`, `make load`, `make chaos`.
84
+ - Scenario harness real-Bedrock mode (`SCN_PROVIDER=aws`): queues stay local, provider calls go to Bedrock; only AWS-safe scenarios run. S01, S03, S16, S17, S29, S40, S41 pass against real Bedrock (~108 calls).
85
+ - Settings: `attempt_timeout_secs`, `provider_read_timeout_secs`, `provider_connect_timeout_secs`, `sqs_send_timeout_secs`, `valkey_wait_failover_secs`, limiter window settings.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The taskiq-llm-queue authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ taskiq-llm-queue
2
+ Copyright (c) 2026 The taskiq-llm-queue authors. Licensed under the MIT License.
3
+
4
+ The SQS broker's design and feature set build on two MIT-licensed projects. This package is
5
+ an independent implementation; any code adapted from them keeps its original notice below.
6
+
7
+ taskiq-aio-sqs — https://github.com/vonsteer/taskiq-aio-sqs
8
+ Copyright (c) 2022-2024 Jesse Constante. MIT License.
9
+ Ideas used: aiobotocore-based broker, visibility heartbeat, message metadata,
10
+ extended (S3) messages, batching, fair queues.
11
+
12
+ taskiq-sqs — https://github.com/taskiq-python/taskiq-sqs
13
+ Copyright (c) 2026 Mike Shultz and Taskiq team. MIT License.
14
+ Ideas used: multi-queue structure, queue declaration, message expiry,
15
+ batch flush on shutdown, S3 offload middleware and result backend.
@@ -0,0 +1,296 @@
1
+ Metadata-Version: 2.5
2
+ Name: taskiq-llm-queue
3
+ Version: 0.1.0a1
4
+ Summary: An LLM request client with its own durable queue: taskiq + Amazon SQS, adaptive limits, retry and fallback.
5
+ Project-URL: Homepage, https://github.com/techiewonk/taskiq-llm-queue
6
+ Project-URL: Documentation, https://github.com/techiewonk/taskiq-llm-queue#readme
7
+ Project-URL: Source, https://github.com/techiewonk/taskiq-llm-queue
8
+ Project-URL: Issues, https://github.com/techiewonk/taskiq-llm-queue/issues
9
+ Project-URL: Changelog, https://github.com/techiewonk/taskiq-llm-queue/blob/main/CHANGELOG.md
10
+ Author: The taskiq-llm-queue authors
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ License-File: NOTICE
14
+ Keywords: bedrock,langchain,langgraph,llm,openai,queue,rate-limit,sqs,taskiq
15
+ Classifier: Development Status :: 2 - Pre-Alpha
16
+ Classifier: Framework :: AsyncIO
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Topic :: System :: Distributed Computing
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: aiobotocore>=2.23
30
+ Requires-Dist: orjson<4,>=3.10
31
+ Requires-Dist: pydantic<3,>=2.10
32
+ Requires-Dist: redis>=5.0.1
33
+ Requires-Dist: structlog>=24.1
34
+ Requires-Dist: taskiq<0.14,>=0.13
35
+ Provides-Extra: langchain-aws
36
+ Requires-Dist: langchain-aws<2,>=0.2.24; extra == 'langchain-aws'
37
+ Requires-Dist: langchain-core<2,>=0.3.68; extra == 'langchain-aws'
38
+ Provides-Extra: langchain-openai
39
+ Requires-Dist: langchain-core<2,>=0.3.68; extra == 'langchain-openai'
40
+ Requires-Dist: langchain-openai<2,>=0.3.26; extra == 'langchain-openai'
41
+ Provides-Extra: metrics
42
+ Requires-Dist: prometheus-client>=0.20; extra == 'metrics'
43
+ Provides-Extra: openai
44
+ Requires-Dist: httpx<1,>=0.27; extra == 'openai'
45
+ Requires-Dist: openai<4,>=1.40; extra == 'openai'
46
+ Provides-Extra: speedups
47
+ Requires-Dist: hiredis>=3; extra == 'speedups'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # taskiq-llm-queue
51
+
52
+ > **Status:** pre-alpha (`0.1.0a1`). Built and tested: the `LLMQueue` client, the SQS broker, adaptive limiter, claims, circuit breaker, retry and fallback, JSON and S3 result backends, Bedrock and OpenAI-compatible providers, LangChain/LangGraph adapters, canary routing, failover and the CLI. 619 tests on Valkey 8.1, ElasticMQ 1.7.1 and a fake LLM server; CI on Python 3.11–3.14. Breaking changes are still possible before 0.1 (see [CHANGELOG](https://github.com/techiewonk/taskiq-llm-queue/blob/main/CHANGELOG.md)).
53
+
54
+ An LLM request client with its own durable queue, built on [taskiq](https://github.com/taskiq-python/taskiq) and Amazon SQS.
55
+
56
+ Your code keeps calling LangChain models (`ainvoke`, `aembed_query`) or a small client API. Each call becomes a task on an SQS queue. Workers in your own app's processes run it under a limit learned from the provider's throttling, retry and fall back when needed, and return the result through Redis/Valkey.
57
+
58
+ ```
59
+ your code ──> client / LangChain adapter ──> SQS (per model: -rt queue first, base, bulk)
60
+ │
61
+ result <── Redis/Valkey <── worker in your pods ──> Bedrock / OpenAI-compatible
62
+ (per-pod dispatcher) (adaptive limiter, retry, fallback)
63
+ ```
64
+
65
+ ## Why
66
+
67
+ - **Spikes wait in a queue instead of failing.** Throttled calls aren't lost or retried in a storm.
68
+ - **No quota numbers to maintain.** The limit for each model and target is learned from throttle errors and shared by all pods.
69
+ - **Chat first.** Separate `-rt` queues and reserved capacity keep live traffic ahead of background and bulk work.
70
+ - **No extra servers.** The same app pods send and execute; SQS is only the broker.
71
+ - **Drop-in.** LangChain/LangGraph code doesn't change: tool calling, callbacks (Langfuse, LangSmith) and response objects are identical.
72
+ - **Safe rollout.** `direct` mode uses the same limiter, and a live canary moves traffic per model and lane without restarts.
73
+
74
+ ## Install
75
+
76
+ ```bash
77
+ uv add "taskiq-llm-queue[langchain-aws]" # Bedrock + LangChain
78
+ uv add "taskiq-llm-queue[openai,langchain-openai]" # OpenAI-compatible + LangChain
79
+ uv add "taskiq-llm-queue[speedups]" # hiredis parser for redis-py
80
+ ```
81
+
82
+ | | Packages |
83
+ |---|---|
84
+ | Core | `taskiq`, `aiobotocore`, `redis>=5.0.1`, `pydantic>=2.10`, `orjson`, `structlog` |
85
+ | `openai` | `openai`, `httpx` |
86
+ | `langchain-aws` / `langchain-openai` | LangChain **0.3 and 1.x** are both supported |
87
+ | `metrics` | `prometheus-client` |
88
+ | `speedups` | `hiredis` |
89
+
90
+ The package declares version **ranges**; pin exact versions in your app's lockfile. `aiobotocore` pins a narrow `botocore` window, so let your resolver pick an `aiobotocore` that matches your `boto3`.
91
+
92
+ ## Quickstart
93
+
94
+ ```python
95
+ from taskiq_llm_queue import LLMQueue, ModelConfig, QueueConfig, Target, request_context
96
+ from taskiq_llm_queue.langchain import QueuedChatBedrockConverse
97
+
98
+ llmq = LLMQueue(QueueConfig(
99
+ key_prefix="llmq:local",
100
+ redis_host="localhost", redis_port=6379, redis_password="localdev", redis_ssl=False,
101
+ sqs_endpoint_url="http://localhost:9324", # ElasticMQ locally; omit on AWS
102
+ models={
103
+ "gpt120b": ModelConfig(
104
+ provider="bedrock", model_id="openai.gpt-oss-120b-1:0",
105
+ targets={"primary": Target(region="ap-south-1"),
106
+ "apac": Target(profile_id="apac.openai.gpt-oss-120b-1:0")},
107
+ queues={"rt": "llm-gpt120b-rt", "default": "llm-gpt120b"}),
108
+ },
109
+ )) # or QueueConfig.from_env() (LLMQ_* variables)
110
+
111
+ async def main():
112
+ await llmq.start(mode="both") # "send" | "work" | "both"
113
+ model = QueuedChatBedrockConverse(llmq=llmq, model_alias="gpt120b")
114
+ with request_context(lane="rt", session_id="s-1", user_id="u-1"):
115
+ reply = await model.ainvoke("Summarise today's market in one line.")
116
+ print(reply.content, reply.response_metadata["served_model"])
117
+ await llmq.stop() # drains in-flight work
118
+ ```
119
+
120
+ In a FastAPI app, call `start()` in the lifespan startup and `stop()` on shutdown. Send-only processes, such as CLIs and scripts, use `mode="send"`.
121
+
122
+ ## Client API
123
+
124
+ Every call returns a result object with `.meta`: `served_model`, `served_target`, `fallback_used`, `attempts`, `queue_wait_ms`, `provider_ms`, `worker`.
125
+
126
+ ```python
127
+ res = await llmq.converse("gpt120b", {"messages": [...], "system": [...], "toolConfig": ...},
128
+ lane="rt", fallback="chain", timeout_secs=90, request_id="req-1")
129
+ res.response # exact Bedrock Converse dict
130
+
131
+ emb = await llmq.embed("titan", ["a", "b"], lane="bulk")
132
+ emb.vectors # list[list[float]]; `emb` itself also works as a list
133
+
134
+ chat = await llmq.chat("llama", {"messages": [{"role": "user", "content": "hi"}]})
135
+ chat.response # OpenAI-compatible chat completion dict
136
+
137
+ raw = await llmq.invoke_model("titan", {"inputText": "a"}, content_type="application/json")
138
+ raw.json(), raw.body, raw.content_type
139
+
140
+ await llmq.cancel("req-1") # not run if not started; the caller gets LLMQueueCancelled
141
+ ```
142
+
143
+ - **Timing:** `timeout_secs` (relative) or `deadline_at` (absolute epoch seconds) on every call.
144
+ - **Model ids** are aliases, optionally with a target: `gpt120b`, `gpt120b@apac`. Unknown aliases are rejected when the config is built.
145
+ - **Errors:** everything derives from `LLMQueueError`: `LLMQueueTimeout`, `LLMQueueCancelled`, `ProviderError(retryable, retry_after_secs)`, `QueueUnavailable`, `ConfigError`.
146
+ - **Sync callers:** `converse_sync`, `chat_sync`, `embed_sync`, `invoke_model_sync` run `direct` under the shared limiter (counted as sync fallbacks). Set `strict_async=True` to raise instead. `close_sync()` (or `stop()`) closes the sync runner.
147
+ - **Jobs:** `register_job(name, fn)` on workers, `send_job(name, payload, timeout_secs=...)` anywhere; jobs run on the job queues with the same claims and DLQ rules.
148
+
149
+ ## LangChain and LangGraph
150
+
151
+ | Class | Based on | How |
152
+ |---|---|---|
153
+ | `QueuedChatBedrockConverse` | `langchain_aws.ChatBedrockConverse` | Builds the request and parses the reply with the installed langchain-aws's own code, so `bind_tools`, structured output, cache usage and service tier match exactly |
154
+ | `QueuedBedrockEmbeddings` | `langchain_aws.BedrockEmbeddings` | `aembed_query` / `aembed_documents` go through the queue |
155
+ | `QueuedChatOpenAI` | `langchain_openai.ChatOpenAI` | `http_async_client` with a queued httpx transport: chat completions and embeddings go through the queue; queue meta lands in `response_metadata`. The Responses API (`/responses`) is not supported yet |
156
+ | `QueuedOpenAIEmbeddings` | `langchain_openai.OpenAIEmbeddings` | Same transport; `check_embedding_ctx_length=False` by default for non-OpenAI servers |
157
+
158
+ - **Model names:** pass the queue alias as `model_alias=` (plus `model_id=` or `llmq=`). Passing an alias as `model=` raises a clear error.
159
+ - **Streaming:** all queued classes force `disable_streaming=True` / `streaming=False`. `astream()` and LangGraph `stream_mode="messages"` return one complete message, so nothing bypasses the queue.
160
+ - **Laziness:** queued classes find the running `LLMQueue` on first use and check routing and the kill switch on every call, so you can create them at import time.
161
+
162
+ ## Configuration
163
+
164
+ `QueueConfig.from_env()` reads `LLMQ_*` variables (durations end in `_SECS`; older `_S` names still work with a one-time warning). Notable settings:
165
+
166
+ | Setting | Default | Note |
167
+ |---|---|---|
168
+ | `aws_region` | `None` | Left to the AWS credential chain / IRSA |
169
+ | `redis_url` or `redis_host`/`port`/`username`/`password`/`ssl` | localhost | `redis_password` is a `SecretStr` |
170
+ | `redis_socket_timeout_secs` / `connect_timeout` / `health_check_interval` | 5 / 2 / 30 | Connection errors retried 3× with backoff |
171
+ | `id_hash_salt` (`LLMQ_ID_HASH_SALT`) | library constant | **Set your own in production**; keys the BLAKE2b hash of session/user ids |
172
+ | `timeout_secs` per lane | rt 90, bg 300, bulk 900 | |
173
+ | `attempt_timeout_secs` | 180 | One provider attempt; a hung target moves to the next fallback step |
174
+ | `provider_read_timeout_secs` / `provider_connect_timeout_secs` | 300 / 10 | Bedrock botocore and OpenAI httpx timeouts |
175
+ | `sqs_send_timeout_secs` | 5 | SQS counts as down only when no send completed for this long; then the call runs direct |
176
+ | `throttle_retry_attempts` | 6 | Tries per fallback step for throttles (backoff holds no slot) |
177
+ | `valkey_wait_failover_secs` | 10 | Result reads failing this long make waiting callers run direct |
178
+ | `redis_max_connections` | 32 | Raised automatically to worker slots + 16 |
179
+
180
+ Redis keys are cluster-safe (hash-tagged per request and per model+target), so cluster-mode Valkey (e.g. ElastiCache Serverless) works.
181
+
182
+ ## Providers
183
+
184
+ ```python
185
+ ModelConfig(provider="openai", model_id="llama-3.1-70b",
186
+ targets={"primary": Target(base_url="http://vllm:8000/v1")}, api_key_env="LOCAL_LLM_KEY")
187
+ QueueConfig(allowed_providers={"bedrock"}, denied_hosts=["*.openai.azure.com"]) # enforced on sender and worker
188
+ QueueConfig(providers={"myprov": "mypkg.providers:MyProvider"}) # custom provider, loaded by every pod
189
+ ```
190
+
191
+ A custom provider implements `async call(op, params, target) -> dict` and `classify(exc) -> ErrorInfo(kind="throttle" | "retryable" | "fatal", retry_after_secs)`. It can also be registered through the `taskiq_llm_queue.providers` entry point. SDK-level retries are off inside workers (botocore `total_max_attempts=1`, openai `max_retries=0`), so throttles reach the limiter.
192
+
193
+ ## Lanes and queues
194
+
195
+ | Lane | Queue | Use |
196
+ |---|---|---|
197
+ | `rt` | `llm-<model>-rt` | Live user requests; polled first; may use the whole limit |
198
+ | `bg` | `llm-<model>` | Background work; at most 80 % of the limit |
199
+ | `bulk` | `llm-<model>-bulk` if configured (e.g. embeddings), else base | Ingestion/batch; lowest priority |
200
+
201
+ A queued `bg`/`bulk` call waits at most 15 s for a slot, then goes back to the queue with a delay instead of holding a worker.
202
+
203
+ ## Results
204
+
205
+ Results are stored as JSON (never pickle) for 5 minutes and read once (`GETDEL`). Each pod runs one result dispatcher: a pub/sub wake reads only the announced results, and a periodic batched sweep catches the rest, so 10K waiting calls don't mean 10K polling loops.
206
+
207
+ ## Fallbacks: how to check and update
208
+
209
+ A fallback chain is tried **inside the worker**. Each step is retried with backoff before moving on:
210
+
211
+ 1. same model, primary target
212
+ 2. same model, another target (e.g. a cross-region profile `gpt120b@apac`)
213
+ 3. another model (only when the call's policy is `chain`)
214
+
215
+ Embeddings never switch model. Throttles shrink the limiter but never trip the circuit breaker; only 5xx and timeouts do.
216
+
217
+ ```bash
218
+ uv run llm-queue fallback get # whole config (JSON); starts from your app's defaults
219
+ uv run llm-queue fallback get --model gpt120b # resolved chain for one model
220
+ uv run llm-queue fallback test --model gpt120b # dry run: which target would serve now
221
+ uv run llm-queue fallback set --model gpt120b --chain "gpt20b"
222
+ uv run llm-queue fallback set --file fallback.json # merges per model; --replace to overwrite all
223
+ uv run llm-queue fallback reset # back to the startup default
224
+ ```
225
+
226
+ Chains are validated and reloaded by every pod within 30 s with no restart. An invalid config is rejected and the last good one stays. Every change is written to an audit stream.
227
+
228
+ ```json
229
+ { "chains": {
230
+ "gpt120b": ["gpt20b"],
231
+ "gpt20b": [{"model": "gpt120b", "require_headroom": true}]
232
+ },
233
+ "skip_targets": { "gpt120b": ["apac"] } }
234
+ ```
235
+
236
+ **To see fallbacks happen:** `res.meta` / `response_metadata` show `served_model`, `served_target`, `fallback_used` and `attempts`, and the `llmq_fallback_total{model,step}` metric counts them. `samples/05_fallback_on_throttle.py` shows it end to end.
237
+
238
+ ## Operating it
239
+
240
+ ```bash
241
+ uv run llm-queue routing set --model gpt20b --lane rt --pct 10 # canary: 10 % of sessions via SQS
242
+ uv run llm-queue routing kill on # everything direct within 30 s
243
+ uv run llm-queue routing reset
244
+ uv run llm-queue limiter show --model gpt120b # learned limit, in-flight, pause
245
+ uv run llm-queue limiter reset --model gpt120b [--target apac] # forget the learned limit
246
+ uv run llm-queue breaker show | reset
247
+ uv run llm-queue queues # messages per queue and DLQ
248
+ uv run llm-queue job send ingest --payload-file payload.json --queue jobs-ingest
249
+ ```
250
+
251
+ ## Failure behaviour
252
+
253
+ | Failure | What happens |
254
+ |---|---|
255
+ | Provider throttles | Limiter shrinks and pauses that model + target; brokers stop receiving it unless another target in the chain has headroom; work waits in SQS |
256
+ | Provider down (5xx, timeouts) | Circuit breaker opens → next fallback step |
257
+ | Worker killed mid-task | Message reappears after the visibility timeout; the claim prevents a second provider call |
258
+ | Rolling deploy | Pods stop receiving at once, release unstarted work, finish running calls, then exit (90 s budget in the reference deployment) |
259
+ | **SQS unreachable** | That model switches to `direct` (+ shared limiter) for 300 s on transient send errors only. An ambiguous send is claimed first, so it can't run twice |
260
+ | **Redis/Valkey unreachable** | Workers release messages instead of losing them; senders run `direct` with a local per-pod cap (last known limit ÷ live pods) |
261
+
262
+ ## Logging
263
+
264
+ The library logs with structlog's key-value API on top of stdlib `logging` and **never configures logging itself**. Records are named `taskiq_llm_queue.*`; the message reads `event key=value ...` and the fields are also on `record.llmq` (a dict). Prompts, responses and params are never logged.
265
+
266
+ ```python
267
+ import logging
268
+ logging.getLogger("taskiq_llm_queue").setLevel(logging.INFO) # stdlib apps: that's all
269
+
270
+ # loguru apps: the usual InterceptHandler picks these up like any stdlib logger.
271
+ # structlog apps: use structlog.stdlib.ProcessorFormatter on your handler to render them.
272
+ ```
273
+
274
+ The `llm-queue` CLI logs warnings and errors to stderr, so its stdout is always plain JSON.
275
+
276
+ ## Security and data
277
+
278
+ - Encryption is server-side on the queues (SSE-SQS or SSE-KMS); the library needs no setting for either.
279
+ - Prompts and responses are never logged. `user_id` / `session_id` are hashed (keyed BLAKE2b) in task labels; raw ids stay inside the message body.
280
+ - Nothing secret goes into messages; credentials come from the AWS chain (IRSA / Pod Identity) or env.
281
+ - Unknown task names or newer message versions are released for another pod, never silently dropped.
282
+
283
+ ## Run it locally (no AWS needed)
284
+
285
+ ```bash
286
+ make up # Valkey (password) + ElasticMQ + fakellm
287
+ make test # unit + integration against the stack
288
+ make down
289
+ # Windows without make: uv run python tools/llmq_dev.py up | test | down
290
+ ```
291
+
292
+ Runnable examples are in [`samples/`](https://github.com/techiewonk/taskiq-llm-queue/tree/main/samples): quickstart, chat, embeddings, lanes, fallback on throttle, send-only and worker pods, LangChain, LangGraph, jobs, cancel and timeouts, sync calls, CLI, and `13_research_assistant_flow.py`, which replays a real app's flows locally or against real Bedrock (`--target aws`, settings from `.env` or AWS Secrets Manager). The stack, capability checks and test layers are described in [docs/TESTING.md](https://github.com/techiewonk/taskiq-llm-queue/blob/main/docs/TESTING.md); delivery-safety rules in [docs/DESIGN.md](https://github.com/techiewonk/taskiq-llm-queue/blob/main/docs/DESIGN.md).
293
+
294
+ ## Credits
295
+
296
+ Broker design and feature set build on [taskiq-aio-sqs](https://github.com/vonsteer/taskiq-aio-sqs) (MIT, © 2022-2024 Jesse Constante) and [taskiq-sqs](https://github.com/taskiq-python/taskiq-sqs) (MIT, © 2026 Mike Shultz and the Taskiq team). Licence: MIT.