@tduncan / Cortana / commits / 86fdaf9

feat: add processing indicator and update chat UI to support message attachments and manual scroll control

abdulmominsakib committed Apr 4, 2026 at 16:54 UTC 86fdaf92c7165642cc38dfbd05f067685beb5caf
20 files changed +29764 -655
docs/openapi.json new
+28602
@@ -0,0 +1,28602 @@
1 +{
2 + "openapi": "3.1.0",
3 + "info": {
4 + "title": "OpenRouter API",
5 + "version": "1.0.0",
6 + "description": "OpenAI-compatible API with additional OpenRouter features",
7 + "contact": {
8 + "name": "OpenRouter Support",
9 + "url": "https://openrouter.ai/docs",
10 + "email": "support@openrouter.ai"
11 + },
12 + "license": {
13 + "name": "MIT",
14 + "url": "https://opensource.org/licenses/MIT"
15 + }
16 + },
17 + "servers": [
18 + {
19 + "url": "https://openrouter.ai/api/v1",
20 + "description": "Production server",
21 + "x-speakeasy-server-id": "production"
22 + }
23 + ],
24 + "security": [
25 + {
26 + "apiKey": []
27 + }
28 + ],
29 + "externalDocs": {
30 + "description": "OpenRouter Documentation",
31 + "url": "https://openrouter.ai/docs"
32 + },
33 + "components": {
34 + "schemas": {
35 + "OpenAIResponsesResponseStatus": {
36 + "type": "string",
37 + "enum": [
38 + "completed",
39 + "incomplete",
40 + "in_progress",
41 + "failed",
42 + "cancelled",
43 + "queued"
44 + ],
45 + "x-speakeasy-unknown-values": "allow"
46 + },
47 + "FileCitation": {
48 + "type": "object",
49 + "properties": {
50 + "type": {
51 + "type": "string",
52 + "enum": [
53 + "file_citation"
54 + ]
55 + },
56 + "file_id": {
57 + "type": "string"
58 + },
59 + "filename": {
60 + "type": "string"
61 + },
62 + "index": {
63 + "type": "number"
64 + }
65 + },
66 + "required": [
67 + "type",
68 + "file_id",
69 + "filename",
70 + "index"
71 + ],
72 + "example": {
73 + "type": "file_citation",
74 + "file_id": "file-abc123",
75 + "filename": "research_paper.pdf",
76 + "index": 0
77 + }
78 + },
79 + "URLCitation": {
80 + "type": "object",
81 + "properties": {
82 + "type": {
83 + "type": "string",
84 + "enum": [
85 + "url_citation"
86 + ]
87 + },
88 + "url": {
89 + "type": "string"
90 + },
91 + "title": {
92 + "type": "string"
93 + },
94 + "start_index": {
95 + "type": "number"
96 + },
97 + "end_index": {
98 + "type": "number"
99 + }
100 + },
101 + "required": [
102 + "type",
103 + "url",
104 + "title",
105 + "start_index",
106 + "end_index"
107 + ],
108 + "example": {
109 + "type": "url_citation",
110 + "start_index": 0,
111 + "end_index": 42,
112 + "title": "OpenRouter Documentation",
113 + "url": "https://openrouter.ai/docs"
114 + }
115 + },
116 + "FilePath": {
117 + "type": "object",
118 + "properties": {
119 + "type": {
120 + "type": "string",
121 + "enum": [
122 + "file_path"
123 + ]
124 + },
125 + "file_id": {
126 + "type": "string"
127 + },
128 + "index": {
129 + "type": "number"
130 + }
131 + },
132 + "required": [
133 + "type",
134 + "file_id",
135 + "index"
136 + ],
137 + "example": {
138 + "type": "file_path",
139 + "file_id": "file-xyz789",
140 + "index": 0
141 + }
142 + },
143 + "OpenAIResponsesAnnotation": {
144 + "anyOf": [
145 + {
146 + "$ref": "#/components/schemas/FileCitation"
147 + },
148 + {
149 + "$ref": "#/components/schemas/URLCitation"
150 + },
151 + {
152 + "$ref": "#/components/schemas/FilePath"
153 + }
154 + ]
155 + },
156 + "ResponseOutputText": {
157 + "type": "object",
158 + "properties": {
159 + "type": {
160 + "type": "string",
161 + "enum": [
162 + "output_text"
163 + ]
164 + },
165 + "text": {
166 + "type": "string"
167 + },
168 + "annotations": {
169 + "type": "array",
170 + "items": {
171 + "$ref": "#/components/schemas/OpenAIResponsesAnnotation"
172 + }
173 + },
174 + "logprobs": {
175 + "type": "array",
176 + "items": {
177 + "type": "object",
178 + "properties": {
179 + "token": {
180 + "type": "string"
181 + },
182 + "bytes": {
183 + "type": "array",
184 + "items": {
185 + "type": "number"
186 + }
187 + },
188 + "logprob": {
189 + "type": "number"
190 + },
191 + "top_logprobs": {
192 + "type": "array",
193 + "items": {
194 + "type": "object",
195 + "properties": {
196 + "token": {
197 + "type": "string"
198 + },
199 + "bytes": {
200 + "type": "array",
201 + "items": {
202 + "type": "number"
203 + }
204 + },
205 + "logprob": {
206 + "type": "number"
207 + }
208 + },
209 + "required": [
210 + "token",
211 + "bytes",
212 + "logprob"
213 + ]
214 + }
215 + }
216 + },
217 + "required": [
218 + "token",
219 + "bytes",
220 + "logprob",
221 + "top_logprobs"
222 + ]
223 + }
224 + }
225 + },
226 + "required": [
227 + "type",
228 + "text"
229 + ],
230 + "example": {
231 + "type": "output_text",
232 + "text": "The capital of France is Paris.",
233 + "annotations": [
234 + {
235 + "type": "url_citation",
236 + "start_index": 0,
237 + "end_index": 42,
238 + "title": "Paris - Wikipedia",
239 + "url": "https://en.wikipedia.org/wiki/Paris"
240 + }
241 + ]
242 + }
243 + },
244 + "OpenAIResponsesRefusalContent": {
245 + "type": "object",
246 + "properties": {
247 + "type": {
248 + "type": "string",
249 + "enum": [
250 + "refusal"
251 + ]
252 + },
253 + "refusal": {
254 + "type": "string"
255 + }
256 + },
257 + "required": [
258 + "type",
259 + "refusal"
260 + ],
261 + "example": {
262 + "type": "refusal",
263 + "refusal": "I'm sorry, I cannot assist with that request"
264 + }
265 + },
266 + "OutputMessage": {
267 + "type": "object",
268 + "properties": {
269 + "id": {
270 + "type": "string"
271 + },
272 + "role": {
273 + "type": "string",
274 + "enum": [
275 + "assistant"
276 + ]
277 + },
278 + "type": {
279 + "type": "string",
280 + "enum": [
281 + "message"
282 + ]
283 + },
284 + "status": {
285 + "anyOf": [
286 + {
287 + "type": "string",
288 + "enum": [
289 + "completed"
290 + ]
291 + },
292 + {
293 + "type": "string",
294 + "enum": [
295 + "incomplete"
296 + ]
297 + },
298 + {
299 + "type": "string",
300 + "enum": [
301 + "in_progress"
302 + ]
303 + }
304 + ]
305 + },
306 + "content": {
307 + "type": "array",
308 + "items": {
309 + "anyOf": [
310 + {
311 + "$ref": "#/components/schemas/ResponseOutputText"
312 + },
313 + {
314 + "$ref": "#/components/schemas/OpenAIResponsesRefusalContent"
315 + }
316 + ]
317 + }
318 + },
319 + "phase": {
320 + "anyOf": [
321 + {
322 + "type": "string",
323 + "enum": [
324 + "commentary"
325 + ]
326 + },
327 + {
328 + "type": "string",
329 + "enum": [
330 + "final_answer"
331 + ]
332 + },
333 + {
334 + "nullable": true
335 + }
336 + ],
337 + "description": "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages."
338 + }
339 + },
340 + "required": [
341 + "id",
342 + "role",
343 + "type",
344 + "content"
345 + ],
346 + "example": {
347 + "id": "msg-abc123",
348 + "role": "assistant",
349 + "type": "message",
350 + "status": "completed",
351 + "content": [
352 + {
353 + "type": "output_text",
354 + "text": "Hello! How can I help you today?"
355 + }
356 + ]
357 + }
358 + },
359 + "OutputMessageItem": {
360 + "allOf": [
361 + {
362 + "$ref": "#/components/schemas/OutputMessage"
363 + },
364 + {
365 + "type": "object",
366 + "properties": {}
367 + }
368 + ],
369 + "example": {
370 + "id": "msg-123",
371 + "type": "message",
372 + "role": "assistant",
373 + "status": "completed",
374 + "content": [
375 + {
376 + "type": "output_text",
377 + "text": "Hello! How can I help you?",
378 + "annotations": []
379 + }
380 + ]
381 + },
382 + "description": "An output message item"
383 + },
384 + "ReasoningTextContent": {
385 + "type": "object",
386 + "properties": {
387 + "type": {
388 + "type": "string",
389 + "enum": [
390 + "reasoning_text"
391 + ]
392 + },
393 + "text": {
394 + "type": "string"
395 + }
396 + },
397 + "required": [
398 + "type",
399 + "text"
400 + ],
401 + "example": {
402 + "type": "reasoning_text",
403 + "text": "Let me think step by step about this problem..."
404 + }
405 + },
406 + "ReasoningSummaryText": {
407 + "type": "object",
408 + "properties": {
409 + "type": {
410 + "type": "string",
411 + "enum": [
412 + "summary_text"
413 + ]
414 + },
415 + "text": {
416 + "type": "string"
417 + }
418 + },
419 + "required": [
420 + "type",
421 + "text"
422 + ],
423 + "example": {
424 + "type": "summary_text",
425 + "text": "Analyzed the problem using first principles"
426 + }
427 + },
428 + "OutputItemReasoning": {
429 + "type": "object",
430 + "properties": {
431 + "type": {
432 + "type": "string",
433 + "enum": [
434 + "reasoning"
435 + ]
436 + },
437 + "id": {
438 + "type": "string"
439 + },
440 + "content": {
441 + "type": "array",
442 + "items": {
443 + "$ref": "#/components/schemas/ReasoningTextContent"
444 + }
445 + },
446 + "summary": {
447 + "type": "array",
448 + "items": {
449 + "$ref": "#/components/schemas/ReasoningSummaryText"
450 + }
451 + },
452 + "encrypted_content": {
453 + "type": "string",
454 + "nullable": true
455 + },
456 + "status": {
457 + "anyOf": [
458 + {
459 + "type": "string",
460 + "enum": [
461 + "completed"
462 + ]
463 + },
464 + {
465 + "type": "string",
466 + "enum": [
467 + "incomplete"
468 + ]
469 + },
470 + {
471 + "type": "string",
472 + "enum": [
473 + "in_progress"
474 + ]
475 + }
476 + ]
477 + }
478 + },
479 + "required": [
480 + "type",
481 + "id",
482 + "summary"
483 + ],
484 + "example": {
485 + "type": "reasoning",
486 + "id": "reasoning-abc123",
487 + "summary": [
488 + {
489 + "type": "summary_text",
490 + "text": "Analyzed the problem using first principles"
491 + }
492 + ]
493 + }
494 + },
495 + "OutputReasoningItem": {
496 + "allOf": [
497 + {
498 + "$ref": "#/components/schemas/OutputItemReasoning"
499 + },
500 + {
501 + "type": "object",
502 + "properties": {
503 + "content": {
504 + "type": "array",
505 + "nullable": true,
506 + "items": {
507 + "$ref": "#/components/schemas/ReasoningTextContent"
508 + }
509 + },
510 + "signature": {
511 + "type": "string",
512 + "nullable": true,
513 + "description": "A signature for the reasoning content, used for verification",
514 + "example": "EvcBCkgIChABGAIqQKkSDbRuVEQUk9qN1odC098l9SEj..."
515 + },
516 + "format": {
517 + "type": "string",
518 + "nullable": true,
519 + "enum": [
520 + "unknown",
521 + "openai-responses-v1",
522 + "azure-openai-responses-v1",
523 + "xai-responses-v1",
524 + "anthropic-claude-v1",
525 + "google-gemini-v1"
526 + ],
527 + "description": "The format of the reasoning content",
528 + "example": "anthropic-claude-v1",
529 + "x-speakeasy-unknown-values": "allow"
530 + }
531 + }
532 + }
533 + ],
534 + "example": {
535 + "id": "reasoning-123",
536 + "type": "reasoning",
537 + "status": "completed",
538 + "summary": [
539 + {
540 + "type": "summary_text",
541 + "text": "Analyzed the problem and found the optimal solution."
542 + }
543 + ],
544 + "content": [
545 + {
546 + "type": "reasoning_text",
547 + "text": "First, we analyze the problem..."
548 + }
549 + ],
550 + "signature": "EvcBCkgIChABGAIqQKkSDbRuVEQUk9qN1odC098l9SEj...",
551 + "format": "anthropic-claude-v1"
552 + },
553 + "description": "An output item containing reasoning"
554 + },
555 + "OutputItemFunctionCall": {
556 + "type": "object",
557 + "properties": {
558 + "type": {
559 + "type": "string",
560 + "enum": [
561 + "function_call"
562 + ]
563 + },
564 + "id": {
565 + "type": "string"
566 + },
567 + "name": {
568 + "type": "string"
569 + },
570 + "arguments": {
571 + "type": "string"
572 + },
573 + "call_id": {
574 + "type": "string"
575 + },
576 + "status": {
577 + "anyOf": [
578 + {
579 + "type": "string",
580 + "enum": [
581 + "completed"
582 + ]
583 + },
584 + {
585 + "type": "string",
586 + "enum": [
587 + "incomplete"
588 + ]
589 + },
590 + {
591 + "type": "string",
592 + "enum": [
593 + "in_progress"
594 + ]
595 + }
596 + ]
597 + }
598 + },
599 + "required": [
600 + "type",
601 + "name",
602 + "arguments",
603 + "call_id"
604 + ],
605 + "example": {
606 + "type": "function_call",
607 + "id": "call-abc123",
608 + "name": "get_weather",
609 + "arguments": "{\"location\":\"San Francisco\",\"unit\":\"celsius\"}",
610 + "call_id": "call-abc123"
611 + }
612 + },
613 + "OutputFunctionCallItem": {
614 + "allOf": [
615 + {
616 + "$ref": "#/components/schemas/OutputItemFunctionCall"
617 + },
618 + {
619 + "type": "object",
620 + "properties": {}
621 + }
622 + ],
623 + "example": {
624 + "type": "function_call",
625 + "id": "call-abc123",
626 + "name": "get_weather",
627 + "arguments": "{\"location\":\"San Francisco\",\"unit\":\"celsius\"}",
628 + "call_id": "call-abc123"
629 + }
630 + },
631 + "WebSearchStatus": {
632 + "type": "string",
633 + "enum": [
634 + "completed",
635 + "searching",
636 + "in_progress",
637 + "failed"
638 + ],
639 + "example": "completed",
640 + "x-speakeasy-unknown-values": "allow"
641 + },
642 + "OutputItemWebSearchCall": {
643 + "type": "object",
644 + "properties": {
645 + "type": {
646 + "type": "string",
647 + "enum": [
648 + "web_search_call"
649 + ]
650 + },
651 + "id": {
652 + "type": "string"
653 + },
654 + "action": {
655 + "oneOf": [
656 + {
657 + "type": "object",
658 + "properties": {
659 + "type": {
660 + "type": "string",
661 + "enum": [
662 + "search"
663 + ]
664 + },
665 + "query": {
666 + "type": "string"
667 + },
668 + "queries": {
669 + "type": "array",
670 + "items": {
671 + "type": "string"
672 + }
673 + },
674 + "sources": {
675 + "type": "array",
676 + "items": {
677 + "type": "object",
678 + "properties": {
679 + "type": {
680 + "type": "string",
681 + "enum": [
682 + "url"
683 + ]
684 + },
685 + "url": {
686 + "type": "string"
687 + }
688 + },
689 + "required": [
690 + "type",
691 + "url"
692 + ]
693 + }
694 + }
695 + },
696 + "required": [
697 + "type",
698 + "query"
699 + ]
700 + },
701 + {
702 + "type": "object",
703 + "properties": {
704 + "type": {
705 + "type": "string",
706 + "enum": [
707 + "open_page"
708 + ]
709 + },
710 + "url": {
711 + "type": "string",
712 + "nullable": true
713 + }
714 + },
715 + "required": [
716 + "type"
717 + ]
718 + },
719 + {
720 + "type": "object",
721 + "properties": {
722 + "type": {
723 + "type": "string",
724 + "enum": [
725 + "find_in_page"
726 + ]
727 + },
728 + "pattern": {
729 + "type": "string"
730 + },
731 + "url": {
732 + "type": "string"
733 + }
734 + },
735 + "required": [
736 + "type",
737 + "pattern",
738 + "url"
739 + ]
740 + }
741 + ]
742 + },
743 + "status": {
744 + "$ref": "#/components/schemas/WebSearchStatus"
745 + }
746 + },
747 + "required": [
748 + "type",
749 + "id",
750 + "action",
751 + "status"
752 + ],
753 + "example": {
754 + "type": "web_search_call",
755 + "id": "search-abc123",
756 + "action": {
757 + "type": "search",
758 + "query": "OpenAI API"
759 + },
760 + "status": "completed"
761 + }
762 + },
763 + "OutputWebSearchCallItem": {
764 + "allOf": [
765 + {
766 + "$ref": "#/components/schemas/OutputItemWebSearchCall"
767 + },
768 + {
769 + "type": "object",
770 + "properties": {}
771 + }
772 + ],
773 + "example": {
774 + "type": "web_search_call",
775 + "id": "search-abc123",
776 + "action": {
777 + "type": "search",
778 + "query": "OpenAI API"
779 + },
780 + "status": "completed"
781 + }
782 + },
783 + "OutputItemFileSearchCall": {
784 + "type": "object",
785 + "properties": {
786 + "type": {
787 + "type": "string",
788 + "enum": [
789 + "file_search_call"
790 + ]
791 + },
792 + "id": {
793 + "type": "string"
794 + },
795 + "queries": {
796 + "type": "array",
797 + "items": {
798 + "type": "string"
799 + }
800 + },
801 + "status": {
802 + "$ref": "#/components/schemas/WebSearchStatus"
803 + }
804 + },
805 + "required": [
806 + "type",
807 + "id",
808 + "queries",
809 + "status"
810 + ],
811 + "example": {
812 + "type": "file_search_call",
813 + "id": "filesearch-abc123",
814 + "queries": [
815 + "machine learning algorithms",
816 + "neural networks"
817 + ],
818 + "status": "completed"
819 + }
820 + },
821 + "OutputFileSearchCallItem": {
822 + "allOf": [
823 + {
824 + "$ref": "#/components/schemas/OutputItemFileSearchCall"
825 + },
826 + {
827 + "type": "object",
828 + "properties": {}
829 + }
830 + ],
831 + "example": {
832 + "type": "file_search_call",
833 + "id": "filesearch-abc123",
834 + "queries": [
835 + "machine learning algorithms",
836 + "neural networks"
837 + ],
838 + "status": "completed"
839 + }
840 + },
841 + "ImageGenerationStatus": {
842 + "type": "string",
843 + "enum": [
844 + "in_progress",
845 + "completed",
846 + "generating",
847 + "failed"
848 + ],
849 + "example": "completed",
850 + "x-speakeasy-unknown-values": "allow"
851 + },
852 + "OutputItemImageGenerationCall": {
853 + "type": "object",
854 + "properties": {
855 + "type": {
856 + "type": "string",
857 + "enum": [
858 + "image_generation_call"
859 + ]
860 + },
861 + "id": {
862 + "type": "string"
863 + },
864 + "result": {
865 + "type": "string",
866 + "nullable": true,
867 + "default": null
868 + },
869 + "status": {
870 + "$ref": "#/components/schemas/ImageGenerationStatus"
871 + }
872 + },
873 + "required": [
874 + "type",
875 + "id",
876 + "status"
877 + ],
878 + "example": {
879 + "type": "image_generation_call",
880 + "id": "imagegen-abc123",
881 + "result": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
882 + "status": "completed"
883 + }
884 + },
885 + "OutputImageGenerationCallItem": {
886 + "allOf": [
887 + {
888 + "$ref": "#/components/schemas/OutputItemImageGenerationCall"
889 + },
890 + {
891 + "type": "object",
892 + "properties": {}
893 + }
894 + ],
895 + "example": {
896 + "type": "image_generation_call",
897 + "id": "imagegen-abc123",
898 + "result": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
899 + "status": "completed"
900 + }
901 + },
902 + "OutputServerToolItem": {
903 + "type": "object",
904 + "properties": {
905 + "type": {
906 + "type": "string",
907 + "pattern": "^openrouter:",
908 + "description": "Server tool type (e.g. openrouter:datetime, openrouter:web_search)"
909 + },
910 + "id": {
911 + "type": "string"
912 + },
913 + "status": {
914 + "type": "string",
915 + "enum": [
916 + "completed",
917 + "in_progress",
918 + "incomplete"
919 + ],
920 + "x-speakeasy-unknown-values": "allow"
921 + }
922 + },
923 + "required": [
924 + "type",
925 + "status"
926 + ],
927 + "additionalProperties": {
928 + "nullable": true
929 + },
930 + "description": "A generic OpenRouter server tool output item",
931 + "example": {
932 + "type": "openrouter:web_search",
933 + "id": "ws_tmp_abc123",
934 + "status": "completed"
935 + }
936 + },
937 + "OutputItems": {
938 + "anyOf": [
939 + {
940 + "$ref": "#/components/schemas/OutputMessageItem"
941 + },
942 + {
943 + "$ref": "#/components/schemas/OutputReasoningItem"
944 + },
945 + {
946 + "$ref": "#/components/schemas/OutputFunctionCallItem"
947 + },
948 + {
949 + "$ref": "#/components/schemas/OutputWebSearchCallItem"
950 + },
951 + {
952 + "$ref": "#/components/schemas/OutputFileSearchCallItem"
953 + },
954 + {
955 + "$ref": "#/components/schemas/OutputImageGenerationCallItem"
956 + },
957 + {
958 + "$ref": "#/components/schemas/OutputServerToolItem"
959 + }
960 + ],
961 + "description": "An output item from the response",
962 + "example": {
963 + "id": "msg-abc123",
964 + "role": "assistant",
965 + "type": "message",
966 + "status": "completed",
967 + "content": [
968 + {
969 + "type": "output_text",
970 + "text": "Hello! How can I help you today?"
971 + }
972 + ]
973 + }
974 + },
975 + "ResponsesErrorField": {
976 + "type": "object",
977 + "nullable": true,
978 + "properties": {
979 + "code": {
980 + "type": "string",
981 + "enum": [
982 + "server_error",
983 + "rate_limit_exceeded",
984 + "invalid_prompt",
985 + "vector_store_timeout",
986 + "invalid_image",
987 + "invalid_image_format",
988 + "invalid_base64_image",
989 + "invalid_image_url",
990 + "image_too_large",
991 + "image_too_small",
992 + "image_parse_error",
993 + "image_content_policy_violation",
994 + "invalid_image_mode",
995 + "image_file_too_large",
996 + "unsupported_image_media_type",
997 + "empty_image_file",
998 + "failed_to_download_image",
999 + "image_file_not_found"
1000 + ],
1001 + "x-speakeasy-unknown-values": "allow"
1002 + },
1003 + "message": {
1004 + "type": "string"
1005 + }
1006 + },
1007 + "required": [
1008 + "code",
1009 + "message"
1010 + ],
1011 + "description": "Error information returned from the API",
1012 + "example": {
1013 + "code": "rate_limit_exceeded",
1014 + "message": "Rate limit exceeded. Please try again later."
1015 + }
1016 + },
1017 + "IncompleteDetails": {
1018 + "type": "object",
1019 + "nullable": true,
1020 + "properties": {
1021 + "reason": {
1022 + "type": "string",
1023 + "enum": [
1024 + "max_output_tokens",
1025 + "content_filter"
1026 + ],
1027 + "x-speakeasy-unknown-values": "allow"
1028 + }
1029 + }
1030 + },
1031 + "OpenAIResponsesUsage": {
1032 + "type": "object",
1033 + "properties": {
1034 + "input_tokens": {
1035 + "type": "number"
1036 + },
1037 + "input_tokens_details": {
1038 + "type": "object",
1039 + "properties": {
1040 + "cached_tokens": {
1041 + "type": "number"
1042 + }
1043 + },
1044 + "required": [
1045 + "cached_tokens"
1046 + ]
1047 + },
1048 + "output_tokens": {
1049 + "type": "number"
1050 + },
1051 + "output_tokens_details": {
1052 + "type": "object",
1053 + "properties": {
1054 + "reasoning_tokens": {
1055 + "type": "number"
1056 + }
1057 + },
1058 + "required": [
1059 + "reasoning_tokens"
1060 + ]
1061 + },
1062 + "total_tokens": {
1063 + "type": "number"
1064 + }
1065 + },
1066 + "required": [
1067 + "input_tokens",
1068 + "input_tokens_details",
1069 + "output_tokens",
1070 + "output_tokens_details",
1071 + "total_tokens"
1072 + ]
1073 + },
1074 + "Usage": {
1075 + "allOf": [
1076 + {
1077 + "$ref": "#/components/schemas/OpenAIResponsesUsage"
1078 + },
1079 + {
1080 + "type": "object",
1081 + "nullable": true,
1082 + "properties": {
1083 + "cost": {
1084 + "type": "number",
1085 + "nullable": true,
1086 + "description": "Cost of the completion"
1087 + },
1088 + "is_byok": {
1089 + "type": "boolean",
1090 + "description": "Whether a request was made using a Bring Your Own Key configuration"
1091 + },
1092 + "cost_details": {
1093 + "type": "object",
1094 + "properties": {
1095 + "upstream_inference_cost": {
1096 + "type": "number",
1097 + "format": "double"
1098 + },
1099 + "upstream_inference_input_cost": {
1100 + "type": "number",
1101 + "format": "double"
1102 + },
1103 + "upstream_inference_output_cost": {
1104 + "type": "number",
1105 + "format": "double"
1106 + }
1107 + },
1108 + "required": [
1109 + "upstream_inference_input_cost",
1110 + "upstream_inference_output_cost"
1111 + ]
1112 + }
1113 + }
1114 + }
1115 + ],
1116 + "description": "Token usage information for the response",
1117 + "example": {
1118 + "input_tokens": 10,
1119 + "output_tokens": 25,
1120 + "total_tokens": 35,
1121 + "input_tokens_details": {
1122 + "cached_tokens": 0
1123 + },
1124 + "output_tokens_details": {
1125 + "reasoning_tokens": 0
1126 + },
1127 + "cost": 0.0012,
1128 + "cost_details": {
1129 + "upstream_inference_cost": null,
1130 + "upstream_inference_input_cost": 0.0008,
1131 + "upstream_inference_output_cost": 0.0004
1132 + }
1133 + }
1134 + },
1135 + "InputText": {
1136 + "type": "object",
1137 + "properties": {
1138 + "type": {
1139 + "type": "string",
1140 + "enum": [
1141 + "input_text"
1142 + ]
1143 + },
1144 + "text": {
1145 + "type": "string"
1146 + }
1147 + },
1148 + "required": [
1149 + "type",
1150 + "text"
1151 + ],
1152 + "description": "Text input content item",
1153 + "example": {
1154 + "type": "input_text",
1155 + "text": "Hello, how can I help you?"
1156 + }
1157 + },
1158 + "InputImage": {
1159 + "type": "object",
1160 + "properties": {
1161 + "type": {
1162 + "type": "string",
1163 + "enum": [
1164 + "input_image"
1165 + ]
1166 + },
1167 + "detail": {
1168 + "type": "string",
1169 + "enum": [
1170 + "auto",
1171 + "high",
1172 + "low"
1173 + ],
1174 + "x-speakeasy-unknown-values": "allow"
1175 + },
1176 + "image_url": {
1177 + "type": "string",
1178 + "nullable": true
1179 + }
1180 + },
1181 + "required": [
1182 + "type",
1183 + "detail"
1184 + ],
1185 + "description": "Image input content item",
1186 + "example": {
1187 + "type": "input_image",
1188 + "detail": "auto",
1189 + "image_url": "https://example.com/image.jpg"
1190 + }
1191 + },
1192 + "InputFile": {
1193 + "type": "object",
1194 + "properties": {
1195 + "type": {
1196 + "type": "string",
1197 + "enum": [
1198 + "input_file"
1199 + ]
1200 + },
1201 + "file_id": {
1202 + "type": "string",
1203 + "nullable": true
1204 + },
1205 + "file_data": {
1206 + "type": "string"
1207 + },
1208 + "filename": {
1209 + "type": "string"
1210 + },
1211 + "file_url": {
1212 + "type": "string"
1213 + }
1214 + },
1215 + "required": [
1216 + "type"
1217 + ],
1218 + "description": "File input content item",
1219 + "example": {
1220 + "type": "input_file",
1221 + "file_id": "file-abc123",
1222 + "filename": "document.pdf"
1223 + }
1224 + },
1225 + "InputAudio": {
1226 + "type": "object",
1227 + "properties": {
1228 + "type": {
1229 + "type": "string",
1230 + "enum": [
1231 + "input_audio"
1232 + ]
1233 + },
1234 + "input_audio": {
1235 + "type": "object",
1236 + "properties": {
1237 + "data": {
1238 + "type": "string"
1239 + },
1240 + "format": {
1241 + "type": "string",
1242 + "enum": [
1243 + "mp3",
1244 + "wav"
1245 + ],
1246 + "x-speakeasy-unknown-values": "allow"
1247 + }
1248 + },
1249 + "required": [
1250 + "data",
1251 + "format"
1252 + ]
1253 + }
1254 + },
1255 + "required": [
1256 + "type",
1257 + "input_audio"
1258 + ],
1259 + "description": "Audio input content item",
1260 + "example": {
1261 + "type": "input_audio",
1262 + "input_audio": {
1263 + "data": "SGVsbG8gV29ybGQ=",
1264 + "format": "mp3"
1265 + }
1266 + }
1267 + },
1268 + "ToolCallStatusEnum": {
1269 + "type": "string",
1270 + "nullable": true,
1271 + "enum": [
1272 + "in_progress",
1273 + "completed",
1274 + "incomplete"
1275 + ],
1276 + "example": "completed",
1277 + "x-speakeasy-unknown-values": "allow"
1278 + },
1279 + "BaseInputs": {
1280 + "anyOf": [
1281 + {
1282 + "type": "string"
1283 + },
1284 + {
1285 + "type": "array",
1286 + "items": {
1287 + "anyOf": [
1288 + {
1289 + "type": "object",
1290 + "properties": {
1291 + "type": {
1292 + "type": "string",
1293 + "enum": [
1294 + "message"
1295 + ]
1296 + },
1297 + "role": {
1298 + "anyOf": [
1299 + {
1300 + "type": "string",
1301 + "enum": [
1302 + "user"
1303 + ]
1304 + },
1305 + {
1306 + "type": "string",
1307 + "enum": [
1308 + "system"
1309 + ]
1310 + },
1311 + {
1312 + "type": "string",
1313 + "enum": [
1314 + "assistant"
1315 + ]
1316 + },
1317 + {
1318 + "type": "string",
1319 + "enum": [
1320 + "developer"
1321 + ]
1322 + }
1323 + ]
1324 + },
1325 + "content": {
1326 + "anyOf": [
1327 + {
1328 + "type": "array",
1329 + "items": {
1330 + "oneOf": [
1331 + {
1332 + "$ref": "#/components/schemas/InputText"
1333 + },
1334 + {
1335 + "$ref": "#/components/schemas/InputImage"
1336 + },
1337 + {
1338 + "$ref": "#/components/schemas/InputFile"
1339 + },
1340 + {
1341 + "$ref": "#/components/schemas/InputAudio"
1342 + }
1343 + ],
1344 + "discriminator": {
1345 + "propertyName": "type",
1346 + "mapping": {
1347 + "input_text": "#/components/schemas/InputText",
1348 + "input_image": "#/components/schemas/InputImage",
1349 + "input_file": "#/components/schemas/InputFile",
1350 + "input_audio": "#/components/schemas/InputAudio"
1351 + }
1352 + }
1353 + }
1354 + },
1355 + {
1356 + "type": "string"
1357 + }
1358 + ]
1359 + },
1360 + "phase": {
1361 + "anyOf": [
1362 + {
1363 + "type": "string",
1364 + "enum": [
1365 + "commentary"
1366 + ]
1367 + },
1368 + {
1369 + "type": "string",
1370 + "enum": [
1371 + "final_answer"
1372 + ]
1373 + },
1374 + {
1375 + "nullable": true
1376 + }
1377 + ]
1378 + }
1379 + },
1380 + "required": [
1381 + "role",
1382 + "content"
1383 + ]
1384 + },
1385 + {
1386 + "type": "object",
1387 + "properties": {
1388 + "id": {
1389 + "type": "string"
1390 + },
1391 + "type": {
1392 + "type": "string",
1393 + "enum": [
1394 + "message"
1395 + ]
1396 + },
1397 + "role": {
1398 + "anyOf": [
1399 + {
1400 + "type": "string",
1401 + "enum": [
1402 + "user"
1403 + ]
1404 + },
1405 + {
1406 + "type": "string",
1407 + "enum": [
1408 + "system"
1409 + ]
1410 + },
1411 + {
1412 + "type": "string",
1413 + "enum": [
1414 + "developer"
1415 + ]
1416 + }
1417 + ]
1418 + },
1419 + "content": {
1420 + "type": "array",
1421 + "items": {
1422 + "oneOf": [
1423 + {
1424 + "$ref": "#/components/schemas/InputText"
1425 + },
1426 + {
1427 + "$ref": "#/components/schemas/InputImage"
1428 + },
1429 + {
1430 + "$ref": "#/components/schemas/InputFile"
1431 + },
1432 + {
1433 + "$ref": "#/components/schemas/InputAudio"
1434 + }
1435 + ],
1436 + "discriminator": {
1437 + "propertyName": "type",
1438 + "mapping": {
1439 + "input_text": "#/components/schemas/InputText",
1440 + "input_image": "#/components/schemas/InputImage",
1441 + "input_file": "#/components/schemas/InputFile",
1442 + "input_audio": "#/components/schemas/InputAudio"
1443 + }
1444 + }
1445 + }
1446 + }
1447 + },
1448 + "required": [
1449 + "id",
1450 + "role",
1451 + "content"
1452 + ]
1453 + },
1454 + {
1455 + "type": "object",
1456 + "properties": {
1457 + "type": {
1458 + "type": "string",
1459 + "enum": [
1460 + "function_call_output"
1461 + ]
1462 + },
1463 + "id": {
1464 + "type": "string",
1465 + "nullable": true
1466 + },
1467 + "call_id": {
1468 + "type": "string"
1469 + },
1470 + "output": {
1471 + "anyOf": [
1472 + {
1473 + "type": "string"
1474 + },
1475 + {
1476 + "type": "array",
1477 + "items": {
1478 + "oneOf": [
1479 + {
1480 + "$ref": "#/components/schemas/InputText"
1481 + },
1482 + {
1483 + "$ref": "#/components/schemas/InputImage"
1484 + },
1485 + {
1486 + "$ref": "#/components/schemas/InputFile"
1487 + }
1488 + ],
1489 + "discriminator": {
1490 + "propertyName": "type",
1491 + "mapping": {
1492 + "input_text": "#/components/schemas/InputText",
1493 + "input_image": "#/components/schemas/InputImage",
1494 + "input_file": "#/components/schemas/InputFile"
1495 + }
1496 + }
1497 + }
1498 + }
1499 + ]
1500 + },
1501 + "status": {
1502 + "$ref": "#/components/schemas/ToolCallStatusEnum"
1503 + }
1504 + },
1505 + "required": [
1506 + "type",
1507 + "call_id",
1508 + "output"
1509 + ]
1510 + },
1511 + {
1512 + "type": "object",
1513 + "properties": {
1514 + "type": {
1515 + "type": "string",
1516 + "enum": [
1517 + "function_call"
1518 + ]
1519 + },
1520 + "call_id": {
1521 + "type": "string"
1522 + },
1523 + "name": {
1524 + "type": "string"
1525 + },
1526 + "arguments": {
1527 + "type": "string"
1528 + },
1529 + "id": {
1530 + "type": "string"
1531 + },
1532 + "status": {
1533 + "$ref": "#/components/schemas/ToolCallStatusEnum"
1534 + }
1535 + },
1536 + "required": [
1537 + "type",
1538 + "call_id",
1539 + "name",
1540 + "arguments"
1541 + ]
1542 + },
1543 + {
1544 + "$ref": "#/components/schemas/OutputItemImageGenerationCall"
1545 + },
1546 + {
1547 + "$ref": "#/components/schemas/OutputMessage"
1548 + }
1549 + ]
1550 + }
1551 + },
1552 + {
1553 + "nullable": true
1554 + }
1555 + ]
1556 + },
1557 + "RequestMetadata": {
1558 + "type": "object",
1559 + "nullable": true,
1560 + "additionalProperties": {
1561 + "type": "string",
1562 + "maxLength": 512
1563 + },
1564 + "description": "Metadata key-value pairs for the request. Keys must be ≤64 characters and cannot contain brackets. Values must be ≤512 characters. Maximum 16 pairs allowed.",
1565 + "example": {
1566 + "user_id": "123",
1567 + "session_id": "abc-def-ghi"
1568 + }
1569 + },
1570 + "FunctionTool": {
1571 + "type": "object",
1572 + "properties": {
1573 + "type": {
1574 + "type": "string",
1575 + "enum": [
1576 + "function"
1577 + ]
1578 + },
1579 + "name": {
1580 + "type": "string"
1581 + },
1582 + "description": {
1583 + "type": "string",
1584 + "nullable": true
1585 + },
1586 + "strict": {
1587 + "type": "boolean",
1588 + "nullable": true
1589 + },
1590 + "parameters": {
1591 + "type": "object",
1592 + "nullable": true,
1593 + "additionalProperties": {
1594 + "nullable": true
1595 + }
1596 + }
1597 + },
1598 + "required": [
1599 + "type",
1600 + "name",
1601 + "parameters"
1602 + ],
1603 + "description": "Function tool definition",
1604 + "example": {
1605 + "type": "function",
1606 + "name": "get_weather",
1607 + "description": "Get the current weather in a location",
1608 + "parameters": {
1609 + "type": "object",
1610 + "properties": {
1611 + "location": {
1612 + "type": "string",
1613 + "description": "The city and state"
1614 + },
1615 + "unit": {
1616 + "type": "string",
1617 + "enum": [
1618 + "celsius",
1619 + "fahrenheit"
1620 + ],
1621 + "x-speakeasy-unknown-values": "allow"
1622 + }
1623 + },
1624 + "required": [
1625 + "location"
1626 + ]
1627 + }
1628 + }
1629 + },
1630 + "SearchContextSizeEnum": {
1631 + "type": "string",
1632 + "enum": [
1633 + "low",
1634 + "medium",
1635 + "high"
1636 + ],
1637 + "description": "Size of the search context for web search tools",
1638 + "example": "medium",
1639 + "x-speakeasy-unknown-values": "allow"
1640 + },
1641 + "Preview_WebSearchUserLocation": {
1642 + "type": "object",
1643 + "nullable": true,
1644 + "properties": {
1645 + "type": {
1646 + "type": "string",
1647 + "enum": [
1648 + "approximate"
1649 + ]
1650 + },
1651 + "city": {
1652 + "type": "string",
1653 + "nullable": true
1654 + },
1655 + "country": {
1656 + "type": "string",
1657 + "nullable": true
1658 + },
1659 + "region": {
1660 + "type": "string",
1661 + "nullable": true
1662 + },
1663 + "timezone": {
1664 + "type": "string",
1665 + "nullable": true
1666 + }
1667 + },
1668 + "required": [
1669 + "type"
1670 + ]
1671 + },
1672 + "Preview_WebSearchServerTool": {
1673 + "type": "object",
1674 + "properties": {
1675 + "type": {
1676 + "type": "string",
1677 + "enum": [
1678 + "web_search_preview"
1679 + ]
1680 + },
1681 + "search_context_size": {
1682 + "$ref": "#/components/schemas/SearchContextSizeEnum"
1683 + },
1684 + "user_location": {
1685 + "$ref": "#/components/schemas/Preview_WebSearchUserLocation"
1686 + },
1687 + "engine": {
1688 + "type": "string",
1689 + "enum": [
1690 + "auto",
1691 + "native",
1692 + "exa",
1693 + "firecrawl",
1694 + "parallel"
1695 + ],
1696 + "description": "Which search engine to use. \"auto\" (default) uses native if the provider supports it, otherwise Exa. \"native\" forces the provider's built-in search. \"exa\" forces the Exa search API. \"firecrawl\" uses Firecrawl (requires BYOK). \"parallel\" uses the Parallel search API.",
1697 + "example": "auto",
1698 + "x-speakeasy-unknown-values": "allow"
1699 + },
1700 + "max_results": {
1701 + "type": "number",
1702 + "minimum": 1,
1703 + "maximum": 25,
1704 + "description": "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, and Parallel engines; ignored with native provider search.",
1705 + "example": 5
1706 + },
1707 + "filters": {
1708 + "type": "object",
1709 + "nullable": true,
1710 + "properties": {
1711 + "allowed_domains": {
1712 + "type": "array",
1713 + "nullable": true,
1714 + "items": {
1715 + "type": "string"
1716 + }
1717 + },
1718 + "excluded_domains": {
1719 + "type": "array",
1720 + "nullable": true,
1721 + "items": {
1722 + "type": "string"
1723 + }
1724 + }
1725 + }
1726 + }
1727 + },
1728 + "required": [
1729 + "type"
1730 + ],
1731 + "description": "Web search preview tool configuration",
1732 + "example": {
1733 + "type": "web_search_preview"
1734 + }
1735 + },
1736 + "Preview_20250311_WebSearchServerTool": {
1737 + "type": "object",
1738 + "properties": {
1739 + "type": {
1740 + "type": "string",
1741 + "enum": [
1742 + "web_search_preview_2025_03_11"
1743 + ]
1744 + },
1745 + "search_context_size": {
1746 + "$ref": "#/components/schemas/SearchContextSizeEnum"
1747 + },
1748 + "user_location": {
1749 + "$ref": "#/components/schemas/Preview_WebSearchUserLocation"
1750 + },
1751 + "engine": {
1752 + "type": "string",
1753 + "enum": [
1754 + "auto",
1755 + "native",
1756 + "exa",
1757 + "firecrawl",
1758 + "parallel"
1759 + ],
1760 + "description": "Which search engine to use. \"auto\" (default) uses native if the provider supports it, otherwise Exa. \"native\" forces the provider's built-in search. \"exa\" forces the Exa search API. \"firecrawl\" uses Firecrawl (requires BYOK). \"parallel\" uses the Parallel search API.",
1761 + "example": "auto",
1762 + "x-speakeasy-unknown-values": "allow"
1763 + },
1764 + "max_results": {
1765 + "type": "number",
1766 + "minimum": 1,
1767 + "maximum": 25,
1768 + "description": "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, and Parallel engines; ignored with native provider search.",
1769 + "example": 5
1770 + },
1771 + "filters": {
1772 + "type": "object",
1773 + "nullable": true,
1774 + "properties": {
1775 + "allowed_domains": {
1776 + "type": "array",
1777 + "nullable": true,
1778 + "items": {
1779 + "type": "string"
1780 + }
1781 + },
1782 + "excluded_domains": {
1783 + "type": "array",
1784 + "nullable": true,
1785 + "items": {
1786 + "type": "string"
1787 + }
1788 + }
1789 + }
1790 + }
1791 + },
1792 + "required": [
1793 + "type"
1794 + ],
1795 + "description": "Web search preview tool configuration (2025-03-11 version)",
1796 + "example": {
1797 + "type": "web_search_preview_2025_03_11"
1798 + }
1799 + },
1800 + "WebSearchUserLocation": {
1801 + "type": "object",
1802 + "nullable": true,
1803 + "properties": {
1804 + "type": {
1805 + "type": "string",
1806 + "enum": [
1807 + "approximate"
1808 + ]
1809 + },
1810 + "city": {
1811 + "type": "string",
1812 + "nullable": true
1813 + },
1814 + "country": {
1815 + "type": "string",
1816 + "nullable": true
1817 + },
1818 + "region": {
1819 + "type": "string",
1820 + "nullable": true
1821 + },
1822 + "timezone": {
1823 + "type": "string",
1824 + "nullable": true
1825 + }
1826 + },
1827 + "description": "User location information for web search",
1828 + "example": {
1829 + "type": "approximate",
1830 + "city": "San Francisco",
1831 + "country": "USA",
1832 + "region": "California",
1833 + "timezone": "America/Los_Angeles"
1834 + }
1835 + },
1836 + "Legacy_WebSearchServerTool": {
1837 + "type": "object",
1838 + "properties": {
1839 + "type": {
1840 + "type": "string",
1841 + "enum": [
1842 + "web_search"
1843 + ]
1844 + },
1845 + "filters": {
1846 + "type": "object",
1847 + "nullable": true,
1848 + "properties": {
1849 + "allowed_domains": {
1850 + "type": "array",
1851 + "nullable": true,
1852 + "items": {
1853 + "type": "string"
1854 + }
1855 + },
1856 + "excluded_domains": {
1857 + "type": "array",
1858 + "nullable": true,
1859 + "items": {
1860 + "type": "string"
1861 + }
1862 + }
1863 + }
1864 + },
1865 + "search_context_size": {
1866 + "$ref": "#/components/schemas/SearchContextSizeEnum"
1867 + },
1868 + "user_location": {
1869 + "$ref": "#/components/schemas/WebSearchUserLocation"
1870 + },
1871 + "engine": {
1872 + "type": "string",
1873 + "enum": [
1874 + "auto",
1875 + "native",
1876 + "exa",
1877 + "firecrawl",
1878 + "parallel"
1879 + ],
1880 + "description": "Which search engine to use. \"auto\" (default) uses native if the provider supports it, otherwise Exa. \"native\" forces the provider's built-in search. \"exa\" forces the Exa search API. \"firecrawl\" uses Firecrawl (requires BYOK). \"parallel\" uses the Parallel search API.",
1881 + "example": "auto",
1882 + "x-speakeasy-unknown-values": "allow"
1883 + },
1884 + "max_results": {
1885 + "type": "number",
1886 + "minimum": 1,
1887 + "maximum": 25,
1888 + "description": "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, and Parallel engines; ignored with native provider search.",
1889 + "example": 5
1890 + }
1891 + },
1892 + "required": [
1893 + "type"
1894 + ],
1895 + "description": "Web search tool configuration",
1896 + "example": {
1897 + "type": "web_search",
1898 + "engine": "auto",
1899 + "filters": {
1900 + "allowed_domains": [
1901 + "example.com"
1902 + ]
1903 + }
1904 + }
1905 + },
1906 + "WebSearchServerTool": {
1907 + "type": "object",
1908 + "properties": {
1909 + "type": {
1910 + "type": "string",
1911 + "enum": [
1912 + "web_search_2025_08_26"
1913 + ]
1914 + },
1915 + "filters": {
1916 + "type": "object",
1917 + "nullable": true,
1918 + "properties": {
1919 + "allowed_domains": {
1920 + "type": "array",
1921 + "nullable": true,
1922 + "items": {
1923 + "type": "string"
1924 + }
1925 + },
1926 + "excluded_domains": {
1927 + "type": "array",
1928 + "nullable": true,
1929 + "items": {
1930 + "type": "string"
1931 + }
1932 + }
1933 + }
1934 + },
1935 + "search_context_size": {
1936 + "$ref": "#/components/schemas/SearchContextSizeEnum"
1937 + },
1938 + "user_location": {
1939 + "$ref": "#/components/schemas/WebSearchUserLocation"
1940 + },
1941 + "engine": {
1942 + "type": "string",
1943 + "enum": [
1944 + "auto",
1945 + "native",
1946 + "exa",
1947 + "firecrawl",
1948 + "parallel"
1949 + ],
1950 + "description": "Which search engine to use. \"auto\" (default) uses native if the provider supports it, otherwise Exa. \"native\" forces the provider's built-in search. \"exa\" forces the Exa search API. \"firecrawl\" uses Firecrawl (requires BYOK). \"parallel\" uses the Parallel search API.",
1951 + "example": "auto",
1952 + "x-speakeasy-unknown-values": "allow"
1953 + },
1954 + "max_results": {
1955 + "type": "number",
1956 + "minimum": 1,
1957 + "maximum": 25,
1958 + "description": "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, and Parallel engines; ignored with native provider search.",
1959 + "example": 5
1960 + }
1961 + },
1962 + "required": [
1963 + "type"
1964 + ],
1965 + "description": "Web search tool configuration (2025-08-26 version)",
1966 + "example": {
1967 + "type": "web_search_2025_08_26",
1968 + "engine": "auto",
1969 + "filters": {
1970 + "allowed_domains": [
1971 + "example.com"
1972 + ]
1973 + }
1974 + }
1975 + },
1976 + "CompoundFilter": {
1977 + "type": "object",
1978 + "properties": {
1979 + "type": {
1980 + "type": "string",
1981 + "enum": [
1982 + "and",
1983 + "or"
1984 + ],
1985 + "x-speakeasy-unknown-values": "allow"
1986 + },
1987 + "filters": {
1988 + "type": "array",
1989 + "items": {
1990 + "type": "object",
1991 + "additionalProperties": {
1992 + "nullable": true
1993 + }
1994 + }
1995 + }
1996 + },
1997 + "required": [
1998 + "type",
1999 + "filters"
2000 + ],
2001 + "description": "A compound filter that combines multiple comparison or compound filters"
2002 + },
2003 + "FileSearchServerTool": {
2004 + "type": "object",
2005 + "properties": {
2006 + "type": {
2007 + "type": "string",
2008 + "enum": [
2009 + "file_search"
2010 + ]
2011 + },
2012 + "vector_store_ids": {
2013 + "type": "array",
2014 + "items": {
2015 + "type": "string"
2016 + }
2017 + },
2018 + "filters": {
2019 + "anyOf": [
2020 + {
2021 + "type": "object",
2022 + "properties": {
2023 + "key": {
2024 + "type": "string"
2025 + },
2026 + "type": {
2027 + "type": "string",
2028 + "enum": [
2029 + "eq",
2030 + "ne",
2031 + "gt",
2032 + "gte",
2033 + "lt",
2034 + "lte"
2035 + ],
2036 + "x-speakeasy-unknown-values": "allow"
2037 + },
2038 + "value": {
2039 + "anyOf": [
2040 + {
2041 + "type": "string"
2042 + },
2043 + {
2044 + "type": "number",
2045 + "format": "double"
2046 + },
2047 + {
2048 + "type": "boolean"
2049 + },
2050 + {
2051 + "type": "array",
2052 + "items": {
2053 + "anyOf": [
2054 + {
2055 + "type": "string"
2056 + },
2057 + {
2058 + "type": "number",
2059 + "format": "double"
2060 + }
2061 + ]
2062 + }
2063 + }
2064 + ]
2065 + }
2066 + },
2067 + "required": [
2068 + "key",
2069 + "type",
2070 + "value"
2071 + ]
2072 + },
2073 + {
2074 + "$ref": "#/components/schemas/CompoundFilter"
2075 + },
2076 + {
2077 + "nullable": true
2078 + }
2079 + ]
2080 + },
2081 + "max_num_results": {
2082 + "type": "integer"
2083 + },
2084 + "ranking_options": {
2085 + "type": "object",
2086 + "properties": {
2087 + "ranker": {
2088 + "type": "string",
2089 + "enum": [
2090 + "auto",
2091 + "default-2024-11-15"
2092 + ],
2093 + "x-speakeasy-unknown-values": "allow"
2094 + },
2095 + "score_threshold": {
2096 + "type": "number",
2097 + "format": "double"
2098 + }
2099 + }
2100 + }
2101 + },
2102 + "required": [
2103 + "type",
2104 + "vector_store_ids"
2105 + ],
2106 + "description": "File search tool configuration",
2107 + "example": {
2108 + "type": "file_search",
2109 + "vector_store_ids": [
2110 + "vs_abc123"
2111 + ]
2112 + }
2113 + },
2114 + "ComputerUseServerTool": {
2115 + "type": "object",
2116 + "properties": {
2117 + "type": {
2118 + "type": "string",
2119 + "enum": [
2120 + "computer_use_preview"
2121 + ]
2122 + },
2123 + "display_height": {
2124 + "type": "integer"
2125 + },
2126 + "display_width": {
2127 + "type": "integer"
2128 + },
2129 + "environment": {
2130 + "type": "string",
2131 + "enum": [
2132 + "windows",
2133 + "mac",
2134 + "linux",
2135 + "ubuntu",
2136 + "browser"
2137 + ],
2138 + "x-speakeasy-unknown-values": "allow"
2139 + }
2140 + },
2141 + "required": [
2142 + "type",
2143 + "display_height",
2144 + "display_width",
2145 + "environment"
2146 + ],
2147 + "description": "Computer use preview tool configuration",
2148 + "example": {
2149 + "type": "computer_use_preview",
2150 + "display_height": 768,
2151 + "display_width": 1024,
2152 + "environment": "linux"
2153 + }
2154 + },
2155 + "CodeInterpreterServerTool": {
2156 + "type": "object",
2157 + "properties": {
2158 + "type": {
2159 + "type": "string",
2160 + "enum": [
2161 + "code_interpreter"
2162 + ]
2163 + },
2164 + "container": {
2165 + "anyOf": [
2166 + {
2167 + "type": "string"
2168 + },
2169 + {
2170 + "type": "object",
2171 + "properties": {
2172 + "type": {
2173 + "type": "string",
2174 + "enum": [
2175 + "auto"
2176 + ]
2177 + },
2178 + "file_ids": {
2179 + "type": "array",
2180 + "items": {
2181 + "type": "string"
2182 + }
2183 + },
2184 + "memory_limit": {
2185 + "type": "string",
2186 + "nullable": true,
2187 + "enum": [
2188 + "1g",
2189 + "4g",
2190 + "16g",
2191 + "64g"
2192 + ],
2193 + "x-speakeasy-unknown-values": "allow"
2194 + }
2195 + },
2196 + "required": [
2197 + "type"
2198 + ]
2199 + }
2200 + ]
2201 + }
2202 + },
2203 + "required": [
2204 + "type",
2205 + "container"
2206 + ],
2207 + "description": "Code interpreter tool configuration",
2208 + "example": {
2209 + "type": "code_interpreter",
2210 + "container": "auto"
2211 + }
2212 + },
2213 + "McpServerTool": {
2214 + "type": "object",
2215 + "properties": {
2216 + "type": {
2217 + "type": "string",
2218 + "enum": [
2219 + "mcp"
2220 + ]
2221 + },
2222 + "server_label": {
2223 + "type": "string"
2224 + },
2225 + "allowed_tools": {
2226 + "anyOf": [
2227 + {
2228 + "type": "array",
2229 + "items": {
2230 + "type": "string"
2231 + }
2232 + },
2233 + {
2234 + "type": "object",
2235 + "properties": {
2236 + "tool_names": {
2237 + "type": "array",
2238 + "items": {
2239 + "type": "string"
2240 + }
2241 + },
2242 + "read_only": {
2243 + "type": "boolean"
2244 + }
2245 + }
2246 + },
2247 + {
2248 + "nullable": true
2249 + },
2250 + {
2251 + "nullable": true
2252 + }
2253 + ]
2254 + },
2255 + "authorization": {
2256 + "type": "string"
2257 + },
2258 + "connector_id": {
2259 + "type": "string",
2260 + "enum": [
2261 + "connector_dropbox",
2262 + "connector_gmail",
2263 + "connector_googlecalendar",
2264 + "connector_googledrive",
2265 + "connector_microsoftteams",
2266 + "connector_outlookcalendar",
2267 + "connector_outlookemail",
2268 + "connector_sharepoint"
2269 + ],
2270 + "x-speakeasy-unknown-values": "allow"
2271 + },
2272 + "headers": {
2273 + "type": "object",
2274 + "nullable": true,
2275 + "additionalProperties": {
2276 + "type": "string"
2277 + }
2278 + },
2279 + "require_approval": {
2280 + "anyOf": [
2281 + {
2282 + "type": "object",
2283 + "properties": {
2284 + "never": {
2285 + "type": "object",
2286 + "properties": {
2287 + "tool_names": {
2288 + "type": "array",
2289 + "items": {
2290 + "type": "string"
2291 + }
2292 + }
2293 + }
2294 + },
2295 + "always": {
2296 + "type": "object",
2297 + "properties": {
2298 + "tool_names": {
2299 + "type": "array",
2300 + "items": {
2301 + "type": "string"
2302 + }
2303 + }
2304 + }
2305 + }
2306 + }
2307 + },
2308 + {
2309 + "type": "string",
2310 + "enum": [
2311 + "always"
2312 + ]
2313 + },
2314 + {
2315 + "type": "string",
2316 + "enum": [
2317 + "never"
2318 + ]
2319 + },
2320 + {
2321 + "nullable": true
2322 + },
2323 + {
2324 + "nullable": true
2325 + }
2326 + ]
2327 + },
2328 + "server_description": {
2329 + "type": "string"
2330 + },
2331 + "server_url": {
2332 + "type": "string"
2333 + }
2334 + },
2335 + "required": [
2336 + "type",
2337 + "server_label"
2338 + ],
2339 + "description": "MCP (Model Context Protocol) tool configuration",
2340 + "example": {
2341 + "type": "mcp",
2342 + "server_label": "my-server",
2343 + "server_url": "https://example.com/mcp"
2344 + }
2345 + },
2346 + "ImageGenerationServerTool": {
2347 + "type": "object",
2348 + "properties": {
2349 + "type": {
2350 + "type": "string",
2351 + "enum": [
2352 + "image_generation"
2353 + ]
2354 + },
2355 + "background": {
2356 + "type": "string",
2357 + "enum": [
2358 + "transparent",
2359 + "opaque",
2360 + "auto"
2361 + ],
2362 + "x-speakeasy-unknown-values": "allow"
2363 + },
2364 + "input_fidelity": {
2365 + "type": "string",
2366 + "nullable": true,
2367 + "enum": [
2368 + "high",
2369 + "low"
2370 + ],
2371 + "x-speakeasy-unknown-values": "allow"
2372 + },
2373 + "input_image_mask": {
2374 + "type": "object",
2375 + "properties": {
2376 + "image_url": {
2377 + "type": "string"
2378 + },
2379 + "file_id": {
2380 + "type": "string"
2381 + }
2382 + }
2383 + },
2384 + "model": {
2385 + "type": "string",
2386 + "enum": [
2387 + "gpt-image-1",
2388 + "gpt-image-1-mini"
2389 + ],
2390 + "x-speakeasy-unknown-values": "allow"
2391 + },
2392 + "moderation": {
2393 + "type": "string",
2394 + "enum": [
2395 + "auto",
2396 + "low"
2397 + ],
2398 + "x-speakeasy-unknown-values": "allow"
2399 + },
2400 + "output_compression": {
2401 + "type": "integer"
2402 + },
2403 + "output_format": {
2404 + "type": "string",
2405 + "enum": [
2406 + "png",
2407 + "webp",
2408 + "jpeg"
2409 + ],
2410 + "x-speakeasy-unknown-values": "allow"
2411 + },
2412 + "partial_images": {
2413 + "type": "integer"
2414 + },
2415 + "quality": {
2416 + "type": "string",
2417 + "enum": [
2418 + "low",
2419 + "medium",
2420 + "high",
2421 + "auto"
2422 + ],
2423 + "x-speakeasy-unknown-values": "allow"
2424 + },
2425 + "size": {
2426 + "type": "string",
2427 + "enum": [
2428 + "1024x1024",
2429 + "1024x1536",
2430 + "1536x1024",
2431 + "auto"
2432 + ],
2433 + "x-speakeasy-unknown-values": "allow"
2434 + }
2435 + },
2436 + "required": [
2437 + "type"
2438 + ],
2439 + "description": "Image generation tool configuration",
2440 + "example": {
2441 + "type": "image_generation",
2442 + "quality": "high"
2443 + }
2444 + },
2445 + "CodexLocalShellTool": {
2446 + "type": "object",
2447 + "properties": {
2448 + "type": {
2449 + "type": "string",
2450 + "enum": [
2451 + "local_shell"
2452 + ]
2453 + }
2454 + },
2455 + "required": [
2456 + "type"
2457 + ],
2458 + "description": "Local shell tool configuration",
2459 + "example": {
2460 + "type": "local_shell"
2461 + }
2462 + },
2463 + "ShellServerTool": {
2464 + "type": "object",
2465 + "properties": {
2466 + "type": {
2467 + "type": "string",
2468 + "enum": [
2469 + "shell"
2470 + ]
2471 + }
2472 + },
2473 + "required": [
2474 + "type"
2475 + ],
2476 + "description": "Shell tool configuration",
2477 + "example": {
2478 + "type": "shell"
2479 + }
2480 + },
2481 + "ApplyPatchServerTool": {
2482 + "type": "object",
2483 + "properties": {
2484 + "type": {
2485 + "type": "string",
2486 + "enum": [
2487 + "apply_patch"
2488 + ]
2489 + }
2490 + },
2491 + "required": [
2492 + "type"
2493 + ],
2494 + "description": "Apply patch tool configuration",
2495 + "example": {
2496 + "type": "apply_patch"
2497 + }
2498 + },
2499 + "CustomTool": {
2500 + "type": "object",
2501 + "properties": {
2502 + "type": {
2503 + "type": "string",
2504 + "enum": [
2505 + "custom"
2506 + ]
2507 + },
2508 + "name": {
2509 + "type": "string"
2510 + },
2511 + "description": {
2512 + "type": "string"
2513 + },
2514 + "format": {
2515 + "anyOf": [
2516 + {
2517 + "type": "object",
2518 + "properties": {
2519 + "type": {
2520 + "type": "string",
2521 + "enum": [
2522 + "text"
2523 + ]
2524 + }
2525 + },
2526 + "required": [
2527 + "type"
2528 + ]
2529 + },
2530 + {
2531 + "type": "object",
2532 + "properties": {
2533 + "type": {
2534 + "type": "string",
2535 + "enum": [
2536 + "grammar"
2537 + ]
2538 + },
2539 + "definition": {
2540 + "type": "string"
2541 + },
2542 + "syntax": {
2543 + "type": "string",
2544 + "enum": [
2545 + "lark",
2546 + "regex"
2547 + ],
2548 + "x-speakeasy-unknown-values": "allow"
2549 + }
2550 + },
2551 + "required": [
2552 + "type",
2553 + "definition",
2554 + "syntax"
2555 + ]
2556 + }
2557 + ]
2558 + }
2559 + },
2560 + "required": [
2561 + "type",
2562 + "name"
2563 + ],
2564 + "description": "Custom tool configuration",
2565 + "example": {
2566 + "type": "custom",
2567 + "name": "my_tool"
2568 + }
2569 + },
2570 + "ToolChoiceAllowed": {
2571 + "type": "object",
2572 + "properties": {
2573 + "type": {
2574 + "type": "string",
2575 + "enum": [
2576 + "allowed_tools"
2577 + ]
2578 + },
2579 + "mode": {
2580 + "anyOf": [
2581 + {
2582 + "type": "string",
2583 + "enum": [
2584 + "auto"
2585 + ]
2586 + },
2587 + {
2588 + "type": "string",
2589 + "enum": [
2590 + "required"
2591 + ]
2592 + }
2593 + ]
2594 + },
2595 + "tools": {
2596 + "type": "array",
2597 + "items": {
2598 + "type": "object",
2599 + "additionalProperties": {
2600 + "nullable": true
2601 + }
2602 + }
2603 + }
2604 + },
2605 + "required": [
2606 + "type",
2607 + "mode",
2608 + "tools"
2609 + ],
2610 + "description": "Constrains the model to a pre-defined set of allowed tools",
2611 + "example": {
2612 + "type": "allowed_tools",
2613 + "mode": "auto",
2614 + "tools": [
2615 + {
2616 + "type": "function",
2617 + "name": "get_weather"
2618 + }
2619 + ]
2620 + }
2621 + },
2622 + "OpenAIResponsesToolChoice": {
2623 + "anyOf": [
2624 + {
2625 + "type": "string",
2626 + "enum": [
2627 + "auto"
2628 + ]
2629 + },
2630 + {
2631 + "type": "string",
2632 + "enum": [
2633 + "none"
2634 + ]
2635 + },
2636 + {
2637 + "type": "string",
2638 + "enum": [
2639 + "required"
2640 + ]
2641 + },
2642 + {
2643 + "type": "object",
2644 + "properties": {
2645 + "type": {
2646 + "type": "string",
2647 + "enum": [
2648 + "function"
2649 + ]
2650 + },
2651 + "name": {
2652 + "type": "string"
2653 + }
2654 + },
2655 + "required": [
2656 + "type",
2657 + "name"
2658 + ]
2659 + },
2660 + {
2661 + "type": "object",
2662 + "properties": {
2663 + "type": {
2664 + "anyOf": [
2665 + {
2666 + "type": "string",
2667 + "enum": [
2668 + "web_search_preview_2025_03_11"
2669 + ]
2670 + },
2671 + {
2672 + "type": "string",
2673 + "enum": [
2674 + "web_search_preview"
2675 + ]
2676 + }
2677 + ]
2678 + }
2679 + },
2680 + "required": [
2681 + "type"
2682 + ]
2683 + },
2684 + {
2685 + "$ref": "#/components/schemas/ToolChoiceAllowed"
2686 + }
2687 + ]
2688 + },
2689 + "StoredPromptTemplate": {
2690 + "type": "object",
2691 + "nullable": true,
2692 + "properties": {
2693 + "id": {
2694 + "type": "string"
2695 + },
2696 + "variables": {
2697 + "type": "object",
2698 + "nullable": true,
2699 + "additionalProperties": {
2700 + "anyOf": [
2701 + {
2702 + "type": "string"
2703 + },
2704 + {
2705 + "$ref": "#/components/schemas/InputText"
2706 + },
2707 + {
2708 + "$ref": "#/components/schemas/InputImage"
2709 + },
2710 + {
2711 + "$ref": "#/components/schemas/InputFile"
2712 + }
2713 + ]
2714 + }
2715 + }
2716 + },
2717 + "required": [
2718 + "id"
2719 + ]
2720 + },
2721 + "ReasoningEffortEnum": {
2722 + "type": "string",
2723 + "nullable": true,
2724 + "enum": [
2725 + "xhigh",
2726 + "high",
2727 + "medium",
2728 + "low",
2729 + "minimal",
2730 + "none"
2731 + ],
2732 + "x-speakeasy-unknown-values": "allow"
2733 + },
2734 + "ReasoningSummaryVerbosityEnum": {
2735 + "type": "string",
2736 + "nullable": true,
2737 + "enum": [
2738 + "auto",
2739 + "concise",
2740 + "detailed"
2741 + ],
2742 + "example": "auto",
2743 + "x-speakeasy-unknown-values": "allow"
2744 + },
2745 + "BaseReasoningConfig": {
2746 + "type": "object",
2747 + "nullable": true,
2748 + "properties": {
2749 + "effort": {
2750 + "$ref": "#/components/schemas/ReasoningEffortEnum"
2751 + },
2752 + "summary": {
2753 + "$ref": "#/components/schemas/ReasoningSummaryVerbosityEnum"
2754 + }
2755 + }
2756 + },
2757 + "TruncationEnum": {
2758 + "type": "string",
2759 + "nullable": true,
2760 + "enum": [
2761 + "auto",
2762 + "disabled"
2763 + ],
2764 + "x-speakeasy-unknown-values": "allow"
2765 + },
2766 + "FormatTextConfig": {
2767 + "type": "object",
2768 + "properties": {
2769 + "type": {
2770 + "type": "string",
2771 + "enum": [
2772 + "text"
2773 + ]
2774 + }
2775 + },
2776 + "required": [
2777 + "type"
2778 + ],
2779 + "description": "Plain text response format",
2780 + "example": {
2781 + "type": "text"
2782 + }
2783 + },
2784 + "FormatJsonObjectConfig": {
2785 + "type": "object",
2786 + "properties": {
2787 + "type": {
2788 + "type": "string",
2789 + "enum": [
2790 + "json_object"
2791 + ]
2792 + }
2793 + },
2794 + "required": [
2795 + "type"
2796 + ],
2797 + "description": "JSON object response format",
2798 + "example": {
2799 + "type": "json_object"
2800 + }
2801 + },
2802 + "FormatJsonSchemaConfig": {
2803 + "type": "object",
2804 + "properties": {
2805 + "type": {
2806 + "type": "string",
2807 + "enum": [
2808 + "json_schema"
2809 + ]
2810 + },
2811 + "name": {
2812 + "type": "string"
2813 + },
2814 + "description": {
2815 + "type": "string"
2816 + },
2817 + "strict": {
2818 + "type": "boolean",
2819 + "nullable": true
2820 + },
2821 + "schema": {
2822 + "type": "object",
2823 + "additionalProperties": {
2824 + "nullable": true
2825 + }
2826 + }
2827 + },
2828 + "required": [
2829 + "type",
2830 + "name",
2831 + "schema"
2832 + ],
2833 + "description": "JSON schema constrained response format",
2834 + "example": {
2835 + "type": "json_schema",
2836 + "name": "user_info",
2837 + "description": "User information schema",
2838 + "schema": {
2839 + "type": "object",
2840 + "properties": {
2841 + "name": {
2842 + "type": "string"
2843 + },
2844 + "age": {
2845 + "type": "number"
2846 + }
2847 + },
2848 + "required": [
2849 + "name"
2850 + ]
2851 + }
2852 + }
2853 + },
2854 + "Formats": {
2855 + "anyOf": [
2856 + {
2857 + "$ref": "#/components/schemas/FormatTextConfig"
2858 + },
2859 + {
2860 + "$ref": "#/components/schemas/FormatJsonObjectConfig"
2861 + },
2862 + {
2863 + "$ref": "#/components/schemas/FormatJsonSchemaConfig"
2864 + }
2865 + ],
2866 + "description": "Text response format configuration",
2867 + "example": {
2868 + "type": "text"
2869 + }
2870 + },
2871 + "TextConfig": {
2872 + "type": "object",
2873 + "properties": {
2874 + "format": {
2875 + "$ref": "#/components/schemas/Formats"
2876 + },
2877 + "verbosity": {
2878 + "type": "string",
2879 + "nullable": true,
2880 + "enum": [
2881 + "high",
2882 + "low",
2883 + "medium"
2884 + ],
2885 + "x-speakeasy-unknown-values": "allow"
2886 + }
2887 + },
2888 + "description": "Text output configuration including format and verbosity",
2889 + "example": {
2890 + "format": {
2891 + "type": "text"
2892 + },
2893 + "verbosity": "medium"
2894 + }
2895 + },
2896 + "ServiceTierEnum": {
2897 + "type": "string",
2898 + "nullable": true,
2899 + "enum": [
2900 + "auto",
2901 + "default",
2902 + "flex",
2903 + "priority",
2904 + "scale"
2905 + ],
2906 + "x-speakeasy-unknown-values": "allow"
2907 + },
2908 + "BaseResponsesResult": {
2909 + "type": "object",
2910 + "properties": {
2911 + "id": {
2912 + "type": "string"
2913 + },
2914 + "object": {
2915 + "type": "string",
2916 + "enum": [
2917 + "response"
2918 + ]
2919 + },
2920 + "created_at": {
2921 + "type": "integer"
2922 + },
2923 + "model": {
2924 + "type": "string"
2925 + },
2926 + "status": {
2927 + "$ref": "#/components/schemas/OpenAIResponsesResponseStatus"
2928 + },
2929 + "completed_at": {
2930 + "type": "integer"
2931 + },
2932 + "output": {
2933 + "type": "array",
2934 + "items": {
2935 + "oneOf": [
2936 + {
2937 + "$ref": "#/components/schemas/OutputMessage"
2938 + },
2939 + {
2940 + "$ref": "#/components/schemas/OutputItemReasoning"
2941 + },
2942 + {
2943 + "$ref": "#/components/schemas/OutputItemFunctionCall"
2944 + },
2945 + {
2946 + "$ref": "#/components/schemas/OutputItemWebSearchCall"
2947 + },
2948 + {
2949 + "$ref": "#/components/schemas/OutputItemFileSearchCall"
2950 + },
2951 + {
2952 + "$ref": "#/components/schemas/OutputItemImageGenerationCall"
2953 + }
2954 + ],
2955 + "discriminator": {
2956 + "propertyName": "type",
2957 + "mapping": {
2958 + "message": "#/components/schemas/OutputMessage",
2959 + "reasoning": "#/components/schemas/OutputItemReasoning",
2960 + "function_call": "#/components/schemas/OutputItemFunctionCall",
2961 + "web_search_call": "#/components/schemas/OutputItemWebSearchCall",
2962 + "file_search_call": "#/components/schemas/OutputItemFileSearchCall",
2963 + "image_generation_call": "#/components/schemas/OutputItemImageGenerationCall"
2964 + }
2965 + }
2966 + }
2967 + },
2968 + "user": {
2969 + "type": "string",
2970 + "nullable": true
2971 + },
2972 + "output_text": {
2973 + "type": "string"
2974 + },
2975 + "prompt_cache_key": {
2976 + "type": "string",
2977 + "nullable": true
2978 + },
2979 + "safety_identifier": {
2980 + "type": "string",
2981 + "nullable": true
2982 + },
2983 + "error": {
2984 + "$ref": "#/components/schemas/ResponsesErrorField"
2985 + },
2986 + "incomplete_details": {
2987 + "$ref": "#/components/schemas/IncompleteDetails"
2988 + },
2989 + "usage": {
2990 + "$ref": "#/components/schemas/OpenAIResponsesUsage"
2991 + },
2992 + "max_tool_calls": {
2993 + "type": "integer"
2994 + },
2995 + "top_logprobs": {
2996 + "type": "integer"
2997 + },
2998 + "max_output_tokens": {
2999 + "type": "integer"
3000 + },
3001 + "temperature": {
3002 + "type": "number",
3003 + "format": "double"
3004 + },
3005 + "top_p": {
3006 + "type": "number",
3007 + "format": "double"
3008 + },
3009 + "presence_penalty": {
3010 + "type": "number",
3011 + "format": "double"
3012 + },
3013 + "frequency_penalty": {
3014 + "type": "number",
3015 + "format": "double"
3016 + },
3017 + "instructions": {
3018 + "$ref": "#/components/schemas/BaseInputs"
3019 + },
3020 + "metadata": {
3021 + "$ref": "#/components/schemas/RequestMetadata"
3022 + },
3023 + "tools": {
3024 + "type": "array",
3025 + "items": {
3026 + "oneOf": [
3027 + {
3028 + "allOf": [
3029 + {
3030 + "$ref": "#/components/schemas/FunctionTool"
3031 + },
3032 + {
3033 + "type": "object",
3034 + "properties": {}
3035 + }
3036 + ],
3037 + "description": "Function tool definition",
3038 + "example": {
3039 + "type": "function",
3040 + "name": "get_weather",
3041 + "description": "Get the current weather in a location",
3042 + "parameters": {
3043 + "type": "object",
3044 + "properties": {
3045 + "location": {
3046 + "type": "string",
3047 + "description": "The city and state"
3048 + },
3049 + "unit": {
3050 + "type": "string",
3051 + "enum": [
3052 + "celsius",
3053 + "fahrenheit"
3054 + ],
3055 + "x-speakeasy-unknown-values": "allow"
3056 + }
3057 + },
3058 + "required": [
3059 + "location"
3060 + ]
3061 + }
3062 + }
3063 + },
3064 + {
3065 + "$ref": "#/components/schemas/Preview_WebSearchServerTool"
3066 + },
3067 + {
3068 + "$ref": "#/components/schemas/Preview_20250311_WebSearchServerTool"
3069 + },
3070 + {
3071 + "$ref": "#/components/schemas/Legacy_WebSearchServerTool"
3072 + },
3073 + {
3074 + "$ref": "#/components/schemas/WebSearchServerTool"
3075 + },
3076 + {
3077 + "$ref": "#/components/schemas/FileSearchServerTool"
3078 + },
3079 + {
3080 + "$ref": "#/components/schemas/ComputerUseServerTool"
3081 + },
3082 + {
3083 + "$ref": "#/components/schemas/CodeInterpreterServerTool"
3084 + },
3085 + {
3086 + "$ref": "#/components/schemas/McpServerTool"
3087 + },
3088 + {
3089 + "$ref": "#/components/schemas/ImageGenerationServerTool"
3090 + },
3091 + {
3092 + "$ref": "#/components/schemas/CodexLocalShellTool"
3093 + },
3094 + {
3095 + "$ref": "#/components/schemas/ShellServerTool"
3096 + },
3097 + {
3098 + "$ref": "#/components/schemas/ApplyPatchServerTool"
3099 + },
3100 + {
3101 + "$ref": "#/components/schemas/CustomTool"
3102 + }
3103 + ]
3104 + }
3105 + },
3106 + "tool_choice": {
3107 + "$ref": "#/components/schemas/OpenAIResponsesToolChoice"
3108 + },
3109 + "parallel_tool_calls": {
3110 + "type": "boolean"
3111 + },
3112 + "prompt": {
3113 + "$ref": "#/components/schemas/StoredPromptTemplate"
3114 + },
3115 + "background": {
3116 + "type": "boolean",
3117 + "nullable": true
3118 + },
3119 + "previous_response_id": {
3120 + "type": "string",
3121 + "nullable": true
3122 + },
3123 + "reasoning": {
3124 + "$ref": "#/components/schemas/BaseReasoningConfig"
3125 + },
3126 + "service_tier": {
3127 + "$ref": "#/components/schemas/ServiceTierEnum"
3128 + },
3129 + "store": {
3130 + "type": "boolean"
3131 + },
3132 + "truncation": {
3133 + "$ref": "#/components/schemas/TruncationEnum"
3134 + },
3135 + "text": {
3136 + "$ref": "#/components/schemas/TextConfig"
3137 + }
3138 + },
3139 + "required": [
3140 + "id",
3141 + "object",
3142 + "created_at",
3143 + "model",
3144 + "status",
3145 + "completed_at",
3146 + "output",
3147 + "error",
3148 + "incomplete_details",
3149 + "temperature",
3150 + "top_p",
3151 + "presence_penalty",
3152 + "frequency_penalty",
3153 + "instructions",
3154 + "metadata",
3155 + "tools",
3156 + "tool_choice",
3157 + "parallel_tool_calls"
3158 + ]
3159 + },
3160 + "OpenResponsesResult": {
3161 + "allOf": [
3162 + {
3163 + "$ref": "#/components/schemas/BaseResponsesResult"
3164 + },
3165 + {
3166 + "type": "object",
3167 + "properties": {
3168 + "output": {
3169 + "type": "array",
3170 + "items": {
3171 + "$ref": "#/components/schemas/OutputItems"
3172 + }
3173 + },
3174 + "usage": {
3175 + "$ref": "#/components/schemas/Usage"
3176 + },
3177 + "service_tier": {
3178 + "type": "string",
3179 + "nullable": true
3180 + }
3181 + }
3182 + }
3183 + ],
3184 + "description": "Complete non-streaming response from the Responses API",
3185 + "example": {
3186 + "id": "resp-abc123",
3187 + "object": "response",
3188 + "created_at": 1704067200,
3189 + "model": "gpt-4",
3190 + "status": "completed",
3191 + "output": [
3192 + {
3193 + "type": "message",
3194 + "id": "msg-abc123",
3195 + "status": "completed",
3196 + "role": "assistant",
3197 + "content": [
3198 + {
3199 + "type": "output_text",
3200 + "text": "Hello! How can I help you today?",
3201 + "annotations": []
3202 + }
3203 + ]
3204 + }
3205 + ],
3206 + "usage": {
3207 + "input_tokens": 10,
3208 + "output_tokens": 25,
3209 + "total_tokens": 35,
3210 + "input_tokens_details": {
3211 + "cached_tokens": 0
3212 + },
3213 + "output_tokens_details": {
3214 + "reasoning_tokens": 0
3215 + }
3216 + },
3217 + "tools": [],
3218 + "tool_choice": "auto",
3219 + "parallel_tool_calls": true,
3220 + "error": null,
3221 + "incomplete_details": null,
3222 + "temperature": null,
3223 + "top_p": null,
3224 + "max_output_tokens": null,
3225 + "metadata": null,
3226 + "instructions": null
3227 + }
3228 + },
3229 + "CreatedEvent": {
3230 + "type": "object",
3231 + "properties": {
3232 + "type": {
3233 + "type": "string",
3234 + "enum": [
3235 + "response.created"
3236 + ]
3237 + },
3238 + "response": {
3239 + "$ref": "#/components/schemas/BaseResponsesResult"
3240 + },
3241 + "sequence_number": {
3242 + "type": "integer"
3243 + }
3244 + },
3245 + "required": [
3246 + "type",
3247 + "response",
3248 + "sequence_number"
3249 + ],
3250 + "description": "Event emitted when a response is created",
3251 + "example": {
3252 + "type": "response.created",
3253 + "response": {
3254 + "id": "resp-abc123",
3255 + "object": "response",
3256 + "created_at": 1704067200,
3257 + "model": "gpt-4",
3258 + "status": "in_progress",
3259 + "output": [],
3260 + "tools": [],
3261 + "tool_choice": "auto",
3262 + "parallel_tool_calls": true,
3263 + "error": null,
3264 + "incomplete_details": null,
3265 + "metadata": null,
3266 + "instructions": null,
3267 + "temperature": null,
3268 + "top_p": null,
3269 + "max_output_tokens": null
3270 + },
3271 + "sequence_number": 0
3272 + }
3273 + },
3274 + "InProgressEvent": {
3275 + "type": "object",
3276 + "properties": {
3277 + "type": {
3278 + "type": "string",
3279 + "enum": [
3280 + "response.in_progress"
3281 + ]
3282 + },
3283 + "response": {
3284 + "$ref": "#/components/schemas/BaseResponsesResult"
3285 + },
3286 + "sequence_number": {
3287 + "type": "integer"
3288 + }
3289 + },
3290 + "required": [
3291 + "type",
3292 + "response",
3293 + "sequence_number"
3294 + ],
3295 + "description": "Event emitted when a response is in progress",
3296 + "example": {
3297 + "type": "response.in_progress",
3298 + "response": {
3299 + "id": "resp-abc123",
3300 + "object": "response",
3301 + "created_at": 1704067200,
3302 + "model": "gpt-4",
3303 + "status": "in_progress",
3304 + "output": [],
3305 + "tools": [],
3306 + "tool_choice": "auto",
3307 + "parallel_tool_calls": true,
3308 + "error": null,
3309 + "incomplete_details": null,
3310 + "metadata": null,
3311 + "instructions": null,
3312 + "temperature": null,
3313 + "top_p": null,
3314 + "max_output_tokens": null
3315 + },
3316 + "sequence_number": 1
3317 + }
3318 + },
3319 + "CompletedEvent": {
3320 + "type": "object",
3321 + "properties": {
3322 + "type": {
3323 + "type": "string",
3324 + "enum": [
3325 + "response.completed"
3326 + ]
3327 + },
3328 + "response": {
3329 + "$ref": "#/components/schemas/BaseResponsesResult"
3330 + },
3331 + "sequence_number": {
3332 + "type": "integer"
3333 + }
3334 + },
3335 + "required": [
3336 + "type",
3337 + "response",
3338 + "sequence_number"
3339 + ],
3340 + "description": "Event emitted when a response has completed successfully",
3341 + "example": {
3342 + "type": "response.completed",
3343 + "response": {
3344 + "id": "resp-abc123",
3345 + "object": "response",
3346 + "created_at": 1704067200,
3347 + "model": "gpt-4",
3348 + "status": "completed",
3349 + "output": [
3350 + {
3351 + "id": "item-1",
3352 + "type": "message",
3353 + "status": "completed",
3354 + "role": "assistant",
3355 + "content": [
3356 + {
3357 + "type": "output_text",
3358 + "text": "Hello! How can I help you?",
3359 + "annotations": []
3360 + }
3361 + ]
3362 + }
3363 + ],
3364 + "tools": [],
3365 + "tool_choice": "auto",
3366 + "parallel_tool_calls": true,
3367 + "error": null,
3368 + "incomplete_details": null,
3369 + "metadata": null,
3370 + "instructions": null,
3371 + "temperature": null,
3372 + "top_p": null,
3373 + "max_output_tokens": null
3374 + },
3375 + "sequence_number": 10
3376 + }
3377 + },
3378 + "IncompleteEvent": {
3379 + "type": "object",
3380 + "properties": {
3381 + "type": {
3382 + "type": "string",
3383 + "enum": [
3384 + "response.incomplete"
3385 + ]
3386 + },
3387 + "response": {
3388 + "$ref": "#/components/schemas/BaseResponsesResult"
3389 + },
3390 + "sequence_number": {
3391 + "type": "integer"
3392 + }
3393 + },
3394 + "required": [
3395 + "type",
3396 + "response",
3397 + "sequence_number"
3398 + ],
3399 + "description": "Event emitted when a response is incomplete",
3400 + "example": {
3401 + "type": "response.incomplete",
3402 + "response": {
3403 + "id": "resp-abc123",
3404 + "object": "response",
3405 + "created_at": 1704067200,
3406 + "model": "gpt-4",
3407 + "status": "incomplete",
3408 + "output": [],
3409 + "tools": [],
3410 + "tool_choice": "auto",
3411 + "parallel_tool_calls": true,
3412 + "error": null,
3413 + "incomplete_details": null,
3414 + "metadata": null,
3415 + "instructions": null,
3416 + "temperature": null,
3417 + "top_p": null,
3418 + "max_output_tokens": null
3419 + },
3420 + "sequence_number": 5
3421 + }
3422 + },
3423 + "FailedEvent": {
3424 + "type": "object",
3425 + "properties": {
3426 + "type": {
3427 + "type": "string",
3428 + "enum": [
3429 + "response.failed"
3430 + ]
3431 + },
3432 + "response": {
3433 + "$ref": "#/components/schemas/BaseResponsesResult"
3434 + },
3435 + "sequence_number": {
3436 + "type": "integer"
3437 + }
3438 + },
3439 + "required": [
3440 + "type",
3441 + "response",
3442 + "sequence_number"
3443 + ],
3444 + "description": "Event emitted when a response has failed",
3445 + "example": {
3446 + "type": "response.failed",
3447 + "response": {
3448 + "id": "resp-abc123",
3449 + "object": "response",
3450 + "created_at": 1704067200,
3451 + "model": "gpt-4",
3452 + "status": "failed",
3453 + "output": [],
3454 + "tools": [],
3455 + "tool_choice": "auto",
3456 + "parallel_tool_calls": true,
3457 + "error": null,
3458 + "incomplete_details": null,
3459 + "metadata": null,
3460 + "instructions": null,
3461 + "temperature": null,
3462 + "top_p": null,
3463 + "max_output_tokens": null
3464 + },
3465 + "sequence_number": 3
3466 + }
3467 + },
3468 + "BaseErrorEvent": {
3469 + "type": "object",
3470 + "properties": {
3471 + "type": {
3472 + "type": "string",
3473 + "enum": [
3474 + "error"
3475 + ]
3476 + },
3477 + "code": {
3478 + "type": "string",
3479 + "nullable": true
3480 + },
3481 + "message": {
3482 + "type": "string"
3483 + },
3484 + "param": {
3485 + "type": "string",
3486 + "nullable": true
3487 + },
3488 + "sequence_number": {
3489 + "type": "integer"
3490 + }
3491 + },
3492 + "required": [
3493 + "type",
3494 + "code",
3495 + "message",
3496 + "param",
3497 + "sequence_number"
3498 + ],
3499 + "description": "Event emitted when an error occurs during streaming",
3500 + "example": {
3501 + "type": "error",
3502 + "code": "rate_limit_exceeded",
3503 + "message": "Rate limit exceeded. Please try again later.",
3504 + "param": null,
3505 + "sequence_number": 2
3506 + }
3507 + },
3508 + "ErrorEvent": {
3509 + "allOf": [
3510 + {
3511 + "$ref": "#/components/schemas/BaseErrorEvent"
3512 + },
3513 + {
3514 + "type": "object",
3515 + "properties": {}
3516 + }
3517 + ],
3518 + "description": "Event emitted when an error occurs during streaming",
3519 + "example": {
3520 + "type": "error",
3521 + "code": "rate_limit_exceeded",
3522 + "message": "Rate limit exceeded. Please try again later.",
3523 + "param": null,
3524 + "sequence_number": 2
3525 + }
3526 + },
3527 + "OutputItemAddedEvent": {
3528 + "type": "object",
3529 + "properties": {
3530 + "type": {
3531 + "type": "string",
3532 + "enum": [
3533 + "response.output_item.added"
3534 + ]
3535 + },
3536 + "output_index": {
3537 + "type": "integer"
3538 + },
3539 + "item": {
3540 + "oneOf": [
3541 + {
3542 + "$ref": "#/components/schemas/OutputMessage"
3543 + },
3544 + {
3545 + "$ref": "#/components/schemas/OutputItemReasoning"
3546 + },
3547 + {
3548 + "$ref": "#/components/schemas/OutputItemFunctionCall"
3549 + },
3550 + {
3551 + "$ref": "#/components/schemas/OutputItemWebSearchCall"
3552 + },
3553 + {
3554 + "$ref": "#/components/schemas/OutputItemFileSearchCall"
3555 + },
3556 + {
3557 + "$ref": "#/components/schemas/OutputItemImageGenerationCall"
3558 + }
3559 + ],
3560 + "discriminator": {
3561 + "propertyName": "type",
3562 + "mapping": {
3563 + "message": "#/components/schemas/OutputMessage",
3564 + "reasoning": "#/components/schemas/OutputItemReasoning",
3565 + "function_call": "#/components/schemas/OutputItemFunctionCall",
3566 + "web_search_call": "#/components/schemas/OutputItemWebSearchCall",
3567 + "file_search_call": "#/components/schemas/OutputItemFileSearchCall",
3568 + "image_generation_call": "#/components/schemas/OutputItemImageGenerationCall"
3569 + }
3570 + }
3571 + },
3572 + "sequence_number": {
3573 + "type": "integer"
3574 + }
3575 + },
3576 + "required": [
3577 + "type",
3578 + "output_index",
3579 + "item",
3580 + "sequence_number"
3581 + ],
3582 + "description": "Event emitted when a new output item is added to the response",
3583 + "example": {
3584 + "type": "response.output_item.added",
3585 + "output_index": 0,
3586 + "item": {
3587 + "id": "item-1",
3588 + "type": "message",
3589 + "status": "in_progress",
3590 + "role": "assistant",
3591 + "content": []
3592 + },
3593 + "sequence_number": 2
3594 + }
3595 + },
3596 + "OutputItemDoneEvent": {
3597 + "type": "object",
3598 + "properties": {
3599 + "type": {
3600 + "type": "string",
3601 + "enum": [
3602 + "response.output_item.done"
3603 + ]
3604 + },
3605 + "output_index": {
3606 + "type": "integer"
3607 + },
3608 + "item": {
3609 + "oneOf": [
3610 + {
3611 + "$ref": "#/components/schemas/OutputMessage"
3612 + },
3613 + {
3614 + "$ref": "#/components/schemas/OutputItemReasoning"
3615 + },
3616 + {
3617 + "$ref": "#/components/schemas/OutputItemFunctionCall"
3618 + },
3619 + {
3620 + "$ref": "#/components/schemas/OutputItemWebSearchCall"
3621 + },
3622 + {
3623 + "$ref": "#/components/schemas/OutputItemFileSearchCall"
3624 + },
3625 + {
3626 + "$ref": "#/components/schemas/OutputItemImageGenerationCall"
3627 + }
3628 + ],
3629 + "discriminator": {
3630 + "propertyName": "type",
3631 + "mapping": {
3632 + "message": "#/components/schemas/OutputMessage",
3633 + "reasoning": "#/components/schemas/OutputItemReasoning",
3634 + "function_call": "#/components/schemas/OutputItemFunctionCall",
3635 + "web_search_call": "#/components/schemas/OutputItemWebSearchCall",
3636 + "file_search_call": "#/components/schemas/OutputItemFileSearchCall",
3637 + "image_generation_call": "#/components/schemas/OutputItemImageGenerationCall"
3638 + }
3639 + }
3640 + },
3641 + "sequence_number": {
3642 + "type": "integer"
3643 + }
3644 + },
3645 + "required": [
3646 + "type",
3647 + "output_index",
3648 + "item",
3649 + "sequence_number"
3650 + ],
3651 + "description": "Event emitted when an output item is complete",
3652 + "example": {
3653 + "type": "response.output_item.done",
3654 + "output_index": 0,
3655 + "item": {
3656 + "id": "item-1",
3657 + "type": "message",
3658 + "status": "completed",
3659 + "role": "assistant",
3660 + "content": [
3661 + {
3662 + "type": "output_text",
3663 + "text": "Hello! How can I help you?",
3664 + "annotations": []
3665 + }
3666 + ]
3667 + },
3668 + "sequence_number": 8
3669 + }
3670 + },
3671 + "BaseContentPartAddedEvent": {
3672 + "type": "object",
3673 + "properties": {
3674 + "type": {
3675 + "type": "string",
3676 + "enum": [
3677 + "response.content_part.added"
3678 + ]
3679 + },
3680 + "output_index": {
3681 + "type": "integer"
3682 + },
3683 + "item_id": {
3684 + "type": "string"
3685 + },
3686 + "content_index": {
3687 + "type": "integer"
3688 + },
3689 + "part": {
3690 + "anyOf": [
3691 + {
3692 + "$ref": "#/components/schemas/ResponseOutputText"
3693 + },
3694 + {
3695 + "$ref": "#/components/schemas/OpenAIResponsesRefusalContent"
3696 + }
3697 + ]
3698 + },
3699 + "sequence_number": {
3700 + "type": "integer"
3701 + }
3702 + },
3703 + "required": [
3704 + "type",
3705 + "output_index",
3706 + "item_id",
3707 + "content_index",
3708 + "part",
3709 + "sequence_number"
3710 + ],
3711 + "description": "Event emitted when a new content part is added to an output item",
3712 + "example": {
3713 + "type": "response.content_part.added",
3714 + "output_index": 0,
3715 + "item_id": "item-1",
3716 + "content_index": 0,
3717 + "part": {
3718 + "type": "output_text",
3719 + "text": "",
3720 + "annotations": []
3721 + },
3722 + "sequence_number": 3
3723 + }
3724 + },
3725 + "ContentPartAddedEvent": {
3726 + "allOf": [
3727 + {
3728 + "$ref": "#/components/schemas/BaseContentPartAddedEvent"
3729 + },
3730 + {
3731 + "type": "object",
3732 + "properties": {
3733 + "part": {
3734 + "anyOf": [
3735 + {
3736 + "$ref": "#/components/schemas/ResponseOutputText"
3737 + },
3738 + {
3739 + "$ref": "#/components/schemas/ReasoningTextContent"
3740 + },
3741 + {
3742 + "$ref": "#/components/schemas/OpenAIResponsesRefusalContent"
3743 + }
3744 + ]
3745 + }
3746 + }
3747 + }
3748 + ],
3749 + "description": "Event emitted when a new content part is added to an output item",
3750 + "example": {
3751 + "type": "response.content_part.added",
3752 + "output_index": 0,
3753 + "item_id": "item-1",
3754 + "content_index": 0,
3755 + "part": {
3756 + "type": "output_text",
3757 + "text": "",
3758 + "annotations": []
3759 + },
3760 + "sequence_number": 3
3761 + }
3762 + },
3763 + "BaseContentPartDoneEvent": {
3764 + "type": "object",
3765 + "properties": {
3766 + "type": {
3767 + "type": "string",
3768 + "enum": [
3769 + "response.content_part.done"
3770 + ]
3771 + },
3772 + "output_index": {
3773 + "type": "integer"
3774 + },
3775 + "item_id": {
3776 + "type": "string"
3777 + },
3778 + "content_index": {
3779 + "type": "integer"
3780 + },
3781 + "part": {
3782 + "anyOf": [
3783 + {
3784 + "$ref": "#/components/schemas/ResponseOutputText"
3785 + },
3786 + {
3787 + "$ref": "#/components/schemas/OpenAIResponsesRefusalContent"
3788 + }
3789 + ]
3790 + },
3791 + "sequence_number": {
3792 + "type": "integer"
3793 + }
3794 + },
3795 + "required": [
3796 + "type",
3797 + "output_index",
3798 + "item_id",
3799 + "content_index",
3800 + "part",
3801 + "sequence_number"
3802 + ],
3803 + "description": "Event emitted when a content part is complete",
3804 + "example": {
3805 + "type": "response.content_part.done",
3806 + "output_index": 0,
3807 + "item_id": "item-1",
3808 + "content_index": 0,
3809 + "part": {
3810 + "type": "output_text",
3811 + "text": "Hello! How can I help you?",
3812 + "annotations": []
3813 + },
3814 + "sequence_number": 7
3815 + }
3816 + },
3817 + "ContentPartDoneEvent": {
3818 + "allOf": [
3819 + {
3820 + "$ref": "#/components/schemas/BaseContentPartDoneEvent"
3821 + },
3822 + {
3823 + "type": "object",
3824 + "properties": {
3825 + "part": {
3826 + "anyOf": [
3827 + {
3828 + "$ref": "#/components/schemas/ResponseOutputText"
3829 + },
3830 + {
3831 + "$ref": "#/components/schemas/ReasoningTextContent"
3832 + },
3833 + {
3834 + "$ref": "#/components/schemas/OpenAIResponsesRefusalContent"
3835 + }
3836 + ]
3837 + }
3838 + }
3839 + }
3840 + ],
3841 + "description": "Event emitted when a content part is complete",
3842 + "example": {
3843 + "type": "response.content_part.done",
3844 + "output_index": 0,
3845 + "item_id": "item-1",
3846 + "content_index": 0,
3847 + "part": {
3848 + "type": "output_text",
3849 + "text": "Hello! How can I help you?",
3850 + "annotations": []
3851 + },
3852 + "sequence_number": 7
3853 + }
3854 + },
3855 + "OpenResponsesTopLogprobs": {
3856 + "type": "object",
3857 + "properties": {
3858 + "token": {
3859 + "type": "string"
3860 + },
3861 + "logprob": {
3862 + "type": "number"
3863 + },
3864 + "bytes": {
3865 + "type": "array",
3866 + "items": {
3867 + "type": "number"
3868 + }
3869 + }
3870 + },
3871 + "description": "Alternative token with its log probability",
3872 + "example": {
3873 + "token": "hello",
3874 + "logprob": -0.5
3875 + }
3876 + },
3877 + "OpenResponsesLogProbs": {
3878 + "type": "object",
3879 + "properties": {
3880 + "logprob": {
3881 + "type": "number"
3882 + },
3883 + "token": {
3884 + "type": "string"
3885 + },
3886 + "top_logprobs": {
3887 + "type": "array",
3888 + "items": {
3889 + "$ref": "#/components/schemas/OpenResponsesTopLogprobs"
3890 + }
3891 + },
3892 + "bytes": {
3893 + "type": "array",
3894 + "items": {
3895 + "type": "number"
3896 + }
3897 + }
3898 + },
3899 + "required": [
3900 + "logprob",
3901 + "token"
3902 + ],
3903 + "description": "Log probability information for a token",
3904 + "example": {
3905 + "logprob": -0.1,
3906 + "token": "world",
3907 + "top_logprobs": [
3908 + {
3909 + "token": "hello",
3910 + "logprob": -0.5
3911 + }
3912 + ]
3913 + }
3914 + },
3915 + "BaseTextDeltaEvent": {
3916 + "type": "object",
3917 + "properties": {
3918 + "type": {
3919 + "type": "string",
3920 + "enum": [
3921 + "response.output_text.delta"
3922 + ]
3923 + },
3924 + "logprobs": {
3925 + "type": "array",
3926 + "items": {
3927 + "$ref": "#/components/schemas/OpenResponsesLogProbs"
3928 + }
3929 + },
3930 + "output_index": {
3931 + "type": "integer"
3932 + },
3933 + "item_id": {
3934 + "type": "string"
3935 + },
3936 + "content_index": {
3937 + "type": "integer"
3938 + },
3939 + "delta": {
3940 + "type": "string"
3941 + },
3942 + "sequence_number": {
3943 + "type": "integer"
3944 + }
3945 + },
3946 + "required": [
3947 + "type",
3948 + "logprobs",
3949 + "output_index",
3950 + "item_id",
3951 + "content_index",
3952 + "delta",
3953 + "sequence_number"
3954 + ],
3955 + "description": "Event emitted when a text delta is streamed",
3956 + "example": {
3957 + "type": "response.output_text.delta",
3958 + "logprobs": [],
3959 + "output_index": 0,
3960 + "item_id": "item-1",
3961 + "content_index": 0,
3962 + "delta": "Hello",
3963 + "sequence_number": 4
3964 + }
3965 + },
3966 + "TextDeltaEvent": {
3967 + "allOf": [
3968 + {
3969 + "$ref": "#/components/schemas/BaseTextDeltaEvent"
3970 + },
3971 + {
3972 + "type": "object",
3973 + "properties": {
3974 + "logprobs": {
3975 + "type": "array",
3976 + "items": {
3977 + "allOf": [
3978 + {
3979 + "$ref": "#/components/schemas/OpenResponsesLogProbs"
3980 + },
3981 + {
3982 + "type": "object",
3983 + "properties": {
3984 + "top_logprobs": {
3985 + "type": "array",
3986 + "items": {
3987 + "allOf": [
3988 + {
3989 + "$ref": "#/components/schemas/OpenResponsesTopLogprobs"
3990 + },
3991 + {
3992 + "type": "object",
3993 + "properties": {
3994 + "bytes": {
3995 + "type": "array",
3996 + "items": {
3997 + "type": "integer"
3998 + }
3999 + }
4000 + }
4001 + }
4002 + ],
4003 + "description": "Alternative token with its log probability",
4004 + "example": {
4005 + "token": "hello",
4006 + "logprob": -0.5
4007 + }
4008 + }
4009 + },
4010 + "bytes": {
4011 + "type": "array",
4012 + "items": {
4013 + "type": "integer"
4014 + }
4015 + }
4016 + }
4017 + }
4018 + ],
4019 + "description": "Log probability information for a token",
4020 + "example": {
4021 + "logprob": -0.1,
4022 + "token": "world",
4023 + "top_logprobs": [
4024 + {
4025 + "token": "hello",
4026 + "logprob": -0.5
4027 + }
4028 + ]
4029 + }
4030 + }
4031 + }
4032 + }
4033 + }
4034 + ],
4035 + "description": "Event emitted when a text delta is streamed",
4036 + "example": {
4037 + "type": "response.output_text.delta",
4038 + "logprobs": [],
4039 + "output_index": 0,
4040 + "item_id": "item-1",
4041 + "content_index": 0,
4042 + "delta": "Hello",
4043 + "sequence_number": 4
4044 + }
4045 + },
4046 + "BaseTextDoneEvent": {
4047 + "type": "object",
4048 + "properties": {
4049 + "type": {
4050 + "type": "string",
4051 + "enum": [
4052 + "response.output_text.done"
4053 + ]
4054 + },
4055 + "output_index": {
4056 + "type": "integer"
4057 + },
4058 + "item_id": {
4059 + "type": "string"
4060 + },
4061 + "content_index": {
4062 + "type": "integer"
4063 + },
4064 + "text": {
4065 + "type": "string"
4066 + },
4067 + "sequence_number": {
4068 + "type": "integer"
4069 + },
4070 + "logprobs": {
4071 + "type": "array",
4072 + "items": {
4073 + "$ref": "#/components/schemas/OpenResponsesLogProbs"
4074 + }
4075 + }
4076 + },
4077 + "required": [
4078 + "type",
4079 + "output_index",
4080 + "item_id",
4081 + "content_index",
4082 + "text",
4083 + "sequence_number",
4084 + "logprobs"
4085 + ],
4086 + "description": "Event emitted when text streaming is complete",
4087 + "example": {
4088 + "type": "response.output_text.done",
4089 + "output_index": 0,
4090 + "item_id": "item-1",
4091 + "content_index": 0,
4092 + "text": "Hello! How can I help you?",
4093 + "sequence_number": 6,
4094 + "logprobs": []
4095 + }
4096 + },
4097 + "TextDoneEvent": {
4098 + "allOf": [
4099 + {
4100 + "$ref": "#/components/schemas/BaseTextDoneEvent"
4101 + },
4102 + {
4103 + "type": "object",
4104 + "properties": {
4105 + "logprobs": {
4106 + "type": "array",
4107 + "items": {
4108 + "allOf": [
4109 + {
4110 + "$ref": "#/components/schemas/OpenResponsesLogProbs"
4111 + },
4112 + {
4113 + "type": "object",
4114 + "properties": {
4115 + "top_logprobs": {
4116 + "type": "array",
4117 + "items": {
4118 + "allOf": [
4119 + {
4120 + "$ref": "#/components/schemas/OpenResponsesTopLogprobs"
4121 + },
4122 + {
4123 + "type": "object",
4124 + "properties": {
4125 + "bytes": {
4126 + "type": "array",
4127 + "items": {
4128 + "type": "integer"
4129 + }
4130 + }
4131 + }
4132 + }
4133 + ],
4134 + "description": "Alternative token with its log probability",
4135 + "example": {
4136 + "token": "hello",
4137 + "logprob": -0.5
4138 + }
4139 + }
4140 + },
4141 + "bytes": {
4142 + "type": "array",
4143 + "items": {
4144 + "type": "integer"
4145 + }
4146 + }
4147 + }
4148 + }
4149 + ],
4150 + "description": "Log probability information for a token",
4151 + "example": {
4152 + "logprob": -0.1,
4153 + "token": "world",
4154 + "top_logprobs": [
4155 + {
4156 + "token": "hello",
4157 + "logprob": -0.5
4158 + }
4159 + ]
4160 + }
4161 + }
4162 + }
4163 + }
4164 + }
4165 + ],
4166 + "description": "Event emitted when text streaming is complete",
4167 + "example": {
4168 + "type": "response.output_text.done",
4169 + "output_index": 0,
4170 + "item_id": "item-1",
4171 + "content_index": 0,
4172 + "text": "Hello! How can I help you?",
4173 + "sequence_number": 6,
4174 + "logprobs": []
4175 + }
4176 + },
4177 + "BaseRefusalDeltaEvent": {
4178 + "type": "object",
4179 + "properties": {
4180 + "type": {
4181 + "type": "string",
4182 + "enum": [
4183 + "response.refusal.delta"
4184 + ]
4185 + },
4186 + "output_index": {
4187 + "type": "integer"
4188 + },
4189 + "item_id": {
4190 + "type": "string"
4191 + },
4192 + "content_index": {
4193 + "type": "integer"
4194 + },
4195 + "delta": {
4196 + "type": "string"
4197 + },
4198 + "sequence_number": {
4199 + "type": "integer"
4200 + }
4201 + },
4202 + "required": [
4203 + "type",
4204 + "output_index",
4205 + "item_id",
4206 + "content_index",
4207 + "delta",
4208 + "sequence_number"
4209 + ],
4210 + "description": "Event emitted when a refusal delta is streamed",
4211 + "example": {
4212 + "type": "response.refusal.delta",
4213 + "output_index": 0,
4214 + "item_id": "item-1",
4215 + "content_index": 0,
4216 + "delta": "I'm sorry",
4217 + "sequence_number": 4
4218 + }
4219 + },
4220 + "RefusalDeltaEvent": {
4221 + "allOf": [
4222 + {
4223 + "$ref": "#/components/schemas/BaseRefusalDeltaEvent"
4224 + },
4225 + {
4226 + "type": "object",
4227 + "properties": {}
4228 + }
4229 + ],
4230 + "description": "Event emitted when a refusal delta is streamed",
4231 + "example": {
4232 + "type": "response.refusal.delta",
4233 + "output_index": 0,
4234 + "item_id": "item-1",
4235 + "content_index": 0,
4236 + "delta": "I'm sorry",
4237 + "sequence_number": 4
4238 + }
4239 + },
4240 + "BaseRefusalDoneEvent": {
4241 + "type": "object",
4242 + "properties": {
4243 + "type": {
4244 + "type": "string",
4245 + "enum": [
4246 + "response.refusal.done"
4247 + ]
4248 + },
4249 + "output_index": {
4250 + "type": "integer"
4251 + },
4252 + "item_id": {
4253 + "type": "string"
4254 + },
4255 + "content_index": {
4256 + "type": "integer"
4257 + },
4258 + "refusal": {
4259 + "type": "string"
4260 + },
4261 + "sequence_number": {
4262 + "type": "integer"
4263 + }
4264 + },
4265 + "required": [
4266 + "type",
4267 + "output_index",
4268 + "item_id",
4269 + "content_index",
4270 + "refusal",
4271 + "sequence_number"
4272 + ],
4273 + "description": "Event emitted when refusal streaming is complete",
4274 + "example": {
4275 + "type": "response.refusal.done",
4276 + "output_index": 0,
4277 + "item_id": "item-1",
4278 + "content_index": 0,
4279 + "refusal": "I'm sorry, but I can't assist with that request.",
4280 + "sequence_number": 6
4281 + }
4282 + },
4283 + "RefusalDoneEvent": {
4284 + "allOf": [
4285 + {
4286 + "$ref": "#/components/schemas/BaseRefusalDoneEvent"
4287 + },
4288 + {
4289 + "type": "object",
4290 + "properties": {}
4291 + }
4292 + ],
4293 + "description": "Event emitted when refusal streaming is complete",
4294 + "example": {
4295 + "type": "response.refusal.done",
4296 + "output_index": 0,
4297 + "item_id": "item-1",
4298 + "content_index": 0,
4299 + "refusal": "I'm sorry, but I can't assist with that request.",
4300 + "sequence_number": 6
4301 + }
4302 + },
4303 + "BaseAnnotationAddedEvent": {
4304 + "type": "object",
4305 + "properties": {
4306 + "type": {
4307 + "type": "string",
4308 + "enum": [
4309 + "response.output_text.annotation.added"
4310 + ]
4311 + },
4312 + "output_index": {
4313 + "type": "integer"
4314 + },
4315 + "item_id": {
4316 + "type": "string"
4317 + },
4318 + "content_index": {
4319 + "type": "integer"
4320 + },
4321 + "sequence_number": {
4322 + "type": "integer"
4323 + },
4324 + "annotation_index": {
4325 + "type": "integer"
4326 + },
4327 + "annotation": {
4328 + "$ref": "#/components/schemas/OpenAIResponsesAnnotation"
4329 + }
4330 + },
4331 + "required": [
4332 + "type",
4333 + "output_index",
4334 + "item_id",
4335 + "content_index",
4336 + "sequence_number",
4337 + "annotation_index",
4338 + "annotation"
4339 + ],
4340 + "description": "Event emitted when a text annotation is added to output",
4341 + "example": {
4342 + "type": "response.output_text.annotation.added",
4343 + "output_index": 0,
4344 + "item_id": "item-1",
4345 + "content_index": 0,
4346 + "sequence_number": 5,
4347 + "annotation_index": 0,
4348 + "annotation": {
4349 + "type": "url_citation",
4350 + "url": "https://example.com",
4351 + "title": "Example",
4352 + "start_index": 0,
4353 + "end_index": 7
4354 + }
4355 + }
4356 + },
4357 + "AnnotationAddedEvent": {
4358 + "allOf": [
4359 + {
4360 + "$ref": "#/components/schemas/BaseAnnotationAddedEvent"
4361 + },
4362 + {
4363 + "type": "object",
4364 + "properties": {}
4365 + }
4366 + ],
4367 + "description": "Event emitted when a text annotation is added to output",
4368 + "example": {
4369 + "type": "response.output_text.annotation.added",
4370 + "output_index": 0,
4371 + "item_id": "item-1",
4372 + "content_index": 0,
4373 + "sequence_number": 5,
4374 + "annotation_index": 0,
4375 + "annotation": {
4376 + "type": "url_citation",
4377 + "url": "https://example.com",
4378 + "title": "Example",
4379 + "start_index": 0,
4380 + "end_index": 7
4381 + }
4382 + }
4383 + },
4384 + "BaseFunctionCallArgsDeltaEvent": {
4385 + "type": "object",
4386 + "properties": {
4387 + "type": {
4388 + "type": "string",
4389 + "enum": [
4390 + "response.function_call_arguments.delta"
4391 + ]
4392 + },
4393 + "item_id": {
4394 + "type": "string"
4395 + },
4396 + "output_index": {
4397 + "type": "integer"
4398 + },
4399 + "delta": {
4400 + "type": "string"
4401 + },
4402 + "sequence_number": {
4403 + "type": "integer"
4404 + }
4405 + },
4406 + "required": [
4407 + "type",
4408 + "item_id",
4409 + "output_index",
4410 + "delta",
4411 + "sequence_number"
4412 + ],
4413 + "description": "Event emitted when function call arguments are being streamed",
4414 + "example": {
4415 + "type": "response.function_call_arguments.delta",
4416 + "item_id": "item-1",
4417 + "output_index": 0,
4418 + "delta": "{\"city\": \"San",
4419 + "sequence_number": 4
4420 + }
4421 + },
4422 + "FunctionCallArgsDeltaEvent": {
4423 + "allOf": [
4424 + {
4425 + "$ref": "#/components/schemas/BaseFunctionCallArgsDeltaEvent"
4426 + },
4427 + {
4428 + "type": "object",
4429 + "properties": {}
4430 + }
4431 + ],
4432 + "description": "Event emitted when function call arguments are being streamed",
4433 + "example": {
4434 + "type": "response.function_call_arguments.delta",
4435 + "item_id": "item-1",
4436 + "output_index": 0,
4437 + "delta": "{\"city\": \"San",
4438 + "sequence_number": 4
4439 + }
4440 + },
4441 + "BaseFunctionCallArgsDoneEvent": {
4442 + "type": "object",
4443 + "properties": {
4444 + "type": {
4445 + "type": "string",
4446 + "enum": [
4447 + "response.function_call_arguments.done"
4448 + ]
4449 + },
4450 + "item_id": {
4451 + "type": "string"
4452 + },
4453 + "output_index": {
4454 + "type": "integer"
4455 + },
4456 + "name": {
4457 + "type": "string"
4458 + },
4459 + "arguments": {
4460 + "type": "string"
4461 + },
4462 + "sequence_number": {
4463 + "type": "integer"
4464 + }
4465 + },
4466 + "required": [
4467 + "type",
4468 + "item_id",
4469 + "output_index",
4470 + "name",
4471 + "arguments",
4472 + "sequence_number"
4473 + ],
4474 + "description": "Event emitted when function call arguments streaming is complete",
4475 + "example": {
4476 + "type": "response.function_call_arguments.done",
4477 + "item_id": "item-1",
4478 + "output_index": 0,
4479 + "name": "get_weather",
4480 + "arguments": "{\"city\": \"San Francisco\", \"units\": \"celsius\"}",
4481 + "sequence_number": 6
4482 + }
4483 + },
4484 + "FunctionCallArgsDoneEvent": {
4485 + "allOf": [
4486 + {
4487 + "$ref": "#/components/schemas/BaseFunctionCallArgsDoneEvent"
4488 + },
4489 + {
4490 + "type": "object",
4491 + "properties": {}
4492 + }
4493 + ],
4494 + "description": "Event emitted when function call arguments streaming is complete",
4495 + "example": {
4496 + "type": "response.function_call_arguments.done",
4497 + "item_id": "item-1",
4498 + "output_index": 0,
4499 + "name": "get_weather",
4500 + "arguments": "{\"city\": \"San Francisco\", \"units\": \"celsius\"}",
4501 + "sequence_number": 6
4502 + }
4503 + },
4504 + "BaseReasoningDeltaEvent": {
4505 + "type": "object",
4506 + "properties": {
4507 + "type": {
4508 + "type": "string",
4509 + "enum": [
4510 + "response.reasoning_text.delta"
4511 + ]
4512 + },
4513 + "output_index": {
4514 + "type": "integer"
4515 + },
4516 + "item_id": {
4517 + "type": "string"
4518 + },
4519 + "content_index": {
4520 + "type": "integer"
4521 + },
4522 + "delta": {
4523 + "type": "string"
4524 + },
4525 + "sequence_number": {
4526 + "type": "integer"
4527 + }
4528 + },
4529 + "required": [
4530 + "type",
4531 + "output_index",
4532 + "item_id",
4533 + "content_index",
4534 + "delta",
4535 + "sequence_number"
4536 + ],
4537 + "description": "Event emitted when reasoning text delta is streamed",
4538 + "example": {
4539 + "type": "response.reasoning_text.delta",
4540 + "output_index": 0,
4541 + "item_id": "item-1",
4542 + "content_index": 0,
4543 + "delta": "First, we need",
4544 + "sequence_number": 4
4545 + }
4546 + },
4547 + "ReasoningDeltaEvent": {
4548 + "allOf": [
4549 + {
4550 + "$ref": "#/components/schemas/BaseReasoningDeltaEvent"
4551 + },
4552 + {
4553 + "type": "object",
4554 + "properties": {}
4555 + }
4556 + ],
4557 + "description": "Event emitted when reasoning text delta is streamed",
4558 + "example": {
4559 + "type": "response.reasoning_text.delta",
4560 + "output_index": 0,
4561 + "item_id": "item-1",
4562 + "content_index": 0,
4563 + "delta": "First, we need",
4564 + "sequence_number": 4
4565 + }
4566 + },
4567 + "BaseReasoningDoneEvent": {
4568 + "type": "object",
4569 + "properties": {
4570 + "type": {
4571 + "type": "string",
4572 + "enum": [
4573 + "response.reasoning_text.done"
4574 + ]
4575 + },
4576 + "output_index": {
4577 + "type": "integer"
4578 + },
4579 + "item_id": {
4580 + "type": "string"
4581 + },
4582 + "content_index": {
4583 + "type": "integer"
4584 + },
4585 + "text": {
4586 + "type": "string"
4587 + },
4588 + "sequence_number": {
4589 + "type": "integer"
4590 + }
4591 + },
4592 + "required": [
4593 + "type",
4594 + "output_index",
4595 + "item_id",
4596 + "content_index",
4597 + "text",
4598 + "sequence_number"
4599 + ],
4600 + "description": "Event emitted when reasoning text streaming is complete",
4601 + "example": {
4602 + "type": "response.reasoning_text.done",
4603 + "output_index": 0,
4604 + "item_id": "item-1",
4605 + "content_index": 0,
4606 + "text": "First, we need to identify the key components and then combine them logically.",
4607 + "sequence_number": 6
4608 + }
4609 + },
4610 + "ReasoningDoneEvent": {
4611 + "allOf": [
4612 + {
4613 + "$ref": "#/components/schemas/BaseReasoningDoneEvent"
4614 + },
4615 + {
4616 + "type": "object",
4617 + "properties": {}
4618 + }
4619 + ],
4620 + "description": "Event emitted when reasoning text streaming is complete",
4621 + "example": {
4622 + "type": "response.reasoning_text.done",
4623 + "output_index": 0,
4624 + "item_id": "item-1",
4625 + "content_index": 0,
4626 + "text": "First, we need to identify the key components and then combine them logically.",
4627 + "sequence_number": 6
4628 + }
4629 + },
4630 + "BaseReasoningSummaryPartAddedEvent": {
4631 + "type": "object",
4632 + "properties": {
4633 + "type": {
4634 + "type": "string",
4635 + "enum": [
4636 + "response.reasoning_summary_part.added"
4637 + ]
4638 + },
4639 + "output_index": {
4640 + "type": "integer"
4641 + },
4642 + "item_id": {
4643 + "type": "string"
4644 + },
4645 + "summary_index": {
4646 + "type": "integer"
4647 + },
4648 + "part": {
4649 + "$ref": "#/components/schemas/ReasoningSummaryText"
4650 + },
4651 + "sequence_number": {
4652 + "type": "integer"
4653 + }
4654 + },
4655 + "required": [
4656 + "type",
4657 + "output_index",
4658 + "item_id",
4659 + "summary_index",
4660 + "part",
4661 + "sequence_number"
4662 + ],
4663 + "description": "Event emitted when a reasoning summary part is added",
4664 + "example": {
4665 + "type": "response.reasoning_summary_part.added",
4666 + "output_index": 0,
4667 + "item_id": "item-1",
4668 + "summary_index": 0,
4669 + "part": {
4670 + "type": "summary_text",
4671 + "text": ""
4672 + },
4673 + "sequence_number": 3
4674 + }
4675 + },
4676 + "ReasoningSummaryPartAddedEvent": {
4677 + "allOf": [
4678 + {
4679 + "$ref": "#/components/schemas/BaseReasoningSummaryPartAddedEvent"
4680 + },
4681 + {
4682 + "type": "object",
4683 + "properties": {}
4684 + }
4685 + ],
4686 + "description": "Event emitted when a reasoning summary part is added",
4687 + "example": {
4688 + "type": "response.reasoning_summary_part.added",
4689 + "output_index": 0,
4690 + "item_id": "item-1",
4691 + "summary_index": 0,
4692 + "part": {
4693 + "type": "summary_text",
4694 + "text": ""
4695 + },
4696 + "sequence_number": 3
4697 + }
4698 + },
4699 + "BaseReasoningSummaryPartDoneEvent": {
4700 + "type": "object",
4701 + "properties": {
4702 + "type": {
4703 + "type": "string",
4704 + "enum": [
4705 + "response.reasoning_summary_part.done"
4706 + ]
4707 + },
4708 + "output_index": {
4709 + "type": "integer"
4710 + },
4711 + "item_id": {
4712 + "type": "string"
4713 + },
4714 + "summary_index": {
4715 + "type": "integer"
4716 + },
4717 + "part": {
4718 + "$ref": "#/components/schemas/ReasoningSummaryText"
4719 + },
4720 + "sequence_number": {
4721 + "type": "integer"
4722 + }
4723 + },
4724 + "required": [
4725 + "type",
4726 + "output_index",
4727 + "item_id",
4728 + "summary_index",
4729 + "part",
4730 + "sequence_number"
4731 + ],
4732 + "description": "Event emitted when a reasoning summary part is complete",
4733 + "example": {
4734 + "type": "response.reasoning_summary_part.done",
4735 + "output_index": 0,
4736 + "item_id": "item-1",
4737 + "summary_index": 0,
4738 + "part": {
4739 + "type": "summary_text",
4740 + "text": "Analyzing the problem step by step to find the optimal solution."
4741 + },
4742 + "sequence_number": 7
4743 + }
4744 + },
4745 + "ReasoningSummaryPartDoneEvent": {
4746 + "allOf": [
4747 + {
4748 + "$ref": "#/components/schemas/BaseReasoningSummaryPartDoneEvent"
4749 + },
4750 + {
4751 + "type": "object",
4752 + "properties": {}
4753 + }
4754 + ],
4755 + "description": "Event emitted when a reasoning summary part is complete",
4756 + "example": {
4757 + "type": "response.reasoning_summary_part.done",
4758 + "output_index": 0,
4759 + "item_id": "item-1",
4760 + "summary_index": 0,
4761 + "part": {
4762 + "type": "summary_text",
4763 + "text": "Analyzing the problem step by step to find the optimal solution."
4764 + },
4765 + "sequence_number": 7
4766 + }
4767 + },
4768 + "BaseReasoningSummaryTextDeltaEvent": {
4769 + "type": "object",
4770 + "properties": {
4771 + "type": {
4772 + "type": "string",
4773 + "enum": [
4774 + "response.reasoning_summary_text.delta"
4775 + ]
4776 + },
4777 + "item_id": {
4778 + "type": "string"
4779 + },
4780 + "output_index": {
4781 + "type": "integer"
4782 + },
4783 + "summary_index": {
4784 + "type": "integer"
4785 + },
4786 + "delta": {
4787 + "type": "string"
4788 + },
4789 + "sequence_number": {
4790 + "type": "integer"
4791 + }
4792 + },
4793 + "required": [
4794 + "type",
4795 + "item_id",
4796 + "output_index",
4797 + "summary_index",
4798 + "delta",
4799 + "sequence_number"
4800 + ],
4801 + "description": "Event emitted when reasoning summary text delta is streamed",
4802 + "example": {
4803 + "type": "response.reasoning_summary_text.delta",
4804 + "item_id": "item-1",
4805 + "output_index": 0,
4806 + "summary_index": 0,
4807 + "delta": "Analyzing",
4808 + "sequence_number": 4
4809 + }
4810 + },
4811 + "ReasoningSummaryTextDeltaEvent": {
4812 + "allOf": [
4813 + {
4814 + "$ref": "#/components/schemas/BaseReasoningSummaryTextDeltaEvent"
4815 + },
4816 + {
4817 + "type": "object",
4818 + "properties": {}
4819 + }
4820 + ],
4821 + "description": "Event emitted when reasoning summary text delta is streamed",
4822 + "example": {
4823 + "type": "response.reasoning_summary_text.delta",
4824 + "item_id": "item-1",
4825 + "output_index": 0,
4826 + "summary_index": 0,
4827 + "delta": "Analyzing",
4828 + "sequence_number": 4
4829 + }
4830 + },
4831 + "BaseReasoningSummaryTextDoneEvent": {
4832 + "type": "object",
4833 + "properties": {
4834 + "type": {
4835 + "type": "string",
4836 + "enum": [
4837 + "response.reasoning_summary_text.done"
4838 + ]
4839 + },
4840 + "item_id": {
4841 + "type": "string"
4842 + },
4843 + "output_index": {
4844 + "type": "integer"
4845 + },
4846 + "summary_index": {
4847 + "type": "integer"
4848 + },
4849 + "text": {
4850 + "type": "string"
4851 + },
4852 + "sequence_number": {
4853 + "type": "integer"
4854 + }
4855 + },
4856 + "required": [
4857 + "type",
4858 + "item_id",
4859 + "output_index",
4860 + "summary_index",
4861 + "text",
4862 + "sequence_number"
4863 + ],
4864 + "description": "Event emitted when reasoning summary text streaming is complete",
4865 + "example": {
4866 + "type": "response.reasoning_summary_text.done",
4867 + "item_id": "item-1",
4868 + "output_index": 0,
4869 + "summary_index": 0,
4870 + "text": "Analyzing the problem step by step to find the optimal solution.",
4871 + "sequence_number": 6
4872 + }
4873 + },
4874 + "ReasoningSummaryTextDoneEvent": {
4875 + "allOf": [
4876 + {
4877 + "$ref": "#/components/schemas/BaseReasoningSummaryTextDoneEvent"
4878 + },
4879 + {
4880 + "type": "object",
4881 + "properties": {}
4882 + }
4883 + ],
4884 + "description": "Event emitted when reasoning summary text streaming is complete",
4885 + "example": {
4886 + "type": "response.reasoning_summary_text.done",
4887 + "item_id": "item-1",
4888 + "output_index": 0,
4889 + "summary_index": 0,
4890 + "text": "Analyzing the problem step by step to find the optimal solution.",
4891 + "sequence_number": 6
4892 + }
4893 + },
4894 + "OpenAIResponsesImageGenCallInProgress": {
4895 + "type": "object",
4896 + "properties": {
4897 + "type": {
4898 + "type": "string",
4899 + "enum": [
4900 + "response.image_generation_call.in_progress"
4901 + ]
4902 + },
4903 + "item_id": {
4904 + "type": "string"
4905 + },
4906 + "output_index": {
4907 + "type": "number"
4908 + },
4909 + "sequence_number": {
4910 + "type": "number"
4911 + }
4912 + },
4913 + "required": [
4914 + "type",
4915 + "item_id",
4916 + "output_index",
4917 + "sequence_number"
4918 + ]
4919 + },
4920 + "ImageGenCallInProgressEvent": {
4921 + "allOf": [
4922 + {
4923 + "$ref": "#/components/schemas/OpenAIResponsesImageGenCallInProgress"
4924 + },
4925 + {
4926 + "type": "object",
4927 + "properties": {}
4928 + }
4929 + ],
4930 + "description": "Image generation call in progress",
4931 + "example": {
4932 + "type": "response.image_generation_call.in_progress",
4933 + "output_index": 0,
4934 + "sequence_number": 1,
4935 + "item_id": "call-123"
4936 + }
4937 + },
4938 + "OpenAIResponsesImageGenCallGenerating": {
4939 + "type": "object",
4940 + "properties": {
4941 + "type": {
4942 + "type": "string",
4943 + "enum": [
4944 + "response.image_generation_call.generating"
4945 + ]
4946 + },
4947 + "item_id": {
4948 + "type": "string"
4949 + },
4950 + "output_index": {
4951 + "type": "number"
4952 + },
4953 + "sequence_number": {
4954 + "type": "number"
4955 + }
4956 + },
4957 + "required": [
4958 + "type",
4959 + "item_id",
4960 + "output_index",
4961 + "sequence_number"
4962 + ]
4963 + },
4964 + "ImageGenCallGeneratingEvent": {
4965 + "allOf": [
4966 + {
4967 + "$ref": "#/components/schemas/OpenAIResponsesImageGenCallGenerating"
4968 + },
4969 + {
4970 + "type": "object",
4971 + "properties": {}
4972 + }
4973 + ],
4974 + "description": "Image generation call is generating",
4975 + "example": {
4976 + "type": "response.image_generation_call.generating",
4977 + "output_index": 0,
4978 + "sequence_number": 2,
4979 + "item_id": "call-123"
4980 + }
4981 + },
4982 + "OpenAIResponsesImageGenCallPartialImage": {
4983 + "type": "object",
4984 + "properties": {
4985 + "type": {
4986 + "type": "string",
4987 + "enum": [
4988 + "response.image_generation_call.partial_image"
4989 + ]
4990 + },
4991 + "item_id": {
4992 + "type": "string"
4993 + },
4994 + "output_index": {
4995 + "type": "number"
4996 + },
4997 + "sequence_number": {
4998 + "type": "number"
4999 + },

This file is too large to show in full.

ios/Podfile.lock
-18
@@ -1,6 +1,4 @@
1 PODS:
2 - - device_info_plus (0.0.1):
3 - - Flutter
2 - DKImagePickerController/Core (4.3.9):
3 - DKImagePickerController/ImageDataManager
4 - DKImagePickerController/Resource
@@ -40,23 +38,16 @@ PODS:
38 - Flutter
39 - haptic_feedback (0.5.1):
40 - Flutter
43 - - irondash_engine_context (0.0.1):
44 - - Flutter
41 - SDWebImage (5.21.7):
42 - SDWebImage/Core (= 5.21.7)
43 - SDWebImage/Core (5.21.7)
48 - - super_native_extensions (0.0.1):
49 - - Flutter
44 - SwiftyGif (5.4.5)
45
46 DEPENDENCIES:
53 - - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
47 - file_picker (from `.symlinks/plugins/file_picker/ios`)
48 - Flutter (from `Flutter`)
49 - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
50 - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
58 - - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
59 - - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
51
52 SPEC REPOS:
53 trunk:
@@ -66,8 +57,6 @@ SPEC REPOS:
57 - SwiftyGif
58
59 EXTERNAL SOURCES:
69 - device_info_plus:
70 - :path: ".symlinks/plugins/device_info_plus/ios"
60 file_picker:
61 :path: ".symlinks/plugins/file_picker/ios"
62 Flutter:
@@ -76,22 +65,15 @@ EXTERNAL SOURCES:
65 :path: ".symlinks/plugins/flutter_tts/ios"
66 haptic_feedback:
67 :path: ".symlinks/plugins/haptic_feedback/ios"
79 - irondash_engine_context:
80 - :path: ".symlinks/plugins/irondash_engine_context/ios"
81 - super_native_extensions:
82 - :path: ".symlinks/plugins/super_native_extensions/ios"
68
69 SPEC CHECKSUMS:
85 - device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
70 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
71 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
72 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
73 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
74 flutter_tts: 91ad1884033cd95ef6e9338754adad54f01f090f
75 haptic_feedback: a366a27ec760f0e865651605b95de8a9bb04ef01
92 - irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
76 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
94 - super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
77 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
78
79 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
ios/Runner/Info.plist
+1 -1
@@ -14,7 +14,7 @@
14 <key>CFBundleDevelopmentRegion</key>
15 <string>$(DEVELOPMENT_LANGUAGE)</string>
16 <key>CFBundleDisplayName</key>
17 - <string>Localmind</string>
17 + <string>LocalMind</string>
18 <key>CFBundleExecutable</key>
19 <string>$(EXECUTABLE_NAME)</string>
20 <key>CFBundleIdentifier</key>
lib/core/logger/app_logger.dart new
+47
@@ -0,0 +1,47 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:logger/logger.dart';
3 +
4 +class Log {
5 + late Logger _logger;
6 + static const lineLength = 80;
7 +
8 + Log._internal() {
9 + if (kDebugMode) {
10 + _logger = Logger(
11 + printer: PrettyPrinter(
12 + methodCount: 2,
13 + errorMethodCount: 8,
14 + lineLength: lineLength,
15 + colors: true,
16 + printEmojis: true,
17 + dateTimeFormat: DateTimeFormat.onlyTime,
18 + ),
19 + );
20 + } else {
21 + _logger = Logger(
22 + printer: PrettyPrinter(
23 + methodCount: 0,
24 + errorMethodCount: 0,
25 + lineLength: 0,
26 + colors: false,
27 + printEmojis: false,
28 + dateTimeFormat: DateTimeFormat.onlyTime,
29 + ),
30 + level: Level.off,
31 + );
32 + }
33 + }
34 +
35 + static final Log _singleton = Log._internal();
36 +
37 + static void fatal({required Object error, required StackTrace stackTrace}) =>
38 + _singleton._logger.f('Fatal', error: error, stackTrace: stackTrace);
39 +
40 + static void debug(String message) => _singleton._logger.d(message);
41 +
42 + static void info(String message) => _singleton._logger.i(message);
43 +
44 + static void error(String message) => _singleton._logger.e(message);
45 +
46 + static void warning(String message) => _singleton._logger.w(message);
47 +}
lib/core/providers/app_providers.dart
+4 -2
@@ -75,8 +75,10 @@ class SettingsNotifier extends Notifier<AppSettings> {
75 void setHasCompletedOnboarding(bool value) =>
76 _update(state.copyWith(hasCompletedOnboarding: value));
77 void setMcpEnabled(bool value) => _update(state.copyWith(mcpEnabled: value));
78 - void setCodeTheme(SyntaxThemeName value) =>
79 - _update(state.copyWith(codeTheme: value));
78 + void setCodeThemeDark(SyntaxThemeName value) =>
79 + _update(state.copyWith(codeThemeDark: value));
80 + void setCodeThemeLight(SyntaxThemeName value) =>
81 + _update(state.copyWith(codeThemeLight: value));
82
83 Future<void> _update(AppSettings updated) async {
84 state = updated;
lib/core/providers/storage_providers.dart
+6
@@ -1,4 +1,6 @@
1 +import 'dart:io';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 +import 'package:path_provider/path_provider.dart';
4 import 'package:hive_ce/hive.dart';
5 import 'package:localmind/features/conversations/data/models/conversation.dart';
6 import 'package:localmind/features/personas/data/models/persona.dart';
@@ -39,3 +41,7 @@ final personasProvider = Provider<List<Persona>>((ref) {
41 final box = ref.watch(personasBoxProvider);
42 return box.values.toList();
43 });
44 +
45 +final storageDirectoryProvider = FutureProvider<Directory>((ref) async {
46 + return await getApplicationDocumentsDirectory();
47 +});
lib/features/chat/data/chat_service.dart
+423 -156
@@ -6,6 +6,7 @@ import 'package:localmind/features/chat/data/models/message.dart';
6 import 'package:localmind/features/chat/data/models/chat_parameters.dart';
7 import 'package:localmind/features/chat/data/models/mcp_integration.dart';
8 import 'package:localmind/core/models/enums.dart';
9 +import 'package:localmind/core/logger/app_logger.dart';
10
11 abstract class ChatService {
12 Stream<ChatResponse> sendMessage({
@@ -51,7 +52,16 @@ class ChatResponse {
52 });
53 }
54
54 -enum ChatResponseType { message, reasoning, toolCall, invalidToolCall, done }
55 +enum ChatResponseType {
56 + message,
57 + reasoning,
58 + toolCall,
59 + invalidToolCall,
60 + done,
61 + processing,
62 + timeoutError,
63 + error,
64 +}
65
66 class ToolCallData {
67 final String tool;
@@ -155,52 +165,62 @@ class LMStudioChatService implements ChatService {
165 body['previous_response_id'] = previousResponseId;
166 }
167
158 - final response = await _dio.post<ResponseBody>(
159 - server.chatEndpoint,
160 - data: body,
161 - options: Options(
162 - responseType: ResponseType.stream,
163 - headers: {
164 - 'Content-Type': 'application/json',
165 - if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
166 - },
167 - ),
168 - cancelToken: _cancelToken,
169 - );
170 -
171 - final stream = response.data!.stream.cast<List<int>>().transform(utf8.decoder);
172 - String buffer = '';
173 - String currentEventType = '';
174 -
175 - await for (final chunk in stream) {
176 - buffer += chunk;
177 - final lines = buffer.split('\n');
178 - buffer = lines.removeLast();
179 -
180 - for (final line in lines) {
181 - final trimmedLine = line.trim();
182 - if (trimmedLine.isEmpty) continue;
183 -
184 - if (trimmedLine.startsWith('event: ')) {
185 - currentEventType = trimmedLine.substring(7);
186 - } else if (trimmedLine.startsWith('data: ')) {
187 - final data = trimmedLine.substring(6);
188 - if (data.isEmpty) continue;
189 -
190 - try {
191 - final json = jsonDecode(data) as Map<String, dynamic>;
192 - await for (final response in _handleSseEvent(
193 - currentEventType,
194 - json,
195 - )) {
196 - yield response;
168 + try {
169 + final response = await _dio.post<ResponseBody>(
170 + server.chatEndpoint,
171 + data: body,
172 + options: Options(
173 + responseType: ResponseType.stream,
174 + headers: {
175 + 'Content-Type': 'application/json',
176 + if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
177 + },
178 + ),
179 + cancelToken: _cancelToken,
180 + );
181 +
182 + final stream = response.data!.stream.cast<List<int>>().transform(
183 + utf8.decoder,
184 + );
185 + String buffer = '';
186 + String currentEventType = '';
187 +
188 + await for (final chunk in stream) {
189 + buffer += chunk;
190 + final lines = buffer.split('\n');
191 + buffer = lines.removeLast();
192 +
193 + for (final line in lines) {
194 + final trimmedLine = line.trim();
195 + if (trimmedLine.isEmpty) continue;
196 +
197 + if (trimmedLine.startsWith('event: ')) {
198 + currentEventType = trimmedLine.substring(7);
199 + } else if (trimmedLine.startsWith('data: ')) {
200 + final data = trimmedLine.substring(6);
201 + if (data.isEmpty) continue;
202 +
203 + try {
204 + final json = jsonDecode(data) as Map<String, dynamic>;
205 + await for (final response in _handleSseEvent(
206 + currentEventType,
207 + json,
208 + )) {
209 + yield response;
210 + }
211 + currentEventType = '';
212 + } catch (e) {
213 + currentEventType = '';
214 }
198 - currentEventType = '';
199 - } catch (e) {
200 - currentEventType = '';
215 }
216 }
217 }
218 + } catch (e) {
219 + Log.error('LMStudio connection error: $e');
220 + yield ChatResponse(
221 + type: ChatResponseType.error,
222 + content: _handleChatError(e),
223 + );
224 }
225 }
226
@@ -375,6 +395,7 @@ class LMStudioChatService implements ChatService {
395 class OpenAICompatibleChatService implements ChatService {
396 final Dio _dio;
397 CancelToken? _cancelToken;
398 + Timer? _timeoutTimer;
399
400 OpenAICompatibleChatService(this._dio);
401
@@ -400,64 +421,156 @@ class OpenAICompatibleChatService implements ChatService {
421 'stream': true,
422 };
423
403 - final response = await _dio.post<ResponseBody>(
404 - server.chatEndpoint,
405 - data: body,
406 - options: Options(
407 - responseType: ResponseType.stream,
408 - headers: {
409 - 'Content-Type': 'application/json',
410 - if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
411 - },
412 - ),
413 - cancelToken: _cancelToken,
424 + Log.debug(
425 + 'OpenAICompatible: Sending request to ${server.chatEndpoint} with model: $modelId',
426 );
427
416 - final stream = response.data!.stream.cast<List<int>>().transform(utf8.decoder);
417 - String buffer = '';
428 + try {
429 + final response = await _dio.post<ResponseBody>(
430 + server.chatEndpoint,
431 + data: body,
432 + options: Options(
433 + responseType: ResponseType.stream,
434 + headers: {
435 + 'Content-Type': 'application/json',
436 + if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
437 + },
438 + ),
439 + cancelToken: _cancelToken,
440 + );
441 +
442 + final stream = response.data!.stream.cast<List<int>>().transform(
443 + utf8.decoder,
444 + );
445 + String buffer = '';
446 + DateTime? lastContentReceived;
447 + int emptyDeltaCount = 0;
448 + int logCounter = 0;
449 + bool timeoutTriggered = false;
450 +
451 + // Start timeout timer
452 + _timeoutTimer = Timer(const Duration(seconds: 45), () {
453 + timeoutTriggered = true;
454 + });
455 +
456 + Log.debug('OpenAICompatible: Starting to receive stream...');
457 +
458 + await for (final chunk in stream) {
459 + // Check if timeout was triggered
460 + if (timeoutTriggered && lastContentReceived == null) {
461 + _timeoutTimer?.cancel();
462 + yield const ChatResponse(
463 + type: ChatResponseType.timeoutError,
464 + content:
465 + 'Model is taking too long to respond. This may happen with free tier models.',
466 + );
467 + return;
468 + }
469
419 - await for (final chunk in stream) {
420 - buffer += chunk;
421 - final lines = buffer.split('\n');
422 - buffer = lines.removeLast();
470 + buffer += chunk;
471 + final lines = buffer.split('\n');
472 + buffer = lines.removeLast();
473
424 - for (final line in lines) {
425 - if (line.startsWith('data: ')) {
426 - final data = line.substring(6);
427 - if (data == '[DONE]') {
428 - yield const ChatResponse(type: ChatResponseType.done);
429 - return;
474 + for (final line in lines) {
475 + logCounter++;
476 + if (logCounter % 10 == 0) {
477 + Log.debug('OpenAICompatible SSE line #$logCounter');
478 }
431 - try {
432 - final json = jsonDecode(data) as Map<String, dynamic>;
433 - final delta = json['choices']?[0]?['delta'] as Map<String, dynamic>?;
434 - if (delta != null) {
435 - final content = delta['content'] as String?;
436 - final reasoning = (delta['reasoning'] ?? delta['reasoning_content']) as String?;
437 -
438 - if (content != null && content.isNotEmpty) {
479 +
480 + if (line.startsWith('data: ')) {
481 + final data = line.substring(6);
482 + if (data == '[DONE]') {
483 + _timeoutTimer?.cancel();
484 + Log.debug('OpenAICompatible: Received [DONE]');
485 + yield const ChatResponse(type: ChatResponseType.done);
486 + return;
487 + }
488 + try {
489 + final json = jsonDecode(data) as Map<String, dynamic>;
490 +
491 + // Check for errors in the response
492 + if (json['error'] != null) {
493 + final error = json['error'] as Map<String, dynamic>;
494 + final errorMsg = error['message'] ?? 'Unknown API error';
495 + Log.error('OpenAICompatible mid-stream error: $errorMsg');
496 yield ChatResponse(
440 - type: ChatResponseType.message,
441 - content: content,
497 + type: ChatResponseType.error,
498 + content: 'API Error: $errorMsg',
499 );
500 + return;
501 }
444 - if (reasoning != null && reasoning.isNotEmpty) {
445 - yield ChatResponse(
446 - type: ChatResponseType.reasoning,
447 - reasoningContent: reasoning,
448 - );
502 +
503 + final choices = json['choices'] as List<dynamic>?;
504 + if (choices == null || choices.isEmpty) {
505 + continue;
506 + }
507 +
508 + final firstChoice = choices[0] as Map<String, dynamic>?;
509 + if (firstChoice == null) continue;
510 +
511 + final delta = firstChoice['delta'];
512 + if (delta == null || delta is! Map<String, dynamic>) continue;
513 +
514 + final content = (delta['content'] ?? delta['text']) as String?;
515 + final reasoning =
516 + (delta['reasoning'] ?? delta['reasoning_content']) as String?;
517 + final refusal = delta['refusal'] as String?;
518 +
519 + // Check if content is empty/null (processing but not outputting)
520 + final hasContent = content != null && content.isNotEmpty;
521 + final hasReasoning = reasoning != null && reasoning.isNotEmpty;
522 + final hasRefusal = refusal != null && refusal.isNotEmpty;
523 +
524 + if (!hasContent && !hasReasoning && !hasRefusal) {
525 + emptyDeltaCount++;
526 + if (emptyDeltaCount % 10 == 0) {
527 + yield const ChatResponse(type: ChatResponseType.processing);
528 + }
529 + } else {
530 + emptyDeltaCount = 0;
531 + lastContentReceived = DateTime.now();
532 +
533 + if (hasRefusal) {
534 + yield ChatResponse(
535 + type: ChatResponseType.error,
536 + content: 'Refusal: $refusal',
537 + );
538 + return;
539 + }
540 + if (hasContent) {
541 + yield ChatResponse(
542 + type: ChatResponseType.message,
543 + content: content,
544 + );
545 + }
546 + if (hasReasoning) {
547 + yield ChatResponse(
548 + type: ChatResponseType.reasoning,
549 + reasoningContent: reasoning,
550 + );
551 + }
552 }
553 + } catch (e) {
554 + Log.error('OpenAICompatible parsing error: $e');
555 }
451 - } catch (e) {
452 - // ignore: empty_catches
556 }
557 }
558 }
559 + } catch (e) {
560 + Log.error('OpenAICompatible connection error: $e');
561 + yield ChatResponse(
562 + type: ChatResponseType.error,
563 + content: _handleChatError(e),
564 + );
565 }
566 +
567 + _timeoutTimer?.cancel();
568 + Log.debug('OpenAICompatible: Stream ended');
569 }
570
571 @override
572 void cancelStream() {
573 + _timeoutTimer?.cancel();
574 _cancelToken?.cancel('User cancelled');
575 }
576 }
@@ -492,41 +605,51 @@ class OllamaChatService implements ChatService {
605 },
606 };
607
495 - final response = await _dio.post<ResponseBody>(
496 - server.chatEndpoint,
497 - data: body,
498 - options: Options(responseType: ResponseType.stream),
499 - cancelToken: _cancelToken,
500 - );
501 -
502 - final stream = response.data!.stream.cast<List<int>>().transform(utf8.decoder);
503 - String buffer = '';
504 -
505 - await for (final chunk in stream) {
506 - buffer += chunk;
507 - final lines = buffer.split('\n');
508 - buffer = lines.removeLast();
509 -
510 - for (final line in lines) {
511 - if (line.isNotEmpty) {
512 - try {
513 - final json = jsonDecode(line) as Map<String, dynamic>;
514 - final content = json['message']?['content'];
515 - if (content != null && content is String) {
516 - yield ChatResponse(
517 - type: ChatResponseType.message,
518 - content: content,
519 - );
520 - }
521 - if (json['done'] == true) {
522 - yield const ChatResponse(type: ChatResponseType.done);
523 - return;
608 + try {
609 + final response = await _dio.post<ResponseBody>(
610 + server.chatEndpoint,
611 + data: body,
612 + options: Options(responseType: ResponseType.stream),
613 + cancelToken: _cancelToken,
614 + );
615 +
616 + final stream = response.data!.stream.cast<List<int>>().transform(
617 + utf8.decoder,
618 + );
619 + String buffer = '';
620 +
621 + await for (final chunk in stream) {
622 + buffer += chunk;
623 + final lines = buffer.split('\n');
624 + buffer = lines.removeLast();
625 +
626 + for (final line in lines) {
627 + if (line.isNotEmpty) {
628 + try {
629 + final json = jsonDecode(line) as Map<String, dynamic>;
630 + final content = json['message']?['content'];
631 + if (content != null && content is String) {
632 + yield ChatResponse(
633 + type: ChatResponseType.message,
634 + content: content,
635 + );
636 + }
637 + if (json['done'] == true) {
638 + yield const ChatResponse(type: ChatResponseType.done);
639 + return;
640 + }
641 + } catch (e) {
642 + // ignore: empty_catches
643 }
525 - } catch (e) {
526 - // ignore: empty_catches
644 }
645 }
646 }
647 + } catch (e) {
648 + Log.error('Ollama connection error: $e');
649 + yield ChatResponse(
650 + type: ChatResponseType.error,
651 + content: _handleChatError(e),
652 + );
653 }
654 }
655
@@ -539,6 +662,7 @@ class OllamaChatService implements ChatService {
662 class OpenRouterChatService implements ChatService {
663 final Dio _dio;
664 CancelToken? _cancelToken;
665 + Timer? _timeoutTimer;
666
667 OpenRouterChatService(this._dio);
668
@@ -564,70 +688,213 @@ class OpenRouterChatService implements ChatService {
688 'stream': true,
689 };
690
567 - final response = await _dio.post<ResponseBody>(
568 - server.chatEndpoint,
569 - data: body,
570 - options: Options(
571 - responseType: ResponseType.stream,
572 - headers: {
573 - 'Content-Type': 'application/json',
574 - 'Authorization': 'Bearer ${server.apiKey}',
575 - 'HTTP-Referer': 'https://localmind.app',
576 - 'X-Title': 'LocalMind',
577 - },
578 - ),
579 - cancelToken: _cancelToken,
691 + Log.debug(
692 + 'OpenRouter: Sending request to ${server.chatEndpoint} with model: $modelId',
693 );
694
582 - final stream = response.data!.stream.cast<List<int>>().transform(utf8.decoder);
583 - String buffer = '';
695 + try {
696 + final response = await _dio.post<ResponseBody>(
697 + server.chatEndpoint,
698 + data: body,
699 + options: Options(
700 + responseType: ResponseType.stream,
701 + headers: {
702 + 'Content-Type': 'application/json',
703 + 'Authorization': 'Bearer ${server.apiKey}',
704 + 'HTTP-Referer': 'https://localmind.app',
705 + 'X-Title': 'LocalMind',
706 + },
707 + ),
708 + cancelToken: _cancelToken,
709 + );
710 +
711 + final stream = response.data!.stream.cast<List<int>>().transform(
712 + utf8.decoder,
713 + );
714 + String buffer = '';
715 + DateTime? lastContentReceived;
716 + int emptyDeltaCount = 0;
717 + int logCounter = 0;
718 + bool timeoutTriggered = false;
719 +
720 + // Start timeout timer
721 + _timeoutTimer = Timer(const Duration(seconds: 45), () {
722 + timeoutTriggered = true;
723 + });
724 +
725 + Log.debug('OpenRouter: Starting to receive stream...');
726 +
727 + await for (final chunk in stream) {
728 + // Check if timeout was triggered
729 + if (timeoutTriggered && lastContentReceived == null) {
730 + _timeoutTimer?.cancel();
731 + yield const ChatResponse(
732 + type: ChatResponseType.timeoutError,
733 + content:
734 + 'Model is taking too long to respond. This may happen with free tier models.',
735 + );
736 + return;
737 + }
738
585 - await for (final chunk in stream) {
586 - buffer += chunk;
587 - final lines = buffer.split('\n');
588 - buffer = lines.removeLast();
739 + buffer += chunk;
740 + final lines = buffer.split('\n');
741 + buffer = lines.removeLast();
742
590 - for (final line in lines) {
591 - if (line.startsWith('data: ')) {
592 - final data = line.substring(6);
593 - if (data == '[DONE]') {
594 - yield const ChatResponse(type: ChatResponseType.done);
595 - return;
743 + for (final line in lines) {
744 + logCounter++;
745 + if (logCounter % 10 == 0) {
746 + Log.debug('OpenRouter SSE line #$logCounter: $line');
747 }
597 - try {
598 - final json = jsonDecode(data) as Map<String, dynamic>;
599 - final delta = json['choices']?[0]?['delta'] as Map<String, dynamic>?;
600 - if (delta != null) {
601 - final content = delta['content'] as String?;
602 - final reasoning = (delta['reasoning'] ?? delta['reasoning_content']) as String?;
603 -
604 - if (content != null && content.isNotEmpty) {
748 +
749 + if (line.startsWith('data: ')) {
750 + final data = line.substring(6);
751 + if (data == '[DONE]') {
752 + _timeoutTimer?.cancel();
753 + Log.debug('OpenRouter: Received [DONE]');
754 + yield const ChatResponse(type: ChatResponseType.done);
755 + return;
756 + }
757 + try {
758 + final json = jsonDecode(data) as Map<String, dynamic>;
759 +
760 + // Check for errors in the response
761 + if (json['error'] != null) {
762 + final error = json['error'] as Map<String, dynamic>;
763 + final errorMsg = error['message'] ?? 'Unknown API error';
764 + Log.error('OpenRouter mid-stream error: $errorMsg');
765 yield ChatResponse(
606 - type: ChatResponseType.message,
607 - content: content,
766 + type: ChatResponseType.error,
767 + content: 'API Error: $errorMsg',
768 );
769 + return;
770 }
610 - if (reasoning != null && reasoning.isNotEmpty) {
611 - yield ChatResponse(
612 - type: ChatResponseType.reasoning,
613 - reasoningContent: reasoning,
614 - );
771 +
772 + final choices = json['choices'] as List<dynamic>?;
773 + if (choices == null || choices.isEmpty) {
774 + continue;
775 + }
776 +
777 + final firstChoice = choices[0] as Map<String, dynamic>?;
778 + if (firstChoice == null) continue;
779 +
780 + final delta = firstChoice['delta'];
781 + if (delta == null || delta is! Map<String, dynamic>) continue;
782 +
783 + final content = (delta['content'] ?? delta['text']) as String?;
784 + final reasoning =
785 + (delta['reasoning'] ?? delta['reasoning_content']) as String?;
786 + final refusal = delta['refusal'] as String?;
787 +
788 + // Check if content is empty/null (processing but not outputting)
789 + final hasContent = content != null && content.isNotEmpty;
790 + final hasReasoning = reasoning != null && reasoning.isNotEmpty;
791 + final hasRefusal = refusal != null && refusal.isNotEmpty;
792 +
793 + if (!hasContent && !hasReasoning && !hasRefusal) {
794 + emptyDeltaCount++;
795 + if (emptyDeltaCount % 10 == 0) {
796 + yield const ChatResponse(type: ChatResponseType.processing);
797 + }
798 + } else {
799 + emptyDeltaCount = 0;
800 + lastContentReceived = DateTime.now();
801 +
802 + if (hasRefusal) {
803 + yield ChatResponse(
804 + type: ChatResponseType.error,
805 + content: 'Refusal: $refusal',
806 + );
807 + return;
808 + }
809 + if (hasContent) {
810 + yield ChatResponse(
811 + type: ChatResponseType.message,
812 + content: content,
813 + );
814 + }
815 + if (hasReasoning) {
816 + yield ChatResponse(
817 + type: ChatResponseType.reasoning,
818 + reasoningContent: reasoning,
819 + );
820 + }
821 }
822 + } catch (e) {
823 + Log.error('OpenRouter parsing error: $e');
824 + }
825 + } else if (line.startsWith(': ')) {
826 + // SSE comment (keep-alive)
827 + if (logCounter % 50 == 0) {
828 + Log.debug('OpenRouter SSE comment: $line');
829 }
617 - } catch (e) {
618 - // ignore: empty_catches
830 }
831 }
832 }
833 + } catch (e) {
834 + Log.error('OpenRouter connection error: $e');
835 + yield ChatResponse(
836 + type: ChatResponseType.error,
837 + content: _handleChatError(e),
838 + );
839 }
840 +
841 + _timeoutTimer?.cancel();
842 + Log.debug('OpenRouter: Stream ended');
843 }
844
845 @override
846 void cancelStream() {
847 + _timeoutTimer?.cancel();
848 _cancelToken?.cancel('User cancelled');
849 }
850 }
851
852 +String _handleChatError(dynamic e) {
853 + if (e is DioException) {
854 + if (e.response != null) {
855 + final status = e.response!.statusCode;
856 +
857 + switch (status) {
858 + case 401:
859 + return 'Invalid API key or unauthorized.';
860 + case 402:
861 + return 'Monthly credit limit reached or insufficient balance.';
862 + case 403:
863 + return 'Access forbidden. Your API key might not have permission for this model.';
864 + case 404:
865 + return 'Model not found or API endpoint is incorrect.';
866 + case 408:
867 + return 'Request timeout. The server took too long to respond.';
868 + case 429:
869 + return 'Rate limit exceeded. Please wait a moment before trying again.';
870 + case 500:
871 + return 'Internal server error from the AI provider.';
872 + case 502:
873 + return 'Bad gateway. The provider is having issues.';
874 + case 503:
875 + return 'Service unavailable. The provider might be overloaded.';
876 + case 504:
877 + return 'Gateway timeout. The model took too long to respond.';
878 + }
879 + return 'Server error ($status): ${e.message}';
880 + }
881 +
882 + switch (e.type) {
883 + case DioExceptionType.connectionTimeout:
884 + case DioExceptionType.sendTimeout:
885 + case DioExceptionType.receiveTimeout:
886 + return 'Connection timed out. Check your internet connection.';
887 + case DioExceptionType.connectionError:
888 + return 'Connection failed. Check if the server is accessible.';
889 + case DioExceptionType.cancel:
890 + return 'Request cancelled.';
891 + default:
892 + return 'Connection error: ${e.message}';
893 + }
894 + }
895 + return e.toString();
896 +}
897 +
898 String _roleToString(MessageRole role) {
899 switch (role) {
900 case MessageRole.user:
lib/features/chat/data/models/message.dart
+6
@@ -47,6 +47,9 @@ class Message extends HiveObject {
47 @HiveField(13)
48 final String? toolCallId;
49
50 + @HiveField(14)
51 + final bool isProcessing;
52 +
53 Message({
54 required this.id,
55 required this.conversationId,
@@ -62,6 +65,7 @@ class Message extends HiveObject {
65 this.reasoningContent,
66 this.toolCalls,
67 this.toolCallId,
68 + this.isProcessing = false,
69 });
70
71 Message copyWith({
@@ -79,6 +83,7 @@ class Message extends HiveObject {
83 String? reasoningContent,
84 List<ToolCallData>? toolCalls,
85 String? toolCallId,
86 + bool? isProcessing,
87 }) {
88 return Message(
89 id: id ?? this.id,
@@ -95,6 +100,7 @@ class Message extends HiveObject {
100 reasoningContent: reasoningContent ?? this.reasoningContent,
101 toolCalls: toolCalls ?? this.toolCalls,
102 toolCallId: toolCallId ?? this.toolCallId,
103 + isProcessing: isProcessing ?? this.isProcessing,
104 );
105 }
106 }
lib/features/chat/data/models/message.g.dart
+5 -2
@@ -33,13 +33,14 @@ class MessageAdapter extends TypeAdapter<Message> {
33 reasoningContent: fields[11] as String?,
34 toolCalls: (fields[12] as List?)?.cast<ToolCallData>(),
35 toolCallId: fields[13] as String?,
36 + isProcessing: fields[14] == null ? false : fields[14] as bool,
37 );
38 }
39
40 @override
41 void write(BinaryWriter writer, Message obj) {
42 writer
42 - ..writeByte(14)
43 + ..writeByte(15)
44 ..writeByte(0)
45 ..write(obj.id)
46 ..writeByte(1)
@@ -67,7 +68,9 @@ class MessageAdapter extends TypeAdapter<Message> {
68 ..writeByte(12)
69 ..write(obj.toolCalls)
70 ..writeByte(13)
70 - ..write(obj.toolCallId);
71 + ..write(obj.toolCallId)
72 + ..writeByte(14)
73 + ..write(obj.isProcessing);
74 }
75
76 @override
lib/features/chat/providers/chat_providers.dart
+179 -40
@@ -1,4 +1,5 @@
1 import 'dart:async';
2 +import 'dart:io';
3 import 'dart:math';
4 import 'package:flutter_riverpod/flutter_riverpod.dart';
5 import 'package:localmind/core/models/enums.dart';
@@ -27,6 +28,10 @@ class SelectedModelNotifier extends Notifier<ModelInfo?> {
28 void setModel(ModelInfo? model) {
29 state = model;
30 }
31 +
32 + void clear() {
33 + state = null;
34 + }
35 }
36
37 final autoSelectFirstLoadedModelProvider = FutureProvider<void>((ref) async {
@@ -313,7 +318,7 @@ class ChatNotifier extends Notifier<ChatState> {
318 .join('-');
319 }
320
316 - Future<void> sendMessage(String content) async {
321 + Future<void> sendMessage(String content, {List<File>? attachments}) async {
322 final server = ref.read(activeServerProvider);
323 final selectedModel = ref.read(selectedModelProvider);
324 final chatParams = ref.read(chatParamsProvider);
@@ -351,6 +356,23 @@ class ChatNotifier extends Notifier<ChatState> {
356 ref.read(selectedPersonaProvider.notifier).clear();
357 }
358
359 + final List<String> savedPaths = [];
360 + if (attachments != null && attachments.isNotEmpty) {
361 + final appDir = await ref.read(storageDirectoryProvider.future);
362 + final attachmentsDir = Directory('${appDir.path}/attachments');
363 + if (!await attachmentsDir.exists()) {
364 + await attachmentsDir.create(recursive: true);
365 + }
366 +
367 + for (final file in attachments) {
368 + final fileName = file.path.split('/').last;
369 + final timestamp = DateTime.now().millisecondsSinceEpoch;
370 + final newPath = '${attachmentsDir.path}/${timestamp}_$fileName';
371 + await file.copy(newPath);
372 + savedPaths.add(newPath);
373 + }
374 + }
375 +
376 final userMessage = Message(
377 id: generateUuid(),
378 conversationId: _currentConversationId!,
@@ -358,6 +380,7 @@ class ChatNotifier extends Notifier<ChatState> {
380 content: content,
381 createdAt: DateTime.now(),
382 status: MessageStatus.complete,
383 + attachmentPaths: savedPaths.isNotEmpty ? savedPaths : null,
384 );
385
386 final assistantMessageId = generateUuid();
@@ -396,22 +419,27 @@ class ChatNotifier extends Notifier<ChatState> {
419 params: chatParams,
420 )
421 .listen(
399 - (response) {
422 + (response) async {
423 switch (response.type) {
424 case ChatResponseType.message:
425 final currentContent = state.streamingMessage?.content ?? '';
426 final updatedMessage =
427 (state.streamingMessage ?? assistantMessage).copyWith(
428 content: currentContent + (response.content ?? ''),
429 + isProcessing: false,
430 );
407 - state = state.copyWith(streamingMessage: updatedMessage);
431 final messageIndex = state.messages.indexWhere(
432 (m) => m.id == assistantMessageId,
433 );
434 if (messageIndex != -1) {
435 final updatedMessages = List<Message>.from(state.messages);
436 updatedMessages[messageIndex] = updatedMessage;
414 - state = state.copyWith(messages: updatedMessages);
437 + state = state.copyWith(
438 + streamingMessage: updatedMessage,
439 + messages: updatedMessages,
440 + );
441 + } else {
442 + state = state.copyWith(streamingMessage: updatedMessage);
443 }
444 break;
445 case ChatResponseType.reasoning:
@@ -419,6 +447,7 @@ class ChatNotifier extends Notifier<ChatState> {
447 final reasoningMessage =
448 (state.streamingMessage ?? assistantMessage).copyWith(
449 reasoningContent: reasoningContent,
450 + isProcessing: false,
451 );
452 state = state.copyWith(streamingMessage: reasoningMessage);
453 final msgIndex = state.messages.indexWhere(
@@ -430,6 +459,54 @@ class ChatNotifier extends Notifier<ChatState> {
459 state = state.copyWith(messages: updatedMessages);
460 }
461 break;
462 + case ChatResponseType.processing:
463 + // Model is working but hasn't output content yet
464 + final processingMessage =
465 + (state.streamingMessage ?? assistantMessage).copyWith(
466 + isProcessing: true,
467 + );
468 + state = state.copyWith(streamingMessage: processingMessage);
469 + final procIndex = state.messages.indexWhere(
470 + (m) => m.id == assistantMessageId,
471 + );
472 + if (procIndex != -1) {
473 + final updatedMessages = List<Message>.from(state.messages);
474 + updatedMessages[procIndex] = processingMessage;
475 + state = state.copyWith(messages: updatedMessages);
476 + }
477 + break;
478 + case ChatResponseType.timeoutError:
479 + case ChatResponseType.error:
480 + // Error reached without receiving content or mid-stream
481 + final errorMessage =
482 + (state.streamingMessage ?? assistantMessage).copyWith(
483 + status: MessageStatus.error,
484 + errorMessage:
485 + response.content ??
486 + (response.type == ChatResponseType.timeoutError
487 + ? 'Model is taking too long to respond. This may happen with free tier models.'
488 + : 'An unknown error occurred.'),
489 + isProcessing: false,
490 + );
491 + state = state.copyWith(
492 + streamingMessage: errorMessage,
493 + isStreaming: false,
494 + );
495 + final errIndex = state.messages.indexWhere(
496 + (m) => m.id == assistantMessageId,
497 + );
498 + if (errIndex != -1) {
499 + final updatedMessages = List<Message>.from(state.messages);
500 + updatedMessages[errIndex] = errorMessage;
501 + state = state.copyWith(
502 + messages: updatedMessages,
503 + errorMessage: errorMessage.errorMessage,
504 + clearStreaming: true,
505 + );
506 + }
507 + // Save error message to storage
508 + await boxes.messages.put(errorMessage.id, errorMessage);
509 + break;
510 case ChatResponseType.toolCall:
511 case ChatResponseType.invalidToolCall:
512 case ChatResponseType.done:
@@ -437,44 +514,77 @@ class ChatNotifier extends Notifier<ChatState> {
514 }
515 },
516 onDone: () async {
440 - final finalMessage = state.streamingMessage?.copyWith(
441 - status: MessageStatus.complete,
442 - );
443 - if (finalMessage != null) {
444 - await boxes.messages.put(finalMessage.id, finalMessage);
445 - final messageIndex = state.messages.indexWhere(
446 - (m) => m.id == assistantMessageId,
447 - );
448 - if (messageIndex != -1) {
449 - final updatedMessages = List<Message>.from(state.messages);
450 - updatedMessages[messageIndex] = finalMessage;
451 - state = state.copyWith(
452 - messages: updatedMessages,
453 - isStreaming: false,
454 - clearStreaming: true,
517 + final streamingMessage = state.streamingMessage;
518 + if (streamingMessage != null) {
519 + // Check if content is empty - this happens when free models refuse or fail
520 + final hasContent =
521 + streamingMessage.content.isNotEmpty ||
522 + (streamingMessage.reasoningContent?.isNotEmpty ?? false);
523 +
524 + if (!hasContent) {
525 + // Mark as error if no content received
526 + final errorMessage = streamingMessage.copyWith(
527 + status: MessageStatus.error,
528 + errorMessage:
529 + 'Model failed to respond. This may happen with free tier models that refuse certain prompts or when the service is busy.',
530 + isProcessing: false,
531 );
456 - }
457 - if (_currentConversationId != null) {
458 - final preview = finalMessage.content.length > 100
459 - ? '${finalMessage.content.substring(0, 100)}...'
460 - : finalMessage.content;
461 - await ref
462 - .read(conv.conversationsProvider.notifier)
463 - .updatePreview(
464 - _currentConversationId!,
465 - preview,
466 - DateTime.now(),
467 - );
468 -
469 - final userMessage = state.messages
470 - .where((m) => m.role == MessageRole.user)
471 - .firstOrNull;
472 - if (userMessage != null && userMessage.content.length > 10) {
473 - _autoGenerateTitle(
474 - userMessage.content,
475 - finalMessage.content,
532 + await boxes.messages.put(errorMessage.id, errorMessage);
533 + final messageIndex = state.messages.indexWhere(
534 + (m) => m.id == assistantMessageId,
535 + );
536 + if (messageIndex != -1) {
537 + final updatedMessages = List<Message>.from(state.messages);
538 + updatedMessages[messageIndex] = errorMessage;
539 + state = state.copyWith(
540 + messages: updatedMessages,
541 + isStreaming: false,
542 + errorMessage: errorMessage.errorMessage,
543 + clearStreaming: true,
544 );
545 }
546 + } else {
547 + // Normal completion with content
548 + final finalMessage = streamingMessage.copyWith(
549 + status: MessageStatus.complete,
550 + isProcessing: false,
551 + );
552 + await boxes.messages.put(finalMessage.id, finalMessage);
553 + final messageIndex = state.messages.indexWhere(
554 + (m) => m.id == assistantMessageId,
555 + );
556 + if (messageIndex != -1) {
557 + final updatedMessages = List<Message>.from(state.messages);
558 + updatedMessages[messageIndex] = finalMessage;
559 + state = state.copyWith(
560 + messages: updatedMessages,
561 + isStreaming: false,
562 + clearStreaming: true,
563 + );
564 + }
565 + if (_currentConversationId != null) {
566 + final preview = finalMessage.content.length > 100
567 + ? '${finalMessage.content.substring(0, 100)}...'
568 + : finalMessage.content;
569 + await ref
570 + .read(conv.conversationsProvider.notifier)
571 + .updatePreview(
572 + _currentConversationId!,
573 + preview,
574 + DateTime.now(),
575 + );
576 +
577 + final userMessage = state.messages
578 + .where((m) => m.role == MessageRole.user)
579 + .firstOrNull;
580 + if (userMessage != null &&
581 + userMessage.content.length > 10) {
582 + _autoGenerateTitle(
583 + userMessage.content,
584 + finalMessage.content,
585 + );
586 + }
587 + }
588 }
589 }
590 },
@@ -645,11 +755,40 @@ class ChatNotifier extends Notifier<ChatState> {
755 clearStreaming: true,
756 );
757
648 - await sendMessage(userMessage.content);
758 + await sendMessage(userMessage.content, attachments: userMessage.attachmentPaths?.map((p) => File(p)).toList());
759 }
760 }
761 }
762
763 + Future<void> retryMessage(String messageId) async {
764 + final messageIndex = state.messages.indexWhere((m) => m.id == messageId);
765 + if (messageIndex == -1) return;
766 +
767 + final message = state.messages[messageIndex];
768 + if (message.role != MessageRole.assistant) return;
769 +
770 + // Find the preceding user message
771 + if (messageIndex > 0 &&
772 + state.messages[messageIndex - 1].role == MessageRole.user) {
773 + final userMessage = state.messages[messageIndex - 1];
774 +
775 + // Remove this assistant message and all subsequent messages
776 + final messagesToRemove = state.messages.sublist(messageIndex);
777 + final boxes = ref.read(hiveBoxesProvider);
778 +
779 + for (final msg in messagesToRemove) {
780 + await boxes.messages.delete(msg.id);
781 + }
782 +
783 + state = state.copyWith(
784 + messages: state.messages.sublist(0, messageIndex),
785 + clearStreaming: true,
786 + );
787 +
788 + await sendMessage(userMessage.content, attachments: userMessage.attachmentPaths?.map((p) => File(p)).toList());
789 + }
790 + }
791 +
792 Future<void> deleteMessage(String messageId) async {
793 final boxes = ref.read(hiveBoxesProvider);
794 await boxes.messages.delete(messageId);
lib/features/chat/views/chat_screen.dart
+44 -140
@@ -30,7 +30,6 @@ class ChatScreen extends ConsumerStatefulWidget {
30
31 class _ChatScreenState extends ConsumerState<ChatScreen> {
32 final ScrollController _scrollController = ScrollController();
33 - bool _showScrollToBottom = false;
33
34 static const List<String> _quickPrompts = [
35 'Help me write a function',
@@ -42,7 +41,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
41 @override
42 void initState() {
43 super.initState();
45 - _scrollController.addListener(_onScroll);
44 }
45
46 @override
@@ -51,26 +49,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
49 super.dispose();
50 }
51
54 - void _onScroll() {
55 - final chatState = ref.read(chatProvider);
56 - if (chatState.isStreaming) return;
57 -
58 - final isNearBottom =
59 - _scrollController.position.pixels >=
60 - _scrollController.position.maxScrollExtent - 100;
61 - if (_showScrollToBottom != !isNearBottom) {
62 - setState(() => _showScrollToBottom = !isNearBottom);
63 - }
64 - }
65 -
66 - void _scrollToBottom() {
67 - _scrollController.animateTo(
68 - _scrollController.position.maxScrollExtent,
69 - duration: const Duration(milliseconds: 300),
70 - curve: Curves.easeOut,
71 - );
72 - }
73 -
52 @override
53 Widget build(BuildContext context) {
54 ref.watch(autoSelectFirstLoadedModelProvider);
@@ -87,27 +65,8 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
65 final theme = Theme.of(context);
66 final isDark = theme.brightness == Brightness.dark;
67
90 - ref.listen(chatProvider, (previous, next) {
91 - final newMessage = previous?.messages.length != next.messages.length;
92 - final newStreaming =
93 - next.isStreaming &&
94 - next.streamingMessage != null &&
95 - next.streamingMessage != previous?.streamingMessage;
96 -
97 - if (newMessage || newStreaming) {
98 - WidgetsBinding.instance.addPostFrameCallback((_) {
99 - if (_scrollController.hasClients) {
100 - if (next.isStreaming) {
101 - _scrollController.jumpTo(
102 - _scrollController.position.maxScrollExtent,
103 - );
104 - } else {
105 - _scrollToBottom();
106 - }
107 - }
108 - });
109 - }
110 - });
68 + // Autoscroll disabled as per user request
69 + // ref.listen(chatProvider, (previous, next) { ... });
70
71 return Column(
72 children: [
@@ -152,10 +111,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
111 height: 8,
112 decoration: BoxDecoration(
113 shape: BoxShape.circle,
155 - color:
156 - connectionStatus == ConnectionStatus.connected
157 - ? Colors.green
158 - : Colors.grey,
114 + color: connectionStatus == ConnectionStatus.connected
115 + ? Colors.green
116 + : Colors.grey,
117 ),
118 ),
119 ],
@@ -270,7 +228,8 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
228 selectedModel: selectedModel,
229 onModelTap: () => _showModelPicker(context),
230 selectedPersona: ref.watch(selectedPersonaProvider),
273 - onPersonaTap: () => _showPersonaPickerForPreselection(context),
231 + onPersonaTap: () =>
232 + _showPersonaPickerForPreselection(context),
233 )
234 : _MessageList(
235 scrollController: _scrollController,
@@ -278,7 +237,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
237 streamingMessage: chatState.streamingMessage,
238 isStreaming: chatState.isStreaming,
239 onRetry: (messageId) {
281 - ref.read(chatProvider.notifier).retryLastMessage();
240 + ref.read(chatProvider.notifier).retryMessage(messageId);
241 },
242 onDelete: (messageId) {
243 ref
@@ -289,75 +248,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
248 !chatState.isStreaming &&
249 ref.read(smartRepliesProvider).isNotEmpty,
250 ),
292 - if (_showScrollToBottom && chatState.messages.isNotEmpty)
293 - Positioned(
294 - bottom: 20,
295 - left: 0,
296 - right: 0,
297 - child: Center(
298 - child: TweenAnimationBuilder<double>(
299 - tween: Tween(begin: 0.0, end: 1.0),
300 - duration: const Duration(milliseconds: 200),
301 - curve: Curves.easeOutCubic,
302 - builder: (context, value, child) {
303 - return Opacity(
304 - opacity: value,
305 - child: Transform.translate(
306 - offset: Offset(0, 20 * (1 - value)),
307 - child: child,
308 - ),
309 - );
310 - },
311 - child: GestureDetector(
312 - onTap: _scrollToBottom,
313 - child: Container(
314 - padding: const EdgeInsets.symmetric(
315 - horizontal: 12,
316 - vertical: 6,
317 - ),
318 - decoration: BoxDecoration(
319 - color: isDark
320 - ? const Color(0xFF3A3A3A)
321 - : const Color(0xFFE5E5E5),
322 - borderRadius: BorderRadius.circular(16),
323 - boxShadow: [
324 - BoxShadow(
325 - color: Colors.black.withValues(alpha: 0.1),
326 - blurRadius: 8,
327 - spreadRadius: 1,
328 - ),
329 - ],
330 - ),
331 - child: Row(
332 - mainAxisSize: MainAxisSize.min,
333 - children: [
334 - Icon(
335 - Icons.arrow_downward,
336 - size: 14,
337 - color: isDark
338 - ? const Color(0xFF888888)
339 - : const Color(0xFF666666),
340 - ),
341 - const SizedBox(width: 4),
342 - Text(
343 - 'New messages',
344 - style: TextStyle(
345 - fontSize: 12,
346 - color: isDark
347 - ? const Color(0xFF888888)
348 - : const Color(0xFF666666),
349 - ),
350 - ),
351 - ],
352 - ),
353 - ),
354 - ),
355 - ),
356 - ),
357 - ),
251 if (!chatState.isStreaming)
252 Positioned(
360 - bottom: 8,
253 + bottom: 90,
254 left: 0,
255 right: 0,
256 child: _SmartReplyChips(
@@ -366,18 +259,25 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
259 },
260 ),
261 ),
262 + Positioned(
263 + bottom: 0,
264 + left: 0,
265 + right: 0,
266 + child: ChatInputBar(
267 + isStreaming: chatState.isStreaming,
268 + onSend: (message, {attachments}) {
269 + ref
270 + .read(chatProvider.notifier)
271 + .sendMessage(message, attachments: attachments);
272 + },
273 + onStop: () {
274 + ref.read(chatProvider.notifier).cancelStream();
275 + },
276 + ),
277 + ),
278 ],
279 ),
280 ),
372 - ChatInputBar(
373 - isStreaming: chatState.isStreaming,
374 - onSend: (message) {
375 - ref.read(chatProvider.notifier).sendMessage(message);
376 - },
377 - onStop: () {
378 - ref.read(chatProvider.notifier).cancelStream();
379 - },
380 - ),
281 ],
282 );
283 }
@@ -626,10 +526,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
526 }
527
528 Widget _buildServerIcon(BuildContext context, Server server) {
629 - final iconData =
630 - server.iconName != null
631 - ? getHugeIconByName(server.iconName)
632 - : getDefaultServerIcon(server.type.name);
529 + final iconData = server.iconName != null
530 + ? getHugeIconByName(server.iconName)
531 + : getDefaultServerIcon(server.type.name);
532
533 if (iconData == null) {
534 return const Icon(Icons.dns, size: 18);
@@ -666,13 +565,19 @@ class _ModelTopBar extends StatelessWidget {
565 padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
566 child: Row(
567 mainAxisAlignment: MainAxisAlignment.center,
568 + mainAxisSize: MainAxisSize.min,
569 children: [
670 - Text(
671 - selectedModel?.displayName ?? 'Select Model',
672 - style: TextStyle(
673 - fontSize: 13,
674 - fontWeight: FontWeight.w500,
675 - color: isDark ? Colors.white70 : Colors.black87,
570 + Flexible(
571 + child: Text(
572 + selectedModel?.displayName ?? 'Select Model',
573 + style: TextStyle(
574 + fontSize: 13,
575 + fontWeight: FontWeight.w500,
576 + color: isDark ? Colors.white70 : Colors.black87,
577 + ),
578 + maxLines: 2,
579 + textAlign: TextAlign.center,
580 + overflow: TextOverflow.ellipsis,
581 ),
582 ),
583 const SizedBox(width: 4),
@@ -1180,9 +1085,10 @@ class _MessageList extends StatelessWidget {
1085
1086 return ListView.builder(
1087 controller: scrollController,
1088 + cacheExtent: 1000,
1089 padding: EdgeInsets.only(
1090 top: 16,
1185 - bottom: 16 + (hasSmartReplies ? 56 : 0),
1091 + bottom: 120 + (hasSmartReplies ? 56 : 0),
1092 ),
1093 itemCount:
1094 allMessages.length +
@@ -1371,6 +1277,7 @@ class _SmartReplyChipsState extends ConsumerState<_SmartReplyChips>
1277 );
1278 }
1279 }
1280 +
1281 class _PersonaIndicator extends StatelessWidget {
1282 const _PersonaIndicator({
1283 required this.persona,
@@ -1407,10 +1314,7 @@ class _PersonaIndicator extends StatelessWidget {
1314 child: Row(
1315 mainAxisSize: MainAxisSize.min,
1316 children: [
1410 - Text(
1411 - persona.emoji,
1412 - style: const TextStyle(fontSize: 16),
1413 - ),
1317 + Text(persona.emoji, style: const TextStyle(fontSize: 16)),
1318 const SizedBox(width: 6),
1319 Text(
1320 persona.name,
lib/features/chat/views/components/chat_bubble.dart
+232 -56
@@ -1,9 +1,11 @@
1 +import 'dart:io';
2 import 'package:flutter/material.dart';
3 import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
4 import 'package:localmind/core/models/enums.dart';
5 import 'package:localmind/features/chat/data/models/message.dart';
6 import 'package:localmind/features/chat/views/components/code_block.dart';
7 import 'package:localmind/features/chat/views/components/message_action_bar.dart';
8 +import 'package:localmind/features/chat/views/components/processing_indicator.dart';
9 import 'package:localmind/features/chat/views/components/typing_indicator.dart';
10 import 'package:localmind/features/chat/views/components/reasoning_widget.dart';
11
@@ -25,6 +27,12 @@ class ChatBubble extends StatelessWidget {
27
28 @override
29 Widget build(BuildContext context) {
30 + return RepaintBoundary(
31 + child: _buildBubble(context),
32 + );
33 + }
34 +
35 + Widget _buildBubble(BuildContext context) {
36 switch (message.role) {
37 case MessageRole.user:
38 return _AnimatedBubble(
@@ -118,6 +126,15 @@ class _UserBubble extends StatelessWidget {
126 child: Column(
127 crossAxisAlignment: CrossAxisAlignment.end,
128 children: [
129 + if (message.attachmentPaths != null &&
130 + message.attachmentPaths!.isNotEmpty)
131 + Padding(
132 + padding: const EdgeInsets.only(bottom: 8),
133 + child: _AttachmentList(
134 + paths: message.attachmentPaths!,
135 + isUser: true,
136 + ),
137 + ),
138 MarkdownBody(
139 data: message.content,
140 styleSheet: MarkdownStyleSheet(
@@ -169,68 +186,80 @@ class _AssistantBubble extends StatelessWidget {
186 final theme = Theme.of(context);
187 final isDark = theme.brightness == Brightness.dark;
188
172 - return Align(
173 - alignment: Alignment.centerLeft,
174 - child: Container(
175 - constraints: BoxConstraints(
176 - maxWidth: MediaQuery.of(context).size.width * 0.8,
177 - ),
178 - margin: const EdgeInsets.only(left: 8, right: 48, top: 4, bottom: 4),
179 - padding: const EdgeInsets.all(12),
180 - decoration: BoxDecoration(
181 - color: isDark ? const Color(0xFF1F1F1F) : const Color(0xFFF5F5F5),
182 - borderRadius: BorderRadius.circular(
183 - 18,
184 - ).copyWith(bottomLeft: const Radius.circular(4)),
185 - border: Border.all(
186 - color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E5E5),
187 - ),
188 - ),
189 - child: Column(
190 - crossAxisAlignment: CrossAxisAlignment.start,
191 - children: [
192 - if (message.reasoningContent != null &&
193 - message.reasoningContent!.isNotEmpty)
194 - ReasoningWidget(
195 - reasoningContent: message.reasoningContent,
196 - isStreaming: isStreaming,
197 - ),
198 - if (isStreaming && message.content.isEmpty)
199 - const TypingIndicator()
200 - else
201 - _MarkdownContent(content: message.content, isDark: isDark),
202 - if (isStreaming && message.content.isNotEmpty)
203 - const StreamingIndicator(),
204 - const SizedBox(height: 8),
205 - Row(
206 - mainAxisSize: MainAxisSize.min,
207 - children: [
208 - Text(
209 - _formatTime(message.createdAt),
189 + return Container(
190 + width: double.infinity,
191 + margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
192 + child: Column(
193 + crossAxisAlignment: CrossAxisAlignment.start,
194 + children: [
195 + if (message.reasoningContent != null &&
196 + message.reasoningContent!.isNotEmpty)
197 + ReasoningWidget(
198 + reasoningContent: message.reasoningContent,
199 + isStreaming: isStreaming,
200 + ),
201 + if (isStreaming && message.content.isEmpty && message.isProcessing)
202 + const ProcessingIndicator()
203 + else if (isStreaming && message.content.isEmpty)
204 + const TypingIndicator()
205 + else
206 + _MarkdownContent(content: message.content, isDark: isDark),
207 + if (message.status == MessageStatus.error &&
208 + message.errorMessage != null &&
209 + message.errorMessage!.isNotEmpty)
210 + Padding(
211 + padding: const EdgeInsets.only(top: 8),
212 + child: Container(
213 + padding: const EdgeInsets.all(12),
214 + decoration: BoxDecoration(
215 + color: Colors.red.withValues(alpha: 0.1),
216 + borderRadius: BorderRadius.circular(8),
217 + border: Border.all(color: Colors.red.withValues(alpha: 0.2)),
218 + ),
219 + child: Text(
220 + message.errorMessage!,
221 style: TextStyle(
211 - fontSize: 11,
212 - color: isDark
213 - ? const Color(0xFF666666)
214 - : const Color(0xFF999999),
222 + color: Colors.red[400],
223 + fontSize: 12,
224 + fontWeight: FontWeight.w500,
225 ),
226 ),
217 - if (message.status == MessageStatus.error) ...[
218 - const SizedBox(width: 4),
219 - Icon(Icons.error_outline, size: 14, color: Colors.red[400]),
220 - ],
221 - ],
227 + ),
228 + ),
229 + if (isStreaming && message.content.isNotEmpty)
230 + const Padding(
231 + padding: EdgeInsets.only(top: 8),
232 + child: StreamingIndicator(),
233 ),
223 - if (!isStreaming && message.status == MessageStatus.complete) ...[
224 - const SizedBox(height: 4),
225 - MessageActionBar(
226 - content: message.content,
227 - onCopy: onCopy,
228 - onRetry: onRetry,
229 - onDelete: onDelete,
234 + const SizedBox(height: 8),
235 + Row(
236 + children: [
237 + Text(
238 + _formatTime(message.createdAt),
239 + style: TextStyle(
240 + fontSize: 11,
241 + color: isDark
242 + ? const Color(0xFF666666)
243 + : const Color(0xFF999999),
244 + ),
245 ),
246 + if (message.status == MessageStatus.error) ...[
247 + const SizedBox(width: 4),
248 + Icon(Icons.error_outline, size: 14, color: Colors.red[400]),
249 + ],
250 + const Spacer(),
251 + if (!isStreaming &&
252 + (message.status == MessageStatus.complete ||
253 + message.status == MessageStatus.error))
254 + MessageActionBar(
255 + content: message.content,
256 + onCopy: onCopy,
257 + onRetry: onRetry,
258 + onDelete: onDelete,
259 + ),
260 ],
232 - ],
233 - ),
261 + ),
262 + ],
263 ),
264 );
265 }
@@ -442,6 +471,153 @@ class _ToolBubble extends StatelessWidget {
471 }
472 }
473
474 +class _AttachmentList extends StatelessWidget {
475 + const _AttachmentList({required this.paths, required this.isUser});
476 +
477 + final List<String> paths;
478 + final bool isUser;
479 +
480 + @override
481 + Widget build(BuildContext context) {
482 + return Wrap(
483 + spacing: 8,
484 + runSpacing: 8,
485 + children: paths.map((path) => _AttachmentItem(path: path)).toList(),
486 + );
487 + }
488 +}
489 +
490 +class _AttachmentItem extends StatelessWidget {
491 + const _AttachmentItem({required this.path});
492 +
493 + final String path;
494 +
495 + bool _isImage(String path) {
496 + final mime = path.toLowerCase();
497 + return mime.endsWith('.jpg') ||
498 + mime.endsWith('.jpeg') ||
499 + mime.endsWith('.png') ||
500 + mime.endsWith('.gif') ||
501 + mime.endsWith('.webp');
502 + }
503 +
504 + void _viewImage(BuildContext context) {
505 + showDialog(
506 + context: context,
507 + useSafeArea: false,
508 + builder: (context) => _ImageViewer(path: path),
509 + );
510 + }
511 +
512 + @override
513 + Widget build(BuildContext context) {
514 + final theme = Theme.of(context);
515 + final isDark = theme.brightness == Brightness.dark;
516 + final file = File(path);
517 + final fileName = path.split('/').last;
518 +
519 + if (_isImage(path)) {
520 + return GestureDetector(
521 + onTap: () => _viewImage(context),
522 + child: ClipRRect(
523 + borderRadius: BorderRadius.circular(12),
524 + child: Image.file(
525 + file,
526 + width: 120,
527 + height: 120,
528 + fit: BoxFit.cover,
529 + errorBuilder: (context, error, stackTrace) => _FilePlaceholder(fileName: fileName),
530 + ),
531 + ),
532 + );
533 + }
534 +
535 + return Container(
536 + width: 160,
537 + padding: const EdgeInsets.all(12),
538 + decoration: BoxDecoration(
539 + color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFF0F0F0),
540 + borderRadius: BorderRadius.circular(12),
541 + border: Border.all(
542 + color: isDark ? const Color(0xFF3A3A3A) : const Color(0xFFE5E5E5),
543 + ),
544 + ),
545 + child: Row(
546 + mainAxisSize: MainAxisSize.min,
547 + children: [
548 + Icon(
549 + Icons.insert_drive_file_outlined,
550 + size: 20,
551 + color: isDark ? const Color(0xFF888888) : const Color(0xFF666666),
552 + ),
553 + const SizedBox(width: 8),
554 + Expanded(
555 + child: Text(
556 + fileName,
557 + style: TextStyle(
558 + fontSize: 12,
559 + color: isDark ? Colors.white70 : Colors.black87,
560 + ),
561 + maxLines: 1,
562 + overflow: TextOverflow.ellipsis,
563 + ),
564 + ),
565 + ],
566 + ),
567 + );
568 + }
569 +}
570 +
571 +class _FilePlaceholder extends StatelessWidget {
572 + const _FilePlaceholder({required this.fileName});
573 + final String fileName;
574 +
575 + @override
576 + Widget build(BuildContext context) {
577 + return Container(
578 + width: 120,
579 + height: 120,
580 + color: Colors.grey[800],
581 + child: Center(
582 + child: Text(
583 + fileName.split('.').last.toUpperCase(),
584 + style: const TextStyle(
585 + color: Colors.white,
586 + fontWeight: FontWeight.bold,
587 + ),
588 + ),
589 + ),
590 + );
591 + }
592 +}
593 +
594 +class _ImageViewer extends StatelessWidget {
595 + const _ImageViewer({required this.path});
596 + final String path;
597 +
598 + @override
599 + Widget build(BuildContext context) {
600 + return Scaffold(
601 + backgroundColor: Colors.black,
602 + appBar: AppBar(
603 + backgroundColor: Colors.transparent,
604 + elevation: 0,
605 + leading: IconButton(
606 + icon: const Icon(Icons.close, color: Colors.white),
607 + onPressed: () => Navigator.pop(context),
608 + ),
609 + ),
610 + body: Center(
611 + child: InteractiveViewer(
612 + minScale: 0.5,
613 + maxScale: 4.0,
614 + child: Image.file(File(path)),
615 + ),
616 + ),
617 + );
618 + }
619 +}
620 +
621 String _formatTime(DateTime dateTime) {
622 final hour = dateTime.hour.toString().padLeft(2, '0');
623 final minute = dateTime.minute.toString().padLeft(2, '0');
lib/features/chat/views/components/chat_input_bar.dart
+138 -229
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
3 import 'package:flutter_riverpod/flutter_riverpod.dart';
4 import 'package:haptic_feedback/haptic_feedback.dart';
5 import 'package:file_picker/file_picker.dart';
6 +import 'package:hugeicons/hugeicons.dart';
7 import 'package:localmind/core/models/enums.dart';
8 import 'package:localmind/features/servers/providers/server_providers.dart';
9
@@ -16,9 +17,9 @@ class ChatInputBar extends ConsumerStatefulWidget {
17 this.isStreaming = false,
18 });
19
19 - final void Function(String message) onSend;
20 + final void Function(String message, {List<File>? attachments}) onSend;
21 final VoidCallback onStop;
21 - final void Function(List<File> onAttach)? onAttach;
22 + final void Function(List<File> attachments)? onAttach;
23 final bool enabled;
24 final bool isStreaming;
25
@@ -30,7 +31,6 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
31 with SingleTickerProviderStateMixin {
32 final _controller = TextEditingController();
33 final _focusNode = FocusNode();
33 - bool _isComposing = false;
34 final List<File> _attachedFiles = [];
35 late AnimationController _sendButtonAnimController;
36 late Animation<double> _sendButtonScale;
@@ -72,12 +72,14 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
72
73 void _handleSubmit() {
74 final text = _controller.text.trim();
75 - if (text.isEmpty) return;
75 + if (text.isEmpty && _attachedFiles.isEmpty) return;
76
77 Haptics.vibrate(HapticsType.medium);
78 - widget.onSend(text);
78 + widget.onSend(text, attachments: List.from(_attachedFiles));
79 _controller.clear();
80 - setState(() => _isComposing = false);
80 + setState(() {
81 + _attachedFiles.clear();
82 + });
83 }
84
85 void _handleStop() {
@@ -85,7 +87,11 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
87 widget.onStop();
88 }
89
88 - Widget _buildSendButton(bool canSend, bool isDark) {
90 + Widget _buildActionButton(bool canSend, bool isDark) {
91 + // Action button theme from image: circular, high contrast (black in light, white in dark)
92 + final backgroundColor = isDark ? Colors.white : Colors.black;
93 + final iconColor = isDark ? Colors.black : Colors.white;
94 +
95 return GestureDetector(
96 onTapDown: canSend ? (_) => _sendButtonAnimController.forward() : null,
97 onTapUp: canSend ? (_) => _sendButtonAnimController.reverse() : null,
@@ -95,24 +101,13 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
101 child: AnimatedContainer(
102 duration: const Duration(milliseconds: 200),
103 curve: Curves.easeOut,
104 + width: 44,
105 + height: 44,
106 decoration: BoxDecoration(
99 - color: canSend
100 - ? (isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB))
101 - : (isDark ? const Color(0xFF3A3A3A) : const Color(0xFFE5E5E5)),
107 + color: backgroundColor.withValues(
108 + alpha: (canSend || widget.isStreaming) ? 1.0 : 0.2,
109 + ),
110 shape: BoxShape.circle,
103 - boxShadow: canSend
104 - ? [
105 - BoxShadow(
106 - color:
107 - (isDark
108 - ? const Color(0xFF3B82F6)
109 - : const Color(0xFF2563EB))
110 - .withValues(alpha: 0.3),
111 - blurRadius: 8,
112 - spreadRadius: 2,
113 - ),
114 - ]
115 - : null,
111 ),
112 child: IconButton(
113 icon: AnimatedSwitcher(
@@ -120,55 +115,30 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
115 transitionBuilder: (child, animation) {
116 return ScaleTransition(scale: animation, child: child);
117 },
123 - child: Icon(
124 - Icons.arrow_upward,
125 - key: ValueKey(canSend),
126 - color: canSend
127 - ? Colors.white
128 - : (isDark
129 - ? const Color(0xFF666666)
130 - : const Color(0xFF999999)),
131 - ),
118 + child: widget.isStreaming
119 + ? HugeIcon(
120 + icon: HugeIcons.strokeRoundedStop,
121 + key: const ValueKey('stop'),
122 + color: iconColor,
123 + size: 20,
124 + )
125 + : HugeIcon(
126 + icon: HugeIcons.strokeRoundedArrowUp01,
127 + key: ValueKey(canSend),
128 + color: iconColor,
129 + size: 20,
130 + ),
131 ),
133 - onPressed: canSend ? _handleSubmit : null,
134 - tooltip: 'Send message',
132 + onPressed: widget.isStreaming
133 + ? _handleStop
134 + : (canSend ? _handleSubmit : null),
135 + tooltip: widget.isStreaming ? 'Stop generation' : 'Send message',
136 ),
137 ),
138 ),
139 );
140 }
141
141 - Widget _buildStopButton() {
142 - return TweenAnimationBuilder<double>(
143 - tween: Tween(begin: 0.8, end: 1.0),
144 - duration: const Duration(milliseconds: 200),
145 - curve: Curves.elasticOut,
146 - builder: (context, scale, child) {
147 - return Transform.scale(
148 - scale: scale,
149 - child: Container(
150 - decoration: BoxDecoration(
151 - color: Colors.red[400],
152 - shape: BoxShape.circle,
153 - boxShadow: [
154 - BoxShadow(
155 - color: Colors.red.withValues(alpha: 0.3),
156 - blurRadius: 8,
157 - spreadRadius: 2,
158 - ),
159 - ],
160 - ),
161 - child: IconButton(
162 - icon: const Icon(Icons.stop, color: Colors.white),
163 - onPressed: _handleStop,
164 - tooltip: 'Stop generating',
165 - ),
166 - ),
167 - );
168 - },
169 - );
170 - }
171 -
142 @override
143 Widget build(BuildContext context) {
144 final theme = Theme.of(context);
@@ -179,215 +149,154 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
149 final canSend =
150 widget.enabled &&
151 isConnected &&
182 - _controller.text.trim().isNotEmpty &&
152 + (_controller.text.trim().isNotEmpty || _attachedFiles.isNotEmpty) &&
153 !widget.isStreaming;
154
155 return Container(
186 - padding: EdgeInsets.only(
187 - left: 12,
188 - right: 12,
189 - top: 8,
190 - bottom: MediaQuery.of(context).padding.bottom + 8,
191 - ),
156 + margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
157 + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
158 decoration: BoxDecoration(
193 - color: isDark ? const Color(0xFF0A0A0A) : const Color(0xFFFAFAFA),
194 - border: Border(
195 - top: BorderSide(
196 - color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E5E5),
159 + color: isDark ? const Color(0xFF1C1C1C) : Colors.white,
160 + borderRadius: BorderRadius.circular(40),
161 + boxShadow: [
162 + BoxShadow(
163 + color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.08),
164 + blurRadius: 20,
165 + offset: const Offset(0, 4),
166 ),
167 + ],
168 + border: Border.all(
169 + color: isDark ? const Color(0xFF2E2E2E) : const Color(0xFFE5E5E5),
170 + width: 1.2,
171 ),
172 ),
173 child: Column(
174 mainAxisSize: MainAxisSize.min,
175 children: [
176 + // Attachment Preview Area
177 AnimatedSize(
178 duration: const Duration(milliseconds: 200),
179 curve: Curves.easeOut,
180 child: _attachedFiles.isEmpty
181 ? const SizedBox.shrink()
182 : Container(
209 - height: 60,
210 - padding: const EdgeInsets.symmetric(
211 - horizontal: 8,
212 - vertical: 4,
183 + height: 70,
184 + padding: const EdgeInsets.only(
185 + left: 12,
186 + right: 12,
187 + top: 8,
188 + bottom: 4,
189 ),
190 child: ListView.separated(
191 scrollDirection: Axis.horizontal,
192 itemCount: _attachedFiles.length,
217 - separatorBuilder: (_, _) => const SizedBox(width: 6),
193 + separatorBuilder: (_, _) => const SizedBox(width: 8),
194 itemBuilder: (context, index) {
195 final file = _attachedFiles[index];
220 - return TweenAnimationBuilder<double>(
221 - tween: Tween(begin: 0.0, end: 1.0),
222 - duration: Duration(milliseconds: 200 + (index * 50)),
223 - curve: Curves.easeOut,
224 - builder: (context, value, child) {
225 - return Opacity(
226 - opacity: value,
227 - child: Transform.scale(
228 - scale: 0.8 + (0.2 * value),
229 - child: child,
196 + return Stack(
197 + clipBehavior: Clip.none,
198 + children: [
199 + Container(
200 + decoration: BoxDecoration(
201 + borderRadius: BorderRadius.circular(12),
202 + border: Border.all(
203 + color: isDark
204 + ? const Color(0xFF333333)
205 + : const Color(0xFFE0E0E0),
206 + ),
207 ),
231 - );
232 - },
233 - child: Stack(
234 - children: [
235 - ClipRRect(
236 - borderRadius: BorderRadius.circular(8),
208 + child: ClipRRect(
209 + borderRadius: BorderRadius.circular(11),
210 child: Image.file(
211 file,
239 - width: 52,
240 - height: 52,
212 + width: 48,
213 + height: 48,
214 fit: BoxFit.cover,
215 ),
216 ),
244 - Positioned(
245 - top: 0,
246 - right: 0,
247 - child: GestureDetector(
248 - onTap: () => setState(
249 - () => _attachedFiles.removeAt(index),
217 + ),
218 + Positioned(
219 + top: -4,
220 + right: -4,
221 + child: GestureDetector(
222 + onTap: () => setState(
223 + () => _attachedFiles.removeAt(index),
224 + ),
225 + child: Container(
226 + width: 18,
227 + height: 18,
228 + decoration: const BoxDecoration(
229 + color: Colors.black,
230 + shape: BoxShape.circle,
231 ),
251 - child: Container(
252 - width: 18,
253 - height: 18,
254 - decoration: BoxDecoration(
255 - color: Colors.black54,
256 - shape: BoxShape.circle,
257 - ),
258 - child: const Icon(
259 - Icons.close,
260 - size: 12,
261 - color: Colors.white,
262 - ),
232 + child: const Icon(
233 + Icons.close,
234 + size: 10,
235 + color: Colors.white,
236 ),
237 ),
238 ),
266 - ],
267 - ),
239 + ),
240 + ],
241 );
242 },
243 ),
244 ),
245 ),
246 + // Input Main Bar
247 Row(
274 - crossAxisAlignment: CrossAxisAlignment.end,
248 children: [
249 + // Add Attachment Button
250 + IconButton(
251 + icon: HugeIcon(
252 + icon: HugeIcons.strokeRoundedPlusSign,
253 + color: isDark ? Colors.white70 : Colors.black87,
254 + size: 22,
255 + ),
256 + onPressed: isConnected ? _handleAttach : null,
257 + tooltip: 'Attach images',
258 + ),
259 + const SizedBox(width: 4),
260 + // Text Field
261 Expanded(
277 - child: Container(
278 - constraints: const BoxConstraints(maxHeight: 150),
279 - decoration: BoxDecoration(
280 - color: isDark
281 - ? const Color(0xFF1F1F1F)
282 - : const Color(0xFFFFFFFF),
283 - borderRadius: BorderRadius.circular(24),
284 - border: Border.all(
285 - color: isDark
286 - ? const Color(0xFF333333)
287 - : const Color(0xFFE5E5E5),
288 - ),
262 + child: TextField(
263 + controller: _controller,
264 + focusNode: _focusNode,
265 + enabled: widget.enabled,
266 + maxLines: 5,
267 + minLines: 1,
268 + textInputAction: TextInputAction.newline,
269 + keyboardType: TextInputType.multiline,
270 + onChanged: (text) {
271 + setState(() {});
272 + },
273 + style: TextStyle(
274 + fontSize: 15,
275 + color: isDark ? Colors.white : Colors.black87,
276 ),
290 - child: Row(
291 - crossAxisAlignment: CrossAxisAlignment.end,
292 - children: [
293 - IconButton(
294 - icon: Icon(
295 - Icons.attach_file,
296 - size: 20,
297 - color: isDark
298 - ? const Color(0xFF888888)
299 - : const Color(0xFF666666),
300 - ),
301 - onPressed: isConnected ? _handleAttach : null,
302 - tooltip: 'Attach image',
303 - ),
304 - Expanded(
305 - child: TextField(
306 - controller: _controller,
307 - focusNode: _focusNode,
308 - enabled: widget.enabled,
309 - maxLines: null,
310 - textInputAction: TextInputAction.newline,
311 - keyboardType: TextInputType.multiline,
312 - onChanged: (text) {
313 - setState(
314 - () => _isComposing = text.trim().isNotEmpty,
315 - );
316 - },
317 - onSubmitted: (_) => _handleSubmit(),
318 - decoration: InputDecoration(
319 - hintText: 'Message LocalMind...',
320 - hintStyle: TextStyle(
321 - color: isDark
322 - ? const Color(0xFF666666)
323 - : const Color(0xFF999999),
324 - ),
325 - border: InputBorder.none,
326 - contentPadding: const EdgeInsets.symmetric(
327 - horizontal: 8,
328 - vertical: 12,
329 - ),
330 - ),
331 - ),
332 - ),
333 - ValueListenableBuilder<TextEditingValue>(
334 - valueListenable: _controller,
335 - builder: (context, value, child) {
336 - final showCounter = _isComposing && value.text.length >= 300;
337 - return AnimatedSize(
338 - duration: const Duration(milliseconds: 150),
339 - curve: Curves.easeOut,
340 - child: AnimatedOpacity(
341 - opacity: showCounter ? 1.0 : 0.0,
342 - duration: const Duration(milliseconds: 150),
343 - child: showCounter
344 - ? Padding(
345 - padding: const EdgeInsets.only(
346 - right: 12,
347 - bottom: 12,
348 - ),
349 - child: TweenAnimationBuilder<double>(
350 - tween: Tween(begin: 0.5, end: 1.0),
351 - duration: const Duration(milliseconds: 150),
352 - builder: (context, scale, child) {
353 - return Transform.scale(
354 - scale: scale,
355 - child: child,
356 - );
357 - },
358 - child: Text(
359 - '${value.text.length}',
360 - style: TextStyle(
361 - fontSize: 11,
362 - color: isDark
363 - ? const Color(0xFF666666)
364 - : const Color(0xFF999999),
365 - ),
366 - ),
367 - ),
368 - )
369 - : const SizedBox(width: 0),
370 - ),
371 - );
372 - },
373 - ),
374 - ],
277 + decoration: InputDecoration(
278 + hintText: 'Reply',
279 + hintStyle: TextStyle(
280 + color: isDark ? Colors.white38 : Colors.black38,
281 + fontSize: 15,
282 + fontWeight: FontWeight.w400,
283 + ),
284 + border: InputBorder.none,
285 + isDense: true,
286 + contentPadding: const EdgeInsets.symmetric(vertical: 12),
287 ),
288 ),
289 ),
290 const SizedBox(width: 8),
379 - AnimatedSwitcher(
380 - duration: const Duration(milliseconds: 200),
381 - transitionBuilder: (child, animation) {
382 - return ScaleTransition(
383 - scale: animation,
384 - child: FadeTransition(opacity: animation, child: child),
385 - );
386 - },
387 - child: widget.isStreaming
388 - ? _buildStopButton()
389 - : _buildSendButton(canSend, isDark),
390 - ),
291 + // Mic Icon (Visual only as per image)
292 + // HugeIcon(
293 + // icon: HugeIcons.strokeRoundedMic01,
294 + // color: isDark ? Colors.white38 : Colors.black38,
295 + // size: 22,
296 + // ),
297 + // const SizedBox(width: 8),
298 + // Action Button (Send/Stop)
299 + _buildActionButton(canSend, isDark),
300 ],
301 ),
302 ],
lib/features/chat/views/components/code_block.dart
+2 -1
@@ -87,7 +87,8 @@ class CodeBlock extends ConsumerWidget {
87 final theme = Theme.of(context);
88 final isDark = theme.brightness == Brightness.dark;
89 final settings = ref.watch(settingsProvider);
90 - final codeTheme = _getSyntaxTheme(settings.codeTheme);
90 + final themeName = isDark ? settings.codeThemeDark : settings.codeThemeLight;
91 + final codeTheme = _getSyntaxTheme(themeName);
92
93 final backgroundColor = isDark
94 ? const Color(0xFF1E1E1E)
lib/features/chat/views/components/processing_indicator.dart new
+35
@@ -0,0 +1,35 @@
1 +import 'package:flutter/material.dart';
2 +
3 +class ProcessingIndicator extends StatelessWidget {
4 + const ProcessingIndicator({super.key});
5 +
6 + @override
7 + Widget build(BuildContext context) {
8 + final isDark = Theme.of(context).brightness == Brightness.dark;
9 +
10 + return Row(
11 + mainAxisSize: MainAxisSize.min,
12 + children: [
13 + SizedBox(
14 + width: 12,
15 + height: 12,
16 + child: CircularProgressIndicator(
17 + strokeWidth: 2,
18 + valueColor: AlwaysStoppedAnimation<Color>(
19 + isDark ? Colors.grey[400]! : Colors.grey[600]!,
20 + ),
21 + ),
22 + ),
23 + const SizedBox(width: 8),
24 + Text(
25 + 'Processing...',
26 + style: TextStyle(
27 + color: isDark ? Colors.grey[400] : Colors.grey[600],
28 + fontSize: 14,
29 + fontStyle: FontStyle.italic,
30 + ),
31 + ),
32 + ],
33 + );
34 + }
35 +}
lib/features/settings/data/models/app_settings.dart
+10 -4
@@ -80,7 +80,10 @@ class AppSettings extends HiveObject {
80 final bool mcpEnabled;
81
82 @HiveField(16, defaultValue: SyntaxThemeName.vscodeDark)
83 - final SyntaxThemeName codeTheme;
83 + final SyntaxThemeName codeThemeDark;
84 +
85 + @HiveField(17, defaultValue: SyntaxThemeName.vscodeLight)
86 + final SyntaxThemeName codeThemeLight;
87
88 AppSettings({
89 this.temperature = 0.7,
@@ -99,7 +102,8 @@ class AppSettings extends HiveObject {
102 this.defaultPersonaId,
103 this.hasCompletedOnboarding = false,
104 this.mcpEnabled = true,
102 - this.codeTheme = SyntaxThemeName.vscodeDark,
105 + this.codeThemeDark = SyntaxThemeName.vscodeDark,
106 + this.codeThemeLight = SyntaxThemeName.vscodeLight,
107 });
108
109 AppSettings copyWith({
@@ -119,7 +123,8 @@ class AppSettings extends HiveObject {
123 String? defaultPersonaId,
124 bool? hasCompletedOnboarding,
125 bool? mcpEnabled,
122 - SyntaxThemeName? codeTheme,
126 + SyntaxThemeName? codeThemeDark,
127 + SyntaxThemeName? codeThemeLight,
128 }) {
129 return AppSettings(
130 temperature: temperature ?? this.temperature,
@@ -140,7 +145,8 @@ class AppSettings extends HiveObject {
145 hasCompletedOnboarding:
146 hasCompletedOnboarding ?? this.hasCompletedOnboarding,
147 mcpEnabled: mcpEnabled ?? this.mcpEnabled,
143 - codeTheme: codeTheme ?? this.codeTheme,
148 + codeThemeDark: codeThemeDark ?? this.codeThemeDark,
149 + codeThemeLight: codeThemeLight ?? this.codeThemeLight,
150 );
151 }
152 }
lib/features/settings/data/models/app_settings.g.dart
+8 -3
@@ -33,16 +33,19 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
33 defaultPersonaId: fields[13] as String?,
34 hasCompletedOnboarding: fields[14] == null ? false : fields[14] as bool,
35 mcpEnabled: fields[15] == null ? true : fields[15] as bool,
36 - codeTheme: fields[16] == null
36 + codeThemeDark: fields[16] == null
37 ? SyntaxThemeName.vscodeDark
38 : fields[16] as SyntaxThemeName,
39 + codeThemeLight: fields[17] == null
40 + ? SyntaxThemeName.vscodeLight
41 + : fields[17] as SyntaxThemeName,
42 );
43 }
44
45 @override
46 void write(BinaryWriter writer, AppSettings obj) {
47 writer
45 - ..writeByte(17)
48 + ..writeByte(18)
49 ..writeByte(0)
50 ..write(obj.temperature)
51 ..writeByte(1)
@@ -76,7 +79,9 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
79 ..writeByte(15)
80 ..write(obj.mcpEnabled)
81 ..writeByte(16)
79 - ..write(obj.codeTheme);
82 + ..write(obj.codeThemeDark)
83 + ..writeByte(17)
84 + ..write(obj.codeThemeLight);
85 }
86
87 @override
lib/features/settings/screens/settings_screen.dart
+13 -3
@@ -84,9 +84,17 @@ class SettingsScreen extends ConsumerWidget {
84 previewText: 'The quick brown fox jumps over the lazy dog.',
85 ),
86 _CodeThemeDropdown(
87 - current: settings.codeTheme,
87 + label: 'Code Theme (Dark)',
88 + current: settings.codeThemeDark,
89 onChanged: (v) =>
89 - ref.read(settingsProvider.notifier).setCodeTheme(v),
90 + ref.read(settingsProvider.notifier).setCodeThemeDark(v),
91 + isDark: isDark,
92 + ),
93 + _CodeThemeDropdown(
94 + label: 'Code Theme (Light)',
95 + current: settings.codeThemeLight,
96 + onChanged: (v) =>
97 + ref.read(settingsProvider.notifier).setCodeThemeLight(v),
98 isDark: isDark,
99 ),
100 const Divider(height: 32),
@@ -723,11 +731,13 @@ class _DropdownSetting extends StatelessWidget {
731
732 class _CodeThemeDropdown extends StatelessWidget {
733 const _CodeThemeDropdown({
734 + required this.label,
735 required this.current,
736 required this.onChanged,
737 required this.isDark,
738 });
739
740 + final String label;
741 final SyntaxThemeName current;
742 final ValueChanged<SyntaxThemeName> onChanged;
743 final bool isDark;
@@ -767,7 +777,7 @@ class _CodeThemeDropdown extends StatelessWidget {
777 crossAxisAlignment: CrossAxisAlignment.start,
778 children: [
779 Text(
770 - 'Code Theme',
780 + label,
781 style: TextStyle(
782 fontSize: 14,
783 fontWeight: FontWeight.w500,
pubspec.lock
+8
@@ -581,6 +581,14 @@ packages:
581 url: "https://pub.dev"
582 source: hosted
583 version: "6.1.0"
584 + logger:
585 + dependency: "direct main"
586 + description:
587 + name: logger
588 + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
589 + url: "https://pub.dev"
590 + source: hosted
591 + version: "2.7.0"
592 logging:
593 dependency: transitive
594 description:
pubspec.yaml
+1
@@ -49,6 +49,7 @@ dependencies:
49 google_fonts: ^6.2.1
50 flutter_syntax_view: 4.1.7
51 animations: 2.1.2
52 + logger: ^2.7.0
53
54 dev_dependencies:
55 flutter_test: