33namespace MoeMizrak \LaravelOpenrouter ;
44
55use GuzzleHttp \Exception \GuzzleException ;
6+ use GuzzleHttp \Promise \PromiseInterface ;
67use MoeMizrak \LaravelOpenrouter \DTO \ChatData ;
78use MoeMizrak \LaravelOpenrouter \DTO \CostResponseData ;
9+ use MoeMizrak \LaravelOpenrouter \DTO \ErrorData ;
810use MoeMizrak \LaravelOpenrouter \DTO \LimitResponseData ;
911use MoeMizrak \LaravelOpenrouter \DTO \ResponseData ;
1012use Psr \Http \Message \ResponseInterface ;
@@ -27,16 +29,24 @@ class OpenRouterRequest extends OpenRouterAPI
2729 * Sends a model request for the given chat conversation.
2830 *
2931 * @param ChatData $chatData
30- * @return ResponseData
3132 *
33+ * @return ErrorData|ResponseData
3234 * @throws GuzzleException
3335 * @throws UnknownProperties
3436 */
35- public function chatRequest (ChatData $ chatData ): ResponseData
37+ public function chatRequest (ChatData $ chatData ): ErrorData | ResponseData
3638 {
3739 // The path for the chat completion request.
3840 $ chatCompletionPath = 'chat/completions ' ;
3941
42+ // Detect if stream chat completion is requested, and return ErrorData stating that chatStreamRequest needs to be used instead.
43+ if ($ chatData ->stream ) {
44+ return new ErrorData ([
45+ 'code ' => 400 ,
46+ 'message ' => 'For stream chat completion please use "chatStreamRequest" method instead! ' ,
47+ ]);
48+ }
49+
4050 // Filter null values from the chatData object and return array.
4151 $ chatData = $ this ->filterNullValuesRecursive ($ chatData );
4252
@@ -52,15 +62,101 @@ public function chatRequest(ChatData $chatData): ResponseData
5262 $ options
5363 );
5464
65+ // Decode the json response
66+ $ response = $ this ->jsonDecode ($ response );
67+
5568 return $ this ->formChatResponse ($ response );
5669 }
5770
71+ /**
72+ * Sends a streaming request for the given chat conversation.
73+ *
74+ * @param ChatData $chatData
75+ * @param int $readByte - Default 4096 byte (4kB) for performance.
76+ *
77+ * @return PromiseInterface
78+ */
79+ public function chatStreamRequest (ChatData $ chatData , int $ readByte = 4096 ): PromiseInterface
80+ {
81+ // The path for the chat completion request.
82+ $ chatCompletionPath = 'chat/completions ' ;
83+
84+ $ chatData ->stream = true ;
85+
86+ // Filter null values from the chatData object and return array.
87+ $ chatData = $ this ->filterNullValuesRecursive ($ chatData );
88+
89+ // Add headers for streaming.
90+ $ headers = [
91+ 'Content-Type ' => 'text/event-stream ' ,
92+ 'Cache-Control ' => 'no-cache '
93+ ];
94+
95+ // Options for the Guzzle request
96+ $ options = [
97+ 'json ' => $ chatData ,
98+ 'headers ' => $ headers
99+ ];
100+
101+ // Send POST request to the OpenRouter API chat completion endpoint and get the streaming response.
102+ $ promise = $ this ->client ->requestAsync (
103+ 'POST ' ,
104+ $ chatCompletionPath ,
105+ $ options
106+ );
107+
108+ /*
109+ * Return streaming response promise which can be resolved with promise->wait().
110+ */
111+ return $ promise ->then (
112+ function (ResponseInterface $ response ) use ($ readByte ) {
113+ $ streamingResponse = $ response ->getBody ()->read ($ readByte );
114+
115+ return $ this ->filterStreamingResponse ($ streamingResponse );
116+ }
117+ );
118+ }
119+
120+ /**
121+ * It filters streaming response string so that response string is mapped into ResponseData.
122+ *
123+ * @param string $streamingResponse
124+ *
125+ * @return array
126+ * @throws UnknownProperties
127+ */
128+ private function filterStreamingResponse (string $ streamingResponse ): array
129+ {
130+ // Split the string by lines
131+ $ lines = explode ("\n" , $ streamingResponse );
132+
133+ // Filter out unnecessary lines
134+ $ filteredLines = array_filter ($ lines , function ($ line ) {
135+ // Check if line starts with "data: {"
136+ return strpos ($ line , 'data: { ' ) === 0 ;
137+ });
138+
139+ // Decode the JSON data in each line
140+ $ responseDataArray = [];
141+
142+ foreach ($ filteredLines as $ line ) {
143+ // Remove "data: " and decode JSON
144+ $ data = json_decode (substr ($ line , strlen ('data: ' )), true );
145+ // Check whether decoding was successful.
146+ if (json_last_error () === JSON_ERROR_NONE ) {
147+ $ responseDataArray [] = $ this ->formChatResponse ($ data );
148+ }
149+ }
150+
151+ return $ responseDataArray ;
152+ }
153+
58154 /**
59155 * Sends a cost request for the given generation id.
60156 *
61157 * @param string $generationId
62- * @return CostResponseData
63158 *
159+ * @return CostResponseData
64160 * @throws GuzzleException
65161 * @throws UnknownProperties
66162 */
@@ -101,17 +197,14 @@ public function limitRequest(): LimitResponseData
101197
102198 /**
103199 * Forms the response as ResponseData including id, model, object created, choices and usage if exits.
104- * First decodes the json response and get the result, then map it in ResponseData to return the response.
105200 *
106- * @param ResponseInterface|null $response
201+ * @param mixed $response
202+ *
107203 * @return ResponseData
108204 * @throws UnknownProperties
109205 */
110- private function formChatResponse (? ResponseInterface $ response = null ) : ResponseData
206+ private function formChatResponse (mixed $ response = null ) : ResponseData
111207 {
112- // Decode the json response
113- $ response = $ this ->jsonDecode ($ response );
114-
115208 // Map the response data to ResponseData and return it.
116209 return new ResponseData ([
117210 'id ' => Arr::get ($ response , 'id ' ),
@@ -128,6 +221,7 @@ private function formChatResponse(?ResponseInterface $response = null) : Respons
128221 * First decodes the json response, then map it in CostResponseData to return the response.
129222 *
130223 * @param ResponseInterface|null $response
224+ *
131225 * @return CostResponseData
132226 * @throws UnknownProperties
133227 */
@@ -167,6 +261,7 @@ private function formCostsResponse(?ResponseInterface $response = null) : CostRe
167261 * First decodes the json response and get the result, then map it in LimitResponseData to return the response.
168262 *
169263 * @param ResponseInterface|null $response
264+ *
170265 * @return LimitResponseData
171266 * @throws UnknownProperties
172267 */
@@ -190,6 +285,7 @@ private function formLimitResponse(?ResponseInterface $response = null): LimitRe
190285 * Decodes response to json.
191286 *
192287 * @param ResponseInterface|null $response
288+ *
193289 * @return mixed|null
194290 */
195291 private function jsonDecode (?ResponseInterface $ response = null ): mixed
0 commit comments