llms-py 2.0.35__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  2. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  4. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  6. llms/__pycache__/llms.cpython-312.pyc +0 -0
  7. llms/__pycache__/main.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-313.pyc +0 -0
  9. llms/__pycache__/main.cpython-314.pyc +0 -0
  10. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  11. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +154 -238
  12. llms/extensions/app/README.md +20 -0
  13. llms/extensions/app/__init__.py +530 -0
  14. llms/extensions/app/__pycache__/__init__.cpython-314.pyc +0 -0
  15. llms/extensions/app/__pycache__/db.cpython-314.pyc +0 -0
  16. llms/extensions/app/__pycache__/db_manager.cpython-314.pyc +0 -0
  17. llms/extensions/app/db.py +644 -0
  18. llms/extensions/app/db_manager.py +195 -0
  19. llms/extensions/app/requests.json +9073 -0
  20. llms/extensions/app/threads.json +15290 -0
  21. llms/{ui → extensions/app/ui}/Recents.mjs +91 -65
  22. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +124 -58
  23. llms/extensions/app/ui/threadStore.mjs +411 -0
  24. llms/extensions/core_tools/CALCULATOR.md +32 -0
  25. llms/extensions/core_tools/__init__.py +598 -0
  26. llms/extensions/core_tools/__pycache__/__init__.cpython-314.pyc +0 -0
  27. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  30. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  31. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  32. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  33. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  34. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  35. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  36. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  37. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  38. llms/extensions/core_tools/ui/codemirror/lib/codemirror.css +344 -0
  39. llms/extensions/core_tools/ui/codemirror/lib/codemirror.js +9884 -0
  40. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  41. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  42. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  43. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  44. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  45. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  46. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  47. llms/extensions/core_tools/ui/index.mjs +650 -0
  48. llms/extensions/gallery/README.md +61 -0
  49. llms/extensions/gallery/__init__.py +61 -0
  50. llms/extensions/gallery/__pycache__/__init__.cpython-314.pyc +0 -0
  51. llms/extensions/gallery/__pycache__/db.cpython-314.pyc +0 -0
  52. llms/extensions/gallery/db.py +298 -0
  53. llms/extensions/gallery/ui/index.mjs +482 -0
  54. llms/extensions/katex/README.md +39 -0
  55. llms/extensions/katex/__init__.py +6 -0
  56. llms/extensions/katex/__pycache__/__init__.cpython-314.pyc +0 -0
  57. llms/extensions/katex/ui/README.md +125 -0
  58. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  59. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  60. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  61. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  62. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  63. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  64. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  65. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  66. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  67. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  68. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  69. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  70. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  71. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  72. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  124. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  125. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  126. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  127. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  128. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  129. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  130. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  131. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  132. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  133. llms/extensions/katex/ui/index.mjs +92 -0
  134. llms/extensions/katex/ui/katex-swap.css +1230 -0
  135. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  136. llms/extensions/katex/ui/katex.css +1230 -0
  137. llms/extensions/katex/ui/katex.js +19080 -0
  138. llms/extensions/katex/ui/katex.min.css +1 -0
  139. llms/extensions/katex/ui/katex.min.js +1 -0
  140. llms/extensions/katex/ui/katex.min.mjs +1 -0
  141. llms/extensions/katex/ui/katex.mjs +18547 -0
  142. llms/extensions/providers/__init__.py +18 -0
  143. llms/extensions/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  144. llms/extensions/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  145. llms/extensions/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  146. llms/extensions/providers/__pycache__/google.cpython-314.pyc +0 -0
  147. llms/extensions/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  148. llms/extensions/providers/__pycache__/openai.cpython-314.pyc +0 -0
  149. llms/extensions/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  150. llms/extensions/providers/anthropic.py +229 -0
  151. llms/extensions/providers/chutes.py +155 -0
  152. llms/extensions/providers/google.py +378 -0
  153. llms/extensions/providers/nvidia.py +105 -0
  154. llms/extensions/providers/openai.py +156 -0
  155. llms/extensions/providers/openrouter.py +72 -0
  156. llms/extensions/system_prompts/README.md +22 -0
  157. llms/extensions/system_prompts/__init__.py +45 -0
  158. llms/extensions/system_prompts/__pycache__/__init__.cpython-314.pyc +0 -0
  159. llms/extensions/system_prompts/ui/index.mjs +280 -0
  160. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  161. llms/extensions/tools/__init__.py +5 -0
  162. llms/extensions/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  163. llms/extensions/tools/ui/index.mjs +204 -0
  164. llms/index.html +35 -77
  165. llms/llms.json +357 -1186
  166. llms/main.py +2349 -591
  167. llms/providers-extra.json +356 -0
  168. llms/providers.json +1 -0
  169. llms/ui/App.mjs +151 -60
  170. llms/ui/ai.mjs +132 -60
  171. llms/ui/app.css +2173 -161
  172. llms/ui/ctx.mjs +365 -0
  173. llms/ui/index.mjs +129 -0
  174. llms/ui/lib/charts.mjs +9 -13
  175. llms/ui/lib/servicestack-vue.mjs +3 -3
  176. llms/ui/lib/vue.min.mjs +10 -9
  177. llms/ui/lib/vue.mjs +1796 -1635
  178. llms/ui/markdown.mjs +18 -7
  179. llms/ui/modules/chat/ChatBody.mjs +691 -0
  180. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +9 -9
  181. llms/ui/modules/chat/index.mjs +828 -0
  182. llms/ui/modules/layout.mjs +243 -0
  183. llms/ui/modules/model-selector.mjs +851 -0
  184. llms/ui/tailwind.input.css +496 -80
  185. llms/ui/utils.mjs +161 -93
  186. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/METADATA +1 -1
  187. llms_py-3.0.0.dist-info/RECORD +202 -0
  188. llms/ui/Avatar.mjs +0 -85
  189. llms/ui/Brand.mjs +0 -52
  190. llms/ui/ChatPrompt.mjs +0 -590
  191. llms/ui/Main.mjs +0 -823
  192. llms/ui/ModelSelector.mjs +0 -78
  193. llms/ui/OAuthSignIn.mjs +0 -92
  194. llms/ui/ProviderIcon.mjs +0 -30
  195. llms/ui/ProviderStatus.mjs +0 -105
  196. llms/ui/SignIn.mjs +0 -64
  197. llms/ui/SystemPromptEditor.mjs +0 -31
  198. llms/ui/SystemPromptSelector.mjs +0 -56
  199. llms/ui/Welcome.mjs +0 -8
  200. llms/ui/threadStore.mjs +0 -563
  201. llms/ui.json +0 -1069
  202. llms_py-2.0.35.dist-info/RECORD +0 -48
  203. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/WHEEL +0 -0
  204. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/entry_points.txt +0 -0
  205. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/licenses/LICENSE +0 -0
  206. {llms_py-2.0.35.dist-info → llms_py-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,378 @@
1
+ import base64
2
+ import io
3
+ import json
4
+ import time
5
+ import wave
6
+
7
+ import aiohttp
8
+
9
+ # class GoogleOpenAiProvider(OpenAiCompatible):
10
+ # sdk = "google-openai-compatible"
11
+
12
+ # def __init__(self, api_key, **kwargs):
13
+ # super().__init__(api="https://generativelanguage.googleapis.com", api_key=api_key, **kwargs)
14
+ # self.chat_url = "https://generativelanguage.googleapis.com/v1beta/chat/completions"
15
+
16
+
17
+ def install_google(ctx):
18
+ from llms.main import OpenAiCompatible
19
+
20
+ def gemini_chat_summary(gemini_chat):
21
+ """Summarize Gemini chat completion request for logging. Replace inline_data with size of content only"""
22
+ clone = json.loads(json.dumps(gemini_chat))
23
+ for content in clone["contents"]:
24
+ for part in content["parts"]:
25
+ if "inline_data" in part:
26
+ data = part["inline_data"]["data"]
27
+ part["inline_data"]["data"] = f"({len(data)})"
28
+ return json.dumps(clone, indent=2)
29
+
30
+ def gemini_response_summary(obj):
31
+ to = {}
32
+ for k, v in obj.items():
33
+ if k == "candidates":
34
+ candidates = []
35
+ for candidate in v:
36
+ c = {}
37
+ for ck, cv in candidate.items():
38
+ if ck == "content":
39
+ content = {}
40
+ for content_k, content_v in cv.items():
41
+ if content_k == "parts":
42
+ parts = []
43
+ for part in content_v:
44
+ p = {}
45
+ for pk, pv in part.items():
46
+ if pk == "inlineData":
47
+ p[pk] = {
48
+ "mimeType": pv.get("mimeType"),
49
+ "data": f"({len(pv.get('data'))})",
50
+ }
51
+ else:
52
+ p[pk] = pv
53
+ parts.append(p)
54
+ content[content_k] = parts
55
+ else:
56
+ content[content_k] = content_v
57
+ c[ck] = content
58
+ else:
59
+ c[ck] = cv
60
+ candidates.append(c)
61
+ to[k] = candidates
62
+ else:
63
+ to[k] = v
64
+ return to
65
+
66
+ class GoogleProvider(OpenAiCompatible):
67
+ sdk = "@ai-sdk/google"
68
+
69
+ def __init__(self, **kwargs):
70
+ new_kwargs = {"api": "https://generativelanguage.googleapis.com", **kwargs}
71
+ super().__init__(**new_kwargs)
72
+ self.safety_settings = kwargs.get("safety_settings")
73
+ self.thinking_config = kwargs.get("thinking_config")
74
+ self.speech_config = kwargs.get("speech_config")
75
+ self.tools = kwargs.get("tools")
76
+ self.curl = kwargs.get("curl")
77
+ self.headers = kwargs.get("headers", {"Content-Type": "application/json"})
78
+ # Google fails when using Authorization header, use query string param instead
79
+ if "Authorization" in self.headers:
80
+ del self.headers["Authorization"]
81
+
82
+ async def chat(self, chat):
83
+ chat["model"] = self.provider_model(chat["model"]) or chat["model"]
84
+
85
+ chat = await self.process_chat(chat)
86
+ generation_config = {}
87
+
88
+ # Filter out system messages and convert to proper Gemini format
89
+ contents = []
90
+ system_prompt = None
91
+
92
+ async with aiohttp.ClientSession() as session:
93
+ for message in chat["messages"]:
94
+ if message["role"] == "system":
95
+ content = message["content"]
96
+ if isinstance(content, list):
97
+ for item in content:
98
+ if "text" in item:
99
+ system_prompt = item["text"]
100
+ break
101
+ elif isinstance(content, str):
102
+ system_prompt = content
103
+ elif "content" in message:
104
+ if isinstance(message["content"], list):
105
+ parts = []
106
+ for item in message["content"]:
107
+ if "type" in item:
108
+ if item["type"] == "image_url" and "image_url" in item:
109
+ image_url = item["image_url"]
110
+ if "url" not in image_url:
111
+ continue
112
+ url = image_url["url"]
113
+ if not url.startswith("data:"):
114
+ raise Exception("Image was not downloaded: " + url)
115
+ # Extract mime type from data uri
116
+ mimetype = url.split(";", 1)[0].split(":", 1)[1] if ";" in url else "image/png"
117
+ base64_data = url.split(",", 1)[1]
118
+ parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
119
+ elif item["type"] == "input_audio" and "input_audio" in item:
120
+ input_audio = item["input_audio"]
121
+ if "data" not in input_audio:
122
+ continue
123
+ data = input_audio["data"]
124
+ format = input_audio["format"]
125
+ mimetype = f"audio/{format}"
126
+ parts.append({"inline_data": {"mime_type": mimetype, "data": data}})
127
+ elif item["type"] == "file" and "file" in item:
128
+ file = item["file"]
129
+ if "file_data" not in file:
130
+ continue
131
+ data = file["file_data"]
132
+ if not data.startswith("data:"):
133
+ raise (Exception("File was not downloaded: " + data))
134
+ # Extract mime type from data uri
135
+ mimetype = (
136
+ data.split(";", 1)[0].split(":", 1)[1]
137
+ if ";" in data
138
+ else "application/octet-stream"
139
+ )
140
+ base64_data = data.split(",", 1)[1]
141
+ parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
142
+ if "text" in item:
143
+ text = item["text"]
144
+ parts.append({"text": text})
145
+ if len(parts) > 0:
146
+ contents.append(
147
+ {
148
+ "role": message["role"]
149
+ if "role" in message and message["role"] == "user"
150
+ else "model",
151
+ "parts": parts,
152
+ }
153
+ )
154
+ else:
155
+ content = message["content"]
156
+ contents.append(
157
+ {
158
+ "role": message["role"]
159
+ if "role" in message and message["role"] == "user"
160
+ else "model",
161
+ "parts": [{"text": content}],
162
+ }
163
+ )
164
+
165
+ gemini_chat = {
166
+ "contents": contents,
167
+ }
168
+
169
+ if self.safety_settings:
170
+ gemini_chat["safetySettings"] = self.safety_settings
171
+
172
+ # Add system instruction if present
173
+ if system_prompt is not None:
174
+ gemini_chat["systemInstruction"] = {"parts": [{"text": system_prompt}]}
175
+
176
+ if "max_completion_tokens" in chat:
177
+ generation_config["maxOutputTokens"] = chat["max_completion_tokens"]
178
+ if "stop" in chat:
179
+ generation_config["stopSequences"] = [chat["stop"]]
180
+ if "temperature" in chat:
181
+ generation_config["temperature"] = chat["temperature"]
182
+ if "top_p" in chat:
183
+ generation_config["topP"] = chat["top_p"]
184
+ if "top_logprobs" in chat:
185
+ generation_config["topK"] = chat["top_logprobs"]
186
+
187
+ if "thinkingConfig" in chat:
188
+ generation_config["thinkingConfig"] = chat["thinkingConfig"]
189
+ elif self.thinking_config:
190
+ generation_config["thinkingConfig"] = self.thinking_config
191
+
192
+ if len(generation_config) > 0:
193
+ gemini_chat["generationConfig"] = generation_config
194
+
195
+ if "tools" in chat:
196
+ # gemini_chat["tools"] = chat["tools"]
197
+ ctx.log("Error: tools not supported in Gemini")
198
+ elif self.tools:
199
+ # gemini_chat["tools"] = self.tools.copy()
200
+ ctx.log("Error: tools not supported in Gemini")
201
+
202
+ if "modalities" in chat:
203
+ generation_config["responseModalities"] = [modality.upper() for modality in chat["modalities"]]
204
+ if "image" in chat["modalities"] and "image_config" in chat:
205
+ # delete thinkingConfig
206
+ del generation_config["thinkingConfig"]
207
+ config_map = {
208
+ "aspect_ratio": "aspectRatio",
209
+ "image_size": "imageSize",
210
+ }
211
+ generation_config["imageConfig"] = {
212
+ config_map[k]: v for k, v in chat["image_config"].items() if k in config_map
213
+ }
214
+ if "audio" in chat["modalities"] and self.speech_config:
215
+ del generation_config["thinkingConfig"]
216
+ generation_config["speechConfig"] = self.speech_config.copy()
217
+ # Currently Google Audio Models only accept AUDIO
218
+ generation_config["responseModalities"] = ["AUDIO"]
219
+
220
+ started_at = int(time.time() * 1000)
221
+ gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
222
+
223
+ ctx.log(f"POST {gemini_chat_url}")
224
+ ctx.log(gemini_chat_summary(gemini_chat))
225
+ started_at = time.time()
226
+
227
+ if ctx.MOCK and "modalities" in chat:
228
+ print("Mocking Google Gemini Image")
229
+ with open(f"{ctx.MOCK_DIR}/gemini-image.json") as f:
230
+ obj = json.load(f)
231
+ else:
232
+ try:
233
+ async with session.post(
234
+ gemini_chat_url,
235
+ headers=self.headers,
236
+ data=json.dumps(gemini_chat),
237
+ timeout=aiohttp.ClientTimeout(total=120),
238
+ ) as res:
239
+ obj = await self.response_json(res)
240
+ except Exception as e:
241
+ ctx.log(f"Error: {res.status} {res.reason}: {e}")
242
+ text = await res.text()
243
+ try:
244
+ obj = json.loads(text)
245
+ except:
246
+ ctx.log(text)
247
+ raise e
248
+
249
+ if "error" in obj:
250
+ ctx.log(f"Error: {obj['error']}")
251
+ raise Exception(obj["error"]["message"])
252
+
253
+ if ctx.debug:
254
+ ctx.dbg(json.dumps(gemini_response_summary(obj), indent=2))
255
+
256
+ # calculate cost per generation
257
+ cost = None
258
+ token_costs = obj.get("metadata", {}).get("pricing", "")
259
+ if token_costs:
260
+ input_price, output_price = token_costs.split("/")
261
+ input_per_token = float(input_price) / 1000000
262
+ output_per_token = float(output_price) / 1000000
263
+ if "usageMetadata" in obj:
264
+ input_tokens = obj["usageMetadata"].get("promptTokenCount", 0)
265
+ output_tokens = obj["usageMetadata"].get("candidatesTokenCount", 0)
266
+ cost = (input_per_token * input_tokens) + (output_per_token * output_tokens)
267
+
268
+ response = {
269
+ "id": f"chatcmpl-{started_at}",
270
+ "created": started_at,
271
+ "model": obj.get("modelVersion", chat["model"]),
272
+ }
273
+ choices = []
274
+ for i, candidate in enumerate(obj["candidates"]):
275
+ role = "assistant"
276
+ if "content" in candidate and "role" in candidate["content"]:
277
+ role = "assistant" if candidate["content"]["role"] == "model" else candidate["content"]["role"]
278
+
279
+ # Safely extract content from all text parts
280
+ content = ""
281
+ reasoning = ""
282
+ images = []
283
+ audios = []
284
+ if "content" in candidate and "parts" in candidate["content"]:
285
+ text_parts = []
286
+ reasoning_parts = []
287
+ for part in candidate["content"]["parts"]:
288
+ if "text" in part:
289
+ if "thought" in part and part["thought"]:
290
+ reasoning_parts.append(part["text"])
291
+ else:
292
+ text_parts.append(part["text"])
293
+ if "inlineData" in part:
294
+ inline_data = part["inlineData"]
295
+ mime_type = inline_data.get("mimeType", "image/png")
296
+ if mime_type.startswith("image"):
297
+ ext = mime_type.split("/")[1]
298
+ base64_data = inline_data["data"]
299
+ filename = f"{chat['model'].split('/')[-1]}-{len(images)}.{ext}"
300
+ ctx.log(f"inlineData {len(base64_data)} {mime_type} {filename}")
301
+ relative_url, info = ctx.save_image_to_cache(
302
+ base64_data,
303
+ filename,
304
+ ctx.to_file_info(chat, {"cost": cost}),
305
+ )
306
+ images.append(
307
+ {
308
+ "type": "image_url",
309
+ "index": len(images),
310
+ "image_url": {
311
+ "url": relative_url,
312
+ },
313
+ }
314
+ )
315
+ elif mime_type.startswith("audio"):
316
+ # mime_type audio/L16;codec=pcm;rate=24000
317
+ base64_data = inline_data["data"]
318
+
319
+ pcm = base64.b64decode(base64_data)
320
+ # Convert PCM to WAV
321
+ wav_io = io.BytesIO()
322
+ with wave.open(wav_io, "wb") as wf:
323
+ wf.setnchannels(1)
324
+ wf.setsampwidth(2)
325
+ wf.setframerate(24000)
326
+ wf.writeframes(pcm)
327
+ wav_data = wav_io.getvalue()
328
+
329
+ ext = mime_type.split("/")[1].split(";")[0]
330
+ pcm_filename = f"{chat['model'].split('/')[-1]}-{len(audios)}.{ext}"
331
+ filename = pcm_filename.replace(f".{ext}", ".wav")
332
+ ctx.log(f"inlineData {len(base64_data)} {mime_type} {filename}")
333
+
334
+ relative_url, info = ctx.save_bytes_to_cache(
335
+ wav_data,
336
+ filename,
337
+ ctx.to_file_info(chat, {"cost": cost}),
338
+ )
339
+
340
+ audios.append(
341
+ {
342
+ "type": "audio_url",
343
+ "index": len(audios),
344
+ "audio_url": {
345
+ "url": relative_url,
346
+ },
347
+ }
348
+ )
349
+ content = " ".join(text_parts)
350
+ reasoning = " ".join(reasoning_parts)
351
+
352
+ choice = {
353
+ "index": i,
354
+ "finish_reason": candidate.get("finishReason", "stop"),
355
+ "message": {
356
+ "role": role,
357
+ "content": content,
358
+ },
359
+ }
360
+ if reasoning:
361
+ choice["message"]["reasoning"] = reasoning
362
+ if len(images) > 0:
363
+ choice["message"]["images"] = images
364
+ if len(audios) > 0:
365
+ choice["message"]["audios"] = audios
366
+ choices.append(choice)
367
+ response["choices"] = choices
368
+ if "usageMetadata" in obj:
369
+ usage = obj["usageMetadata"]
370
+ response["usage"] = {
371
+ "completion_tokens": usage["candidatesTokenCount"],
372
+ "total_tokens": usage["totalTokenCount"],
373
+ "prompt_tokens": usage["promptTokenCount"],
374
+ }
375
+
376
+ return ctx.log_json(self.to_response(response, chat, started_at))
377
+
378
+ ctx.add_provider(GoogleProvider)
@@ -0,0 +1,105 @@
1
+ import json
2
+ import time
3
+
4
+ import aiohttp
5
+
6
+
7
+ def install_nvidia(ctx):
8
+ from llms.main import GeneratorBase
9
+
10
+ class NvidiaGenAi(GeneratorBase):
11
+ sdk = "nvidia/image"
12
+
13
+ def __init__(self, **kwargs):
14
+ super().__init__(**kwargs)
15
+ self.width = int(kwargs.get("width", 1024))
16
+ self.height = int(kwargs.get("height", 1024))
17
+ self.cfg_scale = float(kwargs.get("cfg_scale", 3))
18
+ self.steps = int(kwargs.get("steps", 20))
19
+ self.mode = kwargs.get("mode", "base")
20
+ self.gen_url = kwargs.get("api", "https://ai.api.nvidia.com/v1/genai")
21
+
22
+ def to_response(self, response, chat, started_at):
23
+ if "artifacts" in response:
24
+ for artifact in response["artifacts"]:
25
+ base64 = artifact.get("base64")
26
+ seed = artifact.get("seed")
27
+ filename = f"{seed}.png"
28
+ if "model" in chat:
29
+ last_model = "/" in chat["model"] and chat["model"].split("/")[-1] or chat["model"]
30
+ filename = f"{last_model}_{seed}.png"
31
+
32
+ relative_url, info = ctx.save_image_to_cache(
33
+ base64,
34
+ filename,
35
+ ctx.to_file_info(chat, {"seed": seed}),
36
+ )
37
+ return {
38
+ "choices": [
39
+ {
40
+ "message": {
41
+ "role": "assistant",
42
+ "content": self.default_content,
43
+ "images": [
44
+ {
45
+ "type": "image_url",
46
+ "image_url": {
47
+ "url": relative_url,
48
+ },
49
+ }
50
+ ],
51
+ }
52
+ }
53
+ ]
54
+ }
55
+ raise Exception("No artifacts in response")
56
+
57
+ async def chat(self, chat, provider=None):
58
+ headers = self.get_headers(provider, chat)
59
+ if provider is not None:
60
+ chat["model"] = provider.provider_model(chat["model"]) or chat["model"]
61
+
62
+ prompt = ctx.last_user_prompt(chat)
63
+
64
+ gen_request = {
65
+ "prompt": prompt,
66
+ }
67
+ modalities = chat.get("modalities", ["text"])
68
+ if "image" in modalities:
69
+ image_config = chat.get("image_config", {})
70
+ aspect_ratio = image_config.get("aspect_ratio")
71
+ if aspect_ratio:
72
+ dimension = ctx.app.aspect_ratios.get(aspect_ratio)
73
+ if dimension:
74
+ width, height = dimension.split("×")
75
+ gen_request["width"] = int(width)
76
+ gen_request["height"] = int(height)
77
+ else:
78
+ gen_request["width"] = self.width
79
+ gen_request["height"] = self.height
80
+
81
+ gen_request["mode"] = self.mode
82
+ gen_request["cfg_scale"] = self.cfg_scale
83
+ gen_request["steps"] = self.steps
84
+
85
+ gen_url = f"{self.gen_url}/{chat['model']}"
86
+ ctx.log(f"POST {gen_url}")
87
+ ctx.log(self.gen_summary(gen_request))
88
+ # remove metadata if any (conflicts with some providers, e.g. Z.ai)
89
+ gen_request.pop("metadata", None)
90
+ started_at = time.time()
91
+
92
+ if ctx.MOCK:
93
+ ctx.log("Mocking NvidiaGenAi")
94
+ text = ctx.text_from_file(f"{ctx.MOCK_DIR}/nvidia-image.json")
95
+ return self.to_response(json.loads(text), chat, started_at)
96
+ else:
97
+ async with aiohttp.ClientSession() as session, session.post(
98
+ gen_url,
99
+ headers=headers,
100
+ data=json.dumps(gen_request),
101
+ timeout=aiohttp.ClientTimeout(total=120),
102
+ ) as response:
103
+ return self.to_response(await self.response_json(response), chat, started_at)
104
+
105
+ ctx.add_provider(NvidiaGenAi)
@@ -0,0 +1,156 @@
1
+ import base64
2
+ import json
3
+ import mimetypes
4
+ import time
5
+
6
+ import aiohttp
7
+
8
+
9
+ def install_openai(ctx):
10
+ from llms.main import GeneratorBase, OpenAiCompatible
11
+
12
+ class OpenAiProvider(OpenAiCompatible):
13
+ sdk = "@ai-sdk/openai"
14
+
15
+ def __init__(self, **kwargs):
16
+ if "api" not in kwargs:
17
+ kwargs["api"] = "https://api.openai.com/v1"
18
+ super().__init__(**kwargs)
19
+ self.modalities["image"] = OpenAiGenerator(**kwargs)
20
+
21
+ # https://platform.openai.com/docs/api-reference/images
22
+ class OpenAiGenerator(GeneratorBase):
23
+ sdk = "openai/image"
24
+
25
+ def __init__(self, **kwargs):
26
+ super().__init__(**kwargs)
27
+ self.api = "https://api.openai.com/v1/images/generations"
28
+ self.map_image_models = kwargs.get(
29
+ "map_image_models",
30
+ {
31
+ "gpt-5.1-codex-mini": "gpt-image-1-mini",
32
+ },
33
+ )
34
+
35
+ def aspect_ratio_to_size(self, aspect_ratio, model):
36
+ w, h = aspect_ratio.split(":")
37
+ width = int(w)
38
+ height = int(h)
39
+ if model == "dall-e-2":
40
+ return "1024x1024"
41
+ if model == "dall-e-3":
42
+ if width > height:
43
+ return "1792x1024"
44
+ elif height > width:
45
+ return "1024x1792"
46
+ if width > height:
47
+ return "1536x1024"
48
+ elif height > width:
49
+ return "1024x1536"
50
+ return "1024x1024"
51
+
52
+ async def to_response(self, response, chat, started_at):
53
+ # go through all image responses and save them to cache
54
+ # Try to extract and save images
55
+ images = []
56
+ if "data" in response:
57
+ for i, item in enumerate(response["data"]):
58
+ image_url = item.get("url")
59
+ b64_json = item.get("b64_json")
60
+
61
+ ext = "png"
62
+ image_data = None
63
+
64
+ if b64_json:
65
+ image_data = base64.b64decode(b64_json)
66
+ elif image_url:
67
+ ctx.log(f"GET {image_url}")
68
+ async with aiohttp.ClientSession() as session, await session.get(image_url) as res:
69
+ if res.status == 200:
70
+ image_data = await res.read()
71
+ content_type = res.headers.get("Content-Type")
72
+ if content_type:
73
+ ext = mimetypes.guess_extension(content_type)
74
+ if ext:
75
+ ext = ext.lstrip(".") # remove leading dot
76
+ # Fallback if guess_extension returns None or if we want to be safe
77
+ if not ext:
78
+ ext = "png"
79
+ else:
80
+ raise Exception(f"Failed to download image: {res.status}")
81
+
82
+ if image_data:
83
+ relative_url, info = ctx.save_image_to_cache(
84
+ image_data,
85
+ f"{chat['model']}-{i}.{ext}",
86
+ ctx.to_file_info(chat),
87
+ )
88
+ images.append(
89
+ {
90
+ "type": "image_url",
91
+ "image_url": {
92
+ "url": relative_url,
93
+ },
94
+ }
95
+ )
96
+ else:
97
+ raise Exception("No image data found")
98
+
99
+ return {
100
+ "choices": [
101
+ {
102
+ "message": {
103
+ "role": "assistant",
104
+ "content": self.default_content,
105
+ "images": images,
106
+ }
107
+ }
108
+ ]
109
+ }
110
+ if "error" in response:
111
+ raise Exception(response["error"]["message"])
112
+
113
+ ctx.log(json.dumps(response, indent=2))
114
+ raise Exception("No 'data' field in response.")
115
+
116
+ async def chat(self, chat, provider=None):
117
+ headers = self.get_headers(provider, chat)
118
+
119
+ if chat["model"] in self.map_image_models:
120
+ chat["model"] = self.map_image_models[chat["model"]]
121
+
122
+ aspect_ratio = "1:1"
123
+ if "image_config" in chat and "aspect_ratio" in chat["image_config"]:
124
+ aspect_ratio = chat["image_config"].get("aspect_ratio", "1:1")
125
+ payload = {
126
+ "model": chat["model"],
127
+ "prompt": ctx.last_user_prompt(chat),
128
+ "size": self.aspect_ratio_to_size(aspect_ratio, chat["model"]),
129
+ }
130
+ if provider is not None:
131
+ chat["model"] = provider.provider_model(chat["model"]) or chat["model"]
132
+
133
+ started_at = time.time()
134
+ if ctx.MOCK:
135
+ print("Mocking OpenAiGenerator")
136
+ text = ctx.text_from_file(f"{ctx.MOCK_DIR}/openai-image.json")
137
+ return await self.to_response(json.loads(text), chat, started_at)
138
+ else:
139
+ ctx.log(f"POST {self.api}")
140
+ # _log(json.dumps(headers, indent=2))
141
+ ctx.log(json.dumps(payload, indent=2))
142
+ async with aiohttp.ClientSession() as session, session.post(
143
+ self.api, headers=headers, json=payload
144
+ ) as response:
145
+ text = await response.text()
146
+ ctx.log(text[:1024] + (len(text) > 1024 and "..." or ""))
147
+ if response.status < 300:
148
+ return ctx.log_json(await self.to_response(json.loads(text), chat, started_at))
149
+ else:
150
+ raise Exception(f"Failed to generate image {response.status}")
151
+
152
+ ctx.add_provider(OpenAiProvider)
153
+ ctx.add_provider(OpenAiGenerator)
154
+
155
+
156
+ __install__ = install_openai