-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdeepseek_reasoning.js
309 lines (271 loc) · 12.4 KB
/
deepseek_reasoning.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
// deepseek_reasoning.js
// TypingMind.com extension
// GitHub Pages link: https://asgeirtj.github.io/js/deepseek_reasoning.js
// Source: https://raw.githubusercontent.com/shaggy2626/typingmind-themes-extensions/refs/heads/master/extensions/deepseek_reasoning/deepseek_reasoning.js
// Source: https://cdn.jsdelivr.net/gh/shaggy2626/typingmind-themes-extensions/extensions/deepseek_reasoning/deepseek_reasoning.js
// Version: 1.0
// Remember to increment version number when making changes
(function() {
/**
* ------------------------------------------------------
* 1. Preserve the original fetch for fallback behavior.
* ------------------------------------------------------
*/
const originalFetch = window.fetch;
/**
* ------------------------------------------------------
* 2. Deepseek API endpoint to intercept.
* ------------------------------------------------------
*/
const DEEPSEEK_API_URL = 'api.deepseek.com/v1/chat/completions';
/**
* ------------------------------------------------------
* Helper Function: Check if a given URL is the Deepseek endpoint.
* @param {string} url - The request URL (args[0] from fetch).
* @returns {boolean} - True if the URL includes the Deepseek API endpoint.
* ------------------------------------------------------
*/
function isDeepseekURL(url) {
return url && url.includes(DEEPSEEK_API_URL);
}
/**
* ------------------------------------------------------
* Helper Function: Check if the request body references a Deepseek model.
* @param {Object} body - Parsed JSON request body.
* @returns {boolean} - True if the model field includes 'deepseek'.
* ------------------------------------------------------
*/
function isDeepseekModel(body) {
return !!body.model && body.model.includes('deepseek');
}
/**
* ------------------------------------------------------
* Helper Function: Check if the request is a title generation request.
* @param {Object} body - Parsed JSON request body.
* @returns {boolean} - True if the last message prompts for a short/relevant title.
* ------------------------------------------------------
*/
function isTitleRequest(body) {
if (!body.messages || body.messages.length === 0) return false;
const lastMessage = body.messages[body.messages.length - 1];
return (
!!lastMessage.content &&
lastMessage.content.startsWith('What would be a short and relevant title for this chat?')
);
}
/**
* ------------------------------------------------------
* Function: Process a streaming text/event-stream response from Deepseek,
* intercepting "reasoning_content" and measuring how long
* the "thinking" phase lasted.
* @param {Response} response - The original response from fetch.
* @returns {Promise<Response>}
* ------------------------------------------------------
*/
async function processStreamingResponse(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
// We'll keep track of partial lines in `buffer` until a newline is encountered.
let buffer = '';
let contentStarted = false;
let reasoningEnded = false;
let lastWasNewline = false;
// Track when reasoning starts so we can measure total time.
let startThinkingTime = null;
// Create a new ReadableStream that we will push modified data into.
const stream = new ReadableStream({
async start(controller) {
const textEncoder = new TextEncoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
// Only parse lines that begin with 'data: '
if (line.startsWith('data: ')) {
// If we get '[DONE]', pass it along and continue
if (line.includes('[DONE]')) {
controller.enqueue(textEncoder.encode(`${line}\n`));
continue;
}
try {
const data = JSON.parse(line.slice(6));
// We look for data.choices[0].delta
if (data?.choices?.[0]?.delta) {
const delta = data.choices[0].delta;
// If we find 'reasoning_content', treat it as "thinking"
if (delta.reasoning_content) {
// If this is the first chunk of reasoning, record a start time
if (!contentStarted) {
startThinkingTime = performance.now();
const thinkingHeader = {
...data,
choices: [{
...data.choices[0],
delta: { content: '💭 Thinking...\n\n> ' }
}]
};
controller.enqueue(
textEncoder.encode(`data: ${JSON.stringify(thinkingHeader)}\n\n`)
);
contentStarted = true;
}
let content = delta.reasoning_content;
// If last chunk ended with newline, prefix this line with '>'
if (lastWasNewline && content.trim()) {
content = `> ${content}`;
}
lastWasNewline = content.endsWith('\n');
// Convert reasoning_content to normal "content"
const modifiedData = {
...data,
choices: [{
...data.choices[0],
delta: { content }
}]
};
controller.enqueue(
textEncoder.encode(`data: ${JSON.stringify(modifiedData)}\n\n`)
);
}
// If normal content arrives and we haven't ended reasoning, show total time
else if (delta.content && !reasoningEnded) {
reasoningEnded = true;
// Calculate how many whole seconds were spent "thinking"
const thinkingDuration = Math.round(
(performance.now() - (startThinkingTime || performance.now())) / 1000
);
// Insert a line showing how long it was "thinking"
const separatorData = {
...data,
choices: [{
...data.choices[0],
delta: {
content: `\n\n💡 Thought for ${thinkingDuration} seconds\n\n---\n\n`
}
}]
};
controller.enqueue(
textEncoder.encode(`data: ${JSON.stringify(separatorData)}\n\n`)
);
// Now send along the actual content line
controller.enqueue(textEncoder.encode(`${line}\n`));
} else {
// If there's no reasoning content or reasoning has ended, just pass it along
controller.enqueue(textEncoder.encode(`${line}\n`));
}
} else {
// If there's no delta object, just pass through
controller.enqueue(textEncoder.encode(`${line}\n`));
}
} catch (parseError) {
console.error('Error parsing streaming data:', parseError);
controller.enqueue(textEncoder.encode(`${line}\n`));
}
} else {
// If line doesn't start with 'data: ', pass it as-is
controller.enqueue(textEncoder.encode(`${line}\n`));
}
}
}
// Once the stream is fully read, close the controller
controller.close();
} catch (error) {
controller.error(error);
}
}
});
// Return a new Response that wraps our transformed stream
return new Response(stream, {
headers: response.headers,
status: response.status,
statusText: response.statusText
});
}
/**
* ------------------------------------------------------
* Function: Process a non-streaming JSON response from Deepseek.
* If we see 'reasoning_content' in the JSON body, prefix each line
* with '>' and insert a separator (---) before the original content.
* @param {Response} response - The original fetch response.
* @returns {Promise<Response>}
* ------------------------------------------------------
*/
async function processNonStreamingResponse(response) {
try {
// Clone the response so we can parse it independently
const cloned = response.clone();
const data = await cloned.json();
// Look for reasoning_content in the first choice
if (data?.choices?.[0]?.message?.reasoning_content) {
const message = data.choices[0].message;
const quotedReasoning = message.reasoning_content
.split('\n')
.map(line => (line.trim() ? `> ${line}` : '>'))
.join('\n');
// Insert reasoning before the main content
message.content = `${quotedReasoning}\n\n---\n\n${message.content}`;
// Return a new Response with the updated JSON
return new Response(JSON.stringify(data), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
} catch (error) {
console.error('Error in Deepseek reasoning extension (non-streaming):', error);
}
// If no modifications are needed, return the original response
return response;
}
/**
* ------------------------------------------------------
* Main override: window.fetch
* Intercepts calls to the Deepseek endpoint and modifies
* reasoning content in streaming/non-streaming responses.
* ------------------------------------------------------
*/
window.fetch = async function(...args) {
try {
const [url, options] = args;
const requestBody = options?.body;
// 1. If not a Deepseek URL, fall back to the original fetch
if (!isDeepseekURL(url)) {
return originalFetch.apply(this, args);
}
// 2. If no request body, we can't parse or modify
if (!requestBody) {
return originalFetch.apply(this, args);
}
// 3. Attempt to parse the request body to identify the model
let parsedBody;
try {
parsedBody = JSON.parse(requestBody);
} catch {
// If parse fails, just continue with the original fetch
return originalFetch.apply(this, args);
}
// 4. If it's not a Deepseek model or it's a title request, do nothing special
if (!isDeepseekModel(parsedBody) || isTitleRequest(parsedBody)) {
return originalFetch.apply(this, args);
}
// 5. Make the actual fetch call to get the response
const response = await originalFetch.apply(this, args);
// 6. Check the content-type for streaming
if (response.headers.get('content-type')?.includes('text/event-stream')) {
// Use our special streaming handler
return processStreamingResponse(response);
}
// 7. Otherwise, handle it as a normal JSON response
return processNonStreamingResponse(response);
} catch (error) {
console.error('Error in fetch interceptor:', error);
// If there's an error, fallback to the original fetch
return originalFetch.apply(this, args);
}
};
console.log('Deepseek reasoning extension loaded (with timing & updated emojis)!');
})();