langchain-dev-utils 1.3.7__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 (44) hide show
  1. langchain_dev_utils/__init__.py +1 -0
  2. langchain_dev_utils/_utils.py +131 -0
  3. langchain_dev_utils/agents/__init__.py +4 -0
  4. langchain_dev_utils/agents/factory.py +99 -0
  5. langchain_dev_utils/agents/file_system.py +252 -0
  6. langchain_dev_utils/agents/middleware/__init__.py +21 -0
  7. langchain_dev_utils/agents/middleware/format_prompt.py +66 -0
  8. langchain_dev_utils/agents/middleware/handoffs.py +214 -0
  9. langchain_dev_utils/agents/middleware/model_fallback.py +49 -0
  10. langchain_dev_utils/agents/middleware/model_router.py +200 -0
  11. langchain_dev_utils/agents/middleware/plan.py +367 -0
  12. langchain_dev_utils/agents/middleware/summarization.py +85 -0
  13. langchain_dev_utils/agents/middleware/tool_call_repair.py +96 -0
  14. langchain_dev_utils/agents/middleware/tool_emulator.py +60 -0
  15. langchain_dev_utils/agents/middleware/tool_selection.py +82 -0
  16. langchain_dev_utils/agents/plan.py +188 -0
  17. langchain_dev_utils/agents/wrap.py +324 -0
  18. langchain_dev_utils/chat_models/__init__.py +11 -0
  19. langchain_dev_utils/chat_models/adapters/__init__.py +3 -0
  20. langchain_dev_utils/chat_models/adapters/create_utils.py +53 -0
  21. langchain_dev_utils/chat_models/adapters/openai_compatible.py +715 -0
  22. langchain_dev_utils/chat_models/adapters/register_profiles.py +15 -0
  23. langchain_dev_utils/chat_models/base.py +282 -0
  24. langchain_dev_utils/chat_models/types.py +27 -0
  25. langchain_dev_utils/embeddings/__init__.py +11 -0
  26. langchain_dev_utils/embeddings/adapters/__init__.py +3 -0
  27. langchain_dev_utils/embeddings/adapters/create_utils.py +45 -0
  28. langchain_dev_utils/embeddings/adapters/openai_compatible.py +91 -0
  29. langchain_dev_utils/embeddings/base.py +234 -0
  30. langchain_dev_utils/message_convert/__init__.py +15 -0
  31. langchain_dev_utils/message_convert/content.py +201 -0
  32. langchain_dev_utils/message_convert/format.py +69 -0
  33. langchain_dev_utils/pipeline/__init__.py +7 -0
  34. langchain_dev_utils/pipeline/parallel.py +135 -0
  35. langchain_dev_utils/pipeline/sequential.py +101 -0
  36. langchain_dev_utils/pipeline/types.py +3 -0
  37. langchain_dev_utils/py.typed +0 -0
  38. langchain_dev_utils/tool_calling/__init__.py +14 -0
  39. langchain_dev_utils/tool_calling/human_in_the_loop.py +284 -0
  40. langchain_dev_utils/tool_calling/utils.py +81 -0
  41. langchain_dev_utils-1.3.7.dist-info/METADATA +103 -0
  42. langchain_dev_utils-1.3.7.dist-info/RECORD +44 -0
  43. langchain_dev_utils-1.3.7.dist-info/WHEEL +4 -0
  44. langchain_dev_utils-1.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,715 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Iterator
4
+ from json import JSONDecodeError
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ List,
9
+ Literal,
10
+ Optional,
11
+ Sequence,
12
+ Type,
13
+ TypeVar,
14
+ Union,
15
+ cast,
16
+ )
17
+
18
+ import openai
19
+ from langchain_core.callbacks import (
20
+ AsyncCallbackManagerForLLMRun,
21
+ CallbackManagerForLLMRun,
22
+ )
23
+ from langchain_core.language_models import (
24
+ LangSmithParams,
25
+ LanguageModelInput,
26
+ ModelProfile,
27
+ )
28
+ from langchain_core.messages import (
29
+ AIMessage,
30
+ AIMessageChunk,
31
+ BaseMessage,
32
+ HumanMessage,
33
+ ToolMessage,
34
+ )
35
+ from langchain_core.outputs import ChatGenerationChunk, ChatResult
36
+ from langchain_core.runnables import Runnable
37
+ from langchain_core.tools import BaseTool
38
+ from langchain_core.utils import from_env, secret_from_env
39
+ from langchain_core.utils.function_calling import convert_to_openai_tool
40
+ from langchain_openai.chat_models._compat import _convert_from_v1_to_chat_completions
41
+ from langchain_openai.chat_models.base import BaseChatOpenAI, _convert_message_to_dict
42
+ from pydantic import (
43
+ BaseModel,
44
+ ConfigDict,
45
+ Field,
46
+ PrivateAttr,
47
+ SecretStr,
48
+ create_model,
49
+ model_validator,
50
+ )
51
+ from typing_extensions import Self
52
+
53
+ from ..._utils import (
54
+ _validate_base_url,
55
+ _validate_model_cls_name,
56
+ _validate_provider_name,
57
+ )
58
+ from ..types import (
59
+ CompatibilityOptions,
60
+ ReasoningKeepPolicy,
61
+ ResponseFormatType,
62
+ ToolChoiceType,
63
+ )
64
+ from .register_profiles import (
65
+ _get_profile_by_provider_and_model,
66
+ _register_profile_with_provider,
67
+ )
68
+
69
+ _BM = TypeVar("_BM", bound=BaseModel)
70
+ _DictOrPydanticClass = Union[dict[str, Any], type[_BM], type]
71
+ _DictOrPydantic = Union[dict, _BM]
72
+
73
+
74
+ def _get_last_human_message_index(messages: list[BaseMessage]) -> int:
75
+ """find the index of the last HumanMessage in the messages list, return -1 if not found."""
76
+ return next(
77
+ (
78
+ i
79
+ for i in range(len(messages) - 1, -1, -1)
80
+ if isinstance(messages[i], HumanMessage)
81
+ ),
82
+ -1,
83
+ )
84
+
85
+
86
+ def _transform_video_block(block: dict[str, Any]) -> dict:
87
+ """Transform video block to video_url block."""
88
+ if "url" in block:
89
+ return {
90
+ "type": "video_url",
91
+ "video_url": {
92
+ "url": block["url"],
93
+ },
94
+ }
95
+ if "base64" in block or block.get("source_type") == "base64":
96
+ if "mime_type" not in block:
97
+ error_message = "mime_type key is required for base64 data."
98
+ raise ValueError(error_message)
99
+ mime_type = block["mime_type"]
100
+ base64_data = block["data"] if "data" in block else block["base64"]
101
+ return {
102
+ "type": "video_url",
103
+ "video_url": {
104
+ "url": f"data:{mime_type};base64,{base64_data}",
105
+ },
106
+ }
107
+ error_message = "Unsupported source type. Only 'url' and 'base64' are supported."
108
+ raise ValueError(error_message)
109
+
110
+
111
+ def _process_video_input(message: BaseMessage):
112
+ """
113
+ Process BaseMessage with video input.
114
+
115
+ Args:
116
+ message (BaseMessage): The HumanMessage instance to process.
117
+
118
+ Returns:
119
+ None: The method modifies the message in-place.
120
+ """
121
+ if not message.content:
122
+ return message
123
+ content = message.content
124
+
125
+ if not isinstance(content, list):
126
+ return message
127
+
128
+ formatted_content = []
129
+ for block in content:
130
+ if isinstance(block, dict) and block.get("type") == "video":
131
+ formatted_content.append(_transform_video_block(block))
132
+ else:
133
+ formatted_content.append(block)
134
+ message = message.model_copy(update={"content": formatted_content})
135
+ return message
136
+
137
+
138
+ class _BaseChatOpenAICompatible(BaseChatOpenAI):
139
+ """
140
+ Base template class for OpenAI-compatible chat model implementations.
141
+
142
+ This class provides a foundation for integrating various LLM providers that
143
+ offer OpenAI-compatible APIs. It enhances the base OpenAI functionality by:
144
+
145
+ **1. Supports output of more types of reasoning content (reasoning_content)**
146
+ ChatOpenAI can only output reasoning content natively supported by official
147
+ OpenAI models, while OpenAICompatibleChatModel can output reasoning content
148
+ from other model providers.
149
+
150
+ **2. Dynamically adapts to choose the most suitable structured-output method**
151
+ OpenAICompatibleChatModel selects the best structured-output method (function_calling or json_schema)
152
+ based on the actual capabilities of the model provider.
153
+
154
+ **3. Supports configuration of related parameters**
155
+ For cases where parameters differ from the official OpenAI API, this library
156
+ provides the compatibility_options parameter to address this issue. For
157
+ example, when different model providers have inconsistent support for
158
+ tool_choice, you can adapt by setting supported_tool_choice in
159
+ compatibility_options.
160
+
161
+ Built on top of `langchain-openai`'s `BaseChatOpenAI`, this template class
162
+ extends capabilities to better support diverse OpenAI-compatible model
163
+ providers while maintaining full compatibility with LangChain's chat model
164
+ interface.
165
+
166
+ Note: This is a template class and should not be exported or instantiated
167
+ directly. Instead, use it as a base class and provide the specific provider
168
+ name through inheritance or the factory function
169
+ `create_openai_compatible_model()`.
170
+ """
171
+
172
+ model_name: str = Field(alias="model", default="openai compatible model")
173
+ """The name of the model"""
174
+ api_key: Optional[SecretStr] = Field(
175
+ default_factory=secret_from_env("OPENAI_COMPATIBLE_API_KEY", default=None),
176
+ )
177
+ """OpenAI Compatible API key"""
178
+ api_base: str = Field(
179
+ default_factory=from_env("OPENAI_COMPATIBLE_API_BASE", default=""),
180
+ )
181
+ """OpenAI Compatible API base URL"""
182
+
183
+ model_config = ConfigDict(populate_by_name=True)
184
+
185
+ _provider: str = PrivateAttr(default="openai-compatible")
186
+
187
+ """Provider Compatibility Options"""
188
+ supported_tool_choice: ToolChoiceType = Field(default_factory=list)
189
+ """Supported tool choice"""
190
+ supported_response_format: ResponseFormatType = Field(default_factory=list)
191
+ """Supported response format"""
192
+ reasoning_keep_policy: ReasoningKeepPolicy = Field(default="never")
193
+ """How to keep reasoning content in the messages"""
194
+ include_usage: bool = Field(default=True)
195
+ """Whether to include usage information in the output"""
196
+
197
+ @property
198
+ def _llm_type(self) -> str:
199
+ return f"chat-{self._provider}"
200
+
201
+ @property
202
+ def lc_secrets(self) -> dict[str, str]:
203
+ return {"api_key": f"{self._provider.upper()}_API_KEY"}
204
+
205
+ def _get_ls_params(
206
+ self,
207
+ stop: Optional[list[str]] = None,
208
+ **kwargs: Any,
209
+ ) -> LangSmithParams:
210
+ ls_params = super()._get_ls_params(stop=stop, **kwargs)
211
+ ls_params["ls_provider"] = self._provider
212
+ return ls_params
213
+
214
+ def _get_request_payload(
215
+ self,
216
+ input_: LanguageModelInput,
217
+ *,
218
+ stop: list[str] | None = None,
219
+ **kwargs: Any,
220
+ ) -> dict:
221
+ payload = {**self._default_params, **kwargs}
222
+
223
+ if self._use_responses_api(payload):
224
+ return super()._get_request_payload(input_, stop=stop, **kwargs)
225
+
226
+ messages = self._convert_input(input_).to_messages()
227
+ if stop is not None:
228
+ kwargs["stop"] = stop
229
+
230
+ payload_messages = []
231
+ last_human_index = -1
232
+ if self.reasoning_keep_policy == "current":
233
+ last_human_index = _get_last_human_message_index(messages)
234
+
235
+ for index, m in enumerate(messages):
236
+ if isinstance(m, AIMessage):
237
+ msg_dict = _convert_message_to_dict(
238
+ _convert_from_v1_to_chat_completions(m)
239
+ )
240
+ if self.reasoning_keep_policy == "all" and m.additional_kwargs.get(
241
+ "reasoning_content"
242
+ ):
243
+ msg_dict["reasoning_content"] = m.additional_kwargs.get(
244
+ "reasoning_content"
245
+ )
246
+ elif (
247
+ self.reasoning_keep_policy == "current"
248
+ and index > last_human_index
249
+ and m.additional_kwargs.get("reasoning_content")
250
+ ):
251
+ msg_dict["reasoning_content"] = m.additional_kwargs.get(
252
+ "reasoning_content"
253
+ )
254
+ payload_messages.append(msg_dict)
255
+ else:
256
+ if (
257
+ isinstance(m, HumanMessage) or isinstance(m, ToolMessage)
258
+ ) and isinstance(m.content, list):
259
+ m = _process_video_input(m)
260
+ payload_messages.append(_convert_message_to_dict(m))
261
+
262
+ payload["messages"] = payload_messages
263
+ return payload
264
+
265
+ @model_validator(mode="after")
266
+ def validate_environment(self) -> Self:
267
+ if not (self.api_key and self.api_key.get_secret_value()):
268
+ msg = f"{self._provider.upper()}_API_KEY must be set."
269
+ raise ValueError(msg)
270
+ client_params: dict = {
271
+ k: v
272
+ for k, v in {
273
+ "api_key": self.api_key.get_secret_value() if self.api_key else None,
274
+ "base_url": self.api_base,
275
+ "timeout": self.request_timeout,
276
+ "max_retries": self.max_retries,
277
+ "default_headers": self.default_headers,
278
+ "default_query": self.default_query,
279
+ }.items()
280
+ if v is not None
281
+ }
282
+
283
+ if not (self.client or None):
284
+ sync_specific: dict = {"http_client": self.http_client}
285
+ self.root_client = openai.OpenAI(**client_params, **sync_specific)
286
+ self.client = self.root_client.chat.completions
287
+ if not (self.async_client or None):
288
+ async_specific: dict = {"http_client": self.http_async_client}
289
+ self.root_async_client = openai.AsyncOpenAI(
290
+ **client_params,
291
+ **async_specific,
292
+ )
293
+ self.async_client = self.root_async_client.chat.completions
294
+ return self
295
+
296
+ @model_validator(mode="after")
297
+ def _set_model_profile(self) -> Self:
298
+ """Set model profile if not overridden."""
299
+ if self.profile is None:
300
+ self.profile = cast(
301
+ ModelProfile,
302
+ _get_profile_by_provider_and_model(self._provider, self.model_name),
303
+ )
304
+ return self
305
+
306
+ def _create_chat_result(
307
+ self,
308
+ response: Union[dict, openai.BaseModel],
309
+ generation_info: Optional[dict] = None,
310
+ ) -> ChatResult:
311
+ """Convert API response to LangChain ChatResult with enhanced content processing.
312
+
313
+ Extends base implementation to capture and preserve reasoning content from
314
+ model responses, supporting advanced models that provide reasoning chains
315
+ or thought processes alongside regular responses.
316
+
317
+ Handles multiple response formats:
318
+ - Standard OpenAI response objects with `reasoning_content` attribute
319
+ - Responses with `model_extra` containing reasoning data
320
+ - Dictionary responses (pass-through to base implementation)
321
+
322
+ Args:
323
+ response: Raw API response (OpenAI object or dict)
324
+ generation_info: Additional generation metadata
325
+
326
+ Returns:
327
+ ChatResult with enhanced message containing reasoning content when available
328
+ """
329
+ rtn = super()._create_chat_result(response, generation_info)
330
+
331
+ if not isinstance(response, openai.BaseModel):
332
+ return rtn
333
+
334
+ for generation in rtn.generations:
335
+ if generation.message.response_metadata is None:
336
+ generation.message.response_metadata = {}
337
+ generation.message.response_metadata["model_provider"] = "openai-compatible"
338
+
339
+ choices = getattr(response, "choices", None)
340
+ if choices and hasattr(choices[0].message, "reasoning_content"):
341
+ rtn.generations[0].message.additional_kwargs["reasoning_content"] = choices[
342
+ 0
343
+ ].message.reasoning_content
344
+ elif choices and hasattr(choices[0].message, "model_extra"):
345
+ model_extra = choices[0].message.model_extra
346
+ if isinstance(model_extra, dict) and (
347
+ reasoning := model_extra.get("reasoning")
348
+ ):
349
+ rtn.generations[0].message.additional_kwargs["reasoning_content"] = (
350
+ reasoning
351
+ )
352
+
353
+ return rtn
354
+
355
+ def _convert_chunk_to_generation_chunk(
356
+ self,
357
+ chunk: dict,
358
+ default_chunk_class: type,
359
+ base_generation_info: Optional[dict],
360
+ ) -> Optional[ChatGenerationChunk]:
361
+ """Convert streaming chunk to generation chunk with reasoning content support.
362
+
363
+ Processes streaming response chunks to extract reasoning content alongside
364
+ regular message content, enabling real-time streaming of both response
365
+ text and reasoning chains from compatible models.
366
+
367
+ Args:
368
+ chunk: Raw streaming chunk from API
369
+ default_chunk_class: Expected chunk type for validation
370
+ base_generation_info: Base metadata for the generation
371
+
372
+ Returns:
373
+ ChatGenerationChunk with reasoning content when present in chunk data
374
+ """
375
+ generation_chunk = super()._convert_chunk_to_generation_chunk(
376
+ chunk,
377
+ default_chunk_class,
378
+ base_generation_info,
379
+ )
380
+ if (choices := chunk.get("choices")) and generation_chunk:
381
+ top = choices[0]
382
+ if isinstance(generation_chunk.message, AIMessageChunk):
383
+ generation_chunk.message.response_metadata = {
384
+ **generation_chunk.message.response_metadata,
385
+ "model_provider": "openai-compatible",
386
+ }
387
+ if (
388
+ reasoning_content := top.get("delta", {}).get("reasoning_content")
389
+ ) is not None:
390
+ generation_chunk.message.additional_kwargs["reasoning_content"] = (
391
+ reasoning_content
392
+ )
393
+ elif (reasoning := top.get("delta", {}).get("reasoning")) is not None:
394
+ generation_chunk.message.additional_kwargs["reasoning_content"] = (
395
+ reasoning
396
+ )
397
+
398
+ return generation_chunk
399
+
400
+ def _stream(
401
+ self,
402
+ messages: List[BaseMessage],
403
+ stop: Optional[List[str]] = None,
404
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
405
+ **kwargs: Any,
406
+ ) -> Iterator[ChatGenerationChunk]:
407
+ if self._use_responses_api({**kwargs, **self.model_kwargs}):
408
+ for chunk in super()._stream_responses(
409
+ messages, stop=stop, run_manager=run_manager, **kwargs
410
+ ):
411
+ yield chunk
412
+ else:
413
+ if self.include_usage:
414
+ kwargs["stream_options"] = {"include_usage": True}
415
+ try:
416
+ for chunk in super()._stream(
417
+ messages, stop=stop, run_manager=run_manager, **kwargs
418
+ ):
419
+ yield chunk
420
+ except JSONDecodeError as e:
421
+ raise JSONDecodeError(
422
+ f"{self._provider.title()} API returned an invalid response. "
423
+ "Please check the API status and try again.",
424
+ e.doc,
425
+ e.pos,
426
+ ) from e
427
+
428
+ async def _astream(
429
+ self,
430
+ messages: List[BaseMessage],
431
+ stop: Optional[List[str]] = None,
432
+ run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
433
+ **kwargs: Any,
434
+ ) -> AsyncIterator[ChatGenerationChunk]:
435
+ if self._use_responses_api({**kwargs, **self.model_kwargs}):
436
+ async for chunk in super()._astream_responses(
437
+ messages, stop=stop, run_manager=run_manager, **kwargs
438
+ ):
439
+ yield chunk
440
+ else:
441
+ if self.include_usage:
442
+ kwargs["stream_options"] = {"include_usage": True}
443
+ try:
444
+ async for chunk in super()._astream(
445
+ messages, stop=stop, run_manager=run_manager, **kwargs
446
+ ):
447
+ yield chunk
448
+ except JSONDecodeError as e:
449
+ raise JSONDecodeError(
450
+ f"{self._provider.title()} API returned an invalid response. "
451
+ "Please check the API status and try again.",
452
+ e.doc,
453
+ e.pos,
454
+ ) from e
455
+
456
+ def _generate(
457
+ self,
458
+ messages: List[BaseMessage],
459
+ stop: Optional[List[str]] = None,
460
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
461
+ **kwargs: Any,
462
+ ) -> ChatResult:
463
+ try:
464
+ return super()._generate(
465
+ messages, stop=stop, run_manager=run_manager, **kwargs
466
+ )
467
+ except JSONDecodeError as e:
468
+ raise JSONDecodeError(
469
+ f"{self._provider.title()} API returned an invalid response. "
470
+ "Please check the API status and try again.",
471
+ e.doc,
472
+ e.pos,
473
+ ) from e
474
+
475
+ async def _agenerate(
476
+ self,
477
+ messages: List[BaseMessage],
478
+ stop: Optional[List[str]] = None,
479
+ run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
480
+ **kwargs: Any,
481
+ ) -> ChatResult:
482
+ try:
483
+ return await super()._agenerate(
484
+ messages, stop=stop, run_manager=run_manager, **kwargs
485
+ )
486
+ except JSONDecodeError as e:
487
+ raise JSONDecodeError(
488
+ f"{self._provider.title()} API returned an invalid response. "
489
+ "Please check the API status and try again.",
490
+ e.doc,
491
+ e.pos,
492
+ ) from e
493
+
494
+ def bind_tools(
495
+ self,
496
+ tools: Sequence[dict[str, Any] | type | Callable | BaseTool],
497
+ *,
498
+ tool_choice: dict | str | bool | None = None,
499
+ strict: bool | None = None,
500
+ parallel_tool_calls: bool | None = None,
501
+ **kwargs: Any,
502
+ ) -> Runnable[LanguageModelInput, AIMessage]:
503
+ if parallel_tool_calls is not None:
504
+ kwargs["parallel_tool_calls"] = parallel_tool_calls
505
+ formatted_tools = [
506
+ convert_to_openai_tool(tool, strict=strict) for tool in tools
507
+ ]
508
+
509
+ tool_names = []
510
+ for tool in formatted_tools:
511
+ if "function" in tool:
512
+ tool_names.append(tool["function"]["name"])
513
+ elif "name" in tool:
514
+ tool_names.append(tool["name"])
515
+ else:
516
+ pass
517
+
518
+ support_tool_choice = False
519
+ if tool_choice is not None:
520
+ if isinstance(tool_choice, bool):
521
+ tool_choice = "required"
522
+ if isinstance(tool_choice, str):
523
+ if (
524
+ tool_choice in ["auto", "none", "required"]
525
+ and tool_choice in self.supported_tool_choice
526
+ ):
527
+ support_tool_choice = True
528
+
529
+ elif "specific" in self.supported_tool_choice:
530
+ if tool_choice in tool_names:
531
+ support_tool_choice = True
532
+ tool_choice = {
533
+ "type": "function",
534
+ "function": {"name": tool_choice},
535
+ }
536
+ tool_choice = tool_choice if support_tool_choice else None
537
+ if tool_choice:
538
+ kwargs["tool_choice"] = tool_choice
539
+ return super().bind(tools=formatted_tools, **kwargs)
540
+
541
+ def with_structured_output(
542
+ self,
543
+ schema: Optional[_DictOrPydanticClass] = None,
544
+ *,
545
+ method: Literal[
546
+ "function_calling",
547
+ "json_mode",
548
+ "json_schema",
549
+ ] = "json_schema",
550
+ include_raw: bool = False,
551
+ strict: Optional[bool] = None,
552
+ **kwargs: Any,
553
+ ) -> Runnable[LanguageModelInput, _DictOrPydantic]:
554
+ """Configure structured output extraction with provider compatibility handling.
555
+
556
+ Enables parsing of model outputs into structured formats (Pydantic models
557
+ or dictionaries) while handling provider-specific method compatibility.
558
+ Falls back from json_schema to function_calling for providers that don't
559
+ support the json_schema method.
560
+
561
+ Args:
562
+ schema: Output schema (Pydantic model class or dictionary definition)
563
+ method: Extraction method - defaults to json_schema, it the provider doesn't support json_schema, it will fallback to function_calling
564
+ include_raw: Whether to include raw model response alongside parsed output
565
+ strict: Schema enforcement strictness (provider-dependent)
566
+ **kwargs: Additional structured output parameters
567
+
568
+ Returns:
569
+ Runnable configured for structured output extraction
570
+ """
571
+ if method not in ["function_calling", "json_mode", "json_schema"]:
572
+ raise ValueError(
573
+ f"Unsupported method: {method}. Please choose from 'function_calling', 'json_mode', 'json_schema'."
574
+ )
575
+ if (
576
+ method == "json_schema"
577
+ and "json_schema" not in self.supported_response_format
578
+ ):
579
+ method = "function_calling"
580
+ elif (
581
+ method == "json_mode" and "json_mode" not in self.supported_response_format
582
+ ):
583
+ method = "function_calling"
584
+
585
+ return super().with_structured_output(
586
+ schema,
587
+ method=method,
588
+ include_raw=include_raw,
589
+ strict=strict,
590
+ **kwargs,
591
+ )
592
+
593
+
594
+ def _validate_compatibility_options(
595
+ compatibility_options: Optional[CompatibilityOptions] = None,
596
+ ) -> None:
597
+ """Validate provider configuration against supported features.
598
+
599
+ Args:
600
+ compatibility_options: Optional configuration for the provider
601
+
602
+ Raises:
603
+ ValueError: If provider configuration is invalid
604
+ """
605
+ if compatibility_options is None:
606
+ compatibility_options = {}
607
+
608
+ if "supported_tool_choice" in compatibility_options:
609
+ _supported_tool_choice = compatibility_options["supported_tool_choice"]
610
+ for tool_choice in _supported_tool_choice:
611
+ if tool_choice not in ["auto", "none", "required", "specific"]:
612
+ raise ValueError(
613
+ f"Unsupported tool_choice: {tool_choice}. Please choose from 'auto', 'none', 'required','specific'."
614
+ )
615
+
616
+ if "supported_response_format" in compatibility_options:
617
+ _supported_response_format = compatibility_options["supported_response_format"]
618
+ for response_format in _supported_response_format:
619
+ if response_format not in ["json_schema", "json_mode"]:
620
+ raise ValueError(
621
+ f"Unsupported response_format: {response_format}. Please choose from 'json_schema', 'json_mode'."
622
+ )
623
+
624
+ if "reasoning_keep_policy" in compatibility_options:
625
+ _reasoning_keep_policy = compatibility_options["reasoning_keep_policy"]
626
+ if _reasoning_keep_policy not in ["never", "current", "all"]:
627
+ raise ValueError(
628
+ f"Unsupported reasoning_keep_policy: {_reasoning_keep_policy}. Please choose from 'never', 'current', 'all'."
629
+ )
630
+
631
+ if "include_usage" in compatibility_options:
632
+ _include_usage = compatibility_options["include_usage"]
633
+ if not isinstance(_include_usage, bool):
634
+ raise ValueError(
635
+ f"include_usage must be a boolean value. Received: {_include_usage}"
636
+ )
637
+
638
+
639
+ def _create_openai_compatible_model(
640
+ provider: str,
641
+ base_url: str,
642
+ compatibility_options: Optional[CompatibilityOptions] = None,
643
+ profiles: Optional[dict[str, dict[str, Any]]] = None,
644
+ chat_model_cls_name: Optional[str] = None,
645
+ ) -> Type[_BaseChatOpenAICompatible]:
646
+ """Factory function for creating provider-specific OpenAI-compatible model classes.
647
+
648
+ Dynamically generates model classes for different OpenAI-compatible providers,
649
+ configuring environment variable mappings and default base URLs specific to each provider.
650
+
651
+ Args:
652
+ provider: Provider identifier (e.g.`vllm`)
653
+ base_url: Default API base URL for the provider
654
+ compatibility_options: Optional configuration for the provider
655
+ profiles: Optional profiles for the provider
656
+ chat_model_cls_name: Optional name for the model class
657
+
658
+ Returns:
659
+ Configured model class ready for instantiation with provider-specific settings
660
+ """
661
+ chat_model_cls_name = chat_model_cls_name or f"Chat{provider.title()}"
662
+ if compatibility_options is None:
663
+ compatibility_options = {}
664
+
665
+ if profiles is not None:
666
+ _register_profile_with_provider(provider, profiles)
667
+
668
+ _validate_compatibility_options(compatibility_options)
669
+
670
+ _validate_provider_name(provider)
671
+
672
+ _validate_model_cls_name(chat_model_cls_name)
673
+
674
+ _validate_base_url(base_url)
675
+
676
+ return create_model(
677
+ chat_model_cls_name,
678
+ __base__=_BaseChatOpenAICompatible,
679
+ api_base=(
680
+ str,
681
+ Field(
682
+ default_factory=from_env(
683
+ f"{provider.upper()}_API_BASE", default=base_url
684
+ ),
685
+ ),
686
+ ),
687
+ api_key=(
688
+ str,
689
+ Field(
690
+ default_factory=secret_from_env(
691
+ f"{provider.upper()}_API_KEY", default=None
692
+ ),
693
+ ),
694
+ ),
695
+ _provider=(
696
+ str,
697
+ PrivateAttr(default=provider),
698
+ ),
699
+ supported_tool_choice=(
700
+ ToolChoiceType,
701
+ Field(default=compatibility_options.get("supported_tool_choice", ["auto"])),
702
+ ),
703
+ reasoning_keep_policy=(
704
+ ReasoningKeepPolicy,
705
+ Field(default=compatibility_options.get("reasoning_keep_policy", "never")),
706
+ ),
707
+ supported_response_format=(
708
+ ResponseFormatType,
709
+ Field(default=compatibility_options.get("supported_response_format", [])),
710
+ ),
711
+ include_usage=(
712
+ bool,
713
+ Field(default=compatibility_options.get("include_usage", True)),
714
+ ),
715
+ )