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.
- smolllm-0.1.0/.env.example +5 -0
- smolllm-0.1.0/.gitignore +12 -0
- smolllm-0.1.0/.python-version +1 -0
- smolllm-0.1.0/LICENSE +21 -0
- smolllm-0.1.0/PKG-INFO +133 -0
- smolllm-0.1.0/README.md +111 -0
- smolllm-0.1.0/examples/hello.py +32 -0
- smolllm-0.1.0/examples/llm_func.py +26 -0
- smolllm-0.1.0/providers.json +357 -0
- smolllm-0.1.0/pyproject.toml +36 -0
- smolllm-0.1.0/src/smolllm/__init__.py +13 -0
- smolllm-0.1.0/src/smolllm/balancer.py +60 -0
- smolllm-0.1.0/src/smolllm/core.py +108 -0
- smolllm-0.1.0/src/smolllm/display.py +57 -0
- smolllm-0.1.0/src/smolllm/log.py +21 -0
- smolllm-0.1.0/src/smolllm/providers.py +65 -0
- smolllm-0.1.0/src/smolllm/request.py +53 -0
- smolllm-0.1.0/src/smolllm/stream.py +15 -0
- smolllm-0.1.0/src/smolllm/types.py +6 -0
- smolllm-0.1.0/uv.lock +158 -0
smolllm-0.1.0/.gitignore
ADDED
|
@@ -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)
|
smolllm-0.1.0/README.md
ADDED
|
@@ -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
|
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
|
+
]
|