llms-py 2.0.20__py3-none-any.whl → 3.0.18__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 (207) hide show
  1. llms/__init__.py +3 -1
  2. llms/db.py +359 -0
  3. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
  4. llms/extensions/app/README.md +20 -0
  5. llms/extensions/app/__init__.py +588 -0
  6. llms/extensions/app/db.py +540 -0
  7. llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
  8. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
  9. llms/extensions/app/ui/threadStore.mjs +440 -0
  10. llms/extensions/computer/README.md +96 -0
  11. llms/extensions/computer/__init__.py +59 -0
  12. llms/extensions/computer/base.py +80 -0
  13. llms/extensions/computer/bash.py +185 -0
  14. llms/extensions/computer/computer.py +523 -0
  15. llms/extensions/computer/edit.py +299 -0
  16. llms/extensions/computer/filesystem.py +542 -0
  17. llms/extensions/computer/platform.py +461 -0
  18. llms/extensions/computer/run.py +37 -0
  19. llms/extensions/core_tools/CALCULATOR.md +32 -0
  20. llms/extensions/core_tools/__init__.py +599 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  25. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  26. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  27. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  30. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  31. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  32. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  33. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  34. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  35. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  36. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  37. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  38. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  39. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  40. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  41. llms/extensions/core_tools/ui/index.mjs +650 -0
  42. llms/extensions/gallery/README.md +61 -0
  43. llms/extensions/gallery/__init__.py +63 -0
  44. llms/extensions/gallery/db.py +243 -0
  45. llms/extensions/gallery/ui/index.mjs +482 -0
  46. llms/extensions/katex/README.md +39 -0
  47. llms/extensions/katex/__init__.py +6 -0
  48. llms/extensions/katex/ui/README.md +125 -0
  49. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  50. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  52. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  53. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  55. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  56. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  58. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  59. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  60. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  61. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  62. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  63. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  124. llms/extensions/katex/ui/index.mjs +92 -0
  125. llms/extensions/katex/ui/katex-swap.css +1230 -0
  126. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  127. llms/extensions/katex/ui/katex.css +1230 -0
  128. llms/extensions/katex/ui/katex.js +19080 -0
  129. llms/extensions/katex/ui/katex.min.css +1 -0
  130. llms/extensions/katex/ui/katex.min.js +1 -0
  131. llms/extensions/katex/ui/katex.min.mjs +1 -0
  132. llms/extensions/katex/ui/katex.mjs +18547 -0
  133. llms/extensions/providers/__init__.py +22 -0
  134. llms/extensions/providers/anthropic.py +260 -0
  135. llms/extensions/providers/cerebras.py +36 -0
  136. llms/extensions/providers/chutes.py +153 -0
  137. llms/extensions/providers/google.py +559 -0
  138. llms/extensions/providers/nvidia.py +103 -0
  139. llms/extensions/providers/openai.py +154 -0
  140. llms/extensions/providers/openrouter.py +74 -0
  141. llms/extensions/providers/zai.py +182 -0
  142. llms/extensions/skills/LICENSE +202 -0
  143. llms/extensions/skills/__init__.py +130 -0
  144. llms/extensions/skills/errors.py +25 -0
  145. llms/extensions/skills/models.py +39 -0
  146. llms/extensions/skills/parser.py +178 -0
  147. llms/extensions/skills/ui/index.mjs +376 -0
  148. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  149. llms/extensions/skills/validator.py +177 -0
  150. llms/extensions/system_prompts/README.md +22 -0
  151. llms/extensions/system_prompts/__init__.py +45 -0
  152. llms/extensions/system_prompts/ui/index.mjs +276 -0
  153. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  154. llms/extensions/tools/__init__.py +67 -0
  155. llms/extensions/tools/ui/index.mjs +837 -0
  156. llms/index.html +36 -62
  157. llms/llms.json +180 -879
  158. llms/main.py +4009 -912
  159. llms/providers-extra.json +394 -0
  160. llms/providers.json +1 -0
  161. llms/ui/App.mjs +176 -8
  162. llms/ui/ai.mjs +156 -20
  163. llms/ui/app.css +3768 -321
  164. llms/ui/ctx.mjs +459 -0
  165. llms/ui/index.mjs +131 -0
  166. llms/ui/lib/chart.js +14 -0
  167. llms/ui/lib/charts.mjs +16 -0
  168. llms/ui/lib/color.js +14 -0
  169. llms/ui/lib/highlight.min.mjs +1243 -0
  170. llms/ui/lib/idb.min.mjs +8 -0
  171. llms/ui/lib/marked.min.mjs +8 -0
  172. llms/ui/lib/servicestack-client.mjs +1 -0
  173. llms/ui/lib/servicestack-vue.mjs +37 -0
  174. llms/ui/lib/vue-router.min.mjs +6 -0
  175. llms/ui/lib/vue.min.mjs +13 -0
  176. llms/ui/lib/vue.mjs +18530 -0
  177. llms/ui/markdown.mjs +25 -14
  178. llms/ui/modules/chat/ChatBody.mjs +1156 -0
  179. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
  180. llms/ui/modules/chat/index.mjs +995 -0
  181. llms/ui/modules/icons.mjs +46 -0
  182. llms/ui/modules/layout.mjs +271 -0
  183. llms/ui/modules/model-selector.mjs +811 -0
  184. llms/ui/tailwind.input.css +560 -78
  185. llms/ui/typography.css +54 -36
  186. llms/ui/utils.mjs +221 -92
  187. llms_py-3.0.18.dist-info/METADATA +49 -0
  188. llms_py-3.0.18.dist-info/RECORD +194 -0
  189. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  190. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
  191. llms/ui/Avatar.mjs +0 -28
  192. llms/ui/Brand.mjs +0 -34
  193. llms/ui/ChatPrompt.mjs +0 -443
  194. llms/ui/Main.mjs +0 -740
  195. llms/ui/ModelSelector.mjs +0 -60
  196. llms/ui/ProviderIcon.mjs +0 -29
  197. llms/ui/ProviderStatus.mjs +0 -105
  198. llms/ui/SignIn.mjs +0 -64
  199. llms/ui/SystemPromptEditor.mjs +0 -31
  200. llms/ui/SystemPromptSelector.mjs +0 -36
  201. llms/ui/Welcome.mjs +0 -8
  202. llms/ui/threadStore.mjs +0 -524
  203. llms/ui.json +0 -1069
  204. llms_py-2.0.20.dist-info/METADATA +0 -931
  205. llms_py-2.0.20.dist-info/RECORD +0 -36
  206. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  207. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,559 @@
1
+ import asyncio
2
+ import base64
3
+ import io
4
+ import json
5
+ import time
6
+ import wave
7
+
8
+ import aiohttp
9
+
10
+ # class GoogleOpenAiProvider(OpenAiCompatible):
11
+ # sdk = "google-openai-compatible"
12
+
13
+ # def __init__(self, api_key, **kwargs):
14
+ # super().__init__(api="https://generativelanguage.googleapis.com", api_key=api_key, **kwargs)
15
+ # self.chat_url = "https://generativelanguage.googleapis.com/v1beta/chat/completions"
16
+
17
+
18
+ def install_google(ctx):
19
+ from llms.main import OpenAiCompatible
20
+
21
+ def gemini_chat_summary(gemini_chat):
22
+ """Summarize Gemini chat completion request for logging. Replace inline_data with size of content only"""
23
+ clone = json.loads(json.dumps(gemini_chat))
24
+ for content in clone["contents"]:
25
+ for part in content["parts"]:
26
+ if "inline_data" in part:
27
+ data = part["inline_data"]["data"]
28
+ part["inline_data"]["data"] = f"({len(data)})"
29
+ return json.dumps(clone, indent=2)
30
+
31
+ def gemini_response_summary(obj):
32
+ to = {}
33
+ for k, v in obj.items():
34
+ if k == "candidates":
35
+ candidates = []
36
+ for candidate in v:
37
+ c = {}
38
+ for ck, cv in candidate.items():
39
+ if ck == "content":
40
+ content = {}
41
+ for content_k, content_v in cv.items():
42
+ if content_k == "parts":
43
+ parts = []
44
+ for part in content_v:
45
+ p = {}
46
+ for pk, pv in part.items():
47
+ if pk == "inlineData":
48
+ p[pk] = {
49
+ "mimeType": pv.get("mimeType"),
50
+ "data": f"({len(pv.get('data'))})",
51
+ }
52
+ else:
53
+ p[pk] = pv
54
+ parts.append(p)
55
+ content[content_k] = parts
56
+ else:
57
+ content[content_k] = content_v
58
+ c[ck] = content
59
+ else:
60
+ c[ck] = cv
61
+ candidates.append(c)
62
+ to[k] = candidates
63
+ else:
64
+ to[k] = v
65
+ return to
66
+
67
+ def sanitize_parameters(params):
68
+ """Sanitize tool parameters for Google provider."""
69
+
70
+ if not isinstance(params, dict):
71
+ return params
72
+
73
+ # Create a copy to avoid modifying original tool definition
74
+ p = params.copy()
75
+
76
+ # Remove forbidden fields
77
+ for forbidden in ["$schema", "additionalProperties"]:
78
+ if forbidden in p:
79
+ del p[forbidden]
80
+
81
+ # Recursively sanitize known nesting fields
82
+ # 1. Properties (dict of schemas)
83
+ if "properties" in p:
84
+ for k, v in p["properties"].items():
85
+ p["properties"][k] = sanitize_parameters(v)
86
+
87
+ # 2. Items (schema or list of schemas)
88
+ if "items" in p:
89
+ if isinstance(p["items"], list):
90
+ p["items"] = [sanitize_parameters(i) for i in p["items"]]
91
+ else:
92
+ p["items"] = sanitize_parameters(p["items"])
93
+
94
+ # 3. Combinators (list of schemas)
95
+ for combinator in ["allOf", "anyOf", "oneOf"]:
96
+ if combinator in p:
97
+ p[combinator] = [sanitize_parameters(i) for i in p[combinator]]
98
+
99
+ # 4. Not (schema)
100
+ if "not" in p:
101
+ p["not"] = sanitize_parameters(p["not"])
102
+
103
+ # 5. Definitions (dict of schemas)
104
+ for def_key in ["definitions", "$defs"]:
105
+ if def_key in p:
106
+ for k, v in p[def_key].items():
107
+ p[def_key][k] = sanitize_parameters(v)
108
+
109
+ return p
110
+
111
+ class GoogleProvider(OpenAiCompatible):
112
+ sdk = "@ai-sdk/google"
113
+
114
+ def __init__(self, **kwargs):
115
+ new_kwargs = {"api": "https://generativelanguage.googleapis.com", **kwargs}
116
+ super().__init__(**new_kwargs)
117
+ self.safety_settings = kwargs.get("safety_settings")
118
+ self.thinking_config = kwargs.get("thinking_config")
119
+ self.speech_config = kwargs.get("speech_config")
120
+ self.tools = kwargs.get("tools")
121
+ self.curl = kwargs.get("curl")
122
+ self.headers = kwargs.get("headers", {"Content-Type": "application/json"})
123
+ # Google fails when using Authorization header, use query string param instead
124
+ if "Authorization" in self.headers:
125
+ del self.headers["Authorization"]
126
+
127
+ def provider_model(self, model):
128
+ if model.lower().startswith("gemini-"):
129
+ return model
130
+ return super().provider_model(model)
131
+
132
+ def model_info(self, model):
133
+ info = super().model_info(model)
134
+ if info:
135
+ return info
136
+ if model.lower().startswith("gemini-"):
137
+ return {
138
+ "id": model,
139
+ "name": model,
140
+ "cost": {"input": 0, "output": 0},
141
+ }
142
+ return None
143
+
144
+ async def chat(self, chat, context=None):
145
+ chat["model"] = self.provider_model(chat["model"]) or chat["model"]
146
+ model_info = (context.get("modelInfo") if context is not None else None) or self.model_info(chat["model"])
147
+
148
+ chat = await self.process_chat(chat)
149
+ generation_config = {}
150
+ tools = None
151
+ supports_tool_calls = model_info.get("tool_call", False)
152
+
153
+ if "tools" in chat and supports_tool_calls:
154
+ function_declarations = []
155
+ gemini_tools = {}
156
+
157
+ for tool in chat["tools"]:
158
+ if tool["type"] == "function":
159
+ f = tool["function"]
160
+
161
+ function_declarations.append(
162
+ {
163
+ "name": f["name"],
164
+ "description": f.get("description"),
165
+ "parameters": sanitize_parameters(f.get("parameters")),
166
+ }
167
+ )
168
+ elif tool["type"] == "file_search":
169
+ gemini_tools["file_search"] = tool["file_search"]
170
+
171
+ if function_declarations:
172
+ gemini_tools["function_declarations"] = function_declarations
173
+
174
+ tools = [gemini_tools] if gemini_tools else None
175
+
176
+ # Filter out system messages and convert to proper Gemini format
177
+ contents = []
178
+ system_prompt = None
179
+
180
+ # Track tool call IDs to names for response mapping
181
+ tool_id_map = {}
182
+
183
+ async with aiohttp.ClientSession() as session:
184
+ for message in chat["messages"]:
185
+ if message["role"] == "system":
186
+ content = message["content"]
187
+ if isinstance(content, list):
188
+ for item in content:
189
+ if "text" in item:
190
+ system_prompt = item["text"]
191
+ break
192
+ elif isinstance(content, str):
193
+ system_prompt = content
194
+ elif "content" in message:
195
+ role = "user"
196
+ if "role" in message:
197
+ if message["role"] == "user":
198
+ role = "user"
199
+ elif message["role"] == "assistant":
200
+ role = "model"
201
+ elif message["role"] == "tool":
202
+ role = "function"
203
+
204
+ parts = []
205
+
206
+ # Handle tool calls in assistant messages
207
+ if message.get("role") == "assistant" and "tool_calls" in message:
208
+ for tool_call in message["tool_calls"]:
209
+ tool_id_map[tool_call["id"]] = tool_call["function"]["name"]
210
+ parts.append(
211
+ {
212
+ "functionCall": {
213
+ "name": tool_call["function"]["name"],
214
+ "args": json.loads(tool_call["function"]["arguments"]),
215
+ }
216
+ }
217
+ )
218
+
219
+ # Handle tool responses from user
220
+ if message.get("role") == "tool":
221
+ # Gemini expects function response in 'functionResponse' part
222
+ # We need to find the name associated with this tool_call_id
223
+ tool_call_id = message.get("tool_call_id")
224
+ name = tool_id_map.get(tool_call_id)
225
+ # If we can't find the name (maybe from previous turn not in history or restart),
226
+ # we might have an issue. But let's try to proceed.
227
+ # Fallback: if we can't find the name, skip or try to infer?
228
+ # Gemini strict validation requires the name.
229
+ if name:
230
+ # content is the string response
231
+ # Some implementations pass the content directly.
232
+ # Google docs say: response: { "key": "value" }
233
+ try:
234
+ response_data = json.loads(message["content"])
235
+ if not isinstance(response_data, dict):
236
+ response_data = {"content": message["content"]}
237
+ except Exception:
238
+ response_data = {"content": message["content"]}
239
+
240
+ parts.append(
241
+ {
242
+ "functionResponse": {
243
+ "name": name,
244
+ "response": response_data,
245
+ }
246
+ }
247
+ )
248
+
249
+ if isinstance(message["content"], list):
250
+ for item in message["content"]:
251
+ if "type" in item:
252
+ if item["type"] == "image_url" and "image_url" in item:
253
+ image_url = item["image_url"]
254
+ if "url" not in image_url:
255
+ continue
256
+ url = image_url["url"]
257
+ if not url.startswith("data:"):
258
+ raise Exception("Image was not downloaded: " + url)
259
+ # Extract mime type from data uri
260
+ mimetype = url.split(";", 1)[0].split(":", 1)[1] if ";" in url else "image/png"
261
+ base64_data = url.split(",", 1)[1]
262
+ parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
263
+ elif item["type"] == "input_audio" and "input_audio" in item:
264
+ input_audio = item["input_audio"]
265
+ if "data" not in input_audio:
266
+ continue
267
+ data = input_audio["data"]
268
+ format = input_audio["format"]
269
+ mimetype = f"audio/{format}"
270
+ parts.append({"inline_data": {"mime_type": mimetype, "data": data}})
271
+ elif item["type"] == "file" and "file" in item:
272
+ file = item["file"]
273
+ if "file_data" not in file:
274
+ continue
275
+ data = file["file_data"]
276
+ if not data.startswith("data:"):
277
+ raise (Exception("File was not downloaded: " + data))
278
+ # Extract mime type from data uri
279
+ mimetype = (
280
+ data.split(";", 1)[0].split(":", 1)[1]
281
+ if ";" in data
282
+ else "application/octet-stream"
283
+ )
284
+ base64_data = data.split(",", 1)[1]
285
+ parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
286
+ if "text" in item:
287
+ text = item["text"]
288
+ parts.append({"text": text})
289
+ elif message["content"]: # String content
290
+ parts.append({"text": message["content"]})
291
+
292
+ if len(parts) > 0:
293
+ contents.append(
294
+ {
295
+ "role": role,
296
+ "parts": parts,
297
+ }
298
+ )
299
+
300
+ gemini_chat = {
301
+ "contents": contents,
302
+ }
303
+
304
+ if tools:
305
+ gemini_chat["tools"] = tools
306
+
307
+ if self.safety_settings:
308
+ gemini_chat["safetySettings"] = self.safety_settings
309
+
310
+ # Add system instruction if present
311
+ if system_prompt is not None:
312
+ gemini_chat["systemInstruction"] = {"parts": [{"text": system_prompt}]}
313
+
314
+ if "max_completion_tokens" in chat:
315
+ generation_config["maxOutputTokens"] = chat["max_completion_tokens"]
316
+ if "stop" in chat:
317
+ generation_config["stopSequences"] = [chat["stop"]]
318
+ if "temperature" in chat:
319
+ generation_config["temperature"] = chat["temperature"]
320
+ if "top_p" in chat:
321
+ generation_config["topP"] = chat["top_p"]
322
+ if "top_logprobs" in chat:
323
+ generation_config["topK"] = chat["top_logprobs"]
324
+
325
+ if "thinkingConfig" in chat:
326
+ generation_config["thinkingConfig"] = chat["thinkingConfig"]
327
+ elif self.thinking_config:
328
+ generation_config["thinkingConfig"] = self.thinking_config
329
+
330
+ if len(generation_config) > 0:
331
+ gemini_chat["generationConfig"] = generation_config
332
+
333
+ if "modalities" in chat:
334
+ generation_config["responseModalities"] = [modality.upper() for modality in chat["modalities"]]
335
+ if "image" in chat["modalities"] and "image_config" in chat:
336
+ # delete thinkingConfig
337
+ if "thinkingConfig" in generation_config:
338
+ del generation_config["thinkingConfig"]
339
+ config_map = {
340
+ "aspect_ratio": "aspectRatio",
341
+ "image_size": "imageSize",
342
+ }
343
+ generation_config["imageConfig"] = {
344
+ config_map[k]: v for k, v in chat["image_config"].items() if k in config_map
345
+ }
346
+ if "audio" in chat["modalities"] and self.speech_config:
347
+ if "thinkingConfig" in generation_config:
348
+ del generation_config["thinkingConfig"]
349
+ generation_config["speechConfig"] = self.speech_config.copy()
350
+ # Currently Google Audio Models only accept AUDIO
351
+ generation_config["responseModalities"] = ["AUDIO"]
352
+
353
+ # Ensure generationConfig is set if we added anything to it
354
+ if len(generation_config) > 0:
355
+ gemini_chat["generationConfig"] = generation_config
356
+
357
+ started_at = int(time.time() * 1000)
358
+ gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
359
+
360
+ ctx.log(f"POST {gemini_chat_url}")
361
+ ctx.log(gemini_chat_summary(gemini_chat))
362
+ started_at = time.time()
363
+
364
+ max_retries = 3
365
+ for attempt in range(max_retries):
366
+ if ctx.MOCK and "modalities" in chat:
367
+ print("Mocking Google Gemini Image")
368
+ with open(f"{ctx.MOCK_DIR}/gemini-image.json") as f:
369
+ obj = json.load(f)
370
+ else:
371
+ res = None
372
+ try:
373
+ if attempt > 0:
374
+ await asyncio.sleep(attempt * 0.5)
375
+ ctx.log(f"Retrying request (attempt {attempt + 1}/{max_retries})...")
376
+
377
+ async with session.post(
378
+ gemini_chat_url,
379
+ headers=self.headers,
380
+ data=json.dumps(gemini_chat),
381
+ timeout=aiohttp.ClientTimeout(total=120),
382
+ ) as res:
383
+ obj = await self.response_json(res)
384
+ if context is not None:
385
+ context["providerResponse"] = obj
386
+ except Exception as e:
387
+ if res:
388
+ ctx.err(f"{res.status} {res.reason}", e)
389
+ try:
390
+ text = await res.text()
391
+ obj = json.loads(text)
392
+ except Exception as parseEx:
393
+ ctx.err("Failed to parse error response:\n" + text, parseEx)
394
+ raise e from None
395
+ else:
396
+ ctx.err(f"Request failed: {str(e)}")
397
+ raise e from None
398
+
399
+ if "error" in obj:
400
+ ctx.log(f"Error: {obj['error']}")
401
+ raise Exception(obj["error"]["message"])
402
+
403
+ if ctx.debug:
404
+ ctx.dbg(json.dumps(gemini_response_summary(obj), indent=2))
405
+
406
+ # Check for empty response "anomaly"
407
+ has_candidates = obj.get("candidates") and len(obj["candidates"]) > 0
408
+ if has_candidates:
409
+ candidate = obj["candidates"][0]
410
+ raw_content = candidate.get("content", {})
411
+ raw_parts = raw_content.get("parts", [])
412
+
413
+ if not raw_parts and attempt < max_retries - 1:
414
+ # It's an empty response candidates list
415
+ ctx.dbg("Empty candidates parts detected. Retrying...")
416
+ continue
417
+
418
+ # If we got here, it's either a good response or we ran out of retries
419
+ break
420
+
421
+ # calculate cost per generation
422
+ cost = None
423
+ token_costs = obj.get("metadata", {}).get("pricing", "")
424
+ if token_costs:
425
+ input_price, output_price = token_costs.split("/")
426
+ input_per_token = float(input_price) / 1000000
427
+ output_per_token = float(output_price) / 1000000
428
+ if "usageMetadata" in obj:
429
+ input_tokens = obj["usageMetadata"].get("promptTokenCount", 0)
430
+ output_tokens = obj["usageMetadata"].get("candidatesTokenCount", 0)
431
+ cost = (input_per_token * input_tokens) + (output_per_token * output_tokens)
432
+
433
+ response = {
434
+ "id": f"chatcmpl-{started_at}",
435
+ "created": started_at,
436
+ "model": obj.get("modelVersion", chat["model"]),
437
+ }
438
+ choices = []
439
+ for i, candidate in enumerate(obj.get("candidates", [])):
440
+ role = "assistant"
441
+ if "content" in candidate and "role" in candidate["content"]:
442
+ role = "assistant" if candidate["content"]["role"] == "model" else candidate["content"]["role"]
443
+
444
+ # Safely extract content from all text parts
445
+ content = ""
446
+ reasoning = ""
447
+ images = []
448
+ audios = []
449
+ tool_calls = []
450
+
451
+ if "content" in candidate and "parts" in candidate["content"]:
452
+ text_parts = []
453
+ reasoning_parts = []
454
+ for part in candidate["content"]["parts"]:
455
+ if "text" in part:
456
+ if "thought" in part and part["thought"]:
457
+ reasoning_parts.append(part["text"])
458
+ else:
459
+ text_parts.append(part["text"])
460
+ if "functionCall" in part:
461
+ fc = part["functionCall"]
462
+ tool_calls.append(
463
+ {
464
+ "id": f"call_{len(tool_calls)}_{int(time.time())}", # Gemini doesn't return ID, generate one
465
+ "type": "function",
466
+ "function": {"name": fc["name"], "arguments": json.dumps(fc["args"])},
467
+ }
468
+ )
469
+
470
+ if "inlineData" in part:
471
+ inline_data = part["inlineData"]
472
+ mime_type = inline_data.get("mimeType", "image/png")
473
+ if mime_type.startswith("image"):
474
+ ext = mime_type.split("/")[1]
475
+ base64_data = inline_data["data"]
476
+ filename = f"{chat['model'].split('/')[-1]}-{len(images)}.{ext}"
477
+ ctx.log(f"inlineData {len(base64_data)} {mime_type} {filename}")
478
+ relative_url, info = ctx.save_image_to_cache(
479
+ base64_data,
480
+ filename,
481
+ ctx.to_file_info(chat, {"cost": cost}),
482
+ )
483
+ images.append(
484
+ {
485
+ "type": "image_url",
486
+ "index": len(images),
487
+ "image_url": {
488
+ "url": relative_url,
489
+ },
490
+ }
491
+ )
492
+ elif mime_type.startswith("audio"):
493
+ # mime_type audio/L16;codec=pcm;rate=24000
494
+ base64_data = inline_data["data"]
495
+
496
+ pcm = base64.b64decode(base64_data)
497
+ # Convert PCM to WAV
498
+ wav_io = io.BytesIO()
499
+ with wave.open(wav_io, "wb") as wf:
500
+ wf.setnchannels(1)
501
+ wf.setsampwidth(2)
502
+ wf.setframerate(24000)
503
+ wf.writeframes(pcm)
504
+ wav_data = wav_io.getvalue()
505
+
506
+ ext = mime_type.split("/")[1].split(";")[0]
507
+ pcm_filename = f"{chat['model'].split('/')[-1]}-{len(audios)}.{ext}"
508
+ filename = pcm_filename.replace(f".{ext}", ".wav")
509
+ ctx.log(f"inlineData {len(base64_data)} {mime_type} {filename}")
510
+
511
+ relative_url, info = ctx.save_bytes_to_cache(
512
+ wav_data,
513
+ filename,
514
+ ctx.to_file_info(chat, {"cost": cost}),
515
+ )
516
+
517
+ audios.append(
518
+ {
519
+ "type": "audio_url",
520
+ "index": len(audios),
521
+ "audio_url": {
522
+ "url": relative_url,
523
+ },
524
+ }
525
+ )
526
+ content = " ".join(text_parts)
527
+ reasoning = " ".join(reasoning_parts)
528
+
529
+ choice = {
530
+ "index": i,
531
+ "finish_reason": candidate.get("finishReason", "stop"),
532
+ "message": {
533
+ "role": role,
534
+ "content": content if content else "",
535
+ },
536
+ }
537
+ if reasoning:
538
+ choice["message"]["reasoning"] = reasoning
539
+ if len(images) > 0:
540
+ choice["message"]["images"] = images
541
+ if len(audios) > 0:
542
+ choice["message"]["audios"] = audios
543
+ if len(tool_calls) > 0:
544
+ choice["message"]["tool_calls"] = tool_calls
545
+ # If we have tool calls, content can be null but message should probably exist
546
+
547
+ choices.append(choice)
548
+ response["choices"] = choices
549
+ if "usageMetadata" in obj:
550
+ usage = obj["usageMetadata"]
551
+ response["usage"] = {
552
+ "completion_tokens": usage.get("candidatesTokenCount", 0),
553
+ "total_tokens": usage.get("totalTokenCount", 0),
554
+ "prompt_tokens": usage.get("promptTokenCount", 0),
555
+ }
556
+
557
+ return ctx.log_json(self.to_response(response, chat, started_at))
558
+
559
+ 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)