llms-py 2.0.9__py3-none-any.whl → 3.0.10__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 (194) hide show
  1. llms/__init__.py +4 -0
  2. llms/__main__.py +9 -0
  3. llms/db.py +359 -0
  4. llms/extensions/analytics/ui/index.mjs +1444 -0
  5. llms/extensions/app/README.md +20 -0
  6. llms/extensions/app/__init__.py +589 -0
  7. llms/extensions/app/db.py +536 -0
  8. {llms_py-2.0.9.data/data → llms/extensions/app}/ui/Recents.mjs +100 -73
  9. llms_py-2.0.9.data/data/ui/Sidebar.mjs → llms/extensions/app/ui/index.mjs +150 -79
  10. llms/extensions/app/ui/threadStore.mjs +433 -0
  11. llms/extensions/core_tools/CALCULATOR.md +32 -0
  12. llms/extensions/core_tools/__init__.py +637 -0
  13. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  14. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  15. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  16. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  17. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  18. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  19. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  20. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  22. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  23. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  24. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  25. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  26. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  27. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  28. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  29. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  30. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  31. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  32. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  33. llms/extensions/core_tools/ui/index.mjs +650 -0
  34. llms/extensions/gallery/README.md +61 -0
  35. llms/extensions/gallery/__init__.py +63 -0
  36. llms/extensions/gallery/db.py +243 -0
  37. llms/extensions/gallery/ui/index.mjs +482 -0
  38. llms/extensions/katex/README.md +39 -0
  39. llms/extensions/katex/__init__.py +6 -0
  40. llms/extensions/katex/ui/README.md +125 -0
  41. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  42. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  43. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  44. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  45. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  46. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  47. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  48. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  49. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  50. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  51. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  52. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  53. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  54. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  55. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  56. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  57. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  58. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  59. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  60. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  61. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  62. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  63. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  116. llms/extensions/katex/ui/index.mjs +92 -0
  117. llms/extensions/katex/ui/katex-swap.css +1230 -0
  118. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  119. llms/extensions/katex/ui/katex.css +1230 -0
  120. llms/extensions/katex/ui/katex.js +19080 -0
  121. llms/extensions/katex/ui/katex.min.css +1 -0
  122. llms/extensions/katex/ui/katex.min.js +1 -0
  123. llms/extensions/katex/ui/katex.min.mjs +1 -0
  124. llms/extensions/katex/ui/katex.mjs +18547 -0
  125. llms/extensions/providers/__init__.py +22 -0
  126. llms/extensions/providers/anthropic.py +233 -0
  127. llms/extensions/providers/cerebras.py +37 -0
  128. llms/extensions/providers/chutes.py +153 -0
  129. llms/extensions/providers/google.py +481 -0
  130. llms/extensions/providers/nvidia.py +103 -0
  131. llms/extensions/providers/openai.py +154 -0
  132. llms/extensions/providers/openrouter.py +74 -0
  133. llms/extensions/providers/zai.py +182 -0
  134. llms/extensions/system_prompts/README.md +22 -0
  135. llms/extensions/system_prompts/__init__.py +45 -0
  136. llms/extensions/system_prompts/ui/index.mjs +280 -0
  137. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  138. llms/extensions/tools/__init__.py +144 -0
  139. llms/extensions/tools/ui/index.mjs +706 -0
  140. llms/index.html +58 -0
  141. llms/llms.json +400 -0
  142. llms/main.py +4407 -0
  143. llms/providers-extra.json +394 -0
  144. llms/providers.json +1 -0
  145. llms/ui/App.mjs +188 -0
  146. llms/ui/ai.mjs +217 -0
  147. llms/ui/app.css +7081 -0
  148. llms/ui/ctx.mjs +412 -0
  149. llms/ui/index.mjs +131 -0
  150. llms/ui/lib/chart.js +14 -0
  151. llms/ui/lib/charts.mjs +16 -0
  152. llms/ui/lib/color.js +14 -0
  153. llms/ui/lib/servicestack-vue.mjs +37 -0
  154. llms/ui/lib/vue.min.mjs +13 -0
  155. llms/ui/lib/vue.mjs +18530 -0
  156. {llms_py-2.0.9.data/data → llms}/ui/markdown.mjs +33 -15
  157. llms/ui/modules/chat/ChatBody.mjs +976 -0
  158. llms/ui/modules/chat/SettingsDialog.mjs +374 -0
  159. llms/ui/modules/chat/index.mjs +991 -0
  160. llms/ui/modules/icons.mjs +46 -0
  161. llms/ui/modules/layout.mjs +271 -0
  162. llms/ui/modules/model-selector.mjs +811 -0
  163. llms/ui/tailwind.input.css +742 -0
  164. {llms_py-2.0.9.data/data → llms}/ui/typography.css +133 -7
  165. llms/ui/utils.mjs +261 -0
  166. llms_py-3.0.10.dist-info/METADATA +49 -0
  167. llms_py-3.0.10.dist-info/RECORD +177 -0
  168. llms_py-3.0.10.dist-info/entry_points.txt +2 -0
  169. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
  170. llms.py +0 -1402
  171. llms_py-2.0.9.data/data/index.html +0 -64
  172. llms_py-2.0.9.data/data/llms.json +0 -447
  173. llms_py-2.0.9.data/data/requirements.txt +0 -1
  174. llms_py-2.0.9.data/data/ui/App.mjs +0 -20
  175. llms_py-2.0.9.data/data/ui/ChatPrompt.mjs +0 -389
  176. llms_py-2.0.9.data/data/ui/Main.mjs +0 -680
  177. llms_py-2.0.9.data/data/ui/app.css +0 -3951
  178. llms_py-2.0.9.data/data/ui/lib/servicestack-vue.min.mjs +0 -37
  179. llms_py-2.0.9.data/data/ui/lib/vue.min.mjs +0 -12
  180. llms_py-2.0.9.data/data/ui/tailwind.input.css +0 -261
  181. llms_py-2.0.9.data/data/ui/threadStore.mjs +0 -273
  182. llms_py-2.0.9.data/data/ui/utils.mjs +0 -114
  183. llms_py-2.0.9.data/data/ui.json +0 -1069
  184. llms_py-2.0.9.dist-info/METADATA +0 -941
  185. llms_py-2.0.9.dist-info/RECORD +0 -30
  186. llms_py-2.0.9.dist-info/entry_points.txt +0 -2
  187. {llms_py-2.0.9.data/data → llms}/ui/fav.svg +0 -0
  188. {llms_py-2.0.9.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
  189. {llms_py-2.0.9.data/data → llms}/ui/lib/idb.min.mjs +0 -0
  190. {llms_py-2.0.9.data/data → llms}/ui/lib/marked.min.mjs +0 -0
  191. /llms_py-2.0.9.data/data/ui/lib/servicestack-client.min.mjs → /llms/ui/lib/servicestack-client.mjs +0 -0
  192. {llms_py-2.0.9.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
  193. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
  194. {llms_py-2.0.9.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,481 @@
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
+ def provider_model(self, model):
83
+ if model.lower().startswith("gemini-"):
84
+ return model
85
+ return super().provider_model(model)
86
+
87
+ def model_info(self, model):
88
+ info = super().model_info(model)
89
+ if info:
90
+ return info
91
+ if model.lower().startswith("gemini-"):
92
+ return {
93
+ "id": model,
94
+ "name": model,
95
+ "cost": {"input": 0, "output": 0},
96
+ }
97
+ return None
98
+
99
+ async def chat(self, chat, context=None):
100
+ chat["model"] = self.provider_model(chat["model"]) or chat["model"]
101
+ model_info = (context.get("modelInfo") if context is not None else None) or self.model_info(chat["model"])
102
+
103
+ chat = await self.process_chat(chat)
104
+ generation_config = {}
105
+ tools = None
106
+ supports_tool_calls = model_info.get("tool_call", False)
107
+
108
+ if "tools" in chat and supports_tool_calls:
109
+ function_declarations = []
110
+ gemini_tools = {}
111
+
112
+ for tool in chat["tools"]:
113
+ if tool["type"] == "function":
114
+ f = tool["function"]
115
+ function_declarations.append(
116
+ {
117
+ "name": f["name"],
118
+ "description": f.get("description"),
119
+ "parameters": f.get("parameters"),
120
+ }
121
+ )
122
+ elif tool["type"] == "file_search":
123
+ gemini_tools["file_search"] = tool["file_search"]
124
+
125
+ if function_declarations:
126
+ gemini_tools["function_declarations"] = function_declarations
127
+
128
+ tools = [gemini_tools] if gemini_tools else None
129
+
130
+ # Filter out system messages and convert to proper Gemini format
131
+ contents = []
132
+ system_prompt = None
133
+
134
+ # Track tool call IDs to names for response mapping
135
+ tool_id_map = {}
136
+
137
+ async with aiohttp.ClientSession() as session:
138
+ for message in chat["messages"]:
139
+ if message["role"] == "system":
140
+ content = message["content"]
141
+ if isinstance(content, list):
142
+ for item in content:
143
+ if "text" in item:
144
+ system_prompt = item["text"]
145
+ break
146
+ elif isinstance(content, str):
147
+ system_prompt = content
148
+ elif "content" in message:
149
+ role = "user"
150
+ if "role" in message:
151
+ if message["role"] == "user":
152
+ role = "user"
153
+ elif message["role"] == "assistant":
154
+ role = "model"
155
+ elif message["role"] == "tool":
156
+ role = "function"
157
+
158
+ parts = []
159
+
160
+ # Handle tool calls in assistant messages
161
+ if message.get("role") == "assistant" and "tool_calls" in message:
162
+ for tool_call in message["tool_calls"]:
163
+ tool_id_map[tool_call["id"]] = tool_call["function"]["name"]
164
+ parts.append(
165
+ {
166
+ "functionCall": {
167
+ "name": tool_call["function"]["name"],
168
+ "args": json.loads(tool_call["function"]["arguments"]),
169
+ }
170
+ }
171
+ )
172
+
173
+ # Handle tool responses from user
174
+ if message.get("role") == "tool":
175
+ # Gemini expects function response in 'functionResponse' part
176
+ # We need to find the name associated with this tool_call_id
177
+ tool_call_id = message.get("tool_call_id")
178
+ name = tool_id_map.get(tool_call_id)
179
+ # If we can't find the name (maybe from previous turn not in history or restart),
180
+ # we might have an issue. But let's try to proceed.
181
+ # Fallback: if we can't find the name, skip or try to infer?
182
+ # Gemini strict validation requires the name.
183
+ if name:
184
+ # content is the string response
185
+ # Some implementations pass the content directly.
186
+ # Google docs say: response: { "name": "...", "content": { ... } }
187
+ # Actually "response" field in functionResponse is a Struct/Map.
188
+ parts.append(
189
+ {
190
+ "functionResponse": {
191
+ "name": name,
192
+ "response": {"name": name, "content": message["content"]},
193
+ }
194
+ }
195
+ )
196
+
197
+ if isinstance(message["content"], list):
198
+ for item in message["content"]:
199
+ if "type" in item:
200
+ if item["type"] == "image_url" and "image_url" in item:
201
+ image_url = item["image_url"]
202
+ if "url" not in image_url:
203
+ continue
204
+ url = image_url["url"]
205
+ if not url.startswith("data:"):
206
+ raise Exception("Image was not downloaded: " + url)
207
+ # Extract mime type from data uri
208
+ mimetype = url.split(";", 1)[0].split(":", 1)[1] if ";" in url else "image/png"
209
+ base64_data = url.split(",", 1)[1]
210
+ parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
211
+ elif item["type"] == "input_audio" and "input_audio" in item:
212
+ input_audio = item["input_audio"]
213
+ if "data" not in input_audio:
214
+ continue
215
+ data = input_audio["data"]
216
+ format = input_audio["format"]
217
+ mimetype = f"audio/{format}"
218
+ parts.append({"inline_data": {"mime_type": mimetype, "data": data}})
219
+ elif item["type"] == "file" and "file" in item:
220
+ file = item["file"]
221
+ if "file_data" not in file:
222
+ continue
223
+ data = file["file_data"]
224
+ if not data.startswith("data:"):
225
+ raise (Exception("File was not downloaded: " + data))
226
+ # Extract mime type from data uri
227
+ mimetype = (
228
+ data.split(";", 1)[0].split(":", 1)[1]
229
+ if ";" in data
230
+ else "application/octet-stream"
231
+ )
232
+ base64_data = data.split(",", 1)[1]
233
+ parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
234
+ if "text" in item:
235
+ text = item["text"]
236
+ parts.append({"text": text})
237
+ elif message["content"]: # String content
238
+ parts.append({"text": message["content"]})
239
+
240
+ if len(parts) > 0:
241
+ contents.append(
242
+ {
243
+ "role": role,
244
+ "parts": parts,
245
+ }
246
+ )
247
+
248
+ gemini_chat = {
249
+ "contents": contents,
250
+ }
251
+
252
+ if tools:
253
+ gemini_chat["tools"] = tools
254
+
255
+ if self.safety_settings:
256
+ gemini_chat["safetySettings"] = self.safety_settings
257
+
258
+ # Add system instruction if present
259
+ if system_prompt is not None:
260
+ gemini_chat["systemInstruction"] = {"parts": [{"text": system_prompt}]}
261
+
262
+ if "max_completion_tokens" in chat:
263
+ generation_config["maxOutputTokens"] = chat["max_completion_tokens"]
264
+ if "stop" in chat:
265
+ generation_config["stopSequences"] = [chat["stop"]]
266
+ if "temperature" in chat:
267
+ generation_config["temperature"] = chat["temperature"]
268
+ if "top_p" in chat:
269
+ generation_config["topP"] = chat["top_p"]
270
+ if "top_logprobs" in chat:
271
+ generation_config["topK"] = chat["top_logprobs"]
272
+
273
+ if "thinkingConfig" in chat:
274
+ generation_config["thinkingConfig"] = chat["thinkingConfig"]
275
+ elif self.thinking_config:
276
+ generation_config["thinkingConfig"] = self.thinking_config
277
+
278
+ if len(generation_config) > 0:
279
+ gemini_chat["generationConfig"] = generation_config
280
+
281
+ if "modalities" in chat:
282
+ generation_config["responseModalities"] = [modality.upper() for modality in chat["modalities"]]
283
+ if "image" in chat["modalities"] and "image_config" in chat:
284
+ # delete thinkingConfig
285
+ if "thinkingConfig" in generation_config:
286
+ del generation_config["thinkingConfig"]
287
+ config_map = {
288
+ "aspect_ratio": "aspectRatio",
289
+ "image_size": "imageSize",
290
+ }
291
+ generation_config["imageConfig"] = {
292
+ config_map[k]: v for k, v in chat["image_config"].items() if k in config_map
293
+ }
294
+ if "audio" in chat["modalities"] and self.speech_config:
295
+ if "thinkingConfig" in generation_config:
296
+ del generation_config["thinkingConfig"]
297
+ generation_config["speechConfig"] = self.speech_config.copy()
298
+ # Currently Google Audio Models only accept AUDIO
299
+ generation_config["responseModalities"] = ["AUDIO"]
300
+
301
+ # Ensure generationConfig is set if we added anything to it
302
+ if len(generation_config) > 0:
303
+ gemini_chat["generationConfig"] = generation_config
304
+
305
+ started_at = int(time.time() * 1000)
306
+ gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
307
+
308
+ ctx.log(f"POST {gemini_chat_url}")
309
+ ctx.log(gemini_chat_summary(gemini_chat))
310
+ started_at = time.time()
311
+
312
+ if ctx.MOCK and "modalities" in chat:
313
+ print("Mocking Google Gemini Image")
314
+ with open(f"{ctx.MOCK_DIR}/gemini-image.json") as f:
315
+ obj = json.load(f)
316
+ else:
317
+ try:
318
+ async with session.post(
319
+ gemini_chat_url,
320
+ headers=self.headers,
321
+ data=json.dumps(gemini_chat),
322
+ timeout=aiohttp.ClientTimeout(total=120),
323
+ ) as res:
324
+ obj = await self.response_json(res)
325
+ if context is not None:
326
+ context["providerResponse"] = obj
327
+ except Exception as e:
328
+ ctx.log(f"Error: {res.status} {res.reason}: {e}")
329
+ text = await res.text()
330
+ try:
331
+ obj = json.loads(text)
332
+ except:
333
+ ctx.log(text)
334
+ raise e
335
+
336
+ if "error" in obj:
337
+ ctx.log(f"Error: {obj['error']}")
338
+ raise Exception(obj["error"]["message"])
339
+
340
+ if ctx.debug:
341
+ ctx.dbg(json.dumps(gemini_response_summary(obj), indent=2))
342
+
343
+ # calculate cost per generation
344
+ cost = None
345
+ token_costs = obj.get("metadata", {}).get("pricing", "")
346
+ if token_costs:
347
+ input_price, output_price = token_costs.split("/")
348
+ input_per_token = float(input_price) / 1000000
349
+ output_per_token = float(output_price) / 1000000
350
+ if "usageMetadata" in obj:
351
+ input_tokens = obj["usageMetadata"].get("promptTokenCount", 0)
352
+ output_tokens = obj["usageMetadata"].get("candidatesTokenCount", 0)
353
+ cost = (input_per_token * input_tokens) + (output_per_token * output_tokens)
354
+
355
+ response = {
356
+ "id": f"chatcmpl-{started_at}",
357
+ "created": started_at,
358
+ "model": obj.get("modelVersion", chat["model"]),
359
+ }
360
+ choices = []
361
+ for i, candidate in enumerate(obj.get("candidates", [])):
362
+ role = "assistant"
363
+ if "content" in candidate and "role" in candidate["content"]:
364
+ role = "assistant" if candidate["content"]["role"] == "model" else candidate["content"]["role"]
365
+
366
+ # Safely extract content from all text parts
367
+ content = ""
368
+ reasoning = ""
369
+ images = []
370
+ audios = []
371
+ tool_calls = []
372
+
373
+ if "content" in candidate and "parts" in candidate["content"]:
374
+ text_parts = []
375
+ reasoning_parts = []
376
+ for part in candidate["content"]["parts"]:
377
+ if "text" in part:
378
+ if "thought" in part and part["thought"]:
379
+ reasoning_parts.append(part["text"])
380
+ else:
381
+ text_parts.append(part["text"])
382
+ if "functionCall" in part:
383
+ fc = part["functionCall"]
384
+ tool_calls.append(
385
+ {
386
+ "id": f"call_{len(tool_calls)}_{int(time.time())}", # Gemini doesn't return ID, generate one
387
+ "type": "function",
388
+ "function": {"name": fc["name"], "arguments": json.dumps(fc["args"])},
389
+ }
390
+ )
391
+
392
+ if "inlineData" in part:
393
+ inline_data = part["inlineData"]
394
+ mime_type = inline_data.get("mimeType", "image/png")
395
+ if mime_type.startswith("image"):
396
+ ext = mime_type.split("/")[1]
397
+ base64_data = inline_data["data"]
398
+ filename = f"{chat['model'].split('/')[-1]}-{len(images)}.{ext}"
399
+ ctx.log(f"inlineData {len(base64_data)} {mime_type} {filename}")
400
+ relative_url, info = ctx.save_image_to_cache(
401
+ base64_data,
402
+ filename,
403
+ ctx.to_file_info(chat, {"cost": cost}),
404
+ )
405
+ images.append(
406
+ {
407
+ "type": "image_url",
408
+ "index": len(images),
409
+ "image_url": {
410
+ "url": relative_url,
411
+ },
412
+ }
413
+ )
414
+ elif mime_type.startswith("audio"):
415
+ # mime_type audio/L16;codec=pcm;rate=24000
416
+ base64_data = inline_data["data"]
417
+
418
+ pcm = base64.b64decode(base64_data)
419
+ # Convert PCM to WAV
420
+ wav_io = io.BytesIO()
421
+ with wave.open(wav_io, "wb") as wf:
422
+ wf.setnchannels(1)
423
+ wf.setsampwidth(2)
424
+ wf.setframerate(24000)
425
+ wf.writeframes(pcm)
426
+ wav_data = wav_io.getvalue()
427
+
428
+ ext = mime_type.split("/")[1].split(";")[0]
429
+ pcm_filename = f"{chat['model'].split('/')[-1]}-{len(audios)}.{ext}"
430
+ filename = pcm_filename.replace(f".{ext}", ".wav")
431
+ ctx.log(f"inlineData {len(base64_data)} {mime_type} {filename}")
432
+
433
+ relative_url, info = ctx.save_bytes_to_cache(
434
+ wav_data,
435
+ filename,
436
+ ctx.to_file_info(chat, {"cost": cost}),
437
+ )
438
+
439
+ audios.append(
440
+ {
441
+ "type": "audio_url",
442
+ "index": len(audios),
443
+ "audio_url": {
444
+ "url": relative_url,
445
+ },
446
+ }
447
+ )
448
+ content = " ".join(text_parts)
449
+ reasoning = " ".join(reasoning_parts)
450
+
451
+ choice = {
452
+ "index": i,
453
+ "finish_reason": candidate.get("finishReason", "stop"),
454
+ "message": {
455
+ "role": role,
456
+ "content": content if content else None,
457
+ },
458
+ }
459
+ if reasoning:
460
+ choice["message"]["reasoning"] = reasoning
461
+ if len(images) > 0:
462
+ choice["message"]["images"] = images
463
+ if len(audios) > 0:
464
+ choice["message"]["audios"] = audios
465
+ if len(tool_calls) > 0:
466
+ choice["message"]["tool_calls"] = tool_calls
467
+ # If we have tool calls, content can be null but message should probably exist
468
+
469
+ choices.append(choice)
470
+ response["choices"] = choices
471
+ if "usageMetadata" in obj:
472
+ usage = obj["usageMetadata"]
473
+ response["usage"] = {
474
+ "completion_tokens": usage.get("candidatesTokenCount", 0),
475
+ "total_tokens": usage.get("totalTokenCount", 0),
476
+ "prompt_tokens": usage.get("promptTokenCount", 0),
477
+ }
478
+
479
+ return ctx.log_json(self.to_response(response, chat, started_at))
480
+
481
+ ctx.add_provider(GoogleProvider)
@@ -0,0 +1,103 @@
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, context=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
+ aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
70
+ dimension = ctx.app.aspect_ratios.get(aspect_ratio)
71
+ if dimension:
72
+ width, height = dimension.split("×")
73
+ gen_request["width"] = int(width)
74
+ gen_request["height"] = int(height)
75
+ else:
76
+ gen_request["width"] = self.width
77
+ gen_request["height"] = self.height
78
+
79
+ gen_request["mode"] = self.mode
80
+ gen_request["cfg_scale"] = self.cfg_scale
81
+ gen_request["steps"] = self.steps
82
+
83
+ gen_url = f"{self.gen_url}/{chat['model']}"
84
+ ctx.log(f"POST {gen_url}")
85
+ ctx.log(self.gen_summary(gen_request))
86
+ # remove metadata if any (conflicts with some providers, e.g. Z.ai)
87
+ gen_request.pop("metadata", None)
88
+ started_at = time.time()
89
+
90
+ if ctx.MOCK:
91
+ ctx.log("Mocking NvidiaGenAi")
92
+ text = ctx.text_from_file(f"{ctx.MOCK_DIR}/nvidia-image.json")
93
+ return self.to_response(json.loads(text), chat, started_at)
94
+ else:
95
+ async with aiohttp.ClientSession() as session, session.post(
96
+ gen_url,
97
+ headers=headers,
98
+ data=json.dumps(gen_request),
99
+ timeout=aiohttp.ClientTimeout(total=120),
100
+ ) as response:
101
+ return self.to_response(await self.response_json(response), chat, started_at, context=context)
102
+
103
+ ctx.add_provider(NvidiaGenAi)