llm-deepseek-ya 1.0.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.
- llm_deepseek_ya-1.0.0/LICENSE +21 -0
- llm_deepseek_ya-1.0.0/PKG-INFO +157 -0
- llm_deepseek_ya-1.0.0/README.md +135 -0
- llm_deepseek_ya-1.0.0/llm_deepseek.py +248 -0
- llm_deepseek_ya-1.0.0/llm_deepseek_ya.egg-info/PKG-INFO +157 -0
- llm_deepseek_ya-1.0.0/llm_deepseek_ya.egg-info/SOURCES.txt +11 -0
- llm_deepseek_ya-1.0.0/llm_deepseek_ya.egg-info/dependency_links.txt +1 -0
- llm_deepseek_ya-1.0.0/llm_deepseek_ya.egg-info/entry_points.txt +2 -0
- llm_deepseek_ya-1.0.0/llm_deepseek_ya.egg-info/requires.txt +10 -0
- llm_deepseek_ya-1.0.0/llm_deepseek_ya.egg-info/top_level.txt +1 -0
- llm_deepseek_ya-1.0.0/pyproject.toml +44 -0
- llm_deepseek_ya-1.0.0/setup.cfg +4 -0
- llm_deepseek_ya-1.0.0/tests/test_deepseek.py +257 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 delijati
|
|
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.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-deepseek-ya
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Yet another LLM access to DeepSeek's API
|
|
5
|
+
Author: Josip Delic
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/delijati/llm-deepseek-ya
|
|
8
|
+
Project-URL: Issues, https://github.com/delijati/llm-deepseek-ya/issues
|
|
9
|
+
Project-URL: CI, https://github.com/delijati/llm-deepseek-ya/actions
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: llm
|
|
14
|
+
Requires-Dist: httpx
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-httpx; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-recording; extra == "test"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# llm-deepseek-ya
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/llm-deepseek-ya/0.1.0/)
|
|
26
|
+
[](https://github.com/delijati/llm-deepseek-ya/releases)
|
|
27
|
+
[](https://github.com/delijati/llm-deepseek-ya/blob/main/LICENSE)
|
|
28
|
+
|
|
29
|
+
LLM access to DeepSeek's API
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install this plugin in the same environment as [LLM](https://llm.datasette.io/).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
llm install llm-deepseek-ya
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
First, set an [API key](https://platform.deepseek.com/api_keys) for DeepSeek:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
llm keys set deepseek
|
|
45
|
+
# Paste key here
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `llm models` to list the models, and `llm models --options` to include a list of their options.
|
|
49
|
+
|
|
50
|
+
### Running Prompts
|
|
51
|
+
|
|
52
|
+
Run prompts like this:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
llm -m deepseek-chat "Describe a futuristic city on Mars"
|
|
56
|
+
llm -m deepseek-chat-completion "The AI began to dream, and in its dreams," -o echo true
|
|
57
|
+
llm -m deepseek-reasoner "Write a Python function to sort a list of numbers"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Note: The DeepSeek Reasoner model only supports the chat endpoint, not the completion endpoint.
|
|
61
|
+
|
|
62
|
+
### DeepSeek Reasoner Model
|
|
63
|
+
|
|
64
|
+
The DeepSeek Reasoner model uses a Chain of Thought (CoT) approach to solve complex problems, showing its reasoning process before providing the final answer.
|
|
65
|
+
|
|
66
|
+
The plugin shows the model's chain of thought reasoning by default in non-streaming mode. The reasoning feature is currently only supported in non-streaming mode.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Normal usage - will show reasoning by default
|
|
70
|
+
llm -m deepseek-reasoner "What is 537 * 943?"
|
|
71
|
+
|
|
72
|
+
# Hide reasoning when you only want the final answer
|
|
73
|
+
llm -m deepseek-reasoner "What is 537 * 943?" -o show_reasoning false
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Features
|
|
77
|
+
|
|
78
|
+
#### Prefill
|
|
79
|
+
|
|
80
|
+
The `prefill` option allows you to provide initial text for the model's response. This is useful for guiding the model's output.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
llm -m deepseek-chat "What are some wild and crazy activities for a holiday party?" -o prefill "Here are some off-the-wall ideas to make your holiday party unforgettable [warning: these may not be suitable for work holiday parties]:"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You can also load prefill text from a file:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Create a file with your prefill text
|
|
92
|
+
echo "Here are some unique holiday party ideas:" > prefill.txt
|
|
93
|
+
|
|
94
|
+
# Use the file path as the prefill value
|
|
95
|
+
llm -m deepseek-chat "What are some fun activities for a holiday party?" -o prefill prefill.txt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This is especially useful for longer prefill text that would be unwieldy on the command line.
|
|
99
|
+
|
|
100
|
+
#### JSON Response Format
|
|
101
|
+
|
|
102
|
+
The `response_format` option allows you to specify that the model should output its response in JSON format. To ensure the model outputs valid JSON, include the word "json" in the system or user prompt. Optionally, you can provide an example of the desired JSON format to guide the model.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
llm -m deepseek-chat "What are some fun activities for a holiday party?" -o response_format json_object --system "json"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To guide the model further, you can provide an example JSON structure:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
llm -m deepseek-chat "What are some way to tell if a holiday party is fun?" -o response_format json_object --system 'EXAMPLE JSON OUTPUT: {"event": "holiday_party_fun", "success_metric": ["..."]}'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### JSON Schema Support
|
|
117
|
+
|
|
118
|
+
DeepSeek Chat models support JSON schema output (via LLM's `--schema` option). When you provide a schema, the plugin automatically enables JSON mode and includes the schema in the system message to guide the model's output.
|
|
119
|
+
|
|
120
|
+
**Important Note:** DeepSeek's API does not validate the output against the schema - it only uses JSON mode. The schema is provided to the model as guidance, so the model will attempt to follow it, but strict validation is not enforced.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
llm -m deepseek-chat "Generate a user profile" --schema '{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}, "email": {"type": "string"}}, "required": ["name", "age"]}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
You can also use LLM's schema file support:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Create a schema file
|
|
132
|
+
cat > user_schema.json << 'EOF'
|
|
133
|
+
{
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"name": {"type": "string"},
|
|
137
|
+
"age": {"type": "number"},
|
|
138
|
+
"email": {"type": "string"},
|
|
139
|
+
"interests": {"type": "array", "items": {"type": "string"}}
|
|
140
|
+
},
|
|
141
|
+
"required": ["name", "age"]
|
|
142
|
+
}
|
|
143
|
+
EOF
|
|
144
|
+
|
|
145
|
+
# Use the schema file
|
|
146
|
+
llm -m deepseek-chat "Generate a user profile for a software developer" --schema user_schema.json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
To set up this plugin locally, first checkout the code. Then create a new virtual environment:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd llm-deepseek-ya
|
|
155
|
+
python3 -m venv venv
|
|
156
|
+
source venv/bin/activate
|
|
157
|
+
```
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# llm-deepseek-ya
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/llm-deepseek-ya/0.1.0/)
|
|
4
|
+
[](https://github.com/delijati/llm-deepseek-ya/releases)
|
|
5
|
+
[](https://github.com/delijati/llm-deepseek-ya/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
LLM access to DeepSeek's API
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install this plugin in the same environment as [LLM](https://llm.datasette.io/).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
llm install llm-deepseek-ya
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
First, set an [API key](https://platform.deepseek.com/api_keys) for DeepSeek:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
llm keys set deepseek
|
|
23
|
+
# Paste key here
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run `llm models` to list the models, and `llm models --options` to include a list of their options.
|
|
27
|
+
|
|
28
|
+
### Running Prompts
|
|
29
|
+
|
|
30
|
+
Run prompts like this:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
llm -m deepseek-chat "Describe a futuristic city on Mars"
|
|
34
|
+
llm -m deepseek-chat-completion "The AI began to dream, and in its dreams," -o echo true
|
|
35
|
+
llm -m deepseek-reasoner "Write a Python function to sort a list of numbers"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Note: The DeepSeek Reasoner model only supports the chat endpoint, not the completion endpoint.
|
|
39
|
+
|
|
40
|
+
### DeepSeek Reasoner Model
|
|
41
|
+
|
|
42
|
+
The DeepSeek Reasoner model uses a Chain of Thought (CoT) approach to solve complex problems, showing its reasoning process before providing the final answer.
|
|
43
|
+
|
|
44
|
+
The plugin shows the model's chain of thought reasoning by default in non-streaming mode. The reasoning feature is currently only supported in non-streaming mode.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Normal usage - will show reasoning by default
|
|
48
|
+
llm -m deepseek-reasoner "What is 537 * 943?"
|
|
49
|
+
|
|
50
|
+
# Hide reasoning when you only want the final answer
|
|
51
|
+
llm -m deepseek-reasoner "What is 537 * 943?" -o show_reasoning false
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Features
|
|
55
|
+
|
|
56
|
+
#### Prefill
|
|
57
|
+
|
|
58
|
+
The `prefill` option allows you to provide initial text for the model's response. This is useful for guiding the model's output.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
llm -m deepseek-chat "What are some wild and crazy activities for a holiday party?" -o prefill "Here are some off-the-wall ideas to make your holiday party unforgettable [warning: these may not be suitable for work holiday parties]:"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You can also load prefill text from a file:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Create a file with your prefill text
|
|
70
|
+
echo "Here are some unique holiday party ideas:" > prefill.txt
|
|
71
|
+
|
|
72
|
+
# Use the file path as the prefill value
|
|
73
|
+
llm -m deepseek-chat "What are some fun activities for a holiday party?" -o prefill prefill.txt
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This is especially useful for longer prefill text that would be unwieldy on the command line.
|
|
77
|
+
|
|
78
|
+
#### JSON Response Format
|
|
79
|
+
|
|
80
|
+
The `response_format` option allows you to specify that the model should output its response in JSON format. To ensure the model outputs valid JSON, include the word "json" in the system or user prompt. Optionally, you can provide an example of the desired JSON format to guide the model.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
llm -m deepseek-chat "What are some fun activities for a holiday party?" -o response_format json_object --system "json"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To guide the model further, you can provide an example JSON structure:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
llm -m deepseek-chat "What are some way to tell if a holiday party is fun?" -o response_format json_object --system 'EXAMPLE JSON OUTPUT: {"event": "holiday_party_fun", "success_metric": ["..."]}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### JSON Schema Support
|
|
95
|
+
|
|
96
|
+
DeepSeek Chat models support JSON schema output (via LLM's `--schema` option). When you provide a schema, the plugin automatically enables JSON mode and includes the schema in the system message to guide the model's output.
|
|
97
|
+
|
|
98
|
+
**Important Note:** DeepSeek's API does not validate the output against the schema - it only uses JSON mode. The schema is provided to the model as guidance, so the model will attempt to follow it, but strict validation is not enforced.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
llm -m deepseek-chat "Generate a user profile" --schema '{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}, "email": {"type": "string"}}, "required": ["name", "age"]}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
You can also use LLM's schema file support:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Create a schema file
|
|
110
|
+
cat > user_schema.json << 'EOF'
|
|
111
|
+
{
|
|
112
|
+
"type": "object",
|
|
113
|
+
"properties": {
|
|
114
|
+
"name": {"type": "string"},
|
|
115
|
+
"age": {"type": "number"},
|
|
116
|
+
"email": {"type": "string"},
|
|
117
|
+
"interests": {"type": "array", "items": {"type": "string"}}
|
|
118
|
+
},
|
|
119
|
+
"required": ["name", "age"]
|
|
120
|
+
}
|
|
121
|
+
EOF
|
|
122
|
+
|
|
123
|
+
# Use the schema file
|
|
124
|
+
llm -m deepseek-chat "Generate a user profile for a software developer" --schema user_schema.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
To set up this plugin locally, first checkout the code. Then create a new virtual environment:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd llm-deepseek-ya
|
|
133
|
+
python3 -m venv venv
|
|
134
|
+
source venv/bin/activate
|
|
135
|
+
```
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import llm
|
|
2
|
+
from llm.default_plugins.openai_models import Chat
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import httpx
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
|
|
11
|
+
# Constants for cache timeout and API base URL
|
|
12
|
+
CACHE_TIMEOUT = 3600
|
|
13
|
+
DEEPSEEK_API_BASE = "https://api.deepseek.com/beta" # For inference
|
|
14
|
+
DEEPSEEK_MODELS_URL = "https://api.deepseek.com/models" # For listing models
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_deepseek_models():
|
|
18
|
+
"""Fetch and cache DeepSeek models."""
|
|
19
|
+
key = llm.get_key("", "deepseek", "LLM_DEEPSEEK_KEY")
|
|
20
|
+
headers = {"Authorization": f"Bearer {key}"} if key else None
|
|
21
|
+
ret = fetch_cached_json(
|
|
22
|
+
url=DEEPSEEK_MODELS_URL,
|
|
23
|
+
path=llm.user_dir() / "deepseek_models.json",
|
|
24
|
+
cache_timeout=CACHE_TIMEOUT,
|
|
25
|
+
headers=headers,
|
|
26
|
+
)["data"]
|
|
27
|
+
return ret
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_model_ids_with_aliases(models):
|
|
31
|
+
"""Extract model IDs and create empty aliases list."""
|
|
32
|
+
return [(model["id"], []) for model in models]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DeepSeekChat(Chat):
|
|
36
|
+
needs_key = "deepseek"
|
|
37
|
+
key_env_var = "LLM_DEEPSEEK_KEY"
|
|
38
|
+
|
|
39
|
+
def __init__(self, model_id, **kwargs):
|
|
40
|
+
kwargs.setdefault("supports_schema", True)
|
|
41
|
+
super().__init__(model_id, **kwargs)
|
|
42
|
+
self.api_base = DEEPSEEK_API_BASE
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return f"DeepSeek Chat: {self.model_id}"
|
|
46
|
+
|
|
47
|
+
class Options(Chat.Options):
|
|
48
|
+
prefill: Optional[str] = Field(
|
|
49
|
+
description="Initial text for the model's response (beta feature). Uses DeepSeek's Chat Prefix Completion.",
|
|
50
|
+
default=None,
|
|
51
|
+
)
|
|
52
|
+
response_format: Optional[str] = Field(
|
|
53
|
+
description="Format of the response (e.g., 'json_object').", default=None
|
|
54
|
+
)
|
|
55
|
+
show_reasoning: Optional[bool] = Field(
|
|
56
|
+
description="Show the chain of thought reasoning for the DeepSeek Reasoner model.",
|
|
57
|
+
default=True,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def execute(self, prompt, stream, response, conversation, key=None):
|
|
61
|
+
messages = self._build_messages(conversation, prompt)
|
|
62
|
+
response._prompt_json = {"messages": messages}
|
|
63
|
+
kwargs = self.build_kwargs(prompt, stream)
|
|
64
|
+
|
|
65
|
+
max_tokens = kwargs.pop("max_tokens", 8192)
|
|
66
|
+
|
|
67
|
+
# Enable JSON mode if schema is provided or response_format is set
|
|
68
|
+
if prompt.schema or prompt.options.response_format:
|
|
69
|
+
kwargs["response_format"] = {"type": "json_object"}
|
|
70
|
+
|
|
71
|
+
# If schema is provided, add it to the system message as guidance
|
|
72
|
+
# Note: DeepSeek doesn't validate against the schema, but the model can follow it
|
|
73
|
+
if prompt.schema:
|
|
74
|
+
schema_instruction = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(prompt.schema, indent=2)}"
|
|
75
|
+
# Add schema to system message or create one if it doesn't exist
|
|
76
|
+
if messages and messages[0].get("role") == "system":
|
|
77
|
+
messages[0]["content"] += schema_instruction
|
|
78
|
+
else:
|
|
79
|
+
messages.insert(
|
|
80
|
+
0,
|
|
81
|
+
{
|
|
82
|
+
"role": "system",
|
|
83
|
+
"content": f"You are a helpful assistant.{schema_instruction}",
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Remove options that aren't supported by the OpenAI client
|
|
88
|
+
kwargs.pop("prefill", None)
|
|
89
|
+
kwargs.pop("show_reasoning", None)
|
|
90
|
+
show_reasoning = prompt.options.show_reasoning
|
|
91
|
+
|
|
92
|
+
client = self.get_client(key)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
completion = client.chat.completions.create(
|
|
96
|
+
model=self.model_name,
|
|
97
|
+
messages=messages,
|
|
98
|
+
stream=stream,
|
|
99
|
+
max_tokens=max_tokens,
|
|
100
|
+
**kwargs,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if stream:
|
|
104
|
+
for chunk in completion:
|
|
105
|
+
# Stream both reasoning content and regular content directly
|
|
106
|
+
content = chunk.choices[0].delta.content
|
|
107
|
+
reasoning_content = getattr(
|
|
108
|
+
chunk.choices[0].delta, "reasoning_content", None
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if reasoning_content is not None and show_reasoning:
|
|
112
|
+
yield reasoning_content
|
|
113
|
+
|
|
114
|
+
if content is not None:
|
|
115
|
+
yield content
|
|
116
|
+
else:
|
|
117
|
+
# For non-streaming response
|
|
118
|
+
content = completion.choices[0].message.content
|
|
119
|
+
# If we have reasoning content and want to show it
|
|
120
|
+
if show_reasoning and hasattr(
|
|
121
|
+
completion.choices[0].message, "reasoning_content"
|
|
122
|
+
):
|
|
123
|
+
reasoning = completion.choices[0].message.reasoning_content
|
|
124
|
+
if reasoning:
|
|
125
|
+
yield reasoning
|
|
126
|
+
yield "\n\n"
|
|
127
|
+
# Then output the regular content
|
|
128
|
+
yield content
|
|
129
|
+
|
|
130
|
+
response.response_json = {"content": "".join(response._chunks)}
|
|
131
|
+
|
|
132
|
+
# Store reasoning_content in response if available
|
|
133
|
+
if not stream and hasattr(
|
|
134
|
+
completion.choices[0].message, "reasoning_content"
|
|
135
|
+
):
|
|
136
|
+
response.response_json["reasoning_content"] = completion.choices[
|
|
137
|
+
0
|
|
138
|
+
].message.reasoning_content
|
|
139
|
+
|
|
140
|
+
except httpx.HTTPError as e:
|
|
141
|
+
raise llm.ModelError(f"DeepSeek API error: {str(e)}")
|
|
142
|
+
|
|
143
|
+
def _build_messages(self, conversation, prompt):
|
|
144
|
+
"""Build the messages list for the API call."""
|
|
145
|
+
messages = []
|
|
146
|
+
if conversation:
|
|
147
|
+
for prev_response in conversation.responses:
|
|
148
|
+
messages.append(
|
|
149
|
+
{"role": "user", "content": prev_response.prompt.prompt}
|
|
150
|
+
)
|
|
151
|
+
messages.append({"role": "assistant", "content": prev_response.text()})
|
|
152
|
+
|
|
153
|
+
# Add system message if provided
|
|
154
|
+
if prompt.system:
|
|
155
|
+
messages.append({"role": "system", "content": prompt.system})
|
|
156
|
+
|
|
157
|
+
messages.append({"role": "user", "content": prompt.prompt})
|
|
158
|
+
|
|
159
|
+
if prompt.options.prefill:
|
|
160
|
+
prefill_content = prompt.options.prefill
|
|
161
|
+
# Check if prefill value is a file path
|
|
162
|
+
if os.path.exists(prefill_content) and os.path.isfile(prefill_content):
|
|
163
|
+
try:
|
|
164
|
+
with open(prefill_content, "r") as file:
|
|
165
|
+
prefill_content = file.read()
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(
|
|
168
|
+
f"Warning: Could not read prefill file '{prompt.options.prefill}': {e}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
messages.append(
|
|
172
|
+
{"role": "assistant", "content": prefill_content, "prefix": True}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return messages
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class DownloadError(Exception):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def fetch_cached_json(url, path, cache_timeout, headers=None):
|
|
183
|
+
"""Fetch JSON data from a URL and cache it."""
|
|
184
|
+
path = Path(path)
|
|
185
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
if path.is_file() and time.time() - path.stat().st_mtime < cache_timeout:
|
|
188
|
+
with open(path, "r") as file:
|
|
189
|
+
return json.load(file)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
response = httpx.get(url, headers=headers, follow_redirects=True)
|
|
193
|
+
response.raise_for_status()
|
|
194
|
+
with open(path, "w") as file:
|
|
195
|
+
json.dump(response.json(), file)
|
|
196
|
+
return response.json()
|
|
197
|
+
except httpx.HTTPError:
|
|
198
|
+
if path.is_file():
|
|
199
|
+
with open(path, "r") as file:
|
|
200
|
+
return json.load(file)
|
|
201
|
+
else:
|
|
202
|
+
raise DownloadError(
|
|
203
|
+
f"Failed to download data and no cache is available at {path}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@llm.hookimpl
|
|
208
|
+
def register_models(register):
|
|
209
|
+
key = llm.get_key("", "deepseek", "LLM_DEEPSEEK_KEY")
|
|
210
|
+
if not key:
|
|
211
|
+
return
|
|
212
|
+
try:
|
|
213
|
+
models = get_deepseek_models()
|
|
214
|
+
models_with_aliases = get_model_ids_with_aliases(models)
|
|
215
|
+
|
|
216
|
+
# Register all Chat models
|
|
217
|
+
for model_id, aliases in models_with_aliases:
|
|
218
|
+
register(
|
|
219
|
+
DeepSeekChat(
|
|
220
|
+
model_id=f"deepseek/{model_id}",
|
|
221
|
+
model_name=model_id,
|
|
222
|
+
),
|
|
223
|
+
aliases=[model_id],
|
|
224
|
+
)
|
|
225
|
+
except DownloadError as e:
|
|
226
|
+
print(f"Error fetching DeepSeek models: {e}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@llm.hookimpl
|
|
230
|
+
def register_commands(cli):
|
|
231
|
+
@cli.command()
|
|
232
|
+
def deepseek_models():
|
|
233
|
+
"""List available DeepSeek models."""
|
|
234
|
+
key = llm.get_key("", "deepseek", "LLM_DEEPSEEK_KEY")
|
|
235
|
+
if not key:
|
|
236
|
+
print("DeepSeek API key not set. Use 'llm keys set deepseek' to set it.")
|
|
237
|
+
return
|
|
238
|
+
try:
|
|
239
|
+
models = get_deepseek_models()
|
|
240
|
+
models_with_aliases = get_model_ids_with_aliases(models)
|
|
241
|
+
|
|
242
|
+
# Display all Chat models
|
|
243
|
+
for model_id, aliases in models_with_aliases:
|
|
244
|
+
print(f"DeepSeek Chat: deepseek/{model_id}")
|
|
245
|
+
print(f" Aliases: {model_id}")
|
|
246
|
+
print()
|
|
247
|
+
except DownloadError as e:
|
|
248
|
+
print(f"Error fetching DeepSeek models: {e}")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-deepseek-ya
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Yet another LLM access to DeepSeek's API
|
|
5
|
+
Author: Josip Delic
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/delijati/llm-deepseek-ya
|
|
8
|
+
Project-URL: Issues, https://github.com/delijati/llm-deepseek-ya/issues
|
|
9
|
+
Project-URL: CI, https://github.com/delijati/llm-deepseek-ya/actions
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: llm
|
|
14
|
+
Requires-Dist: httpx
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-httpx; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-recording; extra == "test"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# llm-deepseek-ya
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/llm-deepseek-ya/0.1.0/)
|
|
26
|
+
[](https://github.com/delijati/llm-deepseek-ya/releases)
|
|
27
|
+
[](https://github.com/delijati/llm-deepseek-ya/blob/main/LICENSE)
|
|
28
|
+
|
|
29
|
+
LLM access to DeepSeek's API
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install this plugin in the same environment as [LLM](https://llm.datasette.io/).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
llm install llm-deepseek-ya
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
First, set an [API key](https://platform.deepseek.com/api_keys) for DeepSeek:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
llm keys set deepseek
|
|
45
|
+
# Paste key here
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `llm models` to list the models, and `llm models --options` to include a list of their options.
|
|
49
|
+
|
|
50
|
+
### Running Prompts
|
|
51
|
+
|
|
52
|
+
Run prompts like this:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
llm -m deepseek-chat "Describe a futuristic city on Mars"
|
|
56
|
+
llm -m deepseek-chat-completion "The AI began to dream, and in its dreams," -o echo true
|
|
57
|
+
llm -m deepseek-reasoner "Write a Python function to sort a list of numbers"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Note: The DeepSeek Reasoner model only supports the chat endpoint, not the completion endpoint.
|
|
61
|
+
|
|
62
|
+
### DeepSeek Reasoner Model
|
|
63
|
+
|
|
64
|
+
The DeepSeek Reasoner model uses a Chain of Thought (CoT) approach to solve complex problems, showing its reasoning process before providing the final answer.
|
|
65
|
+
|
|
66
|
+
The plugin shows the model's chain of thought reasoning by default in non-streaming mode. The reasoning feature is currently only supported in non-streaming mode.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Normal usage - will show reasoning by default
|
|
70
|
+
llm -m deepseek-reasoner "What is 537 * 943?"
|
|
71
|
+
|
|
72
|
+
# Hide reasoning when you only want the final answer
|
|
73
|
+
llm -m deepseek-reasoner "What is 537 * 943?" -o show_reasoning false
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Features
|
|
77
|
+
|
|
78
|
+
#### Prefill
|
|
79
|
+
|
|
80
|
+
The `prefill` option allows you to provide initial text for the model's response. This is useful for guiding the model's output.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
llm -m deepseek-chat "What are some wild and crazy activities for a holiday party?" -o prefill "Here are some off-the-wall ideas to make your holiday party unforgettable [warning: these may not be suitable for work holiday parties]:"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You can also load prefill text from a file:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Create a file with your prefill text
|
|
92
|
+
echo "Here are some unique holiday party ideas:" > prefill.txt
|
|
93
|
+
|
|
94
|
+
# Use the file path as the prefill value
|
|
95
|
+
llm -m deepseek-chat "What are some fun activities for a holiday party?" -o prefill prefill.txt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This is especially useful for longer prefill text that would be unwieldy on the command line.
|
|
99
|
+
|
|
100
|
+
#### JSON Response Format
|
|
101
|
+
|
|
102
|
+
The `response_format` option allows you to specify that the model should output its response in JSON format. To ensure the model outputs valid JSON, include the word "json" in the system or user prompt. Optionally, you can provide an example of the desired JSON format to guide the model.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
llm -m deepseek-chat "What are some fun activities for a holiday party?" -o response_format json_object --system "json"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To guide the model further, you can provide an example JSON structure:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
llm -m deepseek-chat "What are some way to tell if a holiday party is fun?" -o response_format json_object --system 'EXAMPLE JSON OUTPUT: {"event": "holiday_party_fun", "success_metric": ["..."]}'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### JSON Schema Support
|
|
117
|
+
|
|
118
|
+
DeepSeek Chat models support JSON schema output (via LLM's `--schema` option). When you provide a schema, the plugin automatically enables JSON mode and includes the schema in the system message to guide the model's output.
|
|
119
|
+
|
|
120
|
+
**Important Note:** DeepSeek's API does not validate the output against the schema - it only uses JSON mode. The schema is provided to the model as guidance, so the model will attempt to follow it, but strict validation is not enforced.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
llm -m deepseek-chat "Generate a user profile" --schema '{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "number"}, "email": {"type": "string"}}, "required": ["name", "age"]}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
You can also use LLM's schema file support:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Create a schema file
|
|
132
|
+
cat > user_schema.json << 'EOF'
|
|
133
|
+
{
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"name": {"type": "string"},
|
|
137
|
+
"age": {"type": "number"},
|
|
138
|
+
"email": {"type": "string"},
|
|
139
|
+
"interests": {"type": "array", "items": {"type": "string"}}
|
|
140
|
+
},
|
|
141
|
+
"required": ["name", "age"]
|
|
142
|
+
}
|
|
143
|
+
EOF
|
|
144
|
+
|
|
145
|
+
# Use the schema file
|
|
146
|
+
llm -m deepseek-chat "Generate a user profile for a software developer" --schema user_schema.json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
To set up this plugin locally, first checkout the code. Then create a new virtual environment:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd llm-deepseek-ya
|
|
155
|
+
python3 -m venv venv
|
|
156
|
+
source venv/bin/activate
|
|
157
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
llm_deepseek.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
llm_deepseek_ya.egg-info/PKG-INFO
|
|
6
|
+
llm_deepseek_ya.egg-info/SOURCES.txt
|
|
7
|
+
llm_deepseek_ya.egg-info/dependency_links.txt
|
|
8
|
+
llm_deepseek_ya.egg-info/entry_points.txt
|
|
9
|
+
llm_deepseek_ya.egg-info/requires.txt
|
|
10
|
+
llm_deepseek_ya.egg-info/top_level.txt
|
|
11
|
+
tests/test_deepseek.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
llm_deepseek
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "llm-deepseek-ya"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Yet another LLM access to DeepSeek's API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = ["llm", "httpx"]
|
|
9
|
+
authors = [{ name = "Josip Delic" }]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://github.com/delijati/llm-deepseek-ya"
|
|
13
|
+
Issues = "https://github.com/delijati/llm-deepseek-ya/issues"
|
|
14
|
+
CI = "https://github.com/delijati/llm-deepseek-ya/actions"
|
|
15
|
+
|
|
16
|
+
[project.entry-points.llm]
|
|
17
|
+
deepseek = "llm_deepseek"
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
test = ["pytest", "pytest-httpx", "pytest-recording"]
|
|
21
|
+
dev = ["ruff"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
py-modules = ["llm_deepseek"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
# Same as flake8 max-line-length
|
|
31
|
+
line-length = 88
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
# E501 - line too long (handled by formatter)
|
|
35
|
+
# E203 - whitespace before ':' (conflicts with black)
|
|
36
|
+
ignore = ["E501", "E203"]
|
|
37
|
+
|
|
38
|
+
# Enable pycodestyle (E, W), pyflakes (F), and mccabe (C) rules
|
|
39
|
+
select = ["E", "W", "F", "C90"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint.mccabe]
|
|
42
|
+
# Equivalent to flake8's max-complexity
|
|
43
|
+
max-complexity = 15
|
|
44
|
+
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from click.testing import CliRunner
|
|
2
|
+
import llm
|
|
3
|
+
from llm.cli import cli
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import pytest
|
|
7
|
+
import pydantic
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
DEEPSEEK_API_KEY = os.environ.get("PYTEST_DEEPSEEK_API_KEY", None) or "sk-..."
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.vcr
|
|
15
|
+
def test_prompt():
|
|
16
|
+
"""Test basic prompt with DeepSeek Chat model"""
|
|
17
|
+
model = llm.get_model("deepseek-chat")
|
|
18
|
+
response = model.prompt(
|
|
19
|
+
"Name for a pet pelican, just the name", key=DEEPSEEK_API_KEY
|
|
20
|
+
)
|
|
21
|
+
text = str(response).strip()
|
|
22
|
+
# DeepSeek should return a response
|
|
23
|
+
assert len(text) > 0
|
|
24
|
+
# DeepSeek should return a response
|
|
25
|
+
assert "content" in response.response_json
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.vcr
|
|
29
|
+
def test_prompt_with_pydantic_schema():
|
|
30
|
+
"""Test prompt with Pydantic schema - DeepSeek will use JSON mode"""
|
|
31
|
+
|
|
32
|
+
class Dog(pydantic.BaseModel):
|
|
33
|
+
name: str
|
|
34
|
+
age: int
|
|
35
|
+
bio: str
|
|
36
|
+
|
|
37
|
+
model = llm.get_model("deepseek-chat")
|
|
38
|
+
response = model.prompt(
|
|
39
|
+
"Invent a cool dog", key=DEEPSEEK_API_KEY, schema=Dog, stream=False
|
|
40
|
+
)
|
|
41
|
+
result = json.loads(response.text())
|
|
42
|
+
|
|
43
|
+
# Verify the response has the required fields
|
|
44
|
+
assert "name" in result
|
|
45
|
+
assert "age" in result
|
|
46
|
+
assert "bio" in result
|
|
47
|
+
assert isinstance(result["name"], str)
|
|
48
|
+
assert isinstance(result["age"], int)
|
|
49
|
+
assert isinstance(result["bio"], str)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.vcr
|
|
53
|
+
def test_prompt_with_multiple_dogs():
|
|
54
|
+
"""Test prompt with nested Pydantic schema"""
|
|
55
|
+
|
|
56
|
+
class Dog(pydantic.BaseModel):
|
|
57
|
+
name: str
|
|
58
|
+
age: int
|
|
59
|
+
bio: str
|
|
60
|
+
|
|
61
|
+
class Dogs(BaseModel):
|
|
62
|
+
dogs: List[Dog]
|
|
63
|
+
|
|
64
|
+
model = llm.get_model("deepseek-chat")
|
|
65
|
+
response = model.prompt(
|
|
66
|
+
"Invent 3 cool dogs", key=DEEPSEEK_API_KEY, schema=Dogs, stream=False
|
|
67
|
+
)
|
|
68
|
+
result = json.loads(response.text())
|
|
69
|
+
|
|
70
|
+
# Verify we got 3 dogs
|
|
71
|
+
assert "dogs" in result
|
|
72
|
+
assert len(result["dogs"]) == 3
|
|
73
|
+
|
|
74
|
+
# Verify each dog has the required fields
|
|
75
|
+
for dog in result["dogs"]:
|
|
76
|
+
assert "name" in dog
|
|
77
|
+
assert "age" in dog
|
|
78
|
+
assert "bio" in dog
|
|
79
|
+
assert isinstance(dog["name"], str)
|
|
80
|
+
assert isinstance(dog["age"], int)
|
|
81
|
+
assert isinstance(dog["bio"], str)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.vcr
|
|
85
|
+
def test_json_response_format():
|
|
86
|
+
"""Test the response_format option"""
|
|
87
|
+
model = llm.get_model("deepseek-chat")
|
|
88
|
+
response = model.prompt(
|
|
89
|
+
"Return a JSON object with keys: name, color, and species for a parrot",
|
|
90
|
+
key=DEEPSEEK_API_KEY,
|
|
91
|
+
response_format="json_object",
|
|
92
|
+
stream=False,
|
|
93
|
+
)
|
|
94
|
+
result = json.loads(response.text())
|
|
95
|
+
|
|
96
|
+
# Should be valid JSON
|
|
97
|
+
assert isinstance(result, dict)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.vcr
|
|
101
|
+
def test_reasoner_model():
|
|
102
|
+
"""Test DeepSeek Reasoner model with reasoning output"""
|
|
103
|
+
model = llm.get_model("deepseek-reasoner")
|
|
104
|
+
response = model.prompt(
|
|
105
|
+
"What is 537 * 943?", key=DEEPSEEK_API_KEY, show_reasoning=True, stream=False
|
|
106
|
+
)
|
|
107
|
+
text = response.text()
|
|
108
|
+
|
|
109
|
+
# Should contain a numeric answer
|
|
110
|
+
assert "506" in text or "943" in text or "537" in text
|
|
111
|
+
|
|
112
|
+
# Check response_json contains reasoning_content if available
|
|
113
|
+
if response.response_json and "reasoning_content" in response.response_json:
|
|
114
|
+
assert isinstance(response.response_json["reasoning_content"], str)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.vcr
|
|
118
|
+
def test_reasoner_model_hide_reasoning():
|
|
119
|
+
"""Test DeepSeek Reasoner model with reasoning hidden"""
|
|
120
|
+
model = llm.get_model("deepseek-reasoner")
|
|
121
|
+
response = model.prompt(
|
|
122
|
+
"What is 2 + 2?", key=DEEPSEEK_API_KEY, show_reasoning=False, stream=False
|
|
123
|
+
)
|
|
124
|
+
text = response.text()
|
|
125
|
+
|
|
126
|
+
# Should contain the answer
|
|
127
|
+
assert "4" in text
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.vcr
|
|
131
|
+
def test_prefill_option():
|
|
132
|
+
"""Test the prefill option for Chat Prefix Completion"""
|
|
133
|
+
model = llm.get_model("deepseek-chat")
|
|
134
|
+
response = model.prompt(
|
|
135
|
+
"Continue this story",
|
|
136
|
+
key=DEEPSEEK_API_KEY,
|
|
137
|
+
prefill="Once upon a time",
|
|
138
|
+
stream=False,
|
|
139
|
+
)
|
|
140
|
+
text = response.text()
|
|
141
|
+
|
|
142
|
+
# Should have generated some text
|
|
143
|
+
assert len(text) > 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.vcr
|
|
147
|
+
def test_nested_model_direct_reference():
|
|
148
|
+
"""Test nested Pydantic models with DeepSeek"""
|
|
149
|
+
|
|
150
|
+
class Address(BaseModel):
|
|
151
|
+
street: str
|
|
152
|
+
city: str
|
|
153
|
+
|
|
154
|
+
class Person(BaseModel):
|
|
155
|
+
name: str
|
|
156
|
+
address: Address
|
|
157
|
+
|
|
158
|
+
model = llm.get_model("deepseek-chat")
|
|
159
|
+
response = model.prompt(
|
|
160
|
+
"Create a person named Alice living in San Francisco",
|
|
161
|
+
key=DEEPSEEK_API_KEY,
|
|
162
|
+
schema=Person,
|
|
163
|
+
stream=False,
|
|
164
|
+
)
|
|
165
|
+
result = json.loads(response.text())
|
|
166
|
+
assert "name" in result
|
|
167
|
+
assert "address" in result
|
|
168
|
+
assert "street" in result["address"]
|
|
169
|
+
assert "city" in result["address"]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@pytest.mark.vcr
|
|
173
|
+
def test_nested_model_optional():
|
|
174
|
+
"""Test optional nested Pydantic models"""
|
|
175
|
+
|
|
176
|
+
class Company(BaseModel):
|
|
177
|
+
company_name: str
|
|
178
|
+
|
|
179
|
+
class Person(BaseModel):
|
|
180
|
+
name: str
|
|
181
|
+
employer: Optional[Company]
|
|
182
|
+
|
|
183
|
+
model = llm.get_model("deepseek-chat")
|
|
184
|
+
response = model.prompt(
|
|
185
|
+
"Create a person named Bob who works at TechCorp",
|
|
186
|
+
key=DEEPSEEK_API_KEY,
|
|
187
|
+
schema=Person,
|
|
188
|
+
stream=False,
|
|
189
|
+
)
|
|
190
|
+
result = json.loads(response.text())
|
|
191
|
+
assert "name" in result
|
|
192
|
+
assert "employer" in result
|
|
193
|
+
if result["employer"] is not None:
|
|
194
|
+
assert "company_name" in result["employer"]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.vcr
|
|
198
|
+
def test_nested_model_deep_composition():
|
|
199
|
+
"""Test deeply nested Pydantic models"""
|
|
200
|
+
|
|
201
|
+
class Item(BaseModel):
|
|
202
|
+
product_name: str
|
|
203
|
+
quantity: int
|
|
204
|
+
|
|
205
|
+
class Order(BaseModel):
|
|
206
|
+
items: List[Item]
|
|
207
|
+
|
|
208
|
+
class Customer(BaseModel):
|
|
209
|
+
name: str
|
|
210
|
+
orders: List[Order]
|
|
211
|
+
|
|
212
|
+
model = llm.get_model("deepseek-chat")
|
|
213
|
+
response = model.prompt(
|
|
214
|
+
"Create a customer named Carol with 2 orders, each containing 2 items",
|
|
215
|
+
key=DEEPSEEK_API_KEY,
|
|
216
|
+
schema=Customer,
|
|
217
|
+
stream=False,
|
|
218
|
+
)
|
|
219
|
+
result = json.loads(response.text())
|
|
220
|
+
assert "name" in result
|
|
221
|
+
assert "orders" in result
|
|
222
|
+
assert len(result["orders"]) > 0
|
|
223
|
+
for order in result["orders"]:
|
|
224
|
+
assert "items" in order
|
|
225
|
+
assert len(order["items"]) > 0
|
|
226
|
+
for item in order["items"]:
|
|
227
|
+
assert "product_name" in item
|
|
228
|
+
assert "quantity" in item
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@pytest.mark.vcr
|
|
232
|
+
def test_cli_deepseek_models(tmpdir, monkeypatch):
|
|
233
|
+
"""Test the deepseek-models CLI command"""
|
|
234
|
+
user_dir = tmpdir / "llm.datasette.io"
|
|
235
|
+
user_dir.mkdir()
|
|
236
|
+
monkeypatch.setenv("LLM_USER_PATH", str(user_dir))
|
|
237
|
+
|
|
238
|
+
# With no key set should show error message
|
|
239
|
+
runner = CliRunner()
|
|
240
|
+
result = runner.invoke(cli, ["deepseek-models"])
|
|
241
|
+
assert result.exit_code == 0
|
|
242
|
+
assert "DeepSeek API key not set" in result.output or result.exit_code == 0
|
|
243
|
+
|
|
244
|
+
# Try with key set
|
|
245
|
+
monkeypatch.setenv("LLM_DEEPSEEK_KEY", DEEPSEEK_API_KEY)
|
|
246
|
+
result2 = runner.invoke(cli, ["deepseek-models"])
|
|
247
|
+
assert result2.exit_code == 0
|
|
248
|
+
# Should list some models
|
|
249
|
+
assert "deepseek" in result2.output.lower() or "DeepSeek" in result2.output
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest.mark.vcr
|
|
253
|
+
def test_supports_schema_attribute():
|
|
254
|
+
"""Test that DeepSeek Chat models have supports_schema set to True"""
|
|
255
|
+
model = llm.get_model("deepseek-chat")
|
|
256
|
+
assert hasattr(model, "supports_schema")
|
|
257
|
+
assert model.supports_schema is True
|