claude-smart 0.2.43 → 0.2.45
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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +1 -1
- package/bin/claude-smart.js +2 -2
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +21 -1
- package/plugin/pyproject.toml +3 -3
- package/plugin/scripts/_lib.sh +72 -28
- package/plugin/scripts/backend-service.sh +55 -7
- package/plugin/scripts/cli.sh +2 -1
- package/plugin/scripts/dashboard-service.sh +7 -5
- package/plugin/scripts/ensure-plugin-root.sh +1 -0
- package/plugin/scripts/hook_entry.sh +7 -5
- package/plugin/scripts/smart-install.sh +2 -2
- package/plugin/src/README.md +57 -0
- package/plugin/uv.lock +126 -5
- package/plugin/vendor/reflexio/.env.example +9 -0
- package/plugin/vendor/reflexio/pyproject.toml +5 -2
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +0 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +7 -4
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +4 -4
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +2 -2
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +13 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +3 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +59 -6
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +7 -1
- package/plugin/vendor/reflexio/reflexio/server/api.py +188 -34
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +33 -11
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +278 -29
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +457 -181
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +22 -12
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +137 -9
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +36 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +27 -23
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +19 -9
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +1 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +44 -2
- package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +91 -24
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +18 -5
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +88 -3
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +36 -5
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +32 -22
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +89 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +46 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +8 -4
|
@@ -8,10 +8,13 @@ using LiteLLM. It maintains the same interface as the existing LLMClient for eas
|
|
|
8
8
|
import base64
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
import multiprocessing
|
|
11
12
|
import os
|
|
13
|
+
import pickle
|
|
14
|
+
import queue
|
|
12
15
|
import re
|
|
13
16
|
import time
|
|
14
|
-
from dataclasses import dataclass
|
|
17
|
+
from dataclasses import dataclass, field
|
|
15
18
|
from functools import lru_cache
|
|
16
19
|
from typing import Any
|
|
17
20
|
|
|
@@ -53,9 +56,6 @@ from reflexio.server.llm.providers.local_embedding_provider import (
|
|
|
53
56
|
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
54
57
|
NomicEmbedder,
|
|
55
58
|
)
|
|
56
|
-
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
57
|
-
is_enabled as _nomic_embedder_enabled,
|
|
58
|
-
)
|
|
59
59
|
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
60
60
|
is_nomic_model as _is_nomic_model,
|
|
61
61
|
)
|
|
@@ -148,6 +148,23 @@ def _get_embedding_encoding(model: str) -> tiktoken.Encoding:
|
|
|
148
148
|
return tiktoken.get_encoding("cl100k_base")
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def _reject_cloud_mode(embedding_model: str, mode: str) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Raise when a local-only embedding model is configured for cloud mode.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
embedding_model (str): The resolved embedding model name.
|
|
157
|
+
mode (str): The resolved embedding provider mode.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
EmbeddingUnavailableError: If ``mode`` is ``"cloud"``.
|
|
161
|
+
"""
|
|
162
|
+
if mode == "cloud":
|
|
163
|
+
raise EmbeddingUnavailableError(
|
|
164
|
+
f"Local embedding model {embedding_model!r} cannot use cloud mode"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
151
168
|
def _truncate_for_embedding(
|
|
152
169
|
text: str, model: str, max_tokens: int | None = None
|
|
153
170
|
) -> str:
|
|
@@ -207,24 +224,49 @@ class LiteLLMConfig:
|
|
|
207
224
|
Configuration for LiteLLM client.
|
|
208
225
|
|
|
209
226
|
Args:
|
|
210
|
-
model: Model name to use (e.g., 'gpt-4o', 'claude-3-5-sonnet-20241022'
|
|
211
|
-
temperature: Temperature for response generation (0.0 to 2.0)
|
|
212
|
-
max_tokens: Maximum tokens to generate
|
|
213
|
-
timeout: Request timeout in seconds
|
|
214
|
-
max_retries: Maximum
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
model: Model name to use (e.g., 'gpt-4o', 'claude-3-5-sonnet-20241022').
|
|
228
|
+
temperature: Temperature for response generation (0.0 to 2.0).
|
|
229
|
+
max_tokens: Maximum tokens to generate.
|
|
230
|
+
timeout: Request timeout in seconds.
|
|
231
|
+
max_retries: Maximum retry attempts on the primary model. Passed
|
|
232
|
+
directly to litellm's num_retries. Default 3.
|
|
233
|
+
retry_delay: Currently unused — LiteLLM owns retry backoff. Kept for
|
|
234
|
+
backward compatibility; remove in a follow-up sweep.
|
|
235
|
+
top_p: Top-p sampling parameter.
|
|
236
|
+
api_key_config: Optional API key configuration from Config (overrides env vars).
|
|
237
|
+
fallback_models: Models LiteLLM tries in order after the primary
|
|
238
|
+
exhausts num_retries. Passed directly to litellm's fallbacks param.
|
|
239
|
+
Default is an empty list (no fallback) so local reflexio and the
|
|
240
|
+
claude-smart integration are never silently routed to an unintended
|
|
241
|
+
provider. Production opts in via the env var
|
|
242
|
+
REFLEXIO_LLM_FALLBACK_MODELS (comma-separated, e.g. "gpt-5.4-mini").
|
|
243
|
+
Self-references are deduped at request time.
|
|
218
244
|
"""
|
|
219
245
|
|
|
220
246
|
model: str
|
|
221
247
|
temperature: float = 0.7
|
|
222
248
|
max_tokens: int | None = None
|
|
223
249
|
timeout: int = 120
|
|
224
|
-
max_retries: int =
|
|
250
|
+
max_retries: int = 3
|
|
225
251
|
retry_delay: float = 1.0
|
|
226
252
|
top_p: float = 1.0
|
|
227
253
|
api_key_config: APIKeyConfig | None = None
|
|
254
|
+
fallback_models: list[str] = field(
|
|
255
|
+
default_factory=lambda: [
|
|
256
|
+
m.strip()
|
|
257
|
+
for m in os.environ.get("REFLEXIO_LLM_FALLBACK_MODELS", "").split(",")
|
|
258
|
+
if m.strip()
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Reasoning models that routinely exceed the default 120s provider timeout on
|
|
264
|
+
# large extraction contexts. Values are floors, not overrides: the effective
|
|
265
|
+
# timeout is max(configured, floor), and an explicit per-call timeout kwarg
|
|
266
|
+
# always wins.
|
|
267
|
+
_MODEL_TIMEOUT_FLOOR_SECONDS: dict[str, int] = {
|
|
268
|
+
"minimax/MiniMax-M3": 240,
|
|
269
|
+
}
|
|
228
270
|
|
|
229
271
|
|
|
230
272
|
@dataclass
|
|
@@ -264,6 +306,131 @@ class StructuredOutputParseError(Exception):
|
|
|
264
306
|
"""
|
|
265
307
|
|
|
266
308
|
|
|
309
|
+
class LLMHardTimeoutError(TimeoutError):
|
|
310
|
+
"""Raised when an LLM call exceeds the client-side wall-clock timeout."""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass
|
|
314
|
+
class _CompletionMessageSnapshot:
|
|
315
|
+
content: str | None = None
|
|
316
|
+
tool_calls: Any | None = None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class _CompletionChoiceSnapshot:
|
|
321
|
+
message: _CompletionMessageSnapshot
|
|
322
|
+
finish_reason: str | None = None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass
|
|
326
|
+
class _PromptTokenDetailsSnapshot:
|
|
327
|
+
cached_tokens: int = 0
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass
|
|
331
|
+
class _CompletionUsageSnapshot:
|
|
332
|
+
prompt_tokens: int | None = None
|
|
333
|
+
completion_tokens: int | None = None
|
|
334
|
+
total_tokens: int | None = None
|
|
335
|
+
prompt_tokens_details: _PromptTokenDetailsSnapshot | None = None
|
|
336
|
+
cache_creation_input_tokens: int | None = None
|
|
337
|
+
cache_read_input_tokens: int | None = None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass
|
|
341
|
+
class _CompletionResponseSnapshot:
|
|
342
|
+
choices: list[_CompletionChoiceSnapshot]
|
|
343
|
+
usage: _CompletionUsageSnapshot | None = None
|
|
344
|
+
model: str | None = None
|
|
345
|
+
_hidden_params: dict[str, Any] = field(default_factory=dict)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass
|
|
349
|
+
class _CompletionErrorSnapshot:
|
|
350
|
+
type_name: str
|
|
351
|
+
message: str
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _ensure_picklable(value: Any) -> Any:
|
|
355
|
+
try:
|
|
356
|
+
pickle.dumps(value)
|
|
357
|
+
except Exception:
|
|
358
|
+
return repr(value)
|
|
359
|
+
return value
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _snapshot_completion_response(response: Any) -> _CompletionResponseSnapshot:
|
|
363
|
+
choices: list[_CompletionChoiceSnapshot] = []
|
|
364
|
+
for choice in getattr(response, "choices", []) or []:
|
|
365
|
+
message = getattr(choice, "message", None)
|
|
366
|
+
choices.append(
|
|
367
|
+
_CompletionChoiceSnapshot(
|
|
368
|
+
message=_CompletionMessageSnapshot(
|
|
369
|
+
content=getattr(message, "content", None),
|
|
370
|
+
tool_calls=_ensure_picklable(getattr(message, "tool_calls", None)),
|
|
371
|
+
),
|
|
372
|
+
finish_reason=getattr(choice, "finish_reason", None),
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
usage = getattr(response, "usage", None)
|
|
377
|
+
usage_snapshot = None
|
|
378
|
+
if usage is not None:
|
|
379
|
+
prompt_details = getattr(usage, "prompt_tokens_details", None)
|
|
380
|
+
prompt_details_snapshot = None
|
|
381
|
+
if prompt_details is not None:
|
|
382
|
+
prompt_details_snapshot = _PromptTokenDetailsSnapshot(
|
|
383
|
+
cached_tokens=int(getattr(prompt_details, "cached_tokens", 0) or 0)
|
|
384
|
+
)
|
|
385
|
+
usage_snapshot = _CompletionUsageSnapshot(
|
|
386
|
+
prompt_tokens=getattr(usage, "prompt_tokens", None),
|
|
387
|
+
completion_tokens=getattr(usage, "completion_tokens", None),
|
|
388
|
+
total_tokens=getattr(usage, "total_tokens", None),
|
|
389
|
+
prompt_tokens_details=prompt_details_snapshot,
|
|
390
|
+
cache_creation_input_tokens=getattr(
|
|
391
|
+
usage, "cache_creation_input_tokens", None
|
|
392
|
+
),
|
|
393
|
+
cache_read_input_tokens=getattr(usage, "cache_read_input_tokens", None),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
hidden_params = getattr(response, "_hidden_params", {}) or {}
|
|
397
|
+
if not isinstance(hidden_params, dict):
|
|
398
|
+
hidden_params = {}
|
|
399
|
+
|
|
400
|
+
return _CompletionResponseSnapshot(
|
|
401
|
+
choices=choices,
|
|
402
|
+
usage=usage_snapshot,
|
|
403
|
+
model=getattr(response, "model", None),
|
|
404
|
+
_hidden_params={str(k): _ensure_picklable(v) for k, v in hidden_params.items()},
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _picklable_completion_result(response: Any) -> Any:
|
|
409
|
+
try:
|
|
410
|
+
pickle.dumps(response)
|
|
411
|
+
except Exception:
|
|
412
|
+
return _snapshot_completion_response(response)
|
|
413
|
+
return response
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _litellm_completion_worker(
|
|
417
|
+
params: dict[str, Any], result_queue: multiprocessing.Queue
|
|
418
|
+
) -> None:
|
|
419
|
+
try:
|
|
420
|
+
result_queue.put(
|
|
421
|
+
("ok", _picklable_completion_result(litellm.completion(**params)))
|
|
422
|
+
)
|
|
423
|
+
except BaseException as exc:
|
|
424
|
+
try:
|
|
425
|
+
pickle.dumps(exc)
|
|
426
|
+
except Exception:
|
|
427
|
+
result_queue.put(
|
|
428
|
+
("error", _CompletionErrorSnapshot(type(exc).__name__, str(exc)))
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
result_queue.put(("error", exc))
|
|
432
|
+
|
|
433
|
+
|
|
267
434
|
class LiteLLMClient:
|
|
268
435
|
"""
|
|
269
436
|
Unified LLM client using LiteLLM for multi-provider support.
|
|
@@ -285,23 +452,10 @@ class LiteLLMClient:
|
|
|
285
452
|
"xai/": "xai",
|
|
286
453
|
}
|
|
287
454
|
|
|
288
|
-
# Non-retryable error patterns
|
|
289
|
-
NON_RETRYABLE_ERRORS = [
|
|
290
|
-
"invalid_api_key",
|
|
291
|
-
"unauthorized",
|
|
292
|
-
"permission_denied",
|
|
293
|
-
"quota_exceeded",
|
|
294
|
-
"billing",
|
|
295
|
-
"invalid_request",
|
|
296
|
-
"authentication",
|
|
297
|
-
"forbidden",
|
|
298
|
-
"rate_limit", # Rate limits are handled by LiteLLM internally
|
|
299
|
-
]
|
|
300
|
-
|
|
301
455
|
# Models that only support temperature=1.0 (custom values cause errors or degraded performance)
|
|
302
456
|
TEMPERATURE_RESTRICTED_MODELS = {
|
|
303
457
|
"gpt-5",
|
|
304
|
-
"gpt-5-mini",
|
|
458
|
+
"gpt-5.4-mini",
|
|
305
459
|
"gpt-5-nano",
|
|
306
460
|
"gpt-5-codex",
|
|
307
461
|
"gemini-3-flash-preview",
|
|
@@ -474,6 +628,8 @@ class LiteLLMClient:
|
|
|
474
628
|
tools: list[Any] | None = None,
|
|
475
629
|
tool_choice: str | dict[str, Any] | None = None,
|
|
476
630
|
model_role: ModelRole | None = None,
|
|
631
|
+
max_retries: int | None = None,
|
|
632
|
+
fallback_models: list[str] | None = None,
|
|
477
633
|
**kwargs: Any,
|
|
478
634
|
) -> str | BaseModel | ToolCallingChatResponse:
|
|
479
635
|
"""
|
|
@@ -489,6 +645,12 @@ class LiteLLMClient:
|
|
|
489
645
|
model_role: Optional ``ModelRole`` to override the model selected for
|
|
490
646
|
this request. The role is resolved via ``resolve_model_name`` using
|
|
491
647
|
the client's ``api_key_config``.
|
|
648
|
+
max_retries (int | None): Optional per-call override for the number of
|
|
649
|
+
retry attempts. When ``None`` (the default), the value falls back to
|
|
650
|
+
``LiteLLMConfig.max_retries``.
|
|
651
|
+
fallback_models (list[str] | None): Optional per-call override for the
|
|
652
|
+
fallback model chain. When ``None`` (the default), the value falls
|
|
653
|
+
back to ``LiteLLMConfig.fallback_models``.
|
|
492
654
|
**kwargs: Additional parameters including:
|
|
493
655
|
- response_format: Pydantic BaseModel class for structured output
|
|
494
656
|
- parse_structured_output: Whether to parse structured output (default True)
|
|
@@ -531,6 +693,10 @@ class LiteLLMClient:
|
|
|
531
693
|
kwargs["tool_choice"] = tool_choice
|
|
532
694
|
if model_role is not None:
|
|
533
695
|
kwargs["model_role"] = model_role
|
|
696
|
+
if max_retries is not None:
|
|
697
|
+
kwargs["max_retries"] = max_retries
|
|
698
|
+
if fallback_models is not None:
|
|
699
|
+
kwargs["fallback_models"] = fallback_models
|
|
534
700
|
|
|
535
701
|
return self._make_request(final_messages, **kwargs)
|
|
536
702
|
|
|
@@ -589,11 +755,11 @@ class LiteLLMClient:
|
|
|
589
755
|
[text], model=embedding_model, dimensions=dimensions
|
|
590
756
|
)[0]
|
|
591
757
|
|
|
592
|
-
# local/nomic-embed-*
|
|
593
|
-
#
|
|
594
|
-
#
|
|
595
|
-
|
|
596
|
-
|
|
758
|
+
# local/nomic-embed-* must stay on the Nomic provider (137M params,
|
|
759
|
+
# 768d Matryoshka-truncated to 512). Falling through to MiniLM would
|
|
760
|
+
# mix embedding models inside existing vector stores.
|
|
761
|
+
if _is_nomic_model(embedding_model):
|
|
762
|
+
_reject_cloud_mode(embedding_model, mode)
|
|
597
763
|
try:
|
|
598
764
|
return NomicEmbedder.get().embed([text])[0]
|
|
599
765
|
except Exception as e:
|
|
@@ -608,10 +774,7 @@ class LiteLLMClient:
|
|
|
608
774
|
# ``CLAUDE_SMART_USE_LOCAL_EMBEDDING``) is enforced earlier in the
|
|
609
775
|
# auto-detection layer (see ``model_defaults._auto_detect_model``).
|
|
610
776
|
if embedding_model.startswith("local/"):
|
|
611
|
-
|
|
612
|
-
raise EmbeddingUnavailableError(
|
|
613
|
-
f"Local embedding model {embedding_model!r} cannot use cloud mode"
|
|
614
|
-
)
|
|
777
|
+
_reject_cloud_mode(embedding_model, mode)
|
|
615
778
|
if not _is_chromadb_importable():
|
|
616
779
|
raise LiteLLMClientError(
|
|
617
780
|
f"Embedding model {embedding_model!r} requires chromadb. "
|
|
@@ -642,7 +805,11 @@ class LiteLLMClient:
|
|
|
642
805
|
if api_version:
|
|
643
806
|
params["api_version"] = api_version
|
|
644
807
|
|
|
645
|
-
response = litellm.embedding(
|
|
808
|
+
response = litellm.embedding(
|
|
809
|
+
**params,
|
|
810
|
+
timeout=self.config.timeout,
|
|
811
|
+
num_retries=self.config.max_retries,
|
|
812
|
+
)
|
|
646
813
|
return response.data[0]["embedding"]
|
|
647
814
|
except Exception as e:
|
|
648
815
|
raise LiteLLMClientError(f"Embedding generation failed: {str(e)}") from e
|
|
@@ -683,7 +850,8 @@ class LiteLLMClient:
|
|
|
683
850
|
)
|
|
684
851
|
|
|
685
852
|
# See matching short-circuits in get_embedding above.
|
|
686
|
-
if _is_nomic_model(embedding_model)
|
|
853
|
+
if _is_nomic_model(embedding_model):
|
|
854
|
+
_reject_cloud_mode(embedding_model, mode)
|
|
687
855
|
try:
|
|
688
856
|
return NomicEmbedder.get().embed(list(texts))
|
|
689
857
|
except Exception as e:
|
|
@@ -692,10 +860,7 @@ class LiteLLMClient:
|
|
|
692
860
|
) from e
|
|
693
861
|
|
|
694
862
|
if embedding_model.startswith("local/"):
|
|
695
|
-
|
|
696
|
-
raise EmbeddingUnavailableError(
|
|
697
|
-
f"Local embedding model {embedding_model!r} cannot use cloud mode"
|
|
698
|
-
)
|
|
863
|
+
_reject_cloud_mode(embedding_model, mode)
|
|
699
864
|
if not _is_chromadb_importable():
|
|
700
865
|
raise LiteLLMClientError(
|
|
701
866
|
f"Embedding model {embedding_model!r} requires chromadb. "
|
|
@@ -726,7 +891,11 @@ class LiteLLMClient:
|
|
|
726
891
|
if api_version:
|
|
727
892
|
params["api_version"] = api_version
|
|
728
893
|
|
|
729
|
-
response = litellm.embedding(
|
|
894
|
+
response = litellm.embedding(
|
|
895
|
+
**params,
|
|
896
|
+
timeout=self.config.timeout,
|
|
897
|
+
num_retries=self.config.max_retries,
|
|
898
|
+
)
|
|
730
899
|
# Response data may not be in order, sort by index to ensure correct ordering
|
|
731
900
|
sorted_data = sorted(response.data, key=lambda x: x["index"])
|
|
732
901
|
return [item["embedding"] for item in sorted_data]
|
|
@@ -737,7 +906,7 @@ class LiteLLMClient:
|
|
|
737
906
|
|
|
738
907
|
def _build_completion_params(
|
|
739
908
|
self, messages: list[dict[str, Any]], **kwargs: Any
|
|
740
|
-
) -> tuple[dict[str, Any], Any, bool, int]:
|
|
909
|
+
) -> tuple[dict[str, Any], Any, bool, int, list[str]]:
|
|
741
910
|
"""Build completion request parameters from messages and kwargs.
|
|
742
911
|
|
|
743
912
|
Args:
|
|
@@ -745,7 +914,9 @@ class LiteLLMClient:
|
|
|
745
914
|
**kwargs: Additional parameters (response_format, max_retries, model, etc.)
|
|
746
915
|
|
|
747
916
|
Returns:
|
|
748
|
-
Tuple of (params dict, response_format, parse_structured_output,
|
|
917
|
+
Tuple of (params dict, response_format, parse_structured_output,
|
|
918
|
+
max_retries, fallback_models). ``fallback_models`` already has any
|
|
919
|
+
entry equal to the primary model removed.
|
|
749
920
|
"""
|
|
750
921
|
response_format = kwargs.pop("response_format", None)
|
|
751
922
|
strict_response_format = kwargs.pop("strict_response_format", True)
|
|
@@ -756,6 +927,14 @@ class LiteLLMClient:
|
|
|
756
927
|
except (TypeError, ValueError):
|
|
757
928
|
max_retries = max(1, int(self.config.max_retries))
|
|
758
929
|
|
|
930
|
+
# Per-call fallback_models wins over config when explicitly provided.
|
|
931
|
+
# Use sentinel-style check so an explicit empty list disables fallback
|
|
932
|
+
# for the call even when the config has fallbacks set.
|
|
933
|
+
if "fallback_models" in kwargs:
|
|
934
|
+
fallback_models_raw = kwargs.pop("fallback_models") or []
|
|
935
|
+
else:
|
|
936
|
+
fallback_models_raw = list(self.config.fallback_models)
|
|
937
|
+
|
|
759
938
|
# Pop tool-calling kwargs before the final params.update(kwargs) so they
|
|
760
939
|
# don't leak into the params dict twice.
|
|
761
940
|
tools = kwargs.pop("tools", None)
|
|
@@ -785,10 +964,15 @@ class LiteLLMClient:
|
|
|
785
964
|
params: dict[str, Any] = {
|
|
786
965
|
"model": actual_model,
|
|
787
966
|
"messages": messages,
|
|
788
|
-
"timeout": kwargs.pop(
|
|
789
|
-
|
|
967
|
+
"timeout": kwargs.pop(
|
|
968
|
+
"timeout", self._effective_timeout_for_model(actual_model)
|
|
969
|
+
),
|
|
790
970
|
}
|
|
791
971
|
|
|
972
|
+
# Drop any fallback entry that points back at the primary — sending the
|
|
973
|
+
# same broken endpoint twice never helps.
|
|
974
|
+
fallback_models = [m for m in fallback_models_raw if m != actual_model]
|
|
975
|
+
|
|
792
976
|
temperature = kwargs.pop("temperature", self.config.temperature)
|
|
793
977
|
if self._is_temperature_restricted_model(actual_model):
|
|
794
978
|
params["temperature"] = 1.0
|
|
@@ -863,7 +1047,13 @@ class LiteLLMClient:
|
|
|
863
1047
|
params["messages"], params["model"]
|
|
864
1048
|
)
|
|
865
1049
|
|
|
866
|
-
return
|
|
1050
|
+
return (
|
|
1051
|
+
params,
|
|
1052
|
+
response_format,
|
|
1053
|
+
parse_structured_output,
|
|
1054
|
+
max_retries,
|
|
1055
|
+
fallback_models,
|
|
1056
|
+
)
|
|
867
1057
|
|
|
868
1058
|
@staticmethod
|
|
869
1059
|
@lru_cache(maxsize=256)
|
|
@@ -917,6 +1107,102 @@ class LiteLLMClient:
|
|
|
917
1107
|
except Exception:
|
|
918
1108
|
return None
|
|
919
1109
|
|
|
1110
|
+
def _completion_with_hard_timeout(self, params: dict[str, Any]) -> Any:
|
|
1111
|
+
"""Run ``litellm.completion`` with a client-side wall-clock bound.
|
|
1112
|
+
|
|
1113
|
+
Some providers can exceed LiteLLM's ``timeout`` kwarg. Run the blocking
|
|
1114
|
+
call in a child process so the caller can fail, release locks, and
|
|
1115
|
+
terminate the in-flight provider request instead of waiting indefinitely.
|
|
1116
|
+
"""
|
|
1117
|
+
provider_timeout = params.get("timeout", self.config.timeout)
|
|
1118
|
+
try:
|
|
1119
|
+
timeout_seconds = float(provider_timeout)
|
|
1120
|
+
except (TypeError, ValueError):
|
|
1121
|
+
timeout_seconds = float(self.config.timeout)
|
|
1122
|
+
grace_seconds = self._hard_timeout_grace_seconds()
|
|
1123
|
+
hard_timeout = max(0.001, timeout_seconds) + max(0.0, grace_seconds)
|
|
1124
|
+
|
|
1125
|
+
if not self._should_process_isolate_completion(timeout_seconds, grace_seconds):
|
|
1126
|
+
return litellm.completion(**params)
|
|
1127
|
+
|
|
1128
|
+
process_context = multiprocessing.get_context()
|
|
1129
|
+
result_queue = process_context.Queue(maxsize=1)
|
|
1130
|
+
process = process_context.Process(
|
|
1131
|
+
target=_litellm_completion_worker,
|
|
1132
|
+
args=(params, result_queue),
|
|
1133
|
+
daemon=True,
|
|
1134
|
+
)
|
|
1135
|
+
process.start()
|
|
1136
|
+
try:
|
|
1137
|
+
process.join(timeout=hard_timeout)
|
|
1138
|
+
if process.is_alive():
|
|
1139
|
+
process.terminate()
|
|
1140
|
+
process.join(timeout=1.0)
|
|
1141
|
+
if process.is_alive():
|
|
1142
|
+
process.kill()
|
|
1143
|
+
process.join(timeout=1.0)
|
|
1144
|
+
raise LLMHardTimeoutError(
|
|
1145
|
+
f"LLM request exceeded hard timeout of {hard_timeout:.3f}s "
|
|
1146
|
+
f"(provider timeout={provider_timeout!r})"
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
try:
|
|
1150
|
+
status, payload = result_queue.get(timeout=1.0)
|
|
1151
|
+
except queue.Empty as exc:
|
|
1152
|
+
raise LiteLLMClientError(
|
|
1153
|
+
"LLM request process exited without returning a result "
|
|
1154
|
+
f"(exitcode={process.exitcode})"
|
|
1155
|
+
) from exc
|
|
1156
|
+
|
|
1157
|
+
if status == "ok":
|
|
1158
|
+
return payload
|
|
1159
|
+
if isinstance(payload, _CompletionErrorSnapshot):
|
|
1160
|
+
raise LiteLLMClientError(
|
|
1161
|
+
f"litellm.completion raised {payload.type_name}: {payload.message}"
|
|
1162
|
+
)
|
|
1163
|
+
raise payload
|
|
1164
|
+
finally:
|
|
1165
|
+
result_queue.close()
|
|
1166
|
+
result_queue.join_thread()
|
|
1167
|
+
|
|
1168
|
+
def _effective_timeout_for_model(self, model: str) -> int:
|
|
1169
|
+
"""Return the configured timeout, raised to the model's floor if one exists.
|
|
1170
|
+
|
|
1171
|
+
Args:
|
|
1172
|
+
model: Resolved model name (e.g. 'minimax/MiniMax-M3').
|
|
1173
|
+
|
|
1174
|
+
Returns:
|
|
1175
|
+
int: max(config.timeout, per-model floor). Callers that pass an
|
|
1176
|
+
explicit timeout kwarg bypass this entirely.
|
|
1177
|
+
"""
|
|
1178
|
+
return max(self.config.timeout, _MODEL_TIMEOUT_FLOOR_SECONDS.get(model, 0))
|
|
1179
|
+
|
|
1180
|
+
def _hard_timeout_grace_seconds(self) -> float:
|
|
1181
|
+
raw = os.environ.get("REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS", "5") or "5"
|
|
1182
|
+
try:
|
|
1183
|
+
return max(0.0, float(raw))
|
|
1184
|
+
except ValueError:
|
|
1185
|
+
self.logger.warning(
|
|
1186
|
+
"Invalid REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS=%r; using 5",
|
|
1187
|
+
raw,
|
|
1188
|
+
)
|
|
1189
|
+
return 5.0
|
|
1190
|
+
|
|
1191
|
+
def _should_process_isolate_completion(
|
|
1192
|
+
self, timeout_seconds: float, grace_seconds: float
|
|
1193
|
+
) -> bool:
|
|
1194
|
+
"""Use process isolation for real LiteLLM calls while preserving test doubles.
|
|
1195
|
+
|
|
1196
|
+
Unit tests often monkeypatch ``litellm.completion`` with local closures
|
|
1197
|
+
that capture params in parent memory. Those closures cannot be observed
|
|
1198
|
+
through a subprocess, so only real LiteLLM functions and explicit short
|
|
1199
|
+
timeout tests go through the process path.
|
|
1200
|
+
"""
|
|
1201
|
+
completion_module = getattr(litellm.completion, "__module__", "")
|
|
1202
|
+
if completion_module.startswith("litellm"):
|
|
1203
|
+
return True
|
|
1204
|
+
return timeout_seconds + grace_seconds < 1.0
|
|
1205
|
+
|
|
920
1206
|
def _log_token_usage(self, params: dict[str, Any], response: Any) -> None:
|
|
921
1207
|
"""Log token usage with cache statistics and cost from an LLM response.
|
|
922
1208
|
|
|
@@ -954,163 +1240,165 @@ class LiteLLMClient:
|
|
|
954
1240
|
cost_suffix,
|
|
955
1241
|
)
|
|
956
1242
|
|
|
957
|
-
def
|
|
958
|
-
self,
|
|
959
|
-
error: Exception,
|
|
960
|
-
params: dict[str, Any],
|
|
961
|
-
attempt: int,
|
|
962
|
-
max_retries: int,
|
|
963
|
-
response_format: Any,
|
|
964
|
-
elapsed_seconds: float,
|
|
1243
|
+
def _emit_fallback_observability(
|
|
1244
|
+
self, response: Any, params: dict[str, Any]
|
|
965
1245
|
) -> None:
|
|
966
|
-
"""
|
|
1246
|
+
"""Surface fallback-routing info to logs and Sentry when applicable.
|
|
967
1247
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
max_retries: Maximum number of retries
|
|
973
|
-
response_format: Response format (for logging)
|
|
974
|
-
elapsed_seconds: Time elapsed for this attempt
|
|
1248
|
+
LiteLLM rewrites ``response.model`` to the model that actually served
|
|
1249
|
+
the call, so we detect a fallback by comparing it against the model
|
|
1250
|
+
we asked for. The check is best-effort: any exception inside this
|
|
1251
|
+
helper is swallowed so observability never breaks the request.
|
|
975
1252
|
|
|
976
|
-
|
|
977
|
-
|
|
1253
|
+
Args:
|
|
1254
|
+
response: The litellm completion response object.
|
|
1255
|
+
params: The params dict that was passed to ``litellm.completion`` —
|
|
1256
|
+
used to read the originally requested primary model name.
|
|
978
1257
|
"""
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
max_retries,
|
|
988
|
-
elapsed_seconds,
|
|
989
|
-
False,
|
|
990
|
-
type(error).__name__,
|
|
991
|
-
str(error),
|
|
992
|
-
)
|
|
1258
|
+
try:
|
|
1259
|
+
primary_model = params.get("model")
|
|
1260
|
+
hidden = getattr(response, "_hidden_params", {}) or {}
|
|
1261
|
+
served_model = (
|
|
1262
|
+
hidden.get("model_id")
|
|
1263
|
+
or hidden.get("model")
|
|
1264
|
+
or getattr(response, "model", None)
|
|
1265
|
+
)
|
|
993
1266
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
raise LiteLLMClientError(f"API call failed: {str(error)}") from error
|
|
1267
|
+
if not served_model or served_model == primary_model:
|
|
1268
|
+
return
|
|
997
1269
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
attempt + 1,
|
|
1003
|
-
max_retries,
|
|
1004
|
-
error,
|
|
1005
|
-
delay,
|
|
1006
|
-
)
|
|
1007
|
-
time.sleep(delay)
|
|
1008
|
-
else:
|
|
1009
|
-
self.logger.error(
|
|
1010
|
-
"LLM request failed (model=%s, has_response_format=%s): %s",
|
|
1011
|
-
params.get("model"),
|
|
1012
|
-
response_format is not None,
|
|
1013
|
-
error,
|
|
1270
|
+
self.logger.info(
|
|
1271
|
+
"event=llm_fallback_used primary_model=%s served_model=%s",
|
|
1272
|
+
primary_model,
|
|
1273
|
+
served_model,
|
|
1014
1274
|
)
|
|
1015
1275
|
|
|
1276
|
+
# Local import keeps sentry out of module-init paths the tests
|
|
1277
|
+
# exercise without a Sentry SDK installed. sentry_sdk is an
|
|
1278
|
+
# enterprise-only dependency; OSS callers run without it and the
|
|
1279
|
+
# ImportError is intentionally absorbed by the outer except.
|
|
1280
|
+
import sentry_sdk # type: ignore[import-not-found]
|
|
1281
|
+
|
|
1282
|
+
sentry_sdk.set_tag("llm.fallback_used", "true")
|
|
1283
|
+
sentry_sdk.set_tag("llm.primary_model", str(primary_model))
|
|
1284
|
+
sentry_sdk.set_tag("llm.fallback_model", str(served_model))
|
|
1285
|
+
except Exception: # noqa: BLE001 — observability must not break the call
|
|
1286
|
+
return
|
|
1287
|
+
|
|
1016
1288
|
def _make_request(
|
|
1017
1289
|
self, messages: list[dict[str, Any]], **kwargs: Any
|
|
1018
1290
|
) -> str | BaseModel | ToolCallingChatResponse:
|
|
1019
1291
|
"""
|
|
1020
|
-
Make a request to the LLM
|
|
1292
|
+
Make a request to the LLM, delegating retries and fallback to litellm.
|
|
1293
|
+
|
|
1294
|
+
Retry and fallback semantics are handed to ``litellm.completion`` via
|
|
1295
|
+
the native ``num_retries`` and ``fallbacks`` kwargs. Per the documented
|
|
1296
|
+
flow at https://docs.litellm.ai/docs/router_architecture, the primary
|
|
1297
|
+
model is tried ``num_retries+1`` times, then each fallback gets a single
|
|
1298
|
+
attempt. The one piece we still own at the client level is a single
|
|
1299
|
+
retry for ``StructuredOutputParseError``: LiteLLM cannot detect a
|
|
1300
|
+
post-hoc Pydantic re-validation failure because it sees a successful
|
|
1301
|
+
HTTP response.
|
|
1021
1302
|
|
|
1022
1303
|
Args:
|
|
1023
1304
|
messages: List of messages to send.
|
|
1024
|
-
**kwargs: Additional parameters
|
|
1305
|
+
**kwargs: Additional parameters (response_format, max_retries,
|
|
1306
|
+
fallback_models, tools, etc.).
|
|
1025
1307
|
|
|
1026
1308
|
Returns:
|
|
1027
1309
|
Response content as string, BaseModel instance, or
|
|
1028
1310
|
ToolCallingChatResponse when the request was in tool-calling mode.
|
|
1029
1311
|
|
|
1030
1312
|
Raises:
|
|
1031
|
-
LiteLLMClientError: If the request fails after all retries
|
|
1313
|
+
LiteLLMClientError: If the request fails after all retries and
|
|
1314
|
+
fallbacks have been exhausted by litellm.
|
|
1032
1315
|
"""
|
|
1033
|
-
params, response_format, parse_structured_output, max_retries = (
|
|
1316
|
+
params, response_format, parse_structured_output, max_retries, fallbacks = (
|
|
1034
1317
|
self._build_completion_params(messages, **kwargs)
|
|
1035
1318
|
)
|
|
1036
1319
|
|
|
1037
|
-
|
|
1038
|
-
#
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1320
|
+
# Hand retries + fallbacks to litellm. ``num_retries`` is the documented
|
|
1321
|
+
# alias for max_retries on litellm.completion.
|
|
1322
|
+
params["num_retries"] = max_retries
|
|
1323
|
+
if fallbacks:
|
|
1324
|
+
params["fallbacks"] = fallbacks
|
|
1325
|
+
|
|
1326
|
+
request_start = time.perf_counter()
|
|
1327
|
+
self.logger.info(
|
|
1328
|
+
"event=llm_request_start model=%s timeout=%s has_response_format=%s num_retries=%d fallbacks=%s",
|
|
1329
|
+
params.get("model"),
|
|
1330
|
+
params.get("timeout"),
|
|
1331
|
+
response_format is not None,
|
|
1332
|
+
max_retries,
|
|
1333
|
+
fallbacks,
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
def _call_and_parse() -> str | BaseModel | ToolCallingChatResponse:
|
|
1337
|
+
response = self._completion_with_hard_timeout(params)
|
|
1338
|
+
self._emit_fallback_observability(response, params)
|
|
1339
|
+
message = response.choices[0].message # type: ignore[reportAttributeAccessIssue]
|
|
1340
|
+
content = message.content
|
|
1341
|
+
self._log_token_usage(params, response)
|
|
1046
1342
|
self.logger.info(
|
|
1047
|
-
"event=
|
|
1343
|
+
"event=llm_request_end model=%s timeout=%s has_response_format=%s elapsed_seconds=%.3f success=%s",
|
|
1048
1344
|
params.get("model"),
|
|
1049
1345
|
params.get("timeout"),
|
|
1050
1346
|
response_format is not None,
|
|
1051
|
-
|
|
1052
|
-
|
|
1347
|
+
time.perf_counter() - request_start,
|
|
1348
|
+
True,
|
|
1053
1349
|
)
|
|
1054
|
-
try:
|
|
1055
|
-
response = litellm.completion(**params)
|
|
1056
|
-
message = response.choices[0].message # type: ignore[reportAttributeAccessIssue]
|
|
1057
|
-
content = message.content
|
|
1058
|
-
elapsed_seconds = time.perf_counter() - request_start
|
|
1059
|
-
|
|
1060
|
-
self._log_token_usage(params, response)
|
|
1061
1350
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1351
|
+
# Tool-calling path: return a structured response instead of
|
|
1352
|
+
# going through _maybe_parse_structured_output.
|
|
1353
|
+
if "tools" in params:
|
|
1354
|
+
raw_usage = getattr(response, "usage", None)
|
|
1355
|
+
call_cost = self._compute_cost_usd(response, params.get("model"))
|
|
1356
|
+
return ToolCallingChatResponse(
|
|
1357
|
+
content=content,
|
|
1358
|
+
tool_calls=getattr(message, "tool_calls", None),
|
|
1359
|
+
finish_reason=response.choices[0].finish_reason, # type: ignore[reportAttributeAccessIssue]
|
|
1360
|
+
usage=raw_usage,
|
|
1361
|
+
cost_usd=call_cost,
|
|
1071
1362
|
)
|
|
1072
1363
|
|
|
1073
|
-
|
|
1074
|
-
#
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
return ToolCallingChatResponse(
|
|
1079
|
-
content=content,
|
|
1080
|
-
tool_calls=getattr(message, "tool_calls", None),
|
|
1081
|
-
finish_reason=response.choices[0].finish_reason, # type: ignore[reportAttributeAccessIssue]
|
|
1082
|
-
usage=raw_usage,
|
|
1083
|
-
cost_usd=call_cost,
|
|
1084
|
-
)
|
|
1364
|
+
return self._maybe_parse_structured_output(
|
|
1365
|
+
content, # type: ignore[reportArgumentType]
|
|
1366
|
+
response_format,
|
|
1367
|
+
parse_structured_output,
|
|
1368
|
+
)
|
|
1085
1369
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1370
|
+
try:
|
|
1371
|
+
try:
|
|
1372
|
+
return _call_and_parse()
|
|
1373
|
+
except StructuredOutputParseError:
|
|
1374
|
+
# LiteLLM's num_retries covers API errors, but a Pydantic
|
|
1375
|
+
# re-validation failure happens AFTER litellm sees a
|
|
1376
|
+
# successful 200 — so we owe one explicit second attempt at
|
|
1377
|
+
# the model. PR #121 documented this as a MiniMax-M3
|
|
1378
|
+
# mitigation.
|
|
1379
|
+
self.logger.warning(
|
|
1380
|
+
"event=llm_parse_retry model=%s — primary returned malformed structured output, retrying once",
|
|
1381
|
+
params.get("model"),
|
|
1090
1382
|
)
|
|
1091
|
-
|
|
1092
|
-
except
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
structured_parse_retry_granted = True
|
|
1100
|
-
effective_max_retries += 1
|
|
1101
|
-
self._handle_retry_or_raise(
|
|
1102
|
-
e,
|
|
1103
|
-
params,
|
|
1104
|
-
attempt,
|
|
1105
|
-
effective_max_retries,
|
|
1106
|
-
response_format,
|
|
1107
|
-
elapsed_seconds,
|
|
1383
|
+
return _call_and_parse()
|
|
1384
|
+
except LLMHardTimeoutError:
|
|
1385
|
+
# The hard timeout kills the litellm subprocess, so litellm's
|
|
1386
|
+
# num_retries never gets a chance — we owe one explicit retry
|
|
1387
|
+
# at this level to cover transient provider hangs.
|
|
1388
|
+
self.logger.warning(
|
|
1389
|
+
"event=llm_hard_timeout_retry model=%s — request hit hard timeout, retrying once",
|
|
1390
|
+
params.get("model"),
|
|
1108
1391
|
)
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1392
|
+
return _call_and_parse()
|
|
1393
|
+
except Exception as e:
|
|
1394
|
+
self.logger.error(
|
|
1395
|
+
"event=llm_request_end model=%s elapsed_seconds=%.3f success=False error_type=%s error=%s",
|
|
1396
|
+
params.get("model"),
|
|
1397
|
+
time.perf_counter() - request_start,
|
|
1398
|
+
type(e).__name__,
|
|
1399
|
+
e,
|
|
1400
|
+
)
|
|
1401
|
+
raise LiteLLMClientError(f"API call failed: {e}") from e
|
|
1114
1402
|
|
|
1115
1403
|
def _apply_prompt_caching(
|
|
1116
1404
|
self, messages: list[dict[str, Any]], model: str
|
|
@@ -1509,18 +1797,6 @@ class LiteLLMClient:
|
|
|
1509
1797
|
# Remove trailing commas before } or ]
|
|
1510
1798
|
return re.sub(r",\s*([}\]])", r"\1", s)
|
|
1511
1799
|
|
|
1512
|
-
def _is_non_retryable_error(self, error_str: str) -> bool:
|
|
1513
|
-
"""
|
|
1514
|
-
Check if an error is non-retryable.
|
|
1515
|
-
|
|
1516
|
-
Args:
|
|
1517
|
-
error_str: Error message string.
|
|
1518
|
-
|
|
1519
|
-
Returns:
|
|
1520
|
-
True if error should not be retried.
|
|
1521
|
-
"""
|
|
1522
|
-
return any(pattern in error_str for pattern in self.NON_RETRYABLE_ERRORS)
|
|
1523
|
-
|
|
1524
1800
|
def update_config(self, **kwargs) -> None:
|
|
1525
1801
|
"""
|
|
1526
1802
|
Update client configuration.
|