taskiq-llm-queue 0.1.0a1__py3-none-any.whl
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.
- taskiq_llm_queue/__init__.py +138 -0
- taskiq_llm_queue/_logging.py +40 -0
- taskiq_llm_queue/_version.py +5 -0
- taskiq_llm_queue/broker/__init__.py +86 -0
- taskiq_llm_queue/broker/acker.py +239 -0
- taskiq_llm_queue/broker/batching.py +188 -0
- taskiq_llm_queue/broker/broker.py +937 -0
- taskiq_llm_queue/broker/delivery.py +50 -0
- taskiq_llm_queue/broker/errors.py +178 -0
- taskiq_llm_queue/broker/events.py +51 -0
- taskiq_llm_queue/broker/filters.py +32 -0
- taskiq_llm_queue/broker/heartbeat.py +157 -0
- taskiq_llm_queue/broker/queue.py +141 -0
- taskiq_llm_queue/broker/s3_offload.py +346 -0
- taskiq_llm_queue/broker/scheduler.py +113 -0
- taskiq_llm_queue/broker/serializer.py +32 -0
- taskiq_llm_queue/cli.py +488 -0
- taskiq_llm_queue/client.py +1409 -0
- taskiq_llm_queue/config.py +592 -0
- taskiq_llm_queue/context.py +62 -0
- taskiq_llm_queue/core.py +832 -0
- taskiq_llm_queue/envelope.py +136 -0
- taskiq_llm_queue/errors.py +71 -0
- taskiq_llm_queue/failover.py +286 -0
- taskiq_llm_queue/http_transport.py +220 -0
- taskiq_llm_queue/keys.py +87 -0
- taskiq_llm_queue/langchain/__init__.py +64 -0
- taskiq_llm_queue/langchain/_client.py +142 -0
- taskiq_llm_queue/langchain/bedrock.py +291 -0
- taskiq_llm_queue/langchain/openai.py +103 -0
- taskiq_llm_queue/lifecycle.py +512 -0
- taskiq_llm_queue/metrics/__init__.py +45 -0
- taskiq_llm_queue/metrics/emf.py +100 -0
- taskiq_llm_queue/metrics/prometheus.py +66 -0
- taskiq_llm_queue/metrics_names.py +19 -0
- taskiq_llm_queue/protocols.py +140 -0
- taskiq_llm_queue/providers/__init__.py +62 -0
- taskiq_llm_queue/providers/bedrock.py +188 -0
- taskiq_llm_queue/providers/classify.py +251 -0
- taskiq_llm_queue/providers/embeddings.py +87 -0
- taskiq_llm_queue/providers/hosts.py +34 -0
- taskiq_llm_queue/providers/openai_compat.py +142 -0
- taskiq_llm_queue/providers/registry.py +173 -0
- taskiq_llm_queue/py.typed +0 -0
- taskiq_llm_queue/reliability/__init__.py +59 -0
- taskiq_llm_queue/reliability/_lua.py +461 -0
- taskiq_llm_queue/reliability/breaker.py +86 -0
- taskiq_llm_queue/reliability/claims.py +226 -0
- taskiq_llm_queue/reliability/config_store.py +148 -0
- taskiq_llm_queue/reliability/fallback.py +111 -0
- taskiq_llm_queue/reliability/limiter.py +482 -0
- taskiq_llm_queue/reliability/local_limiter.py +153 -0
- taskiq_llm_queue/results/__init__.py +25 -0
- taskiq_llm_queue/results/backend.py +226 -0
- taskiq_llm_queue/results/dispatcher.py +366 -0
- taskiq_llm_queue/results/s3_backend.py +105 -0
- taskiq_llm_queue/routing.py +173 -0
- taskiq_llm_queue/routing_hooks.py +90 -0
- taskiq_llm_queue/tasks.py +453 -0
- taskiq_llm_queue-0.1.0a1.dist-info/METADATA +296 -0
- taskiq_llm_queue-0.1.0a1.dist-info/RECORD +65 -0
- taskiq_llm_queue-0.1.0a1.dist-info/WHEEL +4 -0
- taskiq_llm_queue-0.1.0a1.dist-info/entry_points.txt +2 -0
- taskiq_llm_queue-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- taskiq_llm_queue-0.1.0a1.dist-info/licenses/NOTICE +15 -0
|
@@ -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.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
taskiq_llm_queue/__init__.py,sha256=YbXgYe6Rnel0PYCxxMvTB18lNmT1IjRhzGlYI3ZYz-8,5061
|
|
2
|
+
taskiq_llm_queue/_logging.py,sha256=10VK1aDkD7rjSCC7F1ti7iKuC5zdbX8qMXbhdGo8hdY,1704
|
|
3
|
+
taskiq_llm_queue/_version.py,sha256=wJ7_Rt22BJ255pg0_dqlU1SMURFvmz5YSNLNi9dSlwg,150
|
|
4
|
+
taskiq_llm_queue/cli.py,sha256=vJlTmIsD1wh8pdi3ovsa4GSA_EM0NxwwS8ASB-g3HIA,20366
|
|
5
|
+
taskiq_llm_queue/client.py,sha256=hSlKCKDb1xDXKM2k-OeUrixIimjBb-7HBVVvdqZDQ4E,56771
|
|
6
|
+
taskiq_llm_queue/config.py,sha256=qdd76IoGxDYor7lrSKqSW2dNos5ZmZd794OoKg6w5KY,25638
|
|
7
|
+
taskiq_llm_queue/context.py,sha256=09AJL3I3vJLK7Ke2B7GeqtN8ElZk7DuN_40nG7jNhN4,1977
|
|
8
|
+
taskiq_llm_queue/core.py,sha256=YdNKGtIZLhOlJdvazojwvWHTsbCIoW8jaMjl2mRSsks,35468
|
|
9
|
+
taskiq_llm_queue/envelope.py,sha256=ZNLbssnB2dQWwYYPk5hMFkBxOPnTIMKUtcqTtesQMNA,4956
|
|
10
|
+
taskiq_llm_queue/errors.py,sha256=z7mkXXW9E-oSmnJx38aULncZ-A0SJEqOBEibuJWVkz0,2339
|
|
11
|
+
taskiq_llm_queue/failover.py,sha256=DNC32e0G2LSxeFY2UFzFT84AA6NryIZtvPXasIdqwvU,12089
|
|
12
|
+
taskiq_llm_queue/http_transport.py,sha256=a9so7lIofhqRsAVDL1iWOTckZa2WI0YDcMk1BQvEolY,9609
|
|
13
|
+
taskiq_llm_queue/keys.py,sha256=Me6C-feM7vOXyNyKUcudSR1OfJ85htY446ngNFI00Fg,3200
|
|
14
|
+
taskiq_llm_queue/lifecycle.py,sha256=NTgsMuVjPCLpdyOXW7FpiAUiNKLuMxzQyRJFFVG_sNc,20279
|
|
15
|
+
taskiq_llm_queue/metrics_names.py,sha256=IOfG6NsAGlkUR-_dIxFCyez-0GwSMnnT2s89UEfuyZQ,771
|
|
16
|
+
taskiq_llm_queue/protocols.py,sha256=mWzPP0mcbYnCvMk2TiaUxk-yDznnbsIUJObjYlGclGs,4408
|
|
17
|
+
taskiq_llm_queue/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
taskiq_llm_queue/routing.py,sha256=xcLM2Nmsz7gYIVqKMtwP-McKuO4Gv5NlJ24yGv9Y9Xo,7061
|
|
19
|
+
taskiq_llm_queue/routing_hooks.py,sha256=s7WI-KhO7LrvEU3G7EHmfN455pw5C2vicRJ2ctxsIQQ,3357
|
|
20
|
+
taskiq_llm_queue/tasks.py,sha256=B3Hd5OrXaAuPYCVAnCIS7j7DLjz5BwqHbbOKSkCeMWo,19505
|
|
21
|
+
taskiq_llm_queue/broker/__init__.py,sha256=Qsiy6A6yTxLeCM_V5Kln161PCXWE-IO3jNrNtR9eOHY,2330
|
|
22
|
+
taskiq_llm_queue/broker/acker.py,sha256=3N4gucLrgpbTONQH-CzqTINm_xW0TFR4WG2tQn_9opA,9288
|
|
23
|
+
taskiq_llm_queue/broker/batching.py,sha256=Nw0FhHxJ8yDgn0HR_CeJfmdvrBvT3oNKUpDnnbRtRQQ,6977
|
|
24
|
+
taskiq_llm_queue/broker/broker.py,sha256=oFEKVZz6T9roMGpzO0V9Z4mvak6EKmyHygVeyoRpvKs,41634
|
|
25
|
+
taskiq_llm_queue/broker/delivery.py,sha256=p6p7xo55f86hBaMoT2u2DfyT92vK_HWoohaBlFvO1-g,1767
|
|
26
|
+
taskiq_llm_queue/broker/errors.py,sha256=cwCSeBn9dZUPSBqUDGLdprKxOtZ9Q4IpWTjq4XJYvH8,6134
|
|
27
|
+
taskiq_llm_queue/broker/events.py,sha256=GGCsXZlA7FGpF2NWDt3OKPWHCAS7LddJCn9Bxym32YI,2113
|
|
28
|
+
taskiq_llm_queue/broker/filters.py,sha256=5WCl66ykcDXtFaRiBwUul3x7bRQ2Pp0MYpxd7c6bpfQ,1195
|
|
29
|
+
taskiq_llm_queue/broker/heartbeat.py,sha256=YoqUfr9esLvk73Omtv2w4RMhphWJccmeG8DlInPPVQY,6387
|
|
30
|
+
taskiq_llm_queue/broker/queue.py,sha256=rb4FsckJz_o5xRss44kFSgK9pQJ70rJh-LigAjhYYz0,5953
|
|
31
|
+
taskiq_llm_queue/broker/s3_offload.py,sha256=jOeIEELqTZSznd8c1ekNF40P12jnpChu36v54jTLho8,14584
|
|
32
|
+
taskiq_llm_queue/broker/scheduler.py,sha256=zf5u13ZzsnlOsU535znZ-_WolhVUYVvWyXV5lNN_be8,4751
|
|
33
|
+
taskiq_llm_queue/broker/serializer.py,sha256=YuPMNqv8X8EFi7EJtriqNx6BxwnYyD-6XmYfJS__uys,1363
|
|
34
|
+
taskiq_llm_queue/langchain/__init__.py,sha256=pleTIl0TOmC9F7wuMU61oPWI4EiYk7mIi_hsvA_QugA,2159
|
|
35
|
+
taskiq_llm_queue/langchain/_client.py,sha256=h6fdA73Tgb51OZkMzQaQxh0vii-icNM7saxrIcA9r7E,4352
|
|
36
|
+
taskiq_llm_queue/langchain/bedrock.py,sha256=UQmpb2bq-yFJ5aI6X9Q3ld-_IqjonxumUwiI1H8Ee6A,11616
|
|
37
|
+
taskiq_llm_queue/langchain/openai.py,sha256=cFwq-EEJMR62xaBrHynKSNH1_WAJxf6dCxIoZVRlhwI,4158
|
|
38
|
+
taskiq_llm_queue/metrics/__init__.py,sha256=XsUAn_NFMgrB8_qXnAFOuweF10qM0biyMy0ZyMk-xB4,1704
|
|
39
|
+
taskiq_llm_queue/metrics/emf.py,sha256=PvR2htszW2Ft8iRbXQeINqloXqj2yAW8Sua3WPASc4c,3919
|
|
40
|
+
taskiq_llm_queue/metrics/prometheus.py,sha256=COSeCv3yDAZCY72GOKCINcOzIK3t81R9Rcnsu7GCV10,2946
|
|
41
|
+
taskiq_llm_queue/providers/__init__.py,sha256=C45nWW0R6bTTioW7G_L3iaaEk-dRptGkBORpX6VotUE,1662
|
|
42
|
+
taskiq_llm_queue/providers/bedrock.py,sha256=X0ex2odFg3qouy4Q82VqtUp7-Oyj9pqGEa6Cw-JY1qs,8128
|
|
43
|
+
taskiq_llm_queue/providers/classify.py,sha256=JJPeAhMDxzTWxxZtz-IEU_Qz8_QhuiTot1w5cDej58U,9667
|
|
44
|
+
taskiq_llm_queue/providers/embeddings.py,sha256=7i6klmXAaau_iyhFdq__ROIhw2jVEybuI4Bbvd-JId8,3371
|
|
45
|
+
taskiq_llm_queue/providers/hosts.py,sha256=aywHB5S7-zI4Cc730_y9FngkvjGwQfRnrKa2rqf8l54,1290
|
|
46
|
+
taskiq_llm_queue/providers/openai_compat.py,sha256=xuz4XkZZhtRpmshJ5Tda_wVnBnHpK_X9OI9B44jxvT0,6191
|
|
47
|
+
taskiq_llm_queue/providers/registry.py,sha256=GsbwqPHB6ymMZYyIRkqax6hhR6KVOp-OFFra0G5IV50,7340
|
|
48
|
+
taskiq_llm_queue/reliability/__init__.py,sha256=FIUW0u-u_XnupyDxQd83VrUZvXNWd-b4OCj2q5QyizI,1502
|
|
49
|
+
taskiq_llm_queue/reliability/_lua.py,sha256=IpqqM-4dn-XGf6_riC2bDcDDuLiFnvCP9ZiPTTRqhRw,18622
|
|
50
|
+
taskiq_llm_queue/reliability/breaker.py,sha256=nbbEICcJmyxmjpkjLsoPKxHqcdiVxN43R-lrQon-FR0,3287
|
|
51
|
+
taskiq_llm_queue/reliability/claims.py,sha256=VbVzRogoMMtxlHtnkmY-eaT51I4_ehM_NCpGYFW0RuI,9838
|
|
52
|
+
taskiq_llm_queue/reliability/config_store.py,sha256=p9ceiNSMTDzqfbSNA-F7NehTq_nopWp56atSN1Z_pPY,5997
|
|
53
|
+
taskiq_llm_queue/reliability/fallback.py,sha256=HaSK5minJBmbvqkT_0tMsTseAypvFNigYdznlt2-03Y,4022
|
|
54
|
+
taskiq_llm_queue/reliability/limiter.py,sha256=bDwnD901gKh_FR604FIUgmkCq80EwygZ9hxSSpLB2Bo,19866
|
|
55
|
+
taskiq_llm_queue/reliability/local_limiter.py,sha256=DC4JDeq-0RVFhlS3igUQpr6fxF_ml8P48jZ0y0OkS7E,5937
|
|
56
|
+
taskiq_llm_queue/results/__init__.py,sha256=u-N4eMjqYHxH20kUIW2agLHrbXu-LZPElKCaQcO-XBY,719
|
|
57
|
+
taskiq_llm_queue/results/backend.py,sha256=mE5i3XXfuZyrJwdejl13UHAjuIwCl1lhMBqgzMDYb6g,9899
|
|
58
|
+
taskiq_llm_queue/results/dispatcher.py,sha256=GZeHiqlLwxuX6ISLhyS7Fc_8X9BtG7TZZPEkn4xmrZs,16444
|
|
59
|
+
taskiq_llm_queue/results/s3_backend.py,sha256=3snDgJhRMjlTPhPNWHVwsQqh4JvdXFFwDxbNCWq0oOA,4104
|
|
60
|
+
taskiq_llm_queue-0.1.0a1.dist-info/METADATA,sha256=OvWaIwuQeqUOaGyJZ0ZUnqiHMu6K_TQARUwzOyxxNX0,18269
|
|
61
|
+
taskiq_llm_queue-0.1.0a1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
62
|
+
taskiq_llm_queue-0.1.0a1.dist-info/entry_points.txt,sha256=u9QOO0rKyinfwfcBrnyNu5LdDproymBwFJSSedoSWBg,64
|
|
63
|
+
taskiq_llm_queue-0.1.0a1.dist-info/licenses/LICENSE,sha256=fyt0lY9XpMIg0h9QEgiY3twFAGBqEiyCBB_9C83IpIc,1085
|
|
64
|
+
taskiq_llm_queue-0.1.0a1.dist-info/licenses/NOTICE,sha256=QBk0_Gzqpez_MheapoFec71cpSSfrPDKQMoK4x1JgYQ,794
|
|
65
|
+
taskiq_llm_queue-0.1.0a1.dist-info/RECORD,,
|
|
@@ -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.
|