synth-ai 0.2.2.dev0__py3-none-any.whl → 0.2.4.dev2__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 (115) hide show
  1. synth_ai/cli/__init__.py +66 -0
  2. synth_ai/cli/balance.py +205 -0
  3. synth_ai/cli/calc.py +70 -0
  4. synth_ai/cli/demo.py +74 -0
  5. synth_ai/{cli.py → cli/legacy_root_backup.py} +60 -15
  6. synth_ai/cli/man.py +103 -0
  7. synth_ai/cli/recent.py +126 -0
  8. synth_ai/cli/root.py +184 -0
  9. synth_ai/cli/status.py +126 -0
  10. synth_ai/cli/traces.py +136 -0
  11. synth_ai/cli/watch.py +508 -0
  12. synth_ai/config/base_url.py +53 -0
  13. synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +252 -0
  14. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_duckdb_v2_backup.py +413 -0
  15. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +760 -0
  16. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_synth.py +34 -0
  17. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/test_crafter_react_agent_lm_synth.py +1740 -0
  18. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/test_crafter_react_agent_lm_synth_v2_backup.py +1318 -0
  19. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_duckdb_v2_backup.py +386 -0
  20. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +580 -0
  21. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v2_backup.py +1352 -0
  22. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +4 -4
  23. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/test_crafter_react_agent_openai_v2_backup.py +2551 -0
  24. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +1 -1
  25. synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +1 -1
  26. synth_ai/environments/examples/crafter_classic/agent_demos/old/traces/session_crafter_episode_16_15227b68-2906-416f-acc4-d6a9b4fa5828_20250725_001154.json +1363 -1
  27. synth_ai/environments/examples/crafter_classic/agent_demos/test_crafter_react_agent.py +3 -3
  28. synth_ai/environments/examples/crafter_classic/environment.py +1 -1
  29. synth_ai/environments/examples/crafter_custom/environment.py +1 -1
  30. synth_ai/environments/examples/enron/dataset/corbt___enron_emails_sample_questions/default/0.0.0/293c9fe8170037e01cc9cf5834e0cd5ef6f1a6bb/dataset_info.json +1 -0
  31. synth_ai/environments/examples/nethack/helpers/achievements.json +64 -0
  32. synth_ai/environments/examples/red/units/test_exploration_strategy.py +1 -1
  33. synth_ai/environments/examples/red/units/test_menu_bug_reproduction.py +5 -5
  34. synth_ai/environments/examples/red/units/test_movement_debug.py +2 -2
  35. synth_ai/environments/examples/red/units/test_retry_movement.py +1 -1
  36. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/available_envs.json +122 -0
  37. synth_ai/environments/examples/sokoban/verified_puzzles.json +54987 -0
  38. synth_ai/environments/service/core_routes.py +1 -1
  39. synth_ai/experimental/synth_oss.py +446 -0
  40. synth_ai/learning/core.py +21 -0
  41. synth_ai/learning/gateway.py +4 -0
  42. synth_ai/learning/prompts/gepa.py +0 -0
  43. synth_ai/learning/prompts/mipro.py +8 -0
  44. synth_ai/lm/__init__.py +3 -0
  45. synth_ai/lm/core/main.py +4 -0
  46. synth_ai/lm/core/main_v3.py +238 -122
  47. synth_ai/lm/core/vendor_clients.py +4 -0
  48. synth_ai/lm/provider_support/openai.py +11 -2
  49. synth_ai/lm/vendors/base.py +7 -0
  50. synth_ai/lm/vendors/openai_standard.py +339 -4
  51. synth_ai/lm/vendors/openai_standard_responses.py +243 -0
  52. synth_ai/lm/vendors/synth_client.py +155 -5
  53. synth_ai/lm/warmup.py +54 -17
  54. synth_ai/tracing/__init__.py +18 -0
  55. synth_ai/tracing_v1/__init__.py +29 -14
  56. synth_ai/tracing_v3/__init__.py +2 -2
  57. synth_ai/tracing_v3/abstractions.py +62 -17
  58. synth_ai/tracing_v3/config.py +13 -7
  59. synth_ai/tracing_v3/db_config.py +6 -6
  60. synth_ai/tracing_v3/hooks.py +1 -1
  61. synth_ai/tracing_v3/llm_call_record_helpers.py +350 -0
  62. synth_ai/tracing_v3/lm_call_record_abstractions.py +257 -0
  63. synth_ai/tracing_v3/session_tracer.py +5 -5
  64. synth_ai/tracing_v3/tests/test_concurrent_operations.py +1 -1
  65. synth_ai/tracing_v3/tests/test_llm_call_records.py +672 -0
  66. synth_ai/tracing_v3/tests/test_session_tracer.py +43 -9
  67. synth_ai/tracing_v3/tests/test_turso_manager.py +1 -1
  68. synth_ai/tracing_v3/turso/manager.py +18 -11
  69. synth_ai/tracing_v3/turso/models.py +1 -0
  70. synth_ai/tui/__main__.py +13 -0
  71. synth_ai/tui/dashboard.py +329 -0
  72. synth_ai/v0/tracing/__init__.py +0 -0
  73. synth_ai/{tracing → v0/tracing}/base_client.py +3 -3
  74. synth_ai/{tracing → v0/tracing}/client_manager.py +1 -1
  75. synth_ai/{tracing → v0/tracing}/context.py +1 -1
  76. synth_ai/{tracing → v0/tracing}/decorators.py +11 -11
  77. synth_ai/v0/tracing/events/__init__.py +0 -0
  78. synth_ai/{tracing → v0/tracing}/events/manage.py +4 -4
  79. synth_ai/{tracing → v0/tracing}/events/scope.py +6 -6
  80. synth_ai/{tracing → v0/tracing}/events/store.py +3 -3
  81. synth_ai/{tracing → v0/tracing}/immediate_client.py +6 -6
  82. synth_ai/{tracing → v0/tracing}/log_client_base.py +2 -2
  83. synth_ai/{tracing → v0/tracing}/retry_queue.py +3 -3
  84. synth_ai/{tracing → v0/tracing}/trackers.py +2 -2
  85. synth_ai/{tracing → v0/tracing}/upload.py +4 -4
  86. synth_ai/v0/tracing_v1/__init__.py +16 -0
  87. synth_ai/{tracing_v1 → v0/tracing_v1}/base_client.py +3 -3
  88. synth_ai/{tracing_v1 → v0/tracing_v1}/client_manager.py +1 -1
  89. synth_ai/{tracing_v1 → v0/tracing_v1}/context.py +1 -1
  90. synth_ai/{tracing_v1 → v0/tracing_v1}/decorators.py +11 -11
  91. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  92. synth_ai/{tracing_v1 → v0/tracing_v1}/events/manage.py +4 -4
  93. synth_ai/{tracing_v1 → v0/tracing_v1}/events/scope.py +6 -6
  94. synth_ai/{tracing_v1 → v0/tracing_v1}/events/store.py +3 -3
  95. synth_ai/{tracing_v1 → v0/tracing_v1}/immediate_client.py +6 -6
  96. synth_ai/{tracing_v1 → v0/tracing_v1}/log_client_base.py +2 -2
  97. synth_ai/{tracing_v1 → v0/tracing_v1}/retry_queue.py +3 -3
  98. synth_ai/{tracing_v1 → v0/tracing_v1}/trackers.py +2 -2
  99. synth_ai/{tracing_v1 → v0/tracing_v1}/upload.py +4 -4
  100. {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.4.dev2.dist-info}/METADATA +100 -5
  101. {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.4.dev2.dist-info}/RECORD +115 -75
  102. /synth_ai/{tracing/events/__init__.py → compound/cais.py} +0 -0
  103. /synth_ai/{tracing_v1/events/__init__.py → environments/examples/crafter_classic/debug_translation.py} +0 -0
  104. /synth_ai/{tracing → v0/tracing}/abstractions.py +0 -0
  105. /synth_ai/{tracing → v0/tracing}/config.py +0 -0
  106. /synth_ai/{tracing → v0/tracing}/local.py +0 -0
  107. /synth_ai/{tracing → v0/tracing}/utils.py +0 -0
  108. /synth_ai/{tracing_v1 → v0/tracing_v1}/abstractions.py +0 -0
  109. /synth_ai/{tracing_v1 → v0/tracing_v1}/config.py +0 -0
  110. /synth_ai/{tracing_v1 → v0/tracing_v1}/local.py +0 -0
  111. /synth_ai/{tracing_v1 → v0/tracing_v1}/utils.py +0 -0
  112. {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.4.dev2.dist-info}/WHEEL +0 -0
  113. {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.4.dev2.dist-info}/entry_points.txt +0 -0
  114. {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.4.dev2.dist-info}/licenses/LICENSE +0 -0
  115. {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.4.dev2.dist-info}/top_level.txt +0 -0
@@ -5,37 +5,39 @@ This module provides the LM class with async v3 tracing support,
5
5
  replacing the v2 DuckDB-based implementation.
6
6
  """
7
7
 
8
- from typing import Any, Dict, List, Literal, Optional, Union
9
- import os
10
- import functools
8
+ from typing import Any, Literal
11
9
  import asyncio
12
10
  import time
13
11
 
14
- from pydantic import BaseModel, Field
12
+ from pydantic import BaseModel
15
13
 
16
- from synth_ai.lm.core.exceptions import StructuredOutputCoercionFailureException
14
+ from synth_ai.lm.config import reasoning_models
17
15
  from synth_ai.lm.core.vendor_clients import (
18
16
  anthropic_naming_regexes,
19
17
  get_client,
20
18
  openai_naming_regexes,
21
19
  )
22
20
  from synth_ai.lm.structured_outputs.handler import StructuredOutputHandler
23
- from synth_ai.lm.vendors.base import VendorBase, BaseLMResponse
24
21
  from synth_ai.lm.tools.base import BaseTool
25
- from synth_ai.lm.config import reasoning_models
22
+ from synth_ai.lm.vendors.base import BaseLMResponse, VendorBase
26
23
 
27
24
  # V3 tracing imports
28
- from synth_ai.tracing_v3.session_tracer import SessionTracer
29
- from synth_ai.tracing_v3.decorators import set_session_id, set_turn_number, set_session_tracer
30
25
  from synth_ai.tracing_v3.abstractions import LMCAISEvent, TimeRecord
26
+ from synth_ai.tracing_v3.decorators import set_turn_number
27
+ from synth_ai.tracing_v3.llm_call_record_helpers import (
28
+ compute_aggregates_from_call_records,
29
+ create_llm_call_record_from_response,
30
+ )
31
+ from synth_ai.tracing_v3.session_tracer import SessionTracer
31
32
 
32
33
 
33
34
  def build_messages(
34
35
  sys_msg: str,
35
36
  user_msg: str,
36
- images_bytes: List = [],
37
- model_name: Optional[str] = None,
38
- ) -> List[Dict]:
37
+ images_bytes: list | None = None,
38
+ model_name: str | None = None,
39
+ ) -> list[dict]:
40
+ images_bytes = images_bytes or []
39
41
  if len(images_bytes) > 0 and any(regex.match(model_name) for regex in openai_naming_regexes):
40
42
  return [
41
43
  {"role": "system", "content": sys_msg},
@@ -51,9 +53,7 @@ def build_messages(
51
53
  ],
52
54
  },
53
55
  ]
54
- elif len(images_bytes) > 0 and any(
55
- regex.match(model_name) for regex in anthropic_naming_regexes
56
- ):
56
+ elif len(images_bytes) > 0 and any(regex.match(model_name) for regex in anthropic_naming_regexes):
57
57
  return [
58
58
  {"role": "system", "content": sys_msg},
59
59
  {
@@ -84,24 +84,27 @@ class LM:
84
84
 
85
85
  def __init__(
86
86
  self,
87
- vendor: Optional[str] = None,
88
- model: Optional[str] = None,
87
+ vendor: str | None = None,
88
+ model: str | None = None,
89
89
  # v2 compatibility parameters
90
- model_name: Optional[str] = None, # Alias for model
91
- formatting_model_name: Optional[str] = None, # For structured outputs
92
- provider: Optional[str] = None, # Alias for vendor
90
+ model_name: str | None = None, # Alias for model
91
+ formatting_model_name: str | None = None, # For structured outputs
92
+ provider: str | None = None, # Alias for vendor
93
93
  synth_logging: bool = True, # v2 compatibility
94
94
  max_retries: Literal["None", "Few", "Many"] = "Few", # v2 compatibility
95
95
  # v3 parameters
96
- is_structured: Optional[bool] = None,
97
- structured_outputs_vendor: Optional[str] = None,
98
- response_format: Union[BaseModel, Dict[str, Any], None] = None,
96
+ is_structured: bool | None = None,
97
+ structured_outputs_vendor: str | None = None,
98
+ response_format: type[BaseModel] | dict[str, Any] | None = None,
99
99
  json_mode: bool = False,
100
100
  temperature: float = 0.8,
101
- session_tracer: Optional[SessionTracer] = None,
102
- system_id: Optional[str] = None,
101
+ session_tracer: SessionTracer | None = None,
102
+ system_id: str | None = None,
103
103
  enable_v3_tracing: bool = True,
104
- enable_v2_tracing: Optional[bool] = None, # v2 compatibility
104
+ enable_v2_tracing: bool | None = None, # v2 compatibility
105
+ # Responses API parameters
106
+ auto_store_responses: bool = True,
107
+ use_responses_api: bool | None = None,
105
108
  **additional_params,
106
109
  ):
107
110
  # Handle v2 compatibility parameters
@@ -116,14 +119,14 @@ class LM:
116
119
  if vendor is None and model is not None:
117
120
  # Import vendor detection logic
118
121
  from synth_ai.lm.core.vendor_clients import (
119
- openai_naming_regexes,
120
122
  anthropic_naming_regexes,
121
- gemini_naming_regexes,
123
+ custom_endpoint_naming_regexes,
122
124
  deepseek_naming_regexes,
123
- groq_naming_regexes,
125
+ gemini_naming_regexes,
124
126
  grok_naming_regexes,
127
+ groq_naming_regexes,
128
+ openai_naming_regexes,
125
129
  openrouter_naming_regexes,
126
- custom_endpoint_naming_regexes,
127
130
  together_naming_regexes,
128
131
  )
129
132
 
@@ -160,18 +163,52 @@ class LM:
160
163
  self.system_id = system_id or f"lm_{self.vendor or 'unknown'}_{self.model or 'unknown'}"
161
164
  self.enable_v3_tracing = enable_v3_tracing
162
165
  self.additional_params = additional_params
166
+
167
+ # Initialize vendor wrapper early, before any potential usage
168
+ # (e.g., within StructuredOutputHandler initialization below)
169
+ self._vendor_wrapper = None
170
+
171
+ # Responses API thread management
172
+ self.auto_store_responses = auto_store_responses
173
+ self.use_responses_api = use_responses_api
174
+ self._last_response_id: str | None = None
163
175
 
164
176
  # Set structured output handler if needed
165
177
  if self.response_format:
166
178
  self.is_structured = True
179
+ # Choose mode automatically: prefer forced_json for OpenAI/reasoning models
180
+ forced_json_preferred = (self.vendor == "openai") or (
181
+ self.model in reasoning_models if self.model else False
182
+ )
183
+ structured_output_mode = "forced_json" if forced_json_preferred else "stringified_json"
184
+
185
+ # Build core and formatting clients
186
+ core_client = get_client(
187
+ self.model,
188
+ with_formatting=(structured_output_mode == "forced_json"),
189
+ provider=self.vendor,
190
+ )
191
+ formatting_model = formatting_model_name or self.model
192
+ formatting_client = get_client(
193
+ formatting_model,
194
+ with_formatting=True,
195
+ provider=self.vendor if self.vendor != "custom_endpoint" else None,
196
+ )
197
+
198
+ # Map retries
199
+ max_retries_dict = {"None": 0, "Few": 2, "Many": 5}
200
+ handler_params = {"max_retries": max_retries_dict.get(max_retries, 2)}
201
+
167
202
  self.structured_output_handler = StructuredOutputHandler(
168
- response_format=self.response_format, vendor_wrapper=self.get_vendor_wrapper()
203
+ core_client,
204
+ formatting_client,
205
+ structured_output_mode,
206
+ handler_params,
169
207
  )
170
208
  else:
171
209
  self.structured_output_handler = None
172
210
 
173
- # Initialize vendor wrapper
174
- self._vendor_wrapper = None
211
+ # Vendor wrapper lazy-instantiated via get_vendor_wrapper()
175
212
 
176
213
  def get_vendor_wrapper(self) -> VendorBase:
177
214
  """Get or create the vendor wrapper."""
@@ -180,31 +217,68 @@ class LM:
180
217
  self._vendor_wrapper = get_client(self.model, provider=self.vendor)
181
218
  return self._vendor_wrapper
182
219
 
220
+ def _should_use_responses_api(self) -> bool:
221
+ """Determine if Responses API should be used."""
222
+ if self.use_responses_api is not None:
223
+ return self.use_responses_api
224
+
225
+ # Auto-detect based on model
226
+ responses_models = {
227
+ "o4-mini", "o3", "o3-mini", # Supported Synth-hosted models
228
+ "gpt-oss-120b", "gpt-oss-20b" # OSS models via Synth
229
+ }
230
+ return self.model in responses_models or (self.model and self.model in reasoning_models)
231
+
232
+ def _should_use_harmony(self) -> bool:
233
+ """Determine if Harmony encoding should be used for OSS models."""
234
+ # Only use Harmony for OSS models when NOT using OpenAI vendor
235
+ # OpenAI hosts these models directly via Responses API
236
+ harmony_models = {"gpt-oss-120b", "gpt-oss-20b"}
237
+ return self.model in harmony_models and self.vendor != "openai"
238
+
183
239
  async def respond_async(
184
240
  self,
185
- system_message: Optional[str] = None,
186
- user_message: Optional[str] = None,
187
- messages: Optional[List[Dict]] = None, # v2 compatibility
188
- images_bytes: List[bytes] = [],
189
- images_as_bytes: Optional[List[bytes]] = None, # v2 compatibility
190
- response_model: Optional[BaseModel] = None, # v2 compatibility
191
- tools: Optional[List[BaseTool]] = None,
192
- turn_number: Optional[int] = None,
241
+ system_message: str | None = None,
242
+ user_message: str | None = None,
243
+ messages: list[dict] | None = None, # v2 compatibility
244
+ images_bytes: list[bytes] | None = None,
245
+ images_as_bytes: list[bytes] | None = None, # v2 compatibility
246
+ response_model: type[BaseModel] | None = None, # v2 compatibility
247
+ tools: list[BaseTool] | None = None,
248
+ turn_number: int | None = None,
249
+ previous_response_id: str | None = None, # Responses API thread management
193
250
  **kwargs,
194
251
  ) -> BaseLMResponse:
195
252
  """Async method to get LM response with v3 tracing."""
196
253
  start_time = time.time()
197
254
 
198
255
  # Handle v2 compatibility
199
- if images_as_bytes is not None:
200
- images_bytes = images_as_bytes
256
+ images_bytes = images_as_bytes if images_as_bytes is not None else (images_bytes or [])
201
257
 
202
- # Handle response_model for structured outputs
258
+ # Handle response_model for structured outputs (runtime-provided)
203
259
  if response_model and not self.response_format:
204
260
  self.response_format = response_model
205
261
  self.is_structured = True
262
+ # Mirror initialization logic from __init__
263
+ forced_json_preferred = (self.vendor == "openai") or (
264
+ self.model in reasoning_models if self.model else False
265
+ )
266
+ structured_output_mode = "forced_json" if forced_json_preferred else "stringified_json"
267
+ core_client = get_client(
268
+ self.model,
269
+ with_formatting=(structured_output_mode == "forced_json"),
270
+ provider=self.vendor,
271
+ )
272
+ formatting_client = get_client(
273
+ self.model,
274
+ with_formatting=True,
275
+ provider=self.vendor if self.vendor != "custom_endpoint" else None,
276
+ )
206
277
  self.structured_output_handler = StructuredOutputHandler(
207
- response_format=self.response_format, vendor_wrapper=self.get_vendor_wrapper()
278
+ core_client,
279
+ formatting_client,
280
+ structured_output_mode,
281
+ {"max_retries": 2},
208
282
  )
209
283
 
210
284
  # Set turn number if provided
@@ -227,57 +301,94 @@ class LM:
227
301
  )
228
302
  messages_to_use = build_messages(system_message, user_message, images_bytes, self.model)
229
303
 
230
- # Get vendor wrapper
231
- vendor_wrapper = self.get_vendor_wrapper()
232
-
233
- # Prepare parameters based on vendor type
234
- if hasattr(vendor_wrapper, "_hit_api_async"):
235
- # OpenAIStandard expects lm_config
236
- lm_config = {"temperature": self.temperature, **self.additional_params, **kwargs}
237
- if self.json_mode:
238
- lm_config["response_format"] = {"type": "json_object"}
239
-
240
- params = {"model": self.model, "messages": messages_to_use, "lm_config": lm_config}
304
+ # If using structured outputs, route through the handler
305
+ if self.structured_output_handler and self.response_format:
241
306
  if tools:
242
- params["tools"] = tools
307
+ raise ValueError("Tools are not supported with structured output mode")
308
+ response = await self.structured_output_handler.call_async(
309
+ messages=messages_to_use,
310
+ model=self.model,
311
+ response_model=self.response_format,
312
+ use_ephemeral_cache_only=False,
313
+ lm_config={"temperature": self.temperature, **self.additional_params, **kwargs},
314
+ reasoning_effort="high",
315
+ )
243
316
  else:
244
- # Other vendors use flat params
245
- params = {
246
- "model": self.model,
247
- "messages": messages_to_use,
248
- "temperature": self.temperature,
249
- **self.additional_params,
250
- **kwargs,
251
- }
317
+ # Get vendor wrapper
318
+ vendor_wrapper = self.get_vendor_wrapper()
252
319
 
253
- if tools:
254
- params["tools"] = [tool.to_dict() for tool in tools]
320
+ # Determine API type to use
321
+ use_responses = self._should_use_responses_api()
322
+ use_harmony = self._should_use_harmony()
255
323
 
256
- if self.json_mode:
257
- params["response_format"] = {"type": "json_object"}
324
+ # Decide response ID to use for thread management
325
+ response_id_to_use = None
326
+ if previous_response_id:
327
+ response_id_to_use = previous_response_id # Manual override
328
+ elif self.auto_store_responses and self._last_response_id:
329
+ response_id_to_use = self._last_response_id # Auto-chain
258
330
 
259
- # Call vendor
260
- try:
261
- # Try the standard method names
331
+ # Prepare parameters based on vendor type
262
332
  if hasattr(vendor_wrapper, "_hit_api_async"):
263
- response = await vendor_wrapper._hit_api_async(**params)
264
- elif hasattr(vendor_wrapper, "respond_async"):
265
- response = await vendor_wrapper.respond_async(**params)
266
- elif hasattr(vendor_wrapper, "respond"):
267
- # Fallback to sync in executor
268
- loop = asyncio.get_event_loop()
269
- response = await loop.run_in_executor(None, vendor_wrapper.respond, params)
333
+ # OpenAIStandard expects lm_config
334
+ lm_config = {"temperature": self.temperature, **self.additional_params, **kwargs}
335
+ if self.json_mode:
336
+ lm_config["response_format"] = {"type": "json_object"}
337
+
338
+ params = {"model": self.model, "messages": messages_to_use, "lm_config": lm_config}
339
+ if tools:
340
+ params["tools"] = tools
270
341
  else:
271
- raise AttributeError(
272
- f"Vendor wrapper {type(vendor_wrapper).__name__} has no suitable response method"
273
- )
274
- except Exception as e:
275
- print(f"Error calling vendor: {e}")
276
- raise
342
+ # Other vendors use flat params
343
+ params = {
344
+ "model": self.model,
345
+ "messages": messages_to_use,
346
+ "temperature": self.temperature,
347
+ **self.additional_params,
348
+ **kwargs,
349
+ }
350
+
351
+ if tools:
352
+ params["tools"] = [tool.to_dict() for tool in tools]
353
+
354
+ if self.json_mode:
355
+ params["response_format"] = {"type": "json_object"}
356
+
357
+ # Call vendor with appropriate API type
358
+ try:
359
+ # Route to appropriate API
360
+ if use_harmony and hasattr(vendor_wrapper, "_hit_api_async_harmony"):
361
+ params["previous_response_id"] = response_id_to_use
362
+ response = await vendor_wrapper._hit_api_async_harmony(**params)
363
+ elif use_responses and hasattr(vendor_wrapper, "_hit_api_async_responses"):
364
+ params["previous_response_id"] = response_id_to_use
365
+ response = await vendor_wrapper._hit_api_async_responses(**params)
366
+ else:
367
+ # Standard chat completions API
368
+ if hasattr(vendor_wrapper, "_hit_api_async"):
369
+ response = await vendor_wrapper._hit_api_async(**params)
370
+ elif hasattr(vendor_wrapper, "respond_async"):
371
+ response = await vendor_wrapper.respond_async(**params)
372
+ elif hasattr(vendor_wrapper, "respond"):
373
+ # Fallback to sync in executor
374
+ loop = asyncio.get_event_loop()
375
+ response = await loop.run_in_executor(None, vendor_wrapper.respond, params)
376
+ else:
377
+ raise AttributeError(
378
+ f"Vendor wrapper {type(vendor_wrapper).__name__} has no suitable response method"
379
+ )
380
+ if not hasattr(response, 'api_type'):
381
+ response.api_type = "chat"
277
382
 
278
- # Handle structured output
279
- if self.structured_output_handler:
280
- response = self.structured_output_handler.process_response(response)
383
+ # Update stored response ID if auto-storing
384
+ if self.auto_store_responses and hasattr(response, 'response_id') and response.response_id:
385
+ self._last_response_id = response.response_id
386
+
387
+ except Exception as e:
388
+ print(f"Error calling vendor: {e}")
389
+ raise
390
+
391
+ # No additional post-processing needed for structured outputs here
281
392
 
282
393
  # Record tracing event if enabled
283
394
  if (
@@ -286,36 +397,40 @@ class LM:
286
397
  and hasattr(self.session_tracer, "current_session")
287
398
  ):
288
399
  latency_ms = int((time.time() - start_time) * 1000)
400
+
401
+ # Create LLMCallRecord from the response
402
+ from datetime import datetime
403
+ started_at = datetime.utcnow()
404
+ completed_at = datetime.utcnow()
405
+
406
+ call_record = create_llm_call_record_from_response(
407
+ response=response,
408
+ model_name=self.model or self.vendor,
409
+ provider=self.vendor,
410
+ messages=messages_to_use,
411
+ temperature=self.temperature,
412
+ request_params={**self.additional_params, **kwargs},
413
+ tools=tools,
414
+ started_at=started_at,
415
+ completed_at=completed_at,
416
+ latency_ms=latency_ms,
417
+ )
418
+
419
+ # Compute aggregates from the call record
420
+ aggregates = compute_aggregates_from_call_records([call_record])
289
421
 
290
- # Extract usage info if available
291
- usage_info = {}
292
- if hasattr(response, "usage") and response.usage:
293
- usage_info = {
294
- "input_tokens": response.usage.get("input_tokens", 0),
295
- "output_tokens": response.usage.get("output_tokens", 0),
296
- "total_tokens": response.usage.get("total_tokens", 0),
297
- "cost_usd": response.usage.get("cost_usd", 0.0),
298
- }
299
- else:
300
- # Default values when usage is not available
301
- usage_info = {
302
- "input_tokens": 0,
303
- "output_tokens": 0,
304
- "total_tokens": 0,
305
- "cost_usd": 0.0,
306
- }
307
-
308
- # Create LM event
422
+ # Create LM event with call_records
309
423
  lm_event = LMCAISEvent(
310
424
  system_instance_id=self.system_id,
311
425
  time_record=TimeRecord(event_time=time.time(), message_time=turn_number),
312
- model_name=self.model or self.vendor,
313
- provider=self.vendor,
314
- input_tokens=usage_info["input_tokens"],
315
- output_tokens=usage_info["output_tokens"],
316
- total_tokens=usage_info["total_tokens"],
317
- cost_usd=usage_info["cost_usd"],
318
- latency_ms=latency_ms,
426
+ # Aggregates at event level
427
+ input_tokens=aggregates["input_tokens"],
428
+ output_tokens=aggregates["output_tokens"],
429
+ total_tokens=aggregates["total_tokens"],
430
+ cost_usd=aggregates["cost_usd"],
431
+ latency_ms=aggregates["latency_ms"],
432
+ # Store the call record
433
+ call_records=[call_record],
319
434
  metadata={
320
435
  "temperature": self.temperature,
321
436
  "json_mode": self.json_mode,
@@ -363,14 +478,15 @@ class LM:
363
478
 
364
479
  def respond(
365
480
  self,
366
- system_message: Optional[str] = None,
367
- user_message: Optional[str] = None,
368
- messages: Optional[List[Dict]] = None, # v2 compatibility
369
- images_bytes: List[bytes] = [],
370
- images_as_bytes: Optional[List[bytes]] = None, # v2 compatibility
371
- response_model: Optional[BaseModel] = None, # v2 compatibility
372
- tools: Optional[List[BaseTool]] = None,
373
- turn_number: Optional[int] = None,
481
+ system_message: str | None = None,
482
+ user_message: str | None = None,
483
+ messages: list[dict] | None = None, # v2 compatibility
484
+ images_bytes: list[bytes] | None = None,
485
+ images_as_bytes: list[bytes] | None = None, # v2 compatibility
486
+ response_model: type[BaseModel] | None = None, # v2 compatibility
487
+ tools: list[BaseTool] | None = None,
488
+ previous_response_id: str | None = None, # Responses API thread management
489
+ turn_number: int | None = None,
374
490
  **kwargs,
375
491
  ) -> BaseLMResponse:
376
492
  """Synchronous wrapper for respond_async."""
@@ -68,6 +68,10 @@ grok_naming_regexes: List[Pattern] = [
68
68
  ]
69
69
 
70
70
 
71
+ openrouter_naming_regexes: List[Pattern] = [
72
+ re.compile(r"^openrouter/.*$"), # openrouter/model-name pattern
73
+ ]
74
+
71
75
  openrouter_naming_regexes: List[Pattern] = [
72
76
  re.compile(r"^openrouter/.*$"), # openrouter/model-name pattern
73
77
  ]
@@ -103,7 +103,7 @@ OPENAI_METHODS_V1 = [
103
103
  sync=False,
104
104
  ),
105
105
  OpenAiDefinition(
106
- module="openai.resources.beta.chat.completions",
106
+ module="openai.resources.chat.completions",
107
107
  object="Completions",
108
108
  method="parse",
109
109
  type="chat",
@@ -111,7 +111,7 @@ OPENAI_METHODS_V1 = [
111
111
  min_version="1.50.0",
112
112
  ),
113
113
  OpenAiDefinition(
114
- module="openai.resources.beta.chat.completions",
114
+ module="openai.resources.chat.completions",
115
115
  object="AsyncCompletions",
116
116
  method="parse",
117
117
  type="chat",
@@ -776,6 +776,15 @@ class OpenAILangfuse:
776
776
  ):
777
777
  continue
778
778
 
779
+ # Check if the method actually exists before trying to wrap it
780
+ try:
781
+ module = __import__(resource.module, fromlist=[resource.object])
782
+ obj = getattr(module, resource.object, None)
783
+ if obj and not hasattr(obj, resource.method):
784
+ continue # Skip if method doesn't exist
785
+ except (ImportError, AttributeError):
786
+ continue # Skip if module or object doesn't exist
787
+
779
788
  wrap_function_wrapper(
780
789
  resource.module,
781
790
  f"{resource.object}.{resource.method}",
@@ -18,10 +18,17 @@ class BaseLMResponse(BaseModel):
18
18
  raw_response: The raw text response from the model
19
19
  structured_output: Optional parsed Pydantic model if structured output was requested
20
20
  tool_calls: Optional list of tool calls if tools were provided
21
+ response_id: Optional response ID for thread management (Responses API)
22
+ reasoning: Optional reasoning trace from the model (o1 models)
23
+ api_type: Optional API type used ("chat", "responses", or "harmony")
21
24
  """
22
25
  raw_response: str
23
26
  structured_output: Optional[BaseModel] = None
24
27
  tool_calls: Optional[List[Dict]] = None
28
+ response_id: Optional[str] = None
29
+ reasoning: Optional[str] = None
30
+ api_type: Optional[str] = None
31
+ usage: Optional[Dict[str, Any]] = None
25
32
 
26
33
 
27
34
  class VendorBase(ABC):