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.
Files changed (65) hide show
  1. taskiq_llm_queue/__init__.py +138 -0
  2. taskiq_llm_queue/_logging.py +40 -0
  3. taskiq_llm_queue/_version.py +5 -0
  4. taskiq_llm_queue/broker/__init__.py +86 -0
  5. taskiq_llm_queue/broker/acker.py +239 -0
  6. taskiq_llm_queue/broker/batching.py +188 -0
  7. taskiq_llm_queue/broker/broker.py +937 -0
  8. taskiq_llm_queue/broker/delivery.py +50 -0
  9. taskiq_llm_queue/broker/errors.py +178 -0
  10. taskiq_llm_queue/broker/events.py +51 -0
  11. taskiq_llm_queue/broker/filters.py +32 -0
  12. taskiq_llm_queue/broker/heartbeat.py +157 -0
  13. taskiq_llm_queue/broker/queue.py +141 -0
  14. taskiq_llm_queue/broker/s3_offload.py +346 -0
  15. taskiq_llm_queue/broker/scheduler.py +113 -0
  16. taskiq_llm_queue/broker/serializer.py +32 -0
  17. taskiq_llm_queue/cli.py +488 -0
  18. taskiq_llm_queue/client.py +1409 -0
  19. taskiq_llm_queue/config.py +592 -0
  20. taskiq_llm_queue/context.py +62 -0
  21. taskiq_llm_queue/core.py +832 -0
  22. taskiq_llm_queue/envelope.py +136 -0
  23. taskiq_llm_queue/errors.py +71 -0
  24. taskiq_llm_queue/failover.py +286 -0
  25. taskiq_llm_queue/http_transport.py +220 -0
  26. taskiq_llm_queue/keys.py +87 -0
  27. taskiq_llm_queue/langchain/__init__.py +64 -0
  28. taskiq_llm_queue/langchain/_client.py +142 -0
  29. taskiq_llm_queue/langchain/bedrock.py +291 -0
  30. taskiq_llm_queue/langchain/openai.py +103 -0
  31. taskiq_llm_queue/lifecycle.py +512 -0
  32. taskiq_llm_queue/metrics/__init__.py +45 -0
  33. taskiq_llm_queue/metrics/emf.py +100 -0
  34. taskiq_llm_queue/metrics/prometheus.py +66 -0
  35. taskiq_llm_queue/metrics_names.py +19 -0
  36. taskiq_llm_queue/protocols.py +140 -0
  37. taskiq_llm_queue/providers/__init__.py +62 -0
  38. taskiq_llm_queue/providers/bedrock.py +188 -0
  39. taskiq_llm_queue/providers/classify.py +251 -0
  40. taskiq_llm_queue/providers/embeddings.py +87 -0
  41. taskiq_llm_queue/providers/hosts.py +34 -0
  42. taskiq_llm_queue/providers/openai_compat.py +142 -0
  43. taskiq_llm_queue/providers/registry.py +173 -0
  44. taskiq_llm_queue/py.typed +0 -0
  45. taskiq_llm_queue/reliability/__init__.py +59 -0
  46. taskiq_llm_queue/reliability/_lua.py +461 -0
  47. taskiq_llm_queue/reliability/breaker.py +86 -0
  48. taskiq_llm_queue/reliability/claims.py +226 -0
  49. taskiq_llm_queue/reliability/config_store.py +148 -0
  50. taskiq_llm_queue/reliability/fallback.py +111 -0
  51. taskiq_llm_queue/reliability/limiter.py +482 -0
  52. taskiq_llm_queue/reliability/local_limiter.py +153 -0
  53. taskiq_llm_queue/results/__init__.py +25 -0
  54. taskiq_llm_queue/results/backend.py +226 -0
  55. taskiq_llm_queue/results/dispatcher.py +366 -0
  56. taskiq_llm_queue/results/s3_backend.py +105 -0
  57. taskiq_llm_queue/routing.py +173 -0
  58. taskiq_llm_queue/routing_hooks.py +90 -0
  59. taskiq_llm_queue/tasks.py +453 -0
  60. taskiq_llm_queue-0.1.0a1.dist-info/METADATA +296 -0
  61. taskiq_llm_queue-0.1.0a1.dist-info/RECORD +65 -0
  62. taskiq_llm_queue-0.1.0a1.dist-info/WHEEL +4 -0
  63. taskiq_llm_queue-0.1.0a1.dist-info/entry_points.txt +2 -0
  64. taskiq_llm_queue-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  65. taskiq_llm_queue-0.1.0a1.dist-info/licenses/NOTICE +15 -0
@@ -0,0 +1,220 @@
1
+ """httpx transport that turns OpenAI SDK HTTP requests into LLM queue calls (D88).
2
+
3
+ Given to `openai.AsyncOpenAI(http_client=httpx.AsyncClient(transport=...))`, it answers
4
+ `/chat/completions` with `llmq.chat` and `/embeddings` with `llmq.embed`, so the SDK and
5
+ LangChain parse replies exactly as from a real server. Queue errors become OpenAI-style error
6
+ bodies with a matching status, so the SDK raises its usual exceptions (429 → `RateLimitError`).
7
+ Chat replies carry how the queue served them under the extra body key `llmq_meta`.
8
+ Needs the `openai` extra (httpx); the core package never imports this module.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import array
14
+ import base64
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ import httpx
18
+ import orjson
19
+
20
+ from taskiq_llm_queue.errors import (
21
+ ConfigError,
22
+ LLMQueueCancelled,
23
+ LLMQueueError,
24
+ LLMQueueTimeout,
25
+ ProviderError,
26
+ QueueUnavailable,
27
+ )
28
+ from taskiq_llm_queue.langchain._client import resolve_queue
29
+
30
+ if TYPE_CHECKING:
31
+ from taskiq_llm_queue.config import Lane
32
+ from taskiq_llm_queue.envelope import FallbackPolicy
33
+ from taskiq_llm_queue.langchain._client import ChatResultLike, QueueClient
34
+
35
+ # Never resolved: every request is answered in-process by the transport.
36
+ QUEUED_BASE_URL = "http://llmq.invalid/v1"
37
+ # Extra key of a queued chat completion body: the call's `CallMeta` (served model, attempts...).
38
+ META_KEY = "llmq_meta"
39
+
40
+
41
+ class _RequestRejected(Exception):
42
+ """A request the transport cannot queue (bad body, streaming, unknown route)."""
43
+
44
+ def __init__(self, status: int, message: str, code: str) -> None:
45
+ super().__init__(message)
46
+ self.status = status
47
+ self.code = code
48
+
49
+
50
+ class QueuedHTTPTransport(httpx.AsyncBaseTransport, httpx.BaseTransport):
51
+ """Answer OpenAI-compatible HTTP requests from the LLM queue instead of the network.
52
+
53
+ `model_alias` overrides the body's `model` (use it when the SDK model name differs from the
54
+ queue alias). `llmq=None` uses the default queue, looked up per request. Auth headers are
55
+ ignored: workers hold the provider credentials. The sync side needs `chat_sync` on the client
56
+ for chat (embeddings use `embed_sync`); without it the SDK gets a 501 error.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ llmq: QueueClient | None = None,
62
+ model_alias: str | None = None,
63
+ *,
64
+ lane: Lane | str | None = None,
65
+ fallback: FallbackPolicy | None = None,
66
+ timeout_secs: float | None = None,
67
+ ) -> None:
68
+ """Keep the queue, alias and per-call overrides; nothing is resolved until a request."""
69
+ self._llmq = llmq
70
+ self._alias = model_alias
71
+ self._lane = lane
72
+ self._fallback = fallback
73
+ self._timeout_secs = timeout_secs
74
+
75
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
76
+ """Route one SDK request through the async queue API."""
77
+ try:
78
+ route, alias, body = self._parse(request, await request.aread())
79
+ queue = resolve_queue(self._llmq)
80
+ if route == "chat":
81
+ chat = await queue.chat(
82
+ alias,
83
+ body,
84
+ lane=self._lane,
85
+ fallback=self._fallback,
86
+ timeout_secs=self._timeout_secs,
87
+ )
88
+ data = _chat_response(chat)
89
+ else:
90
+ texts = _embedding_inputs(body)
91
+ embedded = await queue.embed(
92
+ alias, texts, lane=self._lane, timeout_secs=self._timeout_secs
93
+ )
94
+ data = _embedding_response(alias, embedded.vectors, body.get("encoding_format"))
95
+ except (_RequestRejected, LLMQueueError) as exc:
96
+ return _error_response(request, exc)
97
+ return httpx.Response(200, json=data, request=request)
98
+
99
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
100
+ """Route one SDK request through the sync queue API (`chat_sync` / `embed_sync`)."""
101
+ try:
102
+ route, alias, body = self._parse(request, request.read())
103
+ queue = resolve_queue(self._llmq)
104
+ if route == "chat":
105
+ chat_sync = getattr(queue, "chat_sync", None)
106
+ if chat_sync is None:
107
+ raise _RequestRejected(
108
+ 501, "sync chat is not supported by this queue; use ainvoke", "sync"
109
+ )
110
+ chat = chat_sync(
111
+ alias,
112
+ body,
113
+ lane=self._lane,
114
+ fallback=self._fallback,
115
+ timeout_secs=self._timeout_secs,
116
+ )
117
+ data = _chat_response(chat)
118
+ else:
119
+ texts = _embedding_inputs(body)
120
+ embedded = queue.embed_sync(
121
+ alias, texts, lane=self._lane, timeout_secs=self._timeout_secs
122
+ )
123
+ data = _embedding_response(alias, embedded.vectors, body.get("encoding_format"))
124
+ except (_RequestRejected, LLMQueueError) as exc:
125
+ return _error_response(request, exc)
126
+ return httpx.Response(200, json=data, request=request)
127
+
128
+ def _parse(self, request: httpx.Request, raw: bytes) -> tuple[str, str, dict[str, Any]]:
129
+ """Return (route, alias, JSON body), rejecting what the queue cannot serve."""
130
+ path = request.url.path.rstrip("/")
131
+ if request.method != "POST":
132
+ raise _RequestRejected(405, f"{request.method} {path} is not queued", "method")
133
+ if path.endswith("/chat/completions"):
134
+ route = "chat"
135
+ elif path.endswith("/embeddings"):
136
+ route = "embeddings"
137
+ else:
138
+ raise _RequestRejected(404, f"route {path} is not supported by the queue", "route")
139
+ try:
140
+ body = orjson.loads(raw or b"{}")
141
+ except ValueError as exc:
142
+ raise _RequestRejected(400, f"request body is not JSON: {exc}", "body") from exc
143
+ if not isinstance(body, dict):
144
+ raise _RequestRejected(400, "request body must be a JSON object", "body")
145
+ if body.get("stream"):
146
+ raise _RequestRejected(400, "streaming is not supported through the queue", "stream")
147
+ alias = self._alias or body.get("model")
148
+ if not isinstance(alias, str) or not alias:
149
+ raise _RequestRejected(400, "no model alias in the transport or the body", "model")
150
+ return route, alias, body
151
+
152
+
153
+ def _chat_response(result: ChatResultLike) -> dict[str, Any]:
154
+ """The completion body plus `llmq_meta` (the SDK keeps unknown keys as extras)."""
155
+ return {**result.response, META_KEY: result.meta.model_dump(mode="json")}
156
+
157
+
158
+ def _embedding_inputs(body: dict[str, Any]) -> list[str]:
159
+ """Texts from an embeddings body; token-id inputs are rejected (queue embeds text only)."""
160
+ value = body.get("input")
161
+ if isinstance(value, str):
162
+ return [value]
163
+ if isinstance(value, list) and all(isinstance(v, str) for v in value):
164
+ return value
165
+ raise _RequestRejected(400, "embeddings input must be a string or a list of strings", "input")
166
+
167
+
168
+ def _embedding_response(
169
+ alias: str, vectors: list[list[float]], encoding_format: object
170
+ ) -> dict[str, Any]:
171
+ """OpenAI embeddings body; base64 is float32 in native order, as the SDK decodes it."""
172
+ as_base64 = encoding_format == "base64"
173
+ return {
174
+ "object": "list",
175
+ "model": alias,
176
+ "data": [
177
+ {
178
+ "object": "embedding",
179
+ "index": i,
180
+ "embedding": (
181
+ base64.b64encode(array.array("f", vec).tobytes()).decode("ascii")
182
+ if as_base64
183
+ else vec
184
+ ),
185
+ }
186
+ for i, vec in enumerate(vectors)
187
+ ],
188
+ "usage": {"prompt_tokens": 0, "total_tokens": 0},
189
+ }
190
+
191
+
192
+ def _error_response(request: httpx.Request, exc: Exception) -> httpx.Response:
193
+ """Map a queue error to the status and body an OpenAI server would send.
194
+
195
+ `ProviderError` uses its `status_code` when the client sets one, else 429 when retryable
196
+ (the SDK raises `RateLimitError`) and 400 when not; timeouts are 504, queue outages 503.
197
+ """
198
+ headers = {"x-llmq-error": type(exc).__name__}
199
+ if isinstance(exc, _RequestRejected):
200
+ status, err_type, code = exc.status, "invalid_request_error", exc.code
201
+ elif isinstance(exc, ProviderError):
202
+ explicit = getattr(exc, "status_code", None)
203
+ status = explicit if isinstance(explicit, int) else (429 if exc.retryable else 400)
204
+ err_type = "rate_limit_error" if status == 429 else "provider_error"
205
+ code = "provider_error"
206
+ if exc.retry_after_secs is not None:
207
+ headers["retry-after"] = f"{exc.retry_after_secs:g}"
208
+ headers["retry-after-ms"] = str(int(exc.retry_after_secs * 1000))
209
+ elif isinstance(exc, LLMQueueCancelled):
210
+ status, err_type, code = 499, "cancelled", "cancelled"
211
+ elif isinstance(exc, LLMQueueTimeout):
212
+ status, err_type, code = 504, "timeout", "queue_timeout"
213
+ elif isinstance(exc, QueueUnavailable):
214
+ status, err_type, code = 503, "service_unavailable", "queue_unavailable"
215
+ elif isinstance(exc, ConfigError):
216
+ status, err_type, code = 404, "invalid_request_error", "model_not_found"
217
+ else:
218
+ status, err_type, code = 500, "server_error", "llmq_error"
219
+ body = {"error": {"message": str(exc), "type": err_type, "param": None, "code": code}}
220
+ return httpx.Response(status, json=body, headers=headers, request=request)
@@ -0,0 +1,87 @@
1
+ """Every Redis/Valkey key and channel name, built from one configurable prefix.
2
+
3
+ Keeping names in one place lets several environments share a Redis without collisions and
4
+ keeps the state schema (versioned under `schema`) easy to migrate. Keys that one Lua script
5
+ touches together share a hash tag (`{...}`), so they map to one slot on cluster-mode Valkey.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ # 2: hash tags for cluster mode (`{m:<model>:<target>}`, `{r:<request_id>}`, `{cfg}`).
13
+ SCHEMA_VERSION = 2
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class KeySpace:
18
+ """Key builder for one prefix, e.g. `llmq:prod` -> `llmq:prod:{r:<id>}:job`.
19
+
20
+ Tags group the keys of one script: `{r:<request_id>}` (claim, cancel, params, result),
21
+ `{m:<model>:<target>}` (limiter state, leases, breaker) and `{cfg}` (live config, audit).
22
+ """
23
+
24
+ prefix: str
25
+
26
+ def _request(self, request_id: str, name: str) -> str:
27
+ return f"{self.prefix}:{{r:{request_id}}}:{name}"
28
+
29
+ def _target(self, model: str, target: str, name: str) -> str:
30
+ return f"{self.prefix}:{{m:{model}:{target}}}:{name}"
31
+
32
+ def job(self, request_id: str) -> str:
33
+ """Claim state of one task (hash): state, worker, heartbeat, failures."""
34
+ return self._request(request_id, "job")
35
+
36
+ def params(self, request_id: str) -> str:
37
+ """Large params stored by reference (string)."""
38
+ return self._request(request_id, "params")
39
+
40
+ def cancel(self, request_id: str) -> str:
41
+ """Cancel flag for one task (string)."""
42
+ return self._request(request_id, "cancel")
43
+
44
+ def result(self, task_id: str) -> str:
45
+ """JSON result of one task, deleted on read (string); same slot as its claim."""
46
+ return self._request(task_id, "res")
47
+
48
+ def limiter(self, model: str, target: str) -> str:
49
+ """Adaptive limiter state for one model target (hash, no TTL)."""
50
+ return self._target(model, target, "ac")
51
+
52
+ def leases(self, model: str, target: str) -> str:
53
+ """In-flight slot leases scored by expiry (sorted set)."""
54
+ return self._target(model, target, "lease")
55
+
56
+ def breaker(self, model: str, target: str) -> str:
57
+ """Circuit breaker state for one model target (hash, no TTL)."""
58
+ return self._target(model, target, "cb")
59
+
60
+ def done_channel(self, pod: str) -> str:
61
+ """Pub/sub channel that wakes one pod's result dispatcher."""
62
+ return f"{self.prefix}:done:{pod}"
63
+
64
+ @property
65
+ def routing(self) -> str:
66
+ """Live canary routing config (JSON string)."""
67
+ return f"{self.prefix}:{{cfg}}:routing"
68
+
69
+ @property
70
+ def fallback(self) -> str:
71
+ """Live fallback chains (JSON string)."""
72
+ return f"{self.prefix}:{{cfg}}:fallback"
73
+
74
+ @property
75
+ def audit(self) -> str:
76
+ """Audit stream of config changes (who, before, after)."""
77
+ return f"{self.prefix}:{{cfg}}:audit"
78
+
79
+ @property
80
+ def pods(self) -> str:
81
+ """Live pods of this prefix (sorted set scored by expiry)."""
82
+ return f"{self.prefix}:pods"
83
+
84
+ @property
85
+ def schema(self) -> str:
86
+ """State schema version marker."""
87
+ return f"{self.prefix}:schema"
@@ -0,0 +1,64 @@
1
+ """LangChain adapters that send model calls through the LLM queue (extras `langchain-*`).
2
+
3
+ The adapter classes are loaded on first attribute access, so importing this package (for
4
+ example to call `set_default_queue` from `LLMQueue.start()`) never imports langchain, boto3 or
5
+ httpx. `QueuedChatBedrockConverse` / `QueuedBedrockEmbeddings` need `[langchain-aws]`;
6
+ `QueuedChatOpenAI` / `QueuedOpenAIEmbeddings` need `[openai,langchain-openai]`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from importlib import import_module
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from taskiq_llm_queue.langchain._client import (
15
+ ChatResultLike,
16
+ ConverseResultLike,
17
+ EmbedResultLike,
18
+ QueueClient,
19
+ get_default_queue,
20
+ set_default_queue,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from taskiq_llm_queue.langchain.bedrock import (
25
+ QueuedBedrockEmbeddings,
26
+ QueuedChatBedrockConverse,
27
+ )
28
+ from taskiq_llm_queue.langchain.openai import QueuedChatOpenAI, QueuedOpenAIEmbeddings
29
+
30
+ # Lazy exports: attribute name -> (module, extra that provides its dependencies).
31
+ _LAZY = {
32
+ "QueuedChatBedrockConverse": ("bedrock", "langchain-aws"),
33
+ "QueuedBedrockEmbeddings": ("bedrock", "langchain-aws"),
34
+ "QueuedChatOpenAI": ("openai", "openai,langchain-openai"),
35
+ "QueuedOpenAIEmbeddings": ("openai", "openai,langchain-openai"),
36
+ }
37
+
38
+ __all__ = [
39
+ "ChatResultLike",
40
+ "ConverseResultLike",
41
+ "EmbedResultLike",
42
+ "QueueClient",
43
+ "QueuedBedrockEmbeddings",
44
+ "QueuedChatBedrockConverse",
45
+ "QueuedChatOpenAI",
46
+ "QueuedOpenAIEmbeddings",
47
+ "get_default_queue",
48
+ "set_default_queue",
49
+ ]
50
+
51
+
52
+ def __getattr__(name: str) -> Any:
53
+ """Import an adapter's module on first use, naming the extra to install if it is missing."""
54
+ if name not in _LAZY:
55
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
56
+ module, extra = _LAZY[name]
57
+ try:
58
+ value = getattr(import_module(f"{__name__}.{module}"), name)
59
+ except ImportError as exc:
60
+ raise ImportError(
61
+ f"{name} needs the optional dependencies: uv add 'taskiq-llm-queue[{extra}]'"
62
+ ) from exc
63
+ globals()[name] = value
64
+ return value
@@ -0,0 +1,142 @@
1
+ """The client interface the adapters call, and the process-wide default queue (D85).
2
+
3
+ Adapters type against `QueueClient` instead of importing `LLMQueue`, so they work with any object
4
+ that has the same methods (tests use a fake). Classes created at import time find the running
5
+ queue on first call through `get_default_queue`. This module never imports langchain or httpx.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
11
+
12
+ from taskiq_llm_queue.errors import LLMQueueError
13
+
14
+ if TYPE_CHECKING:
15
+ from taskiq_llm_queue.config import Lane
16
+ from taskiq_llm_queue.envelope import CallMeta, FallbackPolicy
17
+
18
+
19
+ class ConverseResultLike(Protocol):
20
+ """What `converse` returns: the exact Bedrock Converse dict and how it was served."""
21
+
22
+ @property
23
+ def response(self) -> dict[str, Any]: ...
24
+
25
+ @property
26
+ def meta(self) -> CallMeta: ...
27
+
28
+
29
+ class ChatResultLike(Protocol):
30
+ """What `chat` returns: the OpenAI-compatible completion body and how it was served."""
31
+
32
+ @property
33
+ def response(self) -> dict[str, Any]: ...
34
+
35
+ @property
36
+ def meta(self) -> CallMeta: ...
37
+
38
+
39
+ class EmbedResultLike(Protocol):
40
+ """What `embed` returns: one vector per text, in input order, and how they were served."""
41
+
42
+ @property
43
+ def vectors(self) -> list[list[float]]: ...
44
+
45
+ @property
46
+ def meta(self) -> CallMeta: ...
47
+
48
+
49
+ @runtime_checkable
50
+ class QueueClient(Protocol):
51
+ """The subset of `LLMQueue` the LangChain adapters and the HTTP transport use.
52
+
53
+ `lane=None` means "take the lane from `current_context()`"; adapters pass only explicit
54
+ overrides. Sync methods run on the caller's thread (direct path under the shared limiter).
55
+ """
56
+
57
+ async def converse(
58
+ self,
59
+ model: str,
60
+ request: dict[str, Any],
61
+ *,
62
+ lane: Lane | str | None = None,
63
+ fallback: FallbackPolicy | None = None,
64
+ timeout_secs: float | None = None,
65
+ deadline_at: float | None = None,
66
+ request_id: str | None = None,
67
+ ) -> ConverseResultLike: ...
68
+
69
+ def converse_sync(
70
+ self,
71
+ model: str,
72
+ request: dict[str, Any],
73
+ *,
74
+ lane: Lane | str | None = None,
75
+ fallback: FallbackPolicy | None = None,
76
+ timeout_secs: float | None = None,
77
+ deadline_at: float | None = None,
78
+ request_id: str | None = None,
79
+ ) -> ConverseResultLike: ...
80
+
81
+ async def embed(
82
+ self,
83
+ model: str,
84
+ texts: list[str],
85
+ *,
86
+ lane: Lane | str | None = None,
87
+ input_type: str | None = None,
88
+ timeout_secs: float | None = None,
89
+ deadline_at: float | None = None,
90
+ request_id: str | None = None,
91
+ ) -> EmbedResultLike: ...
92
+
93
+ def embed_sync(
94
+ self,
95
+ model: str,
96
+ texts: list[str],
97
+ *,
98
+ lane: Lane | str | None = None,
99
+ input_type: str | None = None,
100
+ timeout_secs: float | None = None,
101
+ deadline_at: float | None = None,
102
+ request_id: str | None = None,
103
+ ) -> EmbedResultLike: ...
104
+
105
+ async def chat(
106
+ self,
107
+ model: str,
108
+ request: dict[str, Any],
109
+ *,
110
+ lane: Lane | str | None = None,
111
+ fallback: FallbackPolicy | None = None,
112
+ timeout_secs: float | None = None,
113
+ deadline_at: float | None = None,
114
+ request_id: str | None = None,
115
+ ) -> ChatResultLike: ...
116
+
117
+
118
+ _default_queue: QueueClient | None = None
119
+
120
+
121
+ def set_default_queue(llmq: QueueClient | None) -> None:
122
+ """Register the queue that adapters built without `llmq=` use; `None` clears it.
123
+
124
+ `LLMQueue.start()` is expected to call this, and `stop()` to clear it.
125
+ """
126
+ global _default_queue
127
+ _default_queue = llmq
128
+
129
+
130
+ def get_default_queue() -> QueueClient:
131
+ """The registered default queue; raises `LLMQueueError` when none has been set yet."""
132
+ if _default_queue is None:
133
+ raise LLMQueueError(
134
+ "no LLM queue: pass llmq=... to the adapter or call "
135
+ "taskiq_llm_queue.langchain.set_default_queue(llmq) (LLMQueue.start() does this)"
136
+ )
137
+ return _default_queue
138
+
139
+
140
+ def resolve_queue(explicit: Any) -> QueueClient:
141
+ """The adapter's own queue if it has one, else the default, looked up on every call."""
142
+ return cast("QueueClient", explicit) if explicit is not None else get_default_queue()