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.
- llms/__init__.py +3 -1
- llms/db.py +359 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +588 -0
- llms/extensions/app/db.py +540 -0
- llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
- llms/extensions/app/ui/threadStore.mjs +440 -0
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/computer/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +599 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +260 -0
- llms/extensions/providers/cerebras.py +36 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +559 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +376 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +276 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +67 -0
- llms/extensions/tools/ui/index.mjs +837 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +4009 -912
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +176 -8
- llms/ui/ai.mjs +156 -20
- llms/ui/app.css +3768 -321
- llms/ui/ctx.mjs +459 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/highlight.min.mjs +1243 -0
- llms/ui/lib/idb.min.mjs +8 -0
- llms/ui/lib/marked.min.mjs +8 -0
- llms/ui/lib/servicestack-client.mjs +1 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue-router.min.mjs +6 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- llms/ui/markdown.mjs +25 -14
- llms/ui/modules/chat/ChatBody.mjs +1156 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +995 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +560 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +221 -92
- llms_py-3.0.18.dist-info/METADATA +49 -0
- llms_py-3.0.18.dist-info/RECORD +194 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
- llms/ui/Avatar.mjs +0 -28
- llms/ui/Brand.mjs +0 -34
- llms/ui/ChatPrompt.mjs +0 -443
- llms/ui/Main.mjs +0 -740
- llms/ui/ModelSelector.mjs +0 -60
- llms/ui/ProviderIcon.mjs +0 -29
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -36
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -524
- llms/ui.json +0 -1069
- llms_py-2.0.20.dist-info/METADATA +0 -931
- llms_py-2.0.20.dist-info/RECORD +0 -36
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {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)
|