llama-index-llms-openai 0.4.2__tar.gz → 0.4.4__tar.gz

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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llama-index-llms-openai
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: llama-index llms openai integration
5
5
  Author: llama-index
6
6
  License-Expression: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: <4.0,>=3.9
9
- Requires-Dist: llama-index-core<0.13,>=0.12.37
9
+ Requires-Dist: llama-index-core<0.13,>=0.12.41
10
10
  Requires-Dist: openai<2,>=1.81.0
11
11
  Description-Content-Type: text/markdown
12
12
 
@@ -77,6 +77,7 @@ from llama_index.llms.openai.utils import (
77
77
  resolve_tool_choice,
78
78
  to_openai_message_dicts,
79
79
  update_tool_calls,
80
+ is_json_schema_supported,
80
81
  )
81
82
  from openai import AsyncOpenAI, AzureOpenAI, AsyncAzureOpenAI
82
83
  from openai import OpenAI as SyncOpenAI
@@ -1000,6 +1001,23 @@ class OpenAI(FunctionCallingLLM):
1000
1001
 
1001
1002
  return tool_selections
1002
1003
 
1004
+ def _prepare_schema(
1005
+ self, llm_kwargs: Optional[Dict[str, Any]], output_cls: Type[Model]
1006
+ ) -> Dict[str, Any]:
1007
+ from openai.resources.beta.chat.completions import _type_to_response_format
1008
+
1009
+ llm_kwargs = llm_kwargs or {}
1010
+ llm_kwargs["response_format"] = _type_to_response_format(output_cls)
1011
+ if "tool_choice" in llm_kwargs:
1012
+ del llm_kwargs["tool_choice"]
1013
+ return llm_kwargs
1014
+
1015
+ def _should_use_structure_outputs(self):
1016
+ return (
1017
+ self.pydantic_program_mode == PydanticProgramMode.DEFAULT
1018
+ and is_json_schema_supported(self.model)
1019
+ )
1020
+
1003
1021
  @dispatcher.span
1004
1022
  def structured_predict(
1005
1023
  self,
@@ -1011,11 +1029,17 @@ class OpenAI(FunctionCallingLLM):
1011
1029
  """Structured predict."""
1012
1030
  llm_kwargs = llm_kwargs or {}
1013
1031
 
1032
+ if self._should_use_structure_outputs():
1033
+ messages = self._extend_messages(prompt.format_messages(**prompt_args))
1034
+ llm_kwargs = self._prepare_schema(llm_kwargs, output_cls)
1035
+ response = self.chat(messages, **llm_kwargs)
1036
+ return output_cls.model_validate_json(str(response.message.content))
1037
+
1038
+ # when uses function calling to extract structured outputs
1039
+ # here we force tool_choice to be required
1014
1040
  llm_kwargs["tool_choice"] = (
1015
1041
  "required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
1016
1042
  )
1017
- # by default structured prediction uses function calling to extract structured outputs
1018
- # here we force tool_choice to be required
1019
1043
  return super().structured_predict(
1020
1044
  output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
1021
1045
  )
@@ -1031,15 +1055,91 @@ class OpenAI(FunctionCallingLLM):
1031
1055
  """Structured predict."""
1032
1056
  llm_kwargs = llm_kwargs or {}
1033
1057
 
1058
+ if self._should_use_structure_outputs():
1059
+ messages = self._extend_messages(prompt.format_messages(**prompt_args))
1060
+ llm_kwargs = self._prepare_schema(llm_kwargs, output_cls)
1061
+ response = await self.achat(messages, **llm_kwargs)
1062
+ return output_cls.model_validate_json(str(response.message.content))
1063
+
1064
+ # when uses function calling to extract structured outputs
1065
+ # here we force tool_choice to be required
1034
1066
  llm_kwargs["tool_choice"] = (
1035
1067
  "required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
1036
1068
  )
1037
- # by default structured prediction uses function calling to extract structured outputs
1038
- # here we force tool_choice to be required
1039
1069
  return await super().astructured_predict(
1040
1070
  output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
1041
1071
  )
1042
1072
 
1073
+ def _structured_stream_call(
1074
+ self,
1075
+ output_cls: Type[Model],
1076
+ prompt: PromptTemplate,
1077
+ llm_kwargs: Optional[Dict[str, Any]] = None,
1078
+ **prompt_args: Any,
1079
+ ) -> Generator[
1080
+ Union[Model, List[Model], "FlexibleModel", List["FlexibleModel"]], None, None
1081
+ ]:
1082
+ if self._should_use_structure_outputs():
1083
+ from llama_index.core.program.streaming_utils import (
1084
+ process_streaming_content_incremental,
1085
+ )
1086
+
1087
+ messages = self._extend_messages(prompt.format_messages(**prompt_args))
1088
+ llm_kwargs = self._prepare_schema(llm_kwargs, output_cls)
1089
+ curr = None
1090
+ for response in self.stream_chat(messages, **llm_kwargs):
1091
+ curr = process_streaming_content_incremental(response, output_cls, curr)
1092
+ yield curr
1093
+ else:
1094
+ llm_kwargs["tool_choice"] = (
1095
+ "required"
1096
+ if "tool_choice" not in llm_kwargs
1097
+ else llm_kwargs["tool_choice"]
1098
+ )
1099
+ yield from super()._structured_stream_call(
1100
+ output_cls, prompt, llm_kwargs, **prompt_args
1101
+ )
1102
+
1103
+ async def _structured_astream_call(
1104
+ self,
1105
+ output_cls: Type[Model],
1106
+ prompt: PromptTemplate,
1107
+ llm_kwargs: Optional[Dict[str, Any]] = None,
1108
+ **prompt_args: Any,
1109
+ ) -> AsyncGenerator[
1110
+ Union[Model, List[Model], "FlexibleModel", List["FlexibleModel"]], None
1111
+ ]:
1112
+ if self._should_use_structure_outputs():
1113
+
1114
+ async def gen(
1115
+ llm_kwargs=llm_kwargs,
1116
+ ) -> AsyncGenerator[
1117
+ Union[Model, List[Model], FlexibleModel, List[FlexibleModel]], None
1118
+ ]:
1119
+ from llama_index.core.program.streaming_utils import (
1120
+ process_streaming_content_incremental,
1121
+ )
1122
+
1123
+ messages = self._extend_messages(prompt.format_messages(**prompt_args))
1124
+ llm_kwargs = self._prepare_schema(llm_kwargs, output_cls)
1125
+ curr = None
1126
+ async for response in await self.astream_chat(messages, **llm_kwargs):
1127
+ curr = process_streaming_content_incremental(
1128
+ response, output_cls, curr
1129
+ )
1130
+ yield curr
1131
+
1132
+ return gen()
1133
+ else:
1134
+ llm_kwargs["tool_choice"] = (
1135
+ "required"
1136
+ if "tool_choice" not in llm_kwargs
1137
+ else llm_kwargs["tool_choice"]
1138
+ )
1139
+ return await super()._structured_astream_call(
1140
+ output_cls, prompt, llm_kwargs, **prompt_args
1141
+ )
1142
+
1043
1143
  @dispatcher.span
1044
1144
  def stream_structured_predict(
1045
1145
  self,
@@ -1051,11 +1151,6 @@ class OpenAI(FunctionCallingLLM):
1051
1151
  """Stream structured predict."""
1052
1152
  llm_kwargs = llm_kwargs or {}
1053
1153
 
1054
- llm_kwargs["tool_choice"] = (
1055
- "required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
1056
- )
1057
- # by default structured prediction uses function calling to extract structured outputs
1058
- # here we force tool_choice to be required
1059
1154
  return super().stream_structured_predict(
1060
1155
  output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
1061
1156
  )
@@ -1070,12 +1165,6 @@ class OpenAI(FunctionCallingLLM):
1070
1165
  ) -> AsyncGenerator[Union[Model, FlexibleModel], None]:
1071
1166
  """Stream structured predict."""
1072
1167
  llm_kwargs = llm_kwargs or {}
1073
-
1074
- llm_kwargs["tool_choice"] = (
1075
- "required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
1076
- )
1077
- # by default structured prediction uses function calling to extract structured outputs
1078
- # here we force tool_choice to be required
1079
1168
  return await super().astream_structured_predict(
1080
1169
  output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
1081
1170
  )
@@ -831,7 +831,10 @@ class OpenAIResponses(FunctionCallingLLM):
831
831
 
832
832
  # openai responses api has a slightly different tool spec format
833
833
  tool_specs = [
834
- {"type": "function", **tool.metadata.to_openai_tool()["function"]}
834
+ {
835
+ "type": "function",
836
+ **tool.metadata.to_openai_tool(skip_length_check=True)["function"],
837
+ }
835
838
  for tool in tools
836
839
  ]
837
840
 
@@ -180,6 +180,32 @@ DISCONTINUED_MODELS = {
180
180
  "code-cushman-001": 2048,
181
181
  }
182
182
 
183
+ JSON_SCHEMA_MODELS = [
184
+ "o4-mini",
185
+ "o1",
186
+ "o1-pro",
187
+ "o3",
188
+ "o3-mini",
189
+ "gpt-4.1",
190
+ "gpt-4o",
191
+ "gpt-4.1",
192
+ ]
193
+
194
+
195
+ def is_json_schema_supported(model: str) -> bool:
196
+ try:
197
+ from openai.resources.beta.chat import completions
198
+
199
+ if not hasattr(completions, "_type_to_response_format"):
200
+ return False
201
+
202
+ return not model.startswith("o1-mini") and any(
203
+ model.startswith(m) for m in JSON_SCHEMA_MODELS
204
+ )
205
+ except ImportError:
206
+ return False
207
+
208
+
183
209
  MISSING_API_KEY_ERROR_MESSAGE = """No API key found for OpenAI.
184
210
  Please set either the OPENAI_API_KEY environment variable or \
185
211
  openai.api_key prior to initialization.
@@ -27,13 +27,13 @@ dev = [
27
27
 
28
28
  [project]
29
29
  name = "llama-index-llms-openai"
30
- version = "0.4.2"
30
+ version = "0.4.4"
31
31
  description = "llama-index llms openai integration"
32
32
  authors = [{name = "llama-index"}]
33
33
  requires-python = ">=3.9,<4.0"
34
34
  readme = "README.md"
35
35
  license = "MIT"
36
- dependencies = ["openai>=1.81.0,<2", "llama-index-core>=0.12.37,<0.13"]
36
+ dependencies = ["openai>=1.81.0,<2", "llama-index-core>=0.12.41,<0.13"]
37
37
 
38
38
  [tool.codespell]
39
39
  check-filenames = true