11import json
2- from typing import Any , Dict , List
2+ from typing import Any , Dict , List , Optional
33
44from ldai import LDMessage , log
55from ldai .providers .model_runner import ModelRunner
6- from ldai .providers .types import LDAIMetrics , ModelResponse , StructuredResponse
6+ from ldai .providers .types import LDAIMetrics , ModelResponse , RunnerResult , StructuredResponse
77from openai import AsyncOpenAI
88
99from ldai_openai .openai_helper import (
1414
1515class OpenAIModelRunner (ModelRunner ):
1616 """
17- ModelRunner implementation for OpenAI.
17+ Runner implementation for OpenAI chat completions .
1818
1919 Holds a fully-configured AsyncOpenAI client, model name, and parameters.
20- Returned by OpenAIConnector.create_model(config).
20+ Returned by ``OpenAIRunnerFactory.create_model(config)``.
21+
22+ Implements the unified :class:`~ldai.providers.runner.Runner` protocol via
23+ :meth:`run`. The legacy :meth:`invoke_model` and :meth:`invoke_structured_model`
24+ methods are preserved for backward compatibility with the managed layer until
25+ its migration to the unified protocol is complete.
2126 """
2227
2328 def __init__ (
@@ -30,13 +35,38 @@ def __init__(
3035 self ._model_name = model_name
3136 self ._parameters = parameters
3237
33- async def invoke_model (self , messages : List [LDMessage ]) -> ModelResponse :
38+ async def run (
39+ self ,
40+ input : Any ,
41+ output_type : Optional [Dict [str , Any ]] = None ,
42+ ) -> RunnerResult :
3443 """
35- Invoke the OpenAI model with an array of messages.
36-
37- :param messages: Array of LDMessage objects representing the conversation
38- :return: ModelResponse containing the model's response and metrics
44+ Run the OpenAI model with the given input.
45+
46+ :param input: A string prompt or a list of :class:`LDMessage` objects
47+ :param output_type: Optional JSON schema dict requesting structured output.
48+ When provided, ``parsed`` on the returned :class:`RunnerResult` is
49+ populated with the parsed JSON document.
50+ :return: :class:`RunnerResult` containing ``content``, ``metrics``,
51+ ``raw`` and (when ``output_type`` is set) ``parsed``.
3952 """
53+ messages = self ._coerce_input (input )
54+
55+ if output_type is not None :
56+ return await self ._run_structured (messages , output_type )
57+ return await self ._run_completion (messages )
58+
59+ @staticmethod
60+ def _coerce_input (input : Any ) -> List [LDMessage ]:
61+ if isinstance (input , str ):
62+ return [LDMessage (role = 'user' , content = input )]
63+ if isinstance (input , list ):
64+ return input
65+ raise TypeError (
66+ f"Unsupported input type for OpenAIModelRunner.run: { type (input ).__name__ } "
67+ )
68+
69+ async def _run_completion (self , messages : List [LDMessage ]) -> RunnerResult :
4070 try :
4171 response = await self ._client .chat .completions .create (
4272 model = self ._model_name ,
@@ -45,40 +75,29 @@ async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse:
4575 )
4676
4777 metrics = get_ai_metrics_from_response (response )
48-
49- content = ''
50- if response .choices and len (response .choices ) > 0 :
51- message = response .choices [0 ].message
52- if message and message .content :
53- content = message .content
78+ content = self ._extract_content (response )
5479
5580 if not content :
5681 log .warning ('OpenAI response has no content available' )
57- metrics = LDAIMetrics (success = False , usage = metrics .usage )
82+ return RunnerResult (
83+ content = '' ,
84+ metrics = LDAIMetrics (success = False , usage = metrics .usage ),
85+ raw = response ,
86+ )
5887
59- return ModelResponse (
60- message = LDMessage (role = 'assistant' , content = content ),
61- metrics = metrics ,
62- )
88+ return RunnerResult (content = content , metrics = metrics , raw = response )
6389 except Exception as error :
6490 log .warning (f'OpenAI model invocation failed: { error } ' )
65- return ModelResponse (
66- message = LDMessage ( role = 'assistant' , content = '' ) ,
91+ return RunnerResult (
92+ content = '' ,
6793 metrics = LDAIMetrics (success = False , usage = None ),
6894 )
6995
70- async def invoke_structured_model (
96+ async def _run_structured (
7197 self ,
7298 messages : List [LDMessage ],
73- response_structure : Dict [str , Any ],
74- ) -> StructuredResponse :
75- """
76- Invoke the OpenAI model with structured output support.
77-
78- :param messages: Array of LDMessage objects representing the conversation
79- :param response_structure: Dictionary defining the JSON schema for output structure
80- :return: StructuredResponse containing the structured data
81- """
99+ output_type : Dict [str , Any ],
100+ ) -> RunnerResult :
82101 try :
83102 response = await self ._client .chat .completions .create (
84103 model = self ._model_name ,
@@ -87,43 +106,85 @@ async def invoke_structured_model(
87106 'type' : 'json_schema' ,
88107 'json_schema' : {
89108 'name' : 'structured_output' ,
90- 'schema' : response_structure ,
109+ 'schema' : output_type ,
91110 'strict' : True ,
92111 },
93112 },
94113 ** self ._parameters ,
95114 )
96115
97116 metrics = get_ai_metrics_from_response (response )
98-
99- content = ''
100- if response .choices and len (response .choices ) > 0 :
101- message = response .choices [0 ].message
102- if message and message .content :
103- content = message .content
117+ content = self ._extract_content (response )
104118
105119 if not content :
106120 log .warning ('OpenAI structured response has no content available' )
107- return StructuredResponse (
108- data = {},
109- raw_response = '' ,
121+ return RunnerResult (
122+ content = '' ,
110123 metrics = LDAIMetrics (success = False , usage = metrics .usage ),
124+ raw = response ,
111125 )
112126
113127 try :
114- data = json .loads (content )
115- return StructuredResponse (data = data , raw_response = content , metrics = metrics )
128+ parsed = json .loads (content )
129+ return RunnerResult (
130+ content = content ,
131+ metrics = metrics ,
132+ raw = response ,
133+ parsed = parsed ,
134+ )
116135 except json .JSONDecodeError as parse_error :
117136 log .warning (f'OpenAI structured response contains invalid JSON: { parse_error } ' )
118- return StructuredResponse (
119- data = {},
120- raw_response = content ,
137+ return RunnerResult (
138+ content = content ,
121139 metrics = LDAIMetrics (success = False , usage = metrics .usage ),
140+ raw = response ,
122141 )
123142 except Exception as error :
124143 log .warning (f'OpenAI structured model invocation failed: { error } ' )
125- return StructuredResponse (
126- data = {},
127- raw_response = '' ,
144+ return RunnerResult (
145+ content = '' ,
128146 metrics = LDAIMetrics (success = False , usage = None ),
129147 )
148+
149+ @staticmethod
150+ def _extract_content (response : Any ) -> str :
151+ if response .choices and len (response .choices ) > 0 :
152+ message = response .choices [0 ].message
153+ if message and message .content :
154+ return message .content
155+ return ''
156+
157+ async def invoke_model (self , messages : List [LDMessage ]) -> ModelResponse :
158+ """
159+ Invoke the OpenAI model with an array of messages.
160+
161+ .. deprecated::
162+ Use :meth:`run` instead. This method delegates to :meth:`run` and
163+ adapts the result to the legacy :class:`ModelResponse` shape so
164+ existing callers in the managed layer continue to function.
165+ """
166+ result = await self ._run_completion (messages )
167+ return ModelResponse (
168+ message = LDMessage (role = 'assistant' , content = result .content ),
169+ metrics = result .metrics ,
170+ )
171+
172+ async def invoke_structured_model (
173+ self ,
174+ messages : List [LDMessage ],
175+ response_structure : Dict [str , Any ],
176+ ) -> StructuredResponse :
177+ """
178+ Invoke the OpenAI model with structured output support.
179+
180+ .. deprecated::
181+ Use :meth:`run` with the ``output_type`` argument instead. This
182+ method delegates to :meth:`run` and adapts the result to the
183+ legacy :class:`StructuredResponse` shape.
184+ """
185+ result = await self ._run_structured (messages , response_structure )
186+ return StructuredResponse (
187+ data = result .parsed or {},
188+ raw_response = result .content ,
189+ metrics = result .metrics ,
190+ )
0 commit comments