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,291 @@
|
|
|
1
|
+
"""Queued Bedrock adapters: `ChatBedrockConverse` and `BedrockEmbeddings` that call the LLM queue.
|
|
2
|
+
|
|
3
|
+
The chat class runs the parent's own `_generate` against a placeholder client that hands the
|
|
4
|
+
Converse request to the queue, so messages, tools, cache points, reasoning and usage are built
|
|
5
|
+
and parsed exactly as the installed langchain-aws does (0.2 and 1.x lines alike). No boto3
|
|
6
|
+
client is ever created: both classes get a placeholder client that refuses direct calls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from langchain_aws import BedrockEmbeddings, ChatBedrockConverse
|
|
17
|
+
from pydantic import Field, model_validator
|
|
18
|
+
|
|
19
|
+
from taskiq_llm_queue.config import Lane
|
|
20
|
+
from taskiq_llm_queue.envelope import FallbackPolicy
|
|
21
|
+
from taskiq_llm_queue.errors import LLMQueueError
|
|
22
|
+
from taskiq_llm_queue.langchain._client import resolve_queue
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from langchain_core.callbacks import (
|
|
26
|
+
AsyncCallbackManagerForLLMRun,
|
|
27
|
+
CallbackManagerForLLMRun,
|
|
28
|
+
)
|
|
29
|
+
from langchain_core.messages import BaseMessage
|
|
30
|
+
from langchain_core.outputs import ChatResult
|
|
31
|
+
|
|
32
|
+
from taskiq_llm_queue.langchain._client import ConverseResultLike, QueueClient
|
|
33
|
+
|
|
34
|
+
# Per-call Converse handler of the placeholder client (a ContextVar: concurrent calls on one
|
|
35
|
+
# model instance never see each other's handler).
|
|
36
|
+
_Handler = Callable[[dict[str, Any]], dict[str, Any]]
|
|
37
|
+
_HANDLER: ContextVar[_Handler | None] = ContextVar("llmq_converse_handler", default=None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _Captured(Exception): # control flow, not an error
|
|
41
|
+
"""Raised by the capturing handler to stop the parent right after it built the request."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, request: dict[str, Any]) -> None:
|
|
44
|
+
super().__init__("captured")
|
|
45
|
+
self.request = request
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _QueueOnlyClient:
|
|
49
|
+
"""Placeholder for the boto3 client: stops the parent creating one and refuses direct calls.
|
|
50
|
+
|
|
51
|
+
`converse` answers only inside a queued call (through the current handler); any other path
|
|
52
|
+
that would reach Bedrock directly (e.g. a parent `_stream`) fails loudly.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def converse(self, **request: Any) -> dict[str, Any]:
|
|
56
|
+
handler = _HANDLER.get()
|
|
57
|
+
if handler is None:
|
|
58
|
+
self._refuse()
|
|
59
|
+
assert handler is not None
|
|
60
|
+
return handler(request)
|
|
61
|
+
|
|
62
|
+
def _refuse(self, *_args: Any, **_kwargs: Any) -> Any:
|
|
63
|
+
raise LLMQueueError("this model sends every call through the LLM queue")
|
|
64
|
+
|
|
65
|
+
converse_stream = invoke_model = get_inference_profile = _refuse
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _queue_only_values(values: Any, *slots: str) -> Any:
|
|
69
|
+
"""Fill the given boto3 client slots with the placeholder unless the caller supplied them."""
|
|
70
|
+
if isinstance(values, dict):
|
|
71
|
+
for slot in slots:
|
|
72
|
+
values.setdefault(slot, _QueueOnlyClient())
|
|
73
|
+
return values
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _looks_like_bedrock_id(model_id: str) -> bool:
|
|
77
|
+
"""Bedrock ids are `provider.model[...]` (optionally region-prefixed) or ARNs."""
|
|
78
|
+
return "." in model_id or model_id.startswith("arn:")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _alias_values(values: Any) -> Any:
|
|
82
|
+
"""Resolve `model_alias` / `model_id` and reject a queue alias passed as the model id.
|
|
83
|
+
|
|
84
|
+
Without `model_id`, it is looked up from `llmq.config` when a queue is given.
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(values, dict):
|
|
87
|
+
return values
|
|
88
|
+
model_id = values.get("model_id", values.get("model"))
|
|
89
|
+
alias = values.get("model_alias")
|
|
90
|
+
if model_id is None and alias is not None:
|
|
91
|
+
config = getattr(values.get("llmq"), "config", None)
|
|
92
|
+
resolve = getattr(config, "resolve", None)
|
|
93
|
+
if not callable(resolve):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"model_alias={alias!r} needs model_id='<Bedrock model id>' "
|
|
96
|
+
"(or llmq=<LLMQueue> so it can be looked up from the queue config)"
|
|
97
|
+
)
|
|
98
|
+
values["model_id"] = resolve(alias)[1].model_id
|
|
99
|
+
elif isinstance(model_id, str) and alias is None and not _looks_like_bedrock_id(model_id):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"model={model_id!r} looks like a queue alias, not a Bedrock model id; pass "
|
|
102
|
+
f"model_alias={model_id!r} and model_id='<Bedrock model id>' "
|
|
103
|
+
"(e.g. 'openai.gpt-oss-120b-1:0'), or model_alias plus llmq=<LLMQueue>"
|
|
104
|
+
)
|
|
105
|
+
return values
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _service_tier(tier: Any) -> dict[str, Any]:
|
|
109
|
+
return tier if isinstance(tier, dict) else {"type": tier}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class QueuedChatBedrockConverse(ChatBedrockConverse):
|
|
113
|
+
"""`ChatBedrockConverse` whose Converse calls go through the LLM queue.
|
|
114
|
+
|
|
115
|
+
`model_alias` is the queue alias (`gpt120b`, `gpt120b@apac`); `model_id` is the Bedrock id
|
|
116
|
+
(looked up from `llmq.config` when omitted) and drives the parent's provider-specific
|
|
117
|
+
request building. Streaming is always off (one full message).
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
llmq: Any = Field(default=None, exclude=True)
|
|
121
|
+
model_alias: str | None = None
|
|
122
|
+
fallback_policy: FallbackPolicy | None = None
|
|
123
|
+
lane: Lane | str | None = None
|
|
124
|
+
service_tier: str | None = None
|
|
125
|
+
queue_timeout_secs: float | None = None
|
|
126
|
+
|
|
127
|
+
@model_validator(mode="before")
|
|
128
|
+
@classmethod
|
|
129
|
+
def _queue_defaults(cls, values: Any) -> Any:
|
|
130
|
+
values = _queue_only_values(_alias_values(values), "client", "bedrock_client")
|
|
131
|
+
if isinstance(values, dict):
|
|
132
|
+
values["disable_streaming"] = True
|
|
133
|
+
return values
|
|
134
|
+
|
|
135
|
+
def _queue(self) -> QueueClient:
|
|
136
|
+
return resolve_queue(self.llmq)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def _alias(self) -> str:
|
|
140
|
+
return self.model_alias or self.model_id
|
|
141
|
+
|
|
142
|
+
def _run_parent(
|
|
143
|
+
self,
|
|
144
|
+
messages: list[BaseMessage],
|
|
145
|
+
stop: list[str] | None,
|
|
146
|
+
kwargs: dict[str, Any],
|
|
147
|
+
handler: _Handler,
|
|
148
|
+
) -> ChatResult:
|
|
149
|
+
token = _HANDLER.set(handler)
|
|
150
|
+
try:
|
|
151
|
+
return super()._generate(messages, stop, None, **kwargs)
|
|
152
|
+
finally:
|
|
153
|
+
_HANDLER.reset(token)
|
|
154
|
+
|
|
155
|
+
def _capture(
|
|
156
|
+
self, messages: list[BaseMessage], stop: list[str] | None, kwargs: dict[str, Any]
|
|
157
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
158
|
+
"""(Converse request without `modelId`, kwargs for the parse pass) from the parent."""
|
|
159
|
+
call_kwargs = dict(kwargs)
|
|
160
|
+
tier = call_kwargs.pop("service_tier", None) # langchain-aws 0.2 has no such kwarg
|
|
161
|
+
|
|
162
|
+
def capture(request: dict[str, Any]) -> dict[str, Any]:
|
|
163
|
+
raise _Captured(request)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
self._run_parent(messages, stop, call_kwargs, capture)
|
|
167
|
+
except _Captured as captured:
|
|
168
|
+
request = dict(captured.request)
|
|
169
|
+
else: # pragma: no cover - the parent always calls converse
|
|
170
|
+
raise LLMQueueError("ChatBedrockConverse did not build a Converse request")
|
|
171
|
+
request.pop("modelId", None) # the queue alias picks the model and target
|
|
172
|
+
if tier is not None:
|
|
173
|
+
request["serviceTier"] = _service_tier(tier)
|
|
174
|
+
elif self.service_tier and "serviceTier" not in request:
|
|
175
|
+
request["serviceTier"] = _service_tier(self.service_tier)
|
|
176
|
+
return request, call_kwargs
|
|
177
|
+
|
|
178
|
+
def _parse(
|
|
179
|
+
self,
|
|
180
|
+
messages: list[BaseMessage],
|
|
181
|
+
stop: list[str] | None,
|
|
182
|
+
call_kwargs: dict[str, Any],
|
|
183
|
+
result: ConverseResultLike,
|
|
184
|
+
) -> ChatResult:
|
|
185
|
+
"""Parse like the parent (same call, canned reply), then add how the queue served it."""
|
|
186
|
+
chat = self._run_parent(messages, stop, call_kwargs, lambda _request: result.response)
|
|
187
|
+
meta = result.meta.model_dump()
|
|
188
|
+
for generation in chat.generations:
|
|
189
|
+
generation.message.response_metadata.update(meta)
|
|
190
|
+
return chat
|
|
191
|
+
|
|
192
|
+
def _generate(
|
|
193
|
+
self,
|
|
194
|
+
messages: list[BaseMessage],
|
|
195
|
+
stop: list[str] | None = None,
|
|
196
|
+
run_manager: CallbackManagerForLLMRun | None = None,
|
|
197
|
+
**kwargs: Any,
|
|
198
|
+
) -> ChatResult:
|
|
199
|
+
"""Sync path: `converse_sync` (direct under the shared limiter, see README)."""
|
|
200
|
+
request, call_kwargs = self._capture(messages, stop, kwargs)
|
|
201
|
+
result = self._queue().converse_sync(
|
|
202
|
+
self._alias,
|
|
203
|
+
request,
|
|
204
|
+
lane=self.lane,
|
|
205
|
+
fallback=self.fallback_policy,
|
|
206
|
+
timeout_secs=self.queue_timeout_secs,
|
|
207
|
+
)
|
|
208
|
+
return self._parse(messages, stop, call_kwargs, result)
|
|
209
|
+
|
|
210
|
+
async def _agenerate(
|
|
211
|
+
self,
|
|
212
|
+
messages: list[BaseMessage],
|
|
213
|
+
stop: list[str] | None = None,
|
|
214
|
+
run_manager: AsyncCallbackManagerForLLMRun | None = None,
|
|
215
|
+
**kwargs: Any,
|
|
216
|
+
) -> ChatResult:
|
|
217
|
+
"""Native async path: one queued Converse call (the parent only has a thread fallback)."""
|
|
218
|
+
request, call_kwargs = self._capture(messages, stop, kwargs)
|
|
219
|
+
result = await self._queue().converse(
|
|
220
|
+
self._alias,
|
|
221
|
+
request,
|
|
222
|
+
lane=self.lane,
|
|
223
|
+
fallback=self.fallback_policy,
|
|
224
|
+
timeout_secs=self.queue_timeout_secs,
|
|
225
|
+
)
|
|
226
|
+
return self._parse(messages, stop, call_kwargs, result)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class QueuedBedrockEmbeddings(BedrockEmbeddings):
|
|
230
|
+
"""`BedrockEmbeddings` whose calls go through the queue's `embed` op (batched per provider).
|
|
231
|
+
|
|
232
|
+
`model_alias` is the queue alias (defaults to `model_id`; the queue builds the provider
|
|
233
|
+
body). Queries use `input_type="search_query"`, documents `"search_document"`; the provider
|
|
234
|
+
ignores it where the model has no such field. `normalize` is applied locally.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
llmq: Any = Field(default=None, exclude=True)
|
|
238
|
+
model_alias: str | None = None
|
|
239
|
+
lane: Lane | str | None = None
|
|
240
|
+
queue_timeout_secs: float | None = None
|
|
241
|
+
|
|
242
|
+
@model_validator(mode="before")
|
|
243
|
+
@classmethod
|
|
244
|
+
def _queue_defaults(cls, values: Any) -> Any:
|
|
245
|
+
return _queue_only_values(values, "client")
|
|
246
|
+
|
|
247
|
+
def _queue(self) -> QueueClient:
|
|
248
|
+
return resolve_queue(self.llmq)
|
|
249
|
+
|
|
250
|
+
def _prepare(self, texts: list[str]) -> list[str]:
|
|
251
|
+
"""Same newline handling as the parent, which says newlines hurt embedding quality."""
|
|
252
|
+
return [text.replace(os.linesep, " ") for text in texts]
|
|
253
|
+
|
|
254
|
+
def _finish(self, vectors: list[list[float]]) -> list[list[float]]:
|
|
255
|
+
return [self._normalize_vector(v) for v in vectors] if self.normalize else vectors
|
|
256
|
+
|
|
257
|
+
def _sync(self, texts: list[str], input_type: str) -> list[list[float]]:
|
|
258
|
+
result = self._queue().embed_sync(
|
|
259
|
+
self.model_alias or self.model_id,
|
|
260
|
+
self._prepare(texts),
|
|
261
|
+
lane=self.lane,
|
|
262
|
+
input_type=input_type,
|
|
263
|
+
timeout_secs=self.queue_timeout_secs,
|
|
264
|
+
)
|
|
265
|
+
return self._finish(result.vectors)
|
|
266
|
+
|
|
267
|
+
async def _async(self, texts: list[str], input_type: str) -> list[list[float]]:
|
|
268
|
+
result = await self._queue().embed(
|
|
269
|
+
self.model_alias or self.model_id,
|
|
270
|
+
self._prepare(texts),
|
|
271
|
+
lane=self.lane,
|
|
272
|
+
input_type=input_type,
|
|
273
|
+
timeout_secs=self.queue_timeout_secs,
|
|
274
|
+
)
|
|
275
|
+
return self._finish(result.vectors)
|
|
276
|
+
|
|
277
|
+
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
278
|
+
"""Embed documents on the sync path (`embed_sync`)."""
|
|
279
|
+
return self._sync(texts, "search_document")
|
|
280
|
+
|
|
281
|
+
def embed_query(self, text: str) -> list[float]:
|
|
282
|
+
"""Embed one query on the sync path (`embed_sync`)."""
|
|
283
|
+
return self._sync([text], "search_query")[0]
|
|
284
|
+
|
|
285
|
+
async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
286
|
+
"""Embed documents through the queue as one call (the parent fans out per text)."""
|
|
287
|
+
return await self._async(texts, "search_document")
|
|
288
|
+
|
|
289
|
+
async def aembed_query(self, text: str) -> list[float]:
|
|
290
|
+
"""Embed one query through the queue."""
|
|
291
|
+
return (await self._async([text], "search_query"))[0]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Queued OpenAI-compatible adapters: `ChatOpenAI` / `OpenAIEmbeddings` over `QueuedHTTPTransport`.
|
|
2
|
+
|
|
3
|
+
Both SDK clients get an httpx client whose transport answers from the LLM queue (D88), so every
|
|
4
|
+
LangChain/SDK code path is queued and parsed unchanged. Streaming and SDK retries are forced off:
|
|
5
|
+
the queue's workers own retry and fallback. Chat replies get the queue's `CallMeta`
|
|
6
|
+
(served model/target, attempts, queue wait) in `response_metadata`, like the Bedrock adapter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
|
15
|
+
from pydantic import Field, model_validator
|
|
16
|
+
|
|
17
|
+
from taskiq_llm_queue.config import Lane
|
|
18
|
+
from taskiq_llm_queue.envelope import FallbackPolicy
|
|
19
|
+
from taskiq_llm_queue.http_transport import META_KEY, QUEUED_BASE_URL, QueuedHTTPTransport
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from langchain_core.outputs import ChatResult
|
|
23
|
+
|
|
24
|
+
# Placeholder only: the SDK requires a key, but workers hold the real provider credentials.
|
|
25
|
+
_PLACEHOLDER_API_KEY = "llmq-queued"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _queued_client_values(values: Any) -> Any:
|
|
29
|
+
"""Point both SDK clients at a queued transport built from the raw constructor values."""
|
|
30
|
+
if not isinstance(values, dict):
|
|
31
|
+
return values
|
|
32
|
+
transport = QueuedHTTPTransport(
|
|
33
|
+
values.get("llmq"),
|
|
34
|
+
values.get("model_alias"),
|
|
35
|
+
lane=values.get("lane"),
|
|
36
|
+
fallback=values.get("fallback_policy"),
|
|
37
|
+
timeout_secs=values.get("queue_timeout_secs"),
|
|
38
|
+
)
|
|
39
|
+
values["http_async_client"] = httpx.AsyncClient(transport=transport, base_url=QUEUED_BASE_URL)
|
|
40
|
+
values["http_client"] = httpx.Client(transport=transport, base_url=QUEUED_BASE_URL)
|
|
41
|
+
values.pop("openai_api_base", None)
|
|
42
|
+
values["base_url"] = QUEUED_BASE_URL
|
|
43
|
+
values["openai_proxy"] = None # an OPENAI_PROXY env var would conflict with http_client
|
|
44
|
+
if values.get("api_key") is None and values.get("openai_api_key") is None:
|
|
45
|
+
values["api_key"] = _PLACEHOLDER_API_KEY
|
|
46
|
+
values["max_retries"] = 0
|
|
47
|
+
return values
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QueuedChatOpenAI(ChatOpenAI):
|
|
51
|
+
"""`ChatOpenAI` whose HTTP calls are answered by the LLM queue's `chat` op.
|
|
52
|
+
|
|
53
|
+
`model_alias` is the queue alias; when unset, the request's `model` (this class's `model`)
|
|
54
|
+
is used as the alias. Streaming is always off, so `astream()` yields one full message.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
llmq: Any = Field(default=None, exclude=True)
|
|
58
|
+
model_alias: str | None = None
|
|
59
|
+
fallback_policy: FallbackPolicy | None = None
|
|
60
|
+
lane: Lane | str | None = None
|
|
61
|
+
queue_timeout_secs: float | None = None
|
|
62
|
+
|
|
63
|
+
@model_validator(mode="before")
|
|
64
|
+
@classmethod
|
|
65
|
+
def _queue_defaults(cls, values: Any) -> Any:
|
|
66
|
+
values = _queued_client_values(values)
|
|
67
|
+
if isinstance(values, dict):
|
|
68
|
+
values["streaming"] = False
|
|
69
|
+
values["disable_streaming"] = True
|
|
70
|
+
return values
|
|
71
|
+
|
|
72
|
+
def _create_chat_result(
|
|
73
|
+
self, response: Any, generation_info: dict[str, Any] | None = None
|
|
74
|
+
) -> ChatResult:
|
|
75
|
+
"""Parse like the parent, then add the queue's `llmq_meta` to `response_metadata`."""
|
|
76
|
+
result = super()._create_chat_result(response, generation_info)
|
|
77
|
+
if isinstance(response, dict):
|
|
78
|
+
meta = response.get(META_KEY)
|
|
79
|
+
else:
|
|
80
|
+
meta = (getattr(response, "model_extra", None) or {}).get(META_KEY)
|
|
81
|
+
if isinstance(meta, dict):
|
|
82
|
+
for generation in result.generations:
|
|
83
|
+
generation.message.response_metadata.update(meta)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class QueuedOpenAIEmbeddings(OpenAIEmbeddings):
|
|
88
|
+
"""`OpenAIEmbeddings` whose HTTP calls are answered by the LLM queue's `embed` op.
|
|
89
|
+
|
|
90
|
+
`check_embedding_ctx_length` defaults to False because the queue embeds text, not tokens,
|
|
91
|
+
and non-OpenAI servers do not share OpenAI's tokenizer.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
llmq: Any = Field(default=None, exclude=True)
|
|
95
|
+
model_alias: str | None = None
|
|
96
|
+
lane: Lane | str | None = None
|
|
97
|
+
queue_timeout_secs: float | None = None
|
|
98
|
+
check_embedding_ctx_length: bool = False
|
|
99
|
+
|
|
100
|
+
@model_validator(mode="before")
|
|
101
|
+
@classmethod
|
|
102
|
+
def _queue_defaults(cls, values: Any) -> Any:
|
|
103
|
+
return _queued_client_values(values)
|