smolllm 0.1.0__tar.gz

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.
@@ -0,0 +1,5 @@
1
+ GEMINI_API_KEY=
2
+ DEEPSEEK_API_KEY=
3
+ OPENAI_API_KEY=
4
+ # ANTHROPIC_API_KEY= dont support yet
5
+
@@ -0,0 +1,12 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .env
@@ -0,0 +1 @@
1
+ 3.12
smolllm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 RoCry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
smolllm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: smolllm
3
+ Version: 0.1.0
4
+ Summary: A minimal Python library for interacting with various LLM providers
5
+ Project-URL: Homepage, https://github.com/RoCry/smolllm
6
+ Project-URL: Repository, https://github.com/RoCry/smolllm.git
7
+ Author-email: RoCry <crysheen@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,anthropic,gemini,llm,openai
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: python-dotenv>=1.0.1
20
+ Requires-Dist: rich>=13.9.4
21
+ Description-Content-Type: text/markdown
22
+
23
+ # SmolLLM
24
+
25
+ A minimal Python library for interacting with various LLM providers, featuring automatic API key load balancing and streaming responses.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install smolllm
31
+ uv add "smolllm @ ../smolllm"
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```python
37
+ from dotenv import load_dotenv
38
+ import asyncio
39
+ from smolllm import ask_llm
40
+
41
+ # Load environment variables at your application startup
42
+ load_dotenv()
43
+
44
+ async def main():
45
+ response = await ask_llm(
46
+ "Say hello world",
47
+ model="gemini/gemini-2.0-flash"
48
+ )
49
+ print(response)
50
+
51
+ if __name__ == "__main__":
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ ## Provider Configuration
56
+
57
+ Format: `provider/model_name` (e.g., `openai/gpt-4`, `gemini/gemini-2.0-flash`)
58
+
59
+ ### API Keys
60
+
61
+ The library looks for API keys in environment variables following the pattern: `{PROVIDER}_API_KEY`
62
+
63
+ Example:
64
+ ```bash
65
+ # .env
66
+ OPENAI_API_KEY=sk-xxx
67
+ GEMINI_API_KEY=key1,key2 # Multiple keys supported
68
+ ```
69
+
70
+ ### Custom Base URLs
71
+
72
+ Override default API endpoints using: `{PROVIDER}_BASE_URL`
73
+
74
+ Example:
75
+ ```bash
76
+ OPENAI_BASE_URL=https://custom.openai.com/v1
77
+ OLLAMA_BASE_URL=http://localhost:11434/v1
78
+ ```
79
+
80
+ ### Advanced Configuration
81
+
82
+ You can combine multiple keys and base URLs in several ways:
83
+
84
+ 1. One key with multiple base URLs:
85
+ ```bash
86
+ OLLAMA_API_KEY=ollama
87
+ OLLAMA_BASE_URL=http://localhost:11434/v1,http://other-server:11434/v1
88
+ ```
89
+
90
+ 2. Multiple keys with one base URL:
91
+ ```bash
92
+ GEMINI_API_KEY=key1,key2
93
+ GEMINI_BASE_URL=https://api.gemini.com/v1
94
+ ```
95
+
96
+ 3. Paired keys and base URLs:
97
+ ```bash
98
+ # Must have equal number of keys and URLs
99
+ # The library will randomly select matching pairs
100
+ GEMINI_API_KEY=key1,key2
101
+ GEMINI_BASE_URL=https://api.gemini.com/v1,https://api.gemini.com/v2
102
+ ```
103
+
104
+ ## Environment Setup Best Practices
105
+
106
+ When using SmolLLM in your project, you should handle environment variables at your application level:
107
+
108
+ 1. Create a `.env` file:
109
+ ```bash
110
+ # .env
111
+ OPENAI_API_KEY=sk-xxx
112
+ GEMINI_API_KEY=xxx,xxx2
113
+ ANTHROPIC_API_KEY=sk-xxx
114
+ ```
115
+
116
+ 2. Load environment variables before using SmolLLM:
117
+ ```python
118
+ from dotenv import load_dotenv
119
+ import os
120
+
121
+ # Load at your application startup
122
+ load_dotenv()
123
+
124
+ # Now you can use SmolLLM
125
+ from smolllm import ask_llm
126
+ ```
127
+
128
+ ## Tips
129
+
130
+ - Keep sensitive API keys in `.env` (add to .gitignore)
131
+ - Create `.env.example` for documentation
132
+ - For production, consider using your platform's secret management system
133
+ - When using multiple keys, separate with commas (no spaces)
@@ -0,0 +1,111 @@
1
+ # SmolLLM
2
+
3
+ A minimal Python library for interacting with various LLM providers, featuring automatic API key load balancing and streaming responses.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install smolllm
9
+ uv add "smolllm @ ../smolllm"
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```python
15
+ from dotenv import load_dotenv
16
+ import asyncio
17
+ from smolllm import ask_llm
18
+
19
+ # Load environment variables at your application startup
20
+ load_dotenv()
21
+
22
+ async def main():
23
+ response = await ask_llm(
24
+ "Say hello world",
25
+ model="gemini/gemini-2.0-flash"
26
+ )
27
+ print(response)
28
+
29
+ if __name__ == "__main__":
30
+ asyncio.run(main())
31
+ ```
32
+
33
+ ## Provider Configuration
34
+
35
+ Format: `provider/model_name` (e.g., `openai/gpt-4`, `gemini/gemini-2.0-flash`)
36
+
37
+ ### API Keys
38
+
39
+ The library looks for API keys in environment variables following the pattern: `{PROVIDER}_API_KEY`
40
+
41
+ Example:
42
+ ```bash
43
+ # .env
44
+ OPENAI_API_KEY=sk-xxx
45
+ GEMINI_API_KEY=key1,key2 # Multiple keys supported
46
+ ```
47
+
48
+ ### Custom Base URLs
49
+
50
+ Override default API endpoints using: `{PROVIDER}_BASE_URL`
51
+
52
+ Example:
53
+ ```bash
54
+ OPENAI_BASE_URL=https://custom.openai.com/v1
55
+ OLLAMA_BASE_URL=http://localhost:11434/v1
56
+ ```
57
+
58
+ ### Advanced Configuration
59
+
60
+ You can combine multiple keys and base URLs in several ways:
61
+
62
+ 1. One key with multiple base URLs:
63
+ ```bash
64
+ OLLAMA_API_KEY=ollama
65
+ OLLAMA_BASE_URL=http://localhost:11434/v1,http://other-server:11434/v1
66
+ ```
67
+
68
+ 2. Multiple keys with one base URL:
69
+ ```bash
70
+ GEMINI_API_KEY=key1,key2
71
+ GEMINI_BASE_URL=https://api.gemini.com/v1
72
+ ```
73
+
74
+ 3. Paired keys and base URLs:
75
+ ```bash
76
+ # Must have equal number of keys and URLs
77
+ # The library will randomly select matching pairs
78
+ GEMINI_API_KEY=key1,key2
79
+ GEMINI_BASE_URL=https://api.gemini.com/v1,https://api.gemini.com/v2
80
+ ```
81
+
82
+ ## Environment Setup Best Practices
83
+
84
+ When using SmolLLM in your project, you should handle environment variables at your application level:
85
+
86
+ 1. Create a `.env` file:
87
+ ```bash
88
+ # .env
89
+ OPENAI_API_KEY=sk-xxx
90
+ GEMINI_API_KEY=xxx,xxx2
91
+ ANTHROPIC_API_KEY=sk-xxx
92
+ ```
93
+
94
+ 2. Load environment variables before using SmolLLM:
95
+ ```python
96
+ from dotenv import load_dotenv
97
+ import os
98
+
99
+ # Load at your application startup
100
+ load_dotenv()
101
+
102
+ # Now you can use SmolLLM
103
+ from smolllm import ask_llm
104
+ ```
105
+
106
+ ## Tips
107
+
108
+ - Keep sensitive API keys in `.env` (add to .gitignore)
109
+ - Create `.env.example` for documentation
110
+ - For production, consider using your platform's secret management system
111
+ - When using multiple keys, separate with commas (no spaces)
@@ -0,0 +1,32 @@
1
+ import asyncio
2
+ import argparse
3
+ from dotenv import load_dotenv
4
+ from smolllm import ask_llm
5
+
6
+
7
+ async def simple(prompt: str = "Say hello world in a creative way"):
8
+ response = await ask_llm(
9
+ prompt,
10
+ model="gemini/gemini-2.0-flash", # Default model can be overridden by SMOLLLM_MODEL
11
+ )
12
+ print(response)
13
+
14
+
15
+ async def main():
16
+ # Load environment variables at application startup
17
+ load_dotenv()
18
+
19
+ parser = argparse.ArgumentParser(description="Ask LLM for creative responses")
20
+ parser.add_argument(
21
+ "prompt",
22
+ nargs="?",
23
+ default="Say hello world in a creative way",
24
+ help="Custom prompt to send to LLM",
25
+ )
26
+
27
+ args = parser.parse_args()
28
+ await simple(args.prompt)
29
+
30
+
31
+ if __name__ == "__main__":
32
+ asyncio.run(main())
@@ -0,0 +1,26 @@
1
+ import asyncio
2
+ from dotenv import load_dotenv
3
+ from smolllm import LLMFunction, ask_llm
4
+ from functools import partial
5
+
6
+ # Load environment variables at application startup
7
+ load_dotenv()
8
+
9
+ # Create a custom LLM function with specific configuration
10
+ custom_ollama = partial(
11
+ ask_llm,
12
+ model="ollama/deepseek-r1:7b",
13
+ base_url="http://rocry-win.local:11434/v1",
14
+ )
15
+
16
+
17
+ def translate(llm: LLMFunction, text: str, to: str = "Chinese"):
18
+ return llm(f"Translate below text to {to}:\n{text}")
19
+
20
+
21
+ async def main():
22
+ print(await translate(custom_ollama, "Show me the money"))
23
+
24
+
25
+ if __name__ == "__main__":
26
+ asyncio.run(main())
@@ -0,0 +1,357 @@
1
+ {
2
+ "openai": {
3
+ "api": {
4
+ "url": "https://api.openai.com"
5
+ },
6
+ "websites": {
7
+ "official": "https://openai.com/",
8
+ "apiKey": "https://platform.openai.com/api-keys",
9
+ "docs": "https://platform.openai.com/docs",
10
+ "models": "https://platform.openai.com/docs/models"
11
+ }
12
+ },
13
+ "ppio": {
14
+ "api": {
15
+ "url": "https://api.ppinfra.com/v3/openai"
16
+ },
17
+ "websites": {
18
+ "official": "https://ppinfra.com/model-api/product/llm-api?utm_source=github_cherry-studio&utm_medium=github_readme&utm_campaign=link",
19
+ "apiKey": "https://ppinfra.com/settings/key-management",
20
+ "docs": "https://ppinfra.com/docs/model-api/reference/llm/llm.html",
21
+ "models": "https://ppinfra.com/model-api/product/llm-api?utm_source=github_cherry-studio&utm_medium=github_readme&utm_campaign=link"
22
+ }
23
+ },
24
+ "gemini": {
25
+ "api": {
26
+ "url": "https://generativelanguage.googleapis.com"
27
+ },
28
+ "websites": {
29
+ "official": "https://gemini.google.com/",
30
+ "apiKey": "https://aistudio.google.com/app/apikey",
31
+ "docs": "https://ai.google.dev/gemini-api/docs",
32
+ "models": "https://ai.google.dev/gemini-api/docs/models/gemini"
33
+ }
34
+ },
35
+ "silicon": {
36
+ "api": {
37
+ "url": "https://api.siliconflow.cn"
38
+ },
39
+ "websites": {
40
+ "official": "https://www.siliconflow.cn/",
41
+ "apiKey": "https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88",
42
+ "docs": "https://docs.siliconflow.cn/",
43
+ "models": "https://docs.siliconflow.cn/docs/model-names"
44
+ }
45
+ },
46
+ "gitee-ai": {
47
+ "api": {
48
+ "url": "https://ai.gitee.com"
49
+ },
50
+ "websites": {
51
+ "official": "https://ai.gitee.com/",
52
+ "apiKey": "https://ai.gitee.com/dashboard/settings/tokens",
53
+ "docs": "https://ai.gitee.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90/POST/chat/completions",
54
+ "models": "https://ai.gitee.com/serverless-api"
55
+ }
56
+ },
57
+ "deepseek": {
58
+ "api": {
59
+ "url": "https://api.deepseek.com"
60
+ },
61
+ "websites": {
62
+ "official": "https://deepseek.com/",
63
+ "apiKey": "https://platform.deepseek.com/api_keys",
64
+ "docs": "https://platform.deepseek.com/api-docs/",
65
+ "models": "https://platform.deepseek.com/api-docs/"
66
+ }
67
+ },
68
+ "ocoolai": {
69
+ "api": {
70
+ "url": "https://api.ocoolai.com"
71
+ },
72
+ "websites": {
73
+ "official": "https://one.ocoolai.com/",
74
+ "apiKey": "https://one.ocoolai.com/token",
75
+ "docs": "https://docs.ooo.cool/",
76
+ "models": "https://docs.ooo.cool/guides/jiage/"
77
+ }
78
+ },
79
+ "together": {
80
+ "api": {
81
+ "url": "https://api.tohgether.xyz"
82
+ },
83
+ "websites": {
84
+ "official": "https://www.together.ai/",
85
+ "apiKey": "https://api.together.ai/settings/api-keys",
86
+ "docs": "https://docs.together.ai/docs/introduction",
87
+ "models": "https://docs.together.ai/docs/chat-models"
88
+ }
89
+ },
90
+ "github": {
91
+ "api": {
92
+ "url": "https://models.inference.ai.azure.com/"
93
+ },
94
+ "websites": {
95
+ "official": "https://github.com/marketplace/models",
96
+ "apiKey": "https://github.com/settings/tokens",
97
+ "docs": "https://docs.github.com/en/github-models",
98
+ "models": "https://github.com/marketplace/models"
99
+ }
100
+ },
101
+ "yi": {
102
+ "api": {
103
+ "url": "https://api.lingyiwanwu.com"
104
+ },
105
+ "websites": {
106
+ "official": "https://platform.lingyiwanwu.com/",
107
+ "apiKey": "https://platform.lingyiwanwu.com/apikeys",
108
+ "docs": "https://platform.lingyiwanwu.com/docs",
109
+ "models": "https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B"
110
+ }
111
+ },
112
+ "zhipu": {
113
+ "api": {
114
+ "url": "https://open.bigmodel.cn/api/paas/v4/"
115
+ },
116
+ "websites": {
117
+ "official": "https://open.bigmodel.cn/",
118
+ "apiKey": "https://open.bigmodel.cn/usercenter/apikeys",
119
+ "docs": "https://open.bigmodel.cn/dev/howuse/introduction",
120
+ "models": "https://open.bigmodel.cn/modelcenter/square"
121
+ }
122
+ },
123
+ "moonshot": {
124
+ "api": {
125
+ "url": "https://api.moonshot.cn"
126
+ },
127
+ "websites": {
128
+ "official": "https://moonshot.ai/",
129
+ "apiKey": "https://platform.moonshot.cn/console/api-keys",
130
+ "docs": "https://platform.moonshot.cn/docs/",
131
+ "models": "https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8"
132
+ }
133
+ },
134
+ "baichuan": {
135
+ "api": {
136
+ "url": "https://api.baichuan-ai.com"
137
+ },
138
+ "websites": {
139
+ "official": "https://www.baichuan-ai.com/",
140
+ "apiKey": "https://platform.baichuan-ai.com/console/apikey",
141
+ "docs": "https://platform.baichuan-ai.com/docs",
142
+ "models": "https://platform.baichuan-ai.com/price"
143
+ }
144
+ },
145
+ "dashscope": {
146
+ "api": {
147
+ "url": "https://dashscope.aliyuncs.com/compatible-mode/v1/"
148
+ },
149
+ "websites": {
150
+ "official": "https://www.aliyun.com/product/bailian",
151
+ "apiKey": "https://bailian.console.aliyun.com/?apiKey=1#/api-key",
152
+ "docs": "https://help.aliyun.com/zh/model-studio/getting-started/",
153
+ "models": "https://bailian.console.aliyun.com/model-market#/model-market"
154
+ }
155
+ },
156
+ "stepfun": {
157
+ "api": {
158
+ "url": "https://api.stepfun.com"
159
+ },
160
+ "websites": {
161
+ "official": "https://platform.stepfun.com/",
162
+ "apiKey": "https://platform.stepfun.com/interface-key",
163
+ "docs": "https://platform.stepfun.com/docs/overview/concept",
164
+ "models": "https://platform.stepfun.com/docs/llm/text"
165
+ }
166
+ },
167
+ "doubao": {
168
+ "api": {
169
+ "url": "https://ark.cn-beijing.volces.com/api/v3/"
170
+ },
171
+ "websites": {
172
+ "official": "https://console.volcengine.com/ark/",
173
+ "apiKey": "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey",
174
+ "docs": "https://www.volcengine.com/docs/82379/1182403",
175
+ "models": "https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint"
176
+ }
177
+ },
178
+ "minimax": {
179
+ "api": {
180
+ "url": "https://api.minimax.chat/v1/"
181
+ },
182
+ "websites": {
183
+ "official": "https://platform.minimaxi.com/",
184
+ "apiKey": "https://platform.minimaxi.com/user-center/basic-information/interface-key",
185
+ "docs": "https://platform.minimaxi.com/document/Announcement",
186
+ "models": "https://platform.minimaxi.com/document/Models"
187
+ }
188
+ },
189
+ "graphrag-kylin-mountain": {
190
+ "api": {
191
+ "url": ""
192
+ }
193
+ },
194
+ "openrouter": {
195
+ "api": {
196
+ "url": "https://openrouter.ai/api/v1/"
197
+ },
198
+ "websites": {
199
+ "official": "https://openrouter.ai/",
200
+ "apiKey": "https://openrouter.ai/settings/keys",
201
+ "docs": "https://openrouter.ai/docs/quick-start",
202
+ "models": "https://openrouter.ai/docs/models"
203
+ }
204
+ },
205
+ "groq": {
206
+ "api": {
207
+ "url": "https://api.groq.com/openai"
208
+ },
209
+ "websites": {
210
+ "official": "https://groq.com/",
211
+ "apiKey": "https://console.groq.com/keys",
212
+ "docs": "https://console.groq.com/docs/quickstart",
213
+ "models": "https://console.groq.com/docs/models"
214
+ }
215
+ },
216
+ "ollama": {
217
+ "api": {
218
+ "url": "http://localhost:11434"
219
+ },
220
+ "websites": {
221
+ "official": "https://ollama.com/",
222
+ "docs": "https://github.com/ollama/ollama/tree/main/docs",
223
+ "models": "https://ollama.com/library"
224
+ }
225
+ },
226
+ "anthropic": {
227
+ "api": {
228
+ "url": "https://api.anthropic.com/"
229
+ },
230
+ "websites": {
231
+ "official": "https://anthropic.com/",
232
+ "apiKey": "https://console.anthropic.com/settings/keys",
233
+ "docs": "https://docs.anthropic.com/en/docs",
234
+ "models": "https://docs.anthropic.com/en/docs/about-claude/models"
235
+ }
236
+ },
237
+ "grok": {
238
+ "api": {
239
+ "url": "https://api.x.ai"
240
+ },
241
+ "websites": {
242
+ "official": "https://x.ai/",
243
+ "docs": "https://docs.x.ai/",
244
+ "models": "https://docs.x.ai/docs#getting-started"
245
+ }
246
+ },
247
+ "hyperbolic": {
248
+ "api": {
249
+ "url": "https://api.hyperbolic.xyz"
250
+ },
251
+ "websites": {
252
+ "official": "https://app.hyperbolic.xyz",
253
+ "apiKey": "https://app.hyperbolic.xyz/settings",
254
+ "docs": "https://docs.hyperbolic.xyz",
255
+ "models": "https://app.hyperbolic.xyz/models"
256
+ }
257
+ },
258
+ "mistral": {
259
+ "api": {
260
+ "url": "https://api.mistral.ai"
261
+ },
262
+ "websites": {
263
+ "official": "https://mistral.ai",
264
+ "apiKey": "https://console.mistral.ai/api-keys/",
265
+ "docs": "https://docs.mistral.ai",
266
+ "models": "https://docs.mistral.ai/getting-started/models/models_overview"
267
+ }
268
+ },
269
+ "jina": {
270
+ "api": {
271
+ "url": "https://api.jina.ai"
272
+ },
273
+ "websites": {
274
+ "official": "https://jina.ai",
275
+ "apiKey": "https://jina.ai/",
276
+ "docs": "https://jina.ai",
277
+ "models": "https://jina.ai"
278
+ }
279
+ },
280
+ "aihubmix": {
281
+ "api": {
282
+ "url": "https://aihubmix.com"
283
+ },
284
+ "websites": {
285
+ "official": "https://aihubmix.com?aff=SJyh",
286
+ "apiKey": "https://aihubmix.com?aff=SJyh",
287
+ "docs": "https://doc.aihubmix.com/",
288
+ "models": "https://aihubmix.com/models"
289
+ }
290
+ },
291
+ "fireworks": {
292
+ "api": {
293
+ "url": "https://api.fireworks.ai/inference"
294
+ },
295
+ "websites": {
296
+ "official": "https://fireworks.ai/",
297
+ "apiKey": "https://fireworks.ai/account/api-keys",
298
+ "docs": "https://docs.fireworks.ai/getting-started/introduction",
299
+ "models": "https://fireworks.ai/dashboard/models"
300
+ }
301
+ },
302
+ "zhinao": {
303
+ "api": {
304
+ "url": "https://api.360.cn"
305
+ },
306
+ "websites": {
307
+ "official": "https://ai.360.com/",
308
+ "apiKey": "https://ai.360.com/platform/keys",
309
+ "docs": "https://ai.360.com/platform/docs/overview",
310
+ "models": "https://ai.360.com/platform/limit"
311
+ }
312
+ },
313
+ "hunyuan": {
314
+ "api": {
315
+ "url": "https://api.hunyuan.cloud.tencent.com"
316
+ },
317
+ "websites": {
318
+ "official": "https://cloud.tencent.com/product/hunyuan",
319
+ "apiKey": "https://console.cloud.tencent.com/hunyuan/api-key",
320
+ "docs": "https://cloud.tencent.com/document/product/1729/111007",
321
+ "models": "https://cloud.tencent.com/document/product/1729/104753"
322
+ }
323
+ },
324
+ "nvidia": {
325
+ "api": {
326
+ "url": "https://integrate.api.nvidia.com"
327
+ },
328
+ "websites": {
329
+ "official": "https://build.nvidia.com/explore/discover",
330
+ "apiKey": "https://build.nvidia.com/meta/llama-3_1-405b-instruct",
331
+ "docs": "https://docs.api.nvidia.com/nim/reference/llm-apis",
332
+ "models": "https://build.nvidia.com/nim"
333
+ }
334
+ },
335
+ "azure-openai": {
336
+ "api": {
337
+ "url": ""
338
+ },
339
+ "websites": {
340
+ "official": "https://azure.microsoft.com/en-us/products/ai-services/openai-service",
341
+ "apiKey": "https://portal.azure.com/#view/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/~/OpenAI",
342
+ "docs": "https://learn.microsoft.com/en-us/azure/ai-services/openai/",
343
+ "models": "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models"
344
+ }
345
+ },
346
+ "baidu-cloud": {
347
+ "api": {
348
+ "url": "https://qianfan.baidubce.com/v2/"
349
+ },
350
+ "websites": {
351
+ "official": "https://cloud.baidu.com/",
352
+ "apiKey": "https://cloud.baidu.com/console/qianfan/apikey",
353
+ "docs": "https://cloud.baidu.com/doc/index.html",
354
+ "models": "https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu"
355
+ }
356
+ }
357
+ }
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "smolllm"
3
+ version = "0.1.0"
4
+ description = "A minimal Python library for interacting with various LLM providers"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "RoCry", email = "crysheen@gmail.com" }
8
+ ]
9
+ license = { text = "MIT" }
10
+ requires-python = ">=3.12"
11
+ keywords = ["llm", "ai", "openai", "gemini", "anthropic"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ dependencies = [
21
+ "python-dotenv>=1.0.1",
22
+ "httpx>=0.28.1",
23
+ "rich>=13.9.4",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/RoCry/smolllm"
28
+ Repository = "https://github.com/RoCry/smolllm.git"
29
+
30
+ [project.scripts]
31
+ smolllm = "smolllm:main"
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
@@ -0,0 +1,13 @@
1
+ """
2
+ smolllm - A minimal LLM library for easy interaction with various LLM providers
3
+ """
4
+
5
+ from .core import ask_llm
6
+ from .types import LLMFunction, StreamHandler
7
+
8
+ __version__ = "0.1.0"
9
+ __all__ = ["ask_llm", "LLMFunction", "StreamHandler"]
10
+
11
+
12
+ def main() -> None:
13
+ print("Hello from smolllm!")
@@ -0,0 +1,60 @@
1
+ import random
2
+ from typing import Dict, Tuple
3
+
4
+
5
+ class SimpleBalancer:
6
+ """Balances API key and URL pairs usage."""
7
+
8
+ def __init__(self):
9
+ self.pair_usage: Dict[Tuple[str, str], int] = {}
10
+
11
+ def _parse_items(self, items: str) -> list[str]:
12
+ return [item.strip() for item in items.split(",")]
13
+
14
+ def choose_pair(self, keys: str, urls: str) -> Tuple[str, str]:
15
+ """Choose a key-URL pair based on usage balancing.
16
+
17
+ Cases:
18
+ 1. 1 key, 1 url -> single pair
19
+ 2. 1 key, n urls -> key paired with each url
20
+ 3. n keys, n urls -> matching pairs
21
+ 4. n keys, 1 url -> each key paired with url
22
+ """
23
+ key_list = self._parse_items(keys)
24
+ url_list = self._parse_items(urls)
25
+
26
+ # Generate valid pairs based on the cases
27
+ if len(url_list) == 1:
28
+ pairs = [(key, url_list[0]) for key in key_list]
29
+ elif len(key_list) == 1:
30
+ pairs = [(key_list[0], url) for url in url_list]
31
+ else:
32
+ if len(key_list) != len(url_list):
33
+ raise ValueError(
34
+ "When using multiple keys and URLs, their counts must match"
35
+ )
36
+ pairs = list(zip(key_list, url_list))
37
+
38
+ # Initialize usage count for new pairs
39
+ for pair in pairs:
40
+ if pair not in self.pair_usage:
41
+ self.pair_usage[pair] = 0
42
+
43
+ # Find and choose least used pair
44
+ min_usage = min(self.pair_usage[pair] for pair in pairs)
45
+ least_used_pairs = [
46
+ pair for pair in pairs if self.pair_usage[pair] == min_usage
47
+ ]
48
+ chosen_pair = random.choice(least_used_pairs)
49
+ self.pair_usage[chosen_pair] += 1
50
+
51
+ return chosen_pair
52
+
53
+ def choose_one(self, keys: str) -> str:
54
+ """Legacy method for backwards compatibility"""
55
+ key, _ = self.choose_pair(keys, "dummy_url")
56
+ return key
57
+
58
+
59
+ # Create a global instance
60
+ balancer = SimpleBalancer()
@@ -0,0 +1,108 @@
1
+ import os
2
+ import json
3
+ from typing import Optional
4
+ import httpx
5
+
6
+ from .types import StreamHandler
7
+ from .providers import parse_model_string
8
+ from .balancer import balancer
9
+ from .stream import handle_chunk
10
+ from .request import prepare_request_data, prepare_client_and_auth
11
+ from .log import logger
12
+ from .display import ResponseDisplay
13
+
14
+
15
+ async def ask_llm(
16
+ prompt: str,
17
+ *,
18
+ system_prompt: Optional[str] = None,
19
+ model: Optional[str] = None,
20
+ api_key: Optional[str] = None,
21
+ base_url: Optional[str] = None,
22
+ handler: Optional[StreamHandler] = None,
23
+ timeout: float = 60.0,
24
+ ) -> str:
25
+ """
26
+ Send a prompt to an LLM and get the response
27
+
28
+ Args:
29
+ prompt: The main prompt text
30
+ system_prompt: Optional system prompt
31
+ model: Model identifier (e.g., "openai/gpt-4" or "google")
32
+ api_key: Optional API key (will fall back to environment variable)
33
+ base_url: Custom base URL for API endpoint
34
+ stream_handler: Optional callback for handling streaming responses
35
+ timeout: Request timeout in seconds
36
+ """
37
+ if not model:
38
+ model = os.getenv("SMOLLLM_MODEL")
39
+ if not model:
40
+ raise ValueError(
41
+ "Model string not found. Set SMOLLLM_MODEL environment variable or pass model parameter"
42
+ )
43
+ provider, model_name = parse_model_string(model)
44
+
45
+ if not base_url:
46
+ env_base_url = os.getenv(f"{provider.name.upper()}_BASE_URL")
47
+ base_url = env_base_url or provider.base_url
48
+
49
+ if not api_key:
50
+ env_key = f"{provider.name.upper()}_API_KEY"
51
+ api_key = os.getenv(env_key)
52
+ if not api_key:
53
+ # special case for convenience: ollama
54
+ if provider.name == "ollama":
55
+ api_key = "ollama"
56
+ else:
57
+ raise ValueError(
58
+ f"API key not found. Set {env_key} environment variable or pass api_key parameter"
59
+ )
60
+
61
+ if not base_url:
62
+ raise ValueError("Base URL is required")
63
+
64
+ api_key, base_url = balancer.choose_pair(api_key, base_url)
65
+ url, data, headers = prepare_request_data(
66
+ prompt, system_prompt, model_name, provider.name, base_url
67
+ )
68
+ client = prepare_client_and_auth(url, provider.name, api_key, headers)
69
+
70
+ api_key_preview = api_key[:5] + "..." + api_key[-4:]
71
+ logger.info(f"Sending {url} model={model_name} api_key={api_key_preview}")
72
+
73
+ try:
74
+ async with client.stream(
75
+ "POST", url, headers=headers, json=data, timeout=timeout
76
+ ) as response:
77
+ if response.status_code >= 400:
78
+ error_text = await response.aread()
79
+ raise httpx.HTTPStatusError(
80
+ f"HTTP Error {response.status_code}: {error_text.decode()}",
81
+ request=response.request,
82
+ response=response,
83
+ )
84
+ return await process_stream_response(response, handler, provider.name)
85
+ except Exception as e:
86
+ logger.error(f"Error: {e}")
87
+ raise
88
+
89
+
90
+ async def process_stream_response(
91
+ response: httpx.Response,
92
+ stream_handler: Optional[StreamHandler],
93
+ provider_name: str,
94
+ ) -> str:
95
+ with ResponseDisplay(stream_handler) as display:
96
+ async for line in response.aiter_lines():
97
+ line = line.strip()
98
+ if not line or line == "data: [DONE]" or not line.startswith("data: "):
99
+ continue
100
+
101
+ try:
102
+ chunk = json.loads(line[6:]) # Remove "data: " prefix
103
+ if delta := await handle_chunk(chunk, provider_name):
104
+ display.update(delta)
105
+ except Exception as e:
106
+ logger.error(f"Error processing chunk: {e}")
107
+
108
+ return display.finalize()
@@ -0,0 +1,57 @@
1
+ from rich.live import Live
2
+ from rich.text import Text
3
+ from rich.markdown import Markdown
4
+ from rich.console import Group
5
+ from rich.rule import Rule
6
+ from typing import Optional
7
+ from .types import StreamHandler
8
+
9
+
10
+ class ResponseDisplay:
11
+ def __init__(self, stream_handler: Optional[StreamHandler] = None):
12
+ self.stream_handler = stream_handler
13
+ self.final_response = ""
14
+ self.live: Optional[Live] = None
15
+
16
+ def __enter__(self):
17
+ self.live = Live(
18
+ Group(Rule(style="grey50"), Text(""), Rule(style="grey50")),
19
+ refresh_per_second=4,
20
+ vertical_overflow="visible",
21
+ ).__enter__()
22
+ return self
23
+
24
+ def __exit__(self, exc_type, exc_val, exc_tb):
25
+ if self.live:
26
+ self.live.__exit__(exc_type, exc_val, exc_tb)
27
+
28
+ def update(self, delta: str):
29
+ """Update display with new content"""
30
+ if self.stream_handler:
31
+ self.stream_handler(delta)
32
+
33
+ self.final_response += delta
34
+ self._update_display(with_cursor=True)
35
+
36
+ def finalize(self):
37
+ """Show final response without cursor"""
38
+ self._update_display(with_cursor=False)
39
+ return self.final_response.strip()
40
+
41
+ def _update_display(self, with_cursor: bool = True):
42
+ """Internal method to update the live display"""
43
+ if not self.live:
44
+ return
45
+
46
+ content = self.final_response + ("\n\n▌" if with_cursor else "")
47
+ try:
48
+ group = Group(Rule(style="grey50"), Markdown(content), Rule(style="grey50"))
49
+ except Exception:
50
+ # Fallback to plain text if markdown parsing fails
51
+ group = Group(
52
+ Rule(style="grey50"),
53
+ Text(content, style="blink" if with_cursor else None),
54
+ Rule(style="grey50"),
55
+ )
56
+
57
+ self.live.update(group)
@@ -0,0 +1,21 @@
1
+ import os
2
+ import logging
3
+ from rich.logging import RichHandler
4
+
5
+ # Get log level from environment or default to INFO
6
+ log_level = os.getenv("LOG_LEVEL", "INFO").upper()
7
+
8
+ # Set root logger to INFO to suppress debug logs from other libraries
9
+ logging.getLogger().setLevel(logging.INFO)
10
+
11
+ # Configure rich handler
12
+ rich_handler = RichHandler(rich_tracebacks=True)
13
+ rich_handler.setFormatter(logging.Formatter("%(message)s"))
14
+
15
+ # Configure our package logger
16
+ logger = logging.getLogger("smolllm")
17
+ logger.setLevel(getattr(logging, log_level, logging.INFO))
18
+ logger.handlers = [rich_handler]
19
+
20
+ # Prevent propagation to root logger to avoid duplicate logs
21
+ logger.propagate = False
@@ -0,0 +1,65 @@
1
+ from dataclasses import dataclass
2
+ import json
3
+ from typing import Optional
4
+
5
+
6
+ @dataclass
7
+ class Provider:
8
+ name: str
9
+ base_url: str
10
+ default_model_name: Optional[str] = None
11
+
12
+ # Guess default model name if not provided
13
+ # this is designed for random choosing from multiple providers
14
+ def guess_default_model_name(self) -> Optional[str]:
15
+ if self.default_model_name:
16
+ return self.default_model_name
17
+
18
+ if self.name == "gemini":
19
+ return "gemini-2.0-flash"
20
+
21
+ return None
22
+
23
+
24
+ def generate_provider_map() -> dict[str, Provider]:
25
+ with open("providers.json", "r") as f:
26
+ raw_providers = json.load(f)
27
+
28
+ provider_map = {}
29
+ for name, config in raw_providers.items():
30
+ provider_map[name] = Provider(
31
+ name=name,
32
+ base_url=config["api"]["url"],
33
+ )
34
+
35
+ # add more
36
+ provider_map["tencent_cloud"] = Provider(
37
+ name="tencent_cloud",
38
+ base_url="https://api.lkeap.cloud.tencent.com",
39
+ )
40
+
41
+ return provider_map
42
+
43
+
44
+ PROVIDERS = generate_provider_map()
45
+
46
+
47
+ # try parse provider and model name from model_str, e.g.
48
+ # "gemini/gemini-2.0-flash" -> ("gemini", "gemini-2.0-flash")
49
+ # "gemini" -> ("gemini", "gemini-2.0-flash") // /w default model for the provider
50
+ def parse_model_string(model_str: str) -> tuple[Provider, str]:
51
+ """Parse model string into provider and model name"""
52
+ if "/" in model_str:
53
+ provider_name, model_name = model_str.split("/", 1)
54
+ else:
55
+ # Use the model string as provider name and get its default model
56
+ provider_name = model_str
57
+
58
+ provider = PROVIDERS.get(provider_name)
59
+ if not provider:
60
+ raise ValueError(f"Unknown provider: {provider_name}")
61
+ model_name = model_name or provider.guess_default_model_name()
62
+ if not model_name:
63
+ raise ValueError(f"Model name not found for provider: {provider_name}")
64
+
65
+ return provider, model_name
@@ -0,0 +1,53 @@
1
+ from typing import Dict, Any
2
+ import httpx
3
+
4
+
5
+ def prepare_request_data(
6
+ prompt: str,
7
+ system_prompt: str | None,
8
+ model_name: str,
9
+ provider_name: str,
10
+ base_url: str,
11
+ ) -> tuple[str, Dict[str, Any], Dict[str, str]]:
12
+ """Prepare request URL, data and headers for the API call"""
13
+ base_url = base_url.rstrip("/")
14
+
15
+ if provider_name == "gemini":
16
+ url = f"{base_url}/v1beta/models/{model_name}:streamGenerateContent?alt=sse"
17
+ headers = {"Content-Type": "application/json"}
18
+ data = {
19
+ "contents": [{"parts": [{"text": prompt}]}],
20
+ }
21
+ if system_prompt:
22
+ data["system_instruction"] = {"parts": [{"text": system_prompt}]}
23
+ else:
24
+ url = f"{base_url}/chat/completions"
25
+ headers = {"Content-Type": "application/json"}
26
+ messages = []
27
+ if system_prompt:
28
+ messages.append({"role": "system", "content": system_prompt})
29
+ messages.append({"role": "user", "content": prompt})
30
+ data = {
31
+ "messages": messages,
32
+ "model": model_name,
33
+ "stream": True,
34
+ }
35
+
36
+ return url, data, headers
37
+
38
+
39
+ def prepare_client_and_auth(
40
+ url: str, provider_name: str, api_key: str, headers: Dict[str, str]
41
+ ) -> httpx.AsyncClient:
42
+ """Prepare HTTP client and handle authentication"""
43
+ # Handle authentication
44
+ if provider_name == "gemini":
45
+ headers["x-goog-api-key"] = api_key
46
+ else:
47
+ headers["Authorization"] = f"Bearer {api_key}"
48
+
49
+ # Prepare client
50
+ unsecure = url.startswith("http://")
51
+ transport = httpx.AsyncHTTPTransport(local_address="0.0.0.0") if unsecure else None
52
+
53
+ return httpx.AsyncClient(verify=not unsecure, transport=transport)
@@ -0,0 +1,15 @@
1
+ from typing import Optional
2
+
3
+
4
+ async def handle_chunk(chunk: dict, provider_name: str) -> Optional[str]:
5
+ """Handle streaming chunks from different providers"""
6
+ if provider_name == "gemini":
7
+ candidates = chunk.get("candidates", [])
8
+ if candidates and "content" in candidates[0]:
9
+ return candidates[0]["content"]["parts"][0]["text"]
10
+ else: # openai-compatible
11
+ choice = chunk.get("choices", [{}])[0]
12
+ if choice.get("finish_reason") is not None:
13
+ return None
14
+ return choice.get("delta", {}).get("content", "")
15
+ return None
@@ -0,0 +1,6 @@
1
+ from typing import Callable, Awaitable, Optional, TypeAlias, Any
2
+
3
+ StreamHandler: TypeAlias = Callable[[str], None]
4
+ LLMFunction: TypeAlias = Callable[
5
+ [str, Optional[str], Any], Awaitable[str]
6
+ ] # (prompt, system_prompt, **kwargs) -> response
smolllm-0.1.0/uv.lock ADDED
@@ -0,0 +1,158 @@
1
+ version = 1
2
+ requires-python = ">=3.12"
3
+
4
+ [[package]]
5
+ name = "anyio"
6
+ version = "4.8.0"
7
+ source = { registry = "https://pypi.org/simple" }
8
+ dependencies = [
9
+ { name = "idna" },
10
+ { name = "sniffio" },
11
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
12
+ ]
13
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
16
+ ]
17
+
18
+ [[package]]
19
+ name = "certifi"
20
+ version = "2025.1.31"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
25
+ ]
26
+
27
+ [[package]]
28
+ name = "h11"
29
+ version = "0.14.0"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
32
+ wheels = [
33
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
34
+ ]
35
+
36
+ [[package]]
37
+ name = "httpcore"
38
+ version = "1.0.7"
39
+ source = { registry = "https://pypi.org/simple" }
40
+ dependencies = [
41
+ { name = "certifi" },
42
+ { name = "h11" },
43
+ ]
44
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
47
+ ]
48
+
49
+ [[package]]
50
+ name = "httpx"
51
+ version = "0.28.1"
52
+ source = { registry = "https://pypi.org/simple" }
53
+ dependencies = [
54
+ { name = "anyio" },
55
+ { name = "certifi" },
56
+ { name = "httpcore" },
57
+ { name = "idna" },
58
+ ]
59
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
60
+ wheels = [
61
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "idna"
66
+ version = "3.10"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
69
+ wheels = [
70
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
71
+ ]
72
+
73
+ [[package]]
74
+ name = "markdown-it-py"
75
+ version = "3.0.0"
76
+ source = { registry = "https://pypi.org/simple" }
77
+ dependencies = [
78
+ { name = "mdurl" },
79
+ ]
80
+ sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
81
+ wheels = [
82
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
83
+ ]
84
+
85
+ [[package]]
86
+ name = "mdurl"
87
+ version = "0.1.2"
88
+ source = { registry = "https://pypi.org/simple" }
89
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "pygments"
96
+ version = "2.19.1"
97
+ source = { registry = "https://pypi.org/simple" }
98
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
99
+ wheels = [
100
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
101
+ ]
102
+
103
+ [[package]]
104
+ name = "python-dotenv"
105
+ version = "1.0.1"
106
+ source = { registry = "https://pypi.org/simple" }
107
+ sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
108
+ wheels = [
109
+ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
110
+ ]
111
+
112
+ [[package]]
113
+ name = "rich"
114
+ version = "13.9.4"
115
+ source = { registry = "https://pypi.org/simple" }
116
+ dependencies = [
117
+ { name = "markdown-it-py" },
118
+ { name = "pygments" },
119
+ ]
120
+ sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
121
+ wheels = [
122
+ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
123
+ ]
124
+
125
+ [[package]]
126
+ name = "smolllm"
127
+ version = "0.1.0"
128
+ source = { editable = "." }
129
+ dependencies = [
130
+ { name = "httpx" },
131
+ { name = "python-dotenv" },
132
+ { name = "rich" },
133
+ ]
134
+
135
+ [package.metadata]
136
+ requires-dist = [
137
+ { name = "httpx", specifier = ">=0.28.1" },
138
+ { name = "python-dotenv", specifier = ">=1.0.1" },
139
+ { name = "rich", specifier = ">=13.9.4" },
140
+ ]
141
+
142
+ [[package]]
143
+ name = "sniffio"
144
+ version = "1.3.1"
145
+ source = { registry = "https://pypi.org/simple" }
146
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
147
+ wheels = [
148
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
149
+ ]
150
+
151
+ [[package]]
152
+ name = "typing-extensions"
153
+ version = "4.12.2"
154
+ source = { registry = "https://pypi.org/simple" }
155
+ sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
156
+ wheels = [
157
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
158
+ ]