claude-smart 0.2.46 → 0.2.48

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 (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,195 @@
1
+ """Structured-output concern for ``LiteLLMClient`` — schema selection + parsing (Tier-2.5).
2
+
3
+ ``StructuredOutputMixin`` holds the response-format selection helpers
4
+ (``_supports_response_schema``, ``_provider_for_model``,
5
+ ``_accepts_json_schema_response_format``, ``_provider_response_format``) and the
6
+ post-hoc parse orchestrator (``_maybe_parse_structured_output``), plus the
7
+ ``_JSON_SCHEMA_PROVIDER_ALLOWLIST`` class constant they use.
8
+
9
+ SINK-1 (patch-where-used): ``assert_provider_safe_schema`` is imported HERE, so
10
+ tests patch it at ``_litellm_structured_output.assert_provider_safe_schema``
11
+ (the real guard raises under pytest; patching the old facade namespace would
12
+ no-op). ``litellm.supports_response_schema``/``get_llm_provider`` are called via
13
+ the shared ``litellm`` module attr.
14
+
15
+ Bodies moved VERBATIM; only the ``self``-typing (per-mixin TYPE_CHECKING stub of
16
+ ``config``, Tier-1b idiom) is added. ``_provider_response_format`` and
17
+ ``_maybe_parse_structured_output`` are the cross-mixin edges text-gen depends on,
18
+ so this module moves BEFORE ``_litellm_text_generation``.
19
+ """
20
+
21
+ import json
22
+ from functools import lru_cache
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ import litellm
26
+ from pydantic import BaseModel
27
+
28
+ from reflexio.server.llm._litellm_json_extraction import (
29
+ _extract_json_from_string,
30
+ _looks_truncated_json,
31
+ _sanitize_json_string,
32
+ )
33
+ from reflexio.server.llm._litellm_types import StructuredOutputParseError
34
+ from reflexio.server.llm.llm_utils import (
35
+ assert_provider_safe_schema,
36
+ is_pydantic_model,
37
+ strict_response_format_for_model,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ from reflexio.server.llm._litellm_types import LiteLLMConfig
42
+
43
+
44
+ class StructuredOutputMixin:
45
+ """Response-format schema selection + structured-output parsing.
46
+
47
+ Mixed into ``LiteLLMClient``; ``self.config`` is owned by the client-core
48
+ ``__init__`` on the facade. The annotation-only stub below (Tier-1b idiom)
49
+ gives pyright the foreign-member type without shared class-level state.
50
+ """
51
+
52
+ # OpenAI-compatible providers that accept a ``json_schema`` response_format
53
+ # but that ``litellm.supports_response_schema`` reports as unsupported. For
54
+ # these, the gate below would fall back to handing LiteLLM the raw Pydantic
55
+ # model; LiteLLM then builds the ``json_schema`` itself and emits ``oneOf``
56
+ # for discriminated unions, which strict structured-output endpoints reject
57
+ # (Sentry PYTHON-FASTAPI-9J). Listing the provider here forces our own
58
+ # normalized strict schema (``oneOf`` folded into ``anyOf``) to be sent.
59
+ _JSON_SCHEMA_PROVIDER_ALLOWLIST: frozenset[str] = frozenset({"minimax"})
60
+
61
+ # Base-owned attribute read for the parse-failure error message (init'd in
62
+ # the facade ``__init__``). Annotation-only; NEVER assign here.
63
+ config: "LiteLLMConfig"
64
+
65
+ @staticmethod
66
+ @lru_cache(maxsize=256)
67
+ def _supports_response_schema(model: str) -> bool:
68
+ try:
69
+ return bool(litellm.supports_response_schema(model=model))
70
+ except Exception:
71
+ return False
72
+
73
+ @staticmethod
74
+ @lru_cache(maxsize=256)
75
+ def _provider_for_model(model: str) -> str | None:
76
+ try:
77
+ return litellm.get_llm_provider(model)[1]
78
+ except Exception:
79
+ return None
80
+
81
+ @classmethod
82
+ def _accepts_json_schema_response_format(cls, model: str) -> bool:
83
+ """Whether to send ``model`` an explicit strict ``json_schema`` schema.
84
+
85
+ True when LiteLLM reports native response-schema support, or when the
86
+ provider is a known OpenAI-compatible endpoint that LiteLLM
87
+ under-reports (see ``_JSON_SCHEMA_PROVIDER_ALLOWLIST``). In the latter
88
+ case LiteLLM would otherwise forward a ``json_schema`` it built itself,
89
+ emitting ``oneOf`` for discriminated unions that the endpoint rejects.
90
+ """
91
+ if cls._supports_response_schema(model):
92
+ return True
93
+ return cls._provider_for_model(model) in cls._JSON_SCHEMA_PROVIDER_ALLOWLIST
94
+
95
+ def _provider_response_format(
96
+ self,
97
+ *,
98
+ response_format: Any,
99
+ model: str,
100
+ strict_response_format: bool,
101
+ ) -> Any:
102
+ """Return the provider-facing response_format while preserving parser schema.
103
+
104
+ Callers pass a Pydantic model so local parsing stays type-safe. When the
105
+ target model accepts a JSON Schema response format — either LiteLLM
106
+ reports native support, or the provider is an OpenAI-compatible endpoint
107
+ LiteLLM under-reports (see ``_accepts_json_schema_response_format``) — we
108
+ send an explicit strict schema to constrain generation. Truly
109
+ unsupported providers keep the existing Pydantic response_format
110
+ behavior.
111
+ """
112
+
113
+ if not is_pydantic_model(response_format):
114
+ return response_format
115
+
116
+ # Build the native schema once and reuse it for both the boundary guard and
117
+ # (when applicable) the strict normalizer, avoiding a second schema build.
118
+ # Boundary guard: models inheriting StrictStructuredOutput are safe by
119
+ # construction; this catches a model that forgot the base (raises under
120
+ # tests, warns in prod) regardless of which path is taken below.
121
+ schema = response_format.model_json_schema()
122
+ assert_provider_safe_schema(schema, name=response_format.__name__)
123
+
124
+ if strict_response_format and self._accepts_json_schema_response_format(model):
125
+ return strict_response_format_for_model(response_format, schema=schema)
126
+ return response_format
127
+
128
+ def _maybe_parse_structured_output(
129
+ self,
130
+ content: str,
131
+ response_format: Any,
132
+ parse_structured_output: bool,
133
+ ) -> str | BaseModel:
134
+ """
135
+ Parse structured output if applicable.
136
+
137
+ Args:
138
+ content: Raw response content.
139
+ response_format: Expected response format (must be a Pydantic BaseModel class).
140
+ parse_structured_output: Whether to parse the output.
141
+
142
+ Returns:
143
+ String for text responses, or BaseModel instance for structured responses.
144
+ """
145
+ if not response_format or not parse_structured_output:
146
+ return content
147
+
148
+ if content is None:
149
+ return content
150
+
151
+ # If content is already a Pydantic model (some providers return parsed)
152
+ if isinstance(content, BaseModel):
153
+ return content
154
+
155
+ # Try to parse JSON and convert to Pydantic model
156
+ # Extract JSON from markdown code blocks if present
157
+ json_str = _extract_json_from_string(content)
158
+ try:
159
+ parsed = json.loads(json_str)
160
+
161
+ # response_format must be a Pydantic model (validated at entry points)
162
+ return response_format.model_validate(parsed)
163
+ except Exception:
164
+ # LLMs sometimes produce Python-style output (single quotes, True/False,
165
+ # trailing commas). Try to sanitize before giving up.
166
+ try:
167
+ sanitized = _sanitize_json_string(json_str)
168
+ parsed = json.loads(sanitized)
169
+ return response_format.model_validate(parsed)
170
+ except Exception:
171
+ # Last resort: json-repair can recover complete responses with
172
+ # small syntax glitches, such as missing commas. Do not repair
173
+ # likely truncation: the retry loop should request a fresh
174
+ # complete response instead of accepting invented tail content.
175
+ try:
176
+ from json_repair import repair_json
177
+
178
+ if _looks_truncated_json(json_str):
179
+ raise StructuredOutputParseError(
180
+ "Structured output appears truncated"
181
+ )
182
+
183
+ repaired = repair_json(json_str, return_objects=True)
184
+ return response_format.model_validate(repaired)
185
+ except Exception as e:
186
+ model = self.config.model
187
+ snippet = (
188
+ content[:200]
189
+ if isinstance(content, str)
190
+ else repr(content)[:200]
191
+ )
192
+ raise StructuredOutputParseError(
193
+ f"Structured output parse failed for model={model!r}: {e}. "
194
+ f"Content snippet: {snippet!r}"
195
+ ) from e
@@ -0,0 +1,152 @@
1
+ """Subprocess-isolation helpers for the hard-timeout completion path (Tier-2.5 leaf).
2
+
3
+ Stateless leaf module: the picklable ``_Completion*Snapshot`` dataclasses plus the
4
+ snapshot builders and the ``_litellm_completion_worker`` that runs
5
+ ``litellm.completion`` inside a child process. Used by ``TextGenerationMixin``'s
6
+ ``_completion_with_hard_timeout`` (Task 4) which sizes and kills the subprocess.
7
+
8
+ LLM-mock: the worker calls ``litellm.completion`` via the MODULE ATTR (never
9
+ ``from litellm import completion``) so the global ``patch("litellm.completion")``
10
+ mock is inherited across the ``fork`` start-method. The multiprocessing
11
+ start-method is intentionally left unchanged.
12
+
13
+ SINK-2 (identity): the 6 snapshot classes and ``_litellm_completion_worker`` are
14
+ re-exported by import binding from the facade — they must be the SAME class/object
15
+ the worker constructs and tests ``isinstance``-check. Bodies moved VERBATIM.
16
+ """
17
+
18
+ import multiprocessing
19
+ import pickle
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+
23
+ import litellm
24
+
25
+
26
+ @dataclass
27
+ class _CompletionMessageSnapshot:
28
+ content: str | None = None
29
+ tool_calls: Any | None = None
30
+
31
+
32
+ @dataclass
33
+ class _CompletionChoiceSnapshot:
34
+ message: _CompletionMessageSnapshot
35
+ finish_reason: str | None = None
36
+
37
+
38
+ @dataclass
39
+ class _PromptTokenDetailsSnapshot:
40
+ cached_tokens: int = 0
41
+
42
+
43
+ @dataclass
44
+ class _CompletionUsageSnapshot:
45
+ prompt_tokens: int | None = None
46
+ completion_tokens: int | None = None
47
+ total_tokens: int | None = None
48
+ prompt_tokens_details: _PromptTokenDetailsSnapshot | None = None
49
+ cache_creation_input_tokens: int | None = None
50
+ cache_read_input_tokens: int | None = None
51
+
52
+
53
+ @dataclass
54
+ class _CompletionResponseSnapshot:
55
+ choices: list[_CompletionChoiceSnapshot]
56
+ usage: _CompletionUsageSnapshot | None = None
57
+ model: str | None = None
58
+ _hidden_params: dict[str, Any] = field(default_factory=dict)
59
+
60
+
61
+ @dataclass
62
+ class _CompletionErrorSnapshot:
63
+ type_name: str
64
+ message: str
65
+ model: str | None = None
66
+ llm_provider: str | None = None
67
+
68
+
69
+ def _snapshot_completion_error(
70
+ exc: BaseException, params: dict[str, Any]
71
+ ) -> _CompletionErrorSnapshot:
72
+ model = getattr(exc, "model", None) or params.get("model")
73
+ llm_provider = getattr(exc, "llm_provider", None)
74
+ return _CompletionErrorSnapshot(
75
+ type_name=type(exc).__name__,
76
+ message=str(exc),
77
+ model=str(model) if model else None,
78
+ llm_provider=str(llm_provider) if llm_provider else None,
79
+ )
80
+
81
+
82
+ def _ensure_picklable(value: Any) -> Any:
83
+ try:
84
+ pickle.dumps(value)
85
+ except Exception:
86
+ return repr(value)
87
+ return value
88
+
89
+
90
+ def _snapshot_completion_response(response: Any) -> _CompletionResponseSnapshot:
91
+ choices: list[_CompletionChoiceSnapshot] = []
92
+ for choice in getattr(response, "choices", []) or []:
93
+ message = getattr(choice, "message", None)
94
+ choices.append(
95
+ _CompletionChoiceSnapshot(
96
+ message=_CompletionMessageSnapshot(
97
+ content=getattr(message, "content", None),
98
+ tool_calls=_ensure_picklable(getattr(message, "tool_calls", None)),
99
+ ),
100
+ finish_reason=getattr(choice, "finish_reason", None),
101
+ )
102
+ )
103
+
104
+ usage = getattr(response, "usage", None)
105
+ usage_snapshot = None
106
+ if usage is not None:
107
+ prompt_details = getattr(usage, "prompt_tokens_details", None)
108
+ prompt_details_snapshot = None
109
+ if prompt_details is not None:
110
+ prompt_details_snapshot = _PromptTokenDetailsSnapshot(
111
+ cached_tokens=int(getattr(prompt_details, "cached_tokens", 0) or 0)
112
+ )
113
+ usage_snapshot = _CompletionUsageSnapshot(
114
+ prompt_tokens=getattr(usage, "prompt_tokens", None),
115
+ completion_tokens=getattr(usage, "completion_tokens", None),
116
+ total_tokens=getattr(usage, "total_tokens", None),
117
+ prompt_tokens_details=prompt_details_snapshot,
118
+ cache_creation_input_tokens=getattr(
119
+ usage, "cache_creation_input_tokens", None
120
+ ),
121
+ cache_read_input_tokens=getattr(usage, "cache_read_input_tokens", None),
122
+ )
123
+
124
+ hidden_params = getattr(response, "_hidden_params", {}) or {}
125
+ if not isinstance(hidden_params, dict):
126
+ hidden_params = {}
127
+
128
+ return _CompletionResponseSnapshot(
129
+ choices=choices,
130
+ usage=usage_snapshot,
131
+ model=getattr(response, "model", None),
132
+ _hidden_params={str(k): _ensure_picklable(v) for k, v in hidden_params.items()},
133
+ )
134
+
135
+
136
+ def _picklable_completion_result(response: Any) -> Any:
137
+ try:
138
+ pickle.dumps(response)
139
+ except Exception:
140
+ return _snapshot_completion_response(response)
141
+ return response
142
+
143
+
144
+ def _litellm_completion_worker(
145
+ params: dict[str, Any], result_queue: multiprocessing.Queue
146
+ ) -> None:
147
+ try:
148
+ result_queue.put(
149
+ ("ok", _picklable_completion_result(litellm.completion(**params)))
150
+ )
151
+ except BaseException as exc:
152
+ result_queue.put(("error", _snapshot_completion_error(exc, params)))