@namzu/deepseek 0.1.0

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.
package/src/client.ts ADDED
@@ -0,0 +1,475 @@
1
+ import type {
2
+ ChatCompletionParams,
3
+ ChatCompletionResponse,
4
+ LLMProvider,
5
+ ModelInfo,
6
+ ProviderCapabilities,
7
+ ReasoningBlock,
8
+ StreamChunk,
9
+ ThinkingConfig,
10
+ TokenUsage,
11
+ ToolChoice,
12
+ } from '@namzu/sdk'
13
+ import {
14
+ ProviderRequestError,
15
+ attributionHeaders,
16
+ isCallerAbortError,
17
+ isProviderRequestError,
18
+ providerVendorError,
19
+ toolResultToText,
20
+ } from '@namzu/sdk'
21
+ import OpenAI from 'openai'
22
+ import type {
23
+ ChatCompletionMessageParam,
24
+ ChatCompletionTool,
25
+ ChatCompletionToolChoiceOption,
26
+ } from 'openai/resources/chat/completions'
27
+ import type { DeepSeekConfig } from './types.js'
28
+
29
+ /**
30
+ * No vision, no documents.
31
+ *
32
+ * The wire is OpenAI's Chat Completions, which has `image_url` content parts —
33
+ * but the models behind this endpoint are text-only, and a capability set is a
34
+ * claim about what a call will actually do, not about what the request format
35
+ * can express. Claiming vision here would route an image-bearing run to a
36
+ * driver that sends the image and gets prose about nothing.
37
+ */
38
+ export const DEEPSEEK_CAPABILITIES: ProviderCapabilities = {
39
+ supportsTools: true,
40
+ supportsStreaming: true,
41
+ supportsFunctionCalling: true,
42
+ supportsVision: false,
43
+ supportsDocuments: false,
44
+ }
45
+
46
+ const DEFAULT_BASE_URL = 'https://api.deepseek.com'
47
+
48
+ type DeepSeekFinishReason =
49
+ | 'stop'
50
+ | 'length'
51
+ | 'tool_calls'
52
+ | 'content_filter'
53
+ | 'insufficient_system_resource'
54
+
55
+ function mapFinishReason(
56
+ reason: DeepSeekFinishReason | string | null | undefined,
57
+ ): ChatCompletionResponse['finishReason'] {
58
+ switch (reason) {
59
+ case 'length':
60
+ return 'length'
61
+ case 'tool_calls':
62
+ return 'tool_calls'
63
+ case 'content_filter':
64
+ return 'content_filter'
65
+ default:
66
+ return 'stop'
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Usage, including the two counts this vendor reports that OpenAI's shape does
72
+ * not.
73
+ *
74
+ * `completion_tokens_details.reasoning_tokens` is billed as output and is not
75
+ * separable from it after the fact, so it is surfaced rather than discarded —
76
+ * a thinking-mode run whose reasoning dwarfs its answer looks like an
77
+ * inexplicably expensive short reply otherwise.
78
+ *
79
+ * `prompt_cache_hit_tokens` is the vendor's own name for what
80
+ * `prompt_tokens_details.cached_tokens` also reports. Both are read, the
81
+ * details field first, because the flat one is the older spelling and a
82
+ * gateway may forward only one.
83
+ */
84
+ interface RawDeepSeekUsage {
85
+ prompt_tokens?: number
86
+ completion_tokens?: number
87
+ total_tokens?: number
88
+ prompt_tokens_details?: { cached_tokens?: number }
89
+ prompt_cache_hit_tokens?: number
90
+ completion_tokens_details?: { reasoning_tokens?: number }
91
+ }
92
+
93
+ function parseUsage(raw?: RawDeepSeekUsage | null): TokenUsage {
94
+ if (!raw) {
95
+ return {
96
+ promptTokens: 0,
97
+ completionTokens: 0,
98
+ totalTokens: 0,
99
+ cachedTokens: 0,
100
+ cacheWriteTokens: 0,
101
+ }
102
+ }
103
+ const promptTokens = raw.prompt_tokens ?? 0
104
+ const completionTokens = raw.completion_tokens ?? 0
105
+ const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens
106
+ return {
107
+ promptTokens,
108
+ completionTokens,
109
+ totalTokens: raw.total_tokens ?? promptTokens + completionTokens,
110
+ cachedTokens: raw.prompt_tokens_details?.cached_tokens ?? raw.prompt_cache_hit_tokens ?? 0,
111
+ // The vendor has no write-side cache charge: a cache entry is created as
112
+ // a side effect of a miss, at the ordinary input rate. Reporting a
113
+ // fabricated number here would be worse than the zero.
114
+ cacheWriteTokens: 0,
115
+ ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
116
+ }
117
+ }
118
+
119
+ function formatToolChoice(tc: ToolChoice | undefined): ChatCompletionToolChoiceOption | undefined {
120
+ if (tc === undefined) return undefined
121
+ if (tc === 'auto' || tc === 'none' || tc === 'required') return tc
122
+ if (typeof tc === 'object' && tc.type === 'function') {
123
+ return { type: 'function', function: { name: tc.function.name } }
124
+ }
125
+ return undefined
126
+ }
127
+
128
+ /** The four the vendor accepts in thinking mode and applies none of. */
129
+ const SAMPLING_FIELDS = ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty'] as const
130
+
131
+ /**
132
+ * Is thinking on for this call?
133
+ *
134
+ * **Absent means ON.** That is the vendor's default, not a guess: a request
135
+ * with no `thinking` key comes back carrying `reasoning_content`. Every rule
136
+ * below that turns on "thinking is enabled" has to read it this way, or it
137
+ * applies to explicit callers only and misses the common case entirely.
138
+ */
139
+ export function thinkingEnabled(thinking: ThinkingConfig | undefined): boolean {
140
+ return thinking?.type !== 'disabled'
141
+ }
142
+
143
+ /**
144
+ * Refuse a sampling parameter that thinking mode will discard.
145
+ *
146
+ * Measured against the live API on 2026-08-17 rather than read off the
147
+ * documentation, which says only that these are "not supported": all four are
148
+ * accepted with HTTP 200 and applied to nothing. So a caller who pinned
149
+ * `temperature: 0` for reproducibility gets sampling, no error, and no way to
150
+ * find out — `refuse-do-not-degrade`, exactly.
151
+ *
152
+ * `samplingInThinkingMode: 'ignore'` opts out, for a host that keeps one
153
+ * parameter set across several providers and would rather this one drop them
154
+ * quietly than make it special.
155
+ */
156
+ export function assertSamplingUsable(
157
+ params: ChatCompletionParams,
158
+ mode: 'refuse' | 'ignore',
159
+ ): void {
160
+ if (mode === 'ignore') return
161
+ if (!thinkingEnabled(params.thinking)) return
162
+ const set = SAMPLING_FIELDS.filter((f) => params[f] !== undefined)
163
+ if (set.length === 0) return
164
+ throw new Error(
165
+ `DeepSeekProvider: ${set.join(', ')} ${set.length === 1 ? 'is' : 'are'} ignored in thinking mode, which is on for this call — the vendor accepts them and applies none of them, so the call would silently sample when you asked it not to. Disable thinking for this call (\`thinking: { type: 'disabled' }\`) to have them honoured, drop them, or set \`samplingInThinkingMode: 'ignore'\` on the provider to send them anyway.`,
166
+ )
167
+ }
168
+
169
+ /**
170
+ * Refuse a reasoning effort this wire cannot carry.
171
+ *
172
+ * `thinking.effort` is accepted here and validated against nothing — measured:
173
+ * `effort: 'bogus'` returns 200, and `effort: 'none'` still produces reasoning
174
+ * tokens. Only `thinking.type` is validated (the vendor's own error names its
175
+ * variants: `adaptive`, `enabled`, `disabled`). So an effort sent on this wire
176
+ * is a parameter accepted and discarded, which is the one thing a driver must
177
+ * not do quietly.
178
+ *
179
+ * The vendor's Anthropic-format endpoint does take `reasoning.effort`. A
180
+ * driver built on that wire could honour this; this one is built on Chat
181
+ * Completions and says so.
182
+ */
183
+ export function assertEffortUnsupported(params: ChatCompletionParams): void {
184
+ if (params.effort === undefined) return
185
+ throw new Error(
186
+ `DeepSeekProvider: \`effort\` is not carried by this endpoint. The vendor's Chat Completions wire validates \`thinking.type\` and ignores any effort sent beside it, so passing one through would change nothing and report success. Use \`thinking: { type: 'adaptive' | 'enabled' | 'disabled' }\` to steer this driver.`,
187
+ )
188
+ }
189
+
190
+ /**
191
+ * The reasoning blocks an assistant message carries, in wire form.
192
+ *
193
+ * The vendor's rule: with tool calls in play, an assistant turn's
194
+ * `reasoning_content` **must** be replayed in every later turn of the same
195
+ * flow; without tool calls it is ignored if sent. Replayed unconditionally
196
+ * here, which satisfies both halves and matches what `AssistantMessage.reasoning`
197
+ * already promises — "replayed verbatim and ahead of the text/tool blocks".
198
+ *
199
+ * Measured note, because it matters to anyone debugging this: as of
200
+ * 2026-08-17 omitting the replay does NOT produce the documented 400 on either
201
+ * `deepseek-v4-flash` or `deepseek-v4-pro`. The rule is still followed, because
202
+ * a contract the vendor states and does not currently enforce is a contract
203
+ * that can start being enforced in any release.
204
+ */
205
+ function replayReasoning(reasoning: readonly ReasoningBlock[] | undefined): string | undefined {
206
+ if (!reasoning || reasoning.length === 0) return undefined
207
+ const text = reasoning
208
+ .filter((b) => b.type === 'thinking' && typeof b.text === 'string')
209
+ .map((b) => b.text as string)
210
+ .join('')
211
+ return text.length > 0 ? text : undefined
212
+ }
213
+
214
+ export function toDeepSeekMessages(
215
+ messages: ChatCompletionParams['messages'],
216
+ ): ChatCompletionMessageParam[] {
217
+ return messages.map((msg): ChatCompletionMessageParam => {
218
+ if (msg.role === 'system') return { role: 'system', content: msg.content }
219
+ if (msg.role === 'user') {
220
+ // No attachment branch, deliberately. `DEEPSEEK_CAPABILITIES` says
221
+ // this driver has neither vision nor documents, and a driver that
222
+ // quietly encoded an image the model cannot read would answer about
223
+ // a picture it never saw.
224
+ if (msg.attachments && msg.attachments.length > 0) {
225
+ throw new Error(
226
+ 'DeepSeekProvider: this driver has no vision or document support, and a message carried attachments. Route multimodal turns to a provider whose capabilities include them.',
227
+ )
228
+ }
229
+ return { role: 'user', content: msg.content }
230
+ }
231
+ if (msg.role === 'tool') {
232
+ return {
233
+ role: 'tool',
234
+ content: toolResultToText(msg.content),
235
+ tool_call_id: msg.toolCallId,
236
+ }
237
+ }
238
+ const assistant: ChatCompletionMessageParam = { role: 'assistant', content: msg.content }
239
+ if ('toolCalls' in msg && msg.toolCalls && msg.toolCalls.length > 0) {
240
+ ;(assistant as { tool_calls?: unknown }).tool_calls = msg.toolCalls.map((tc) => ({
241
+ id: tc.id,
242
+ type: 'function' as const,
243
+ function: { name: tc.function.name, arguments: tc.function.arguments },
244
+ }))
245
+ }
246
+ const replayed = replayReasoning((msg as { reasoning?: readonly ReasoningBlock[] }).reasoning)
247
+ if (replayed !== undefined) {
248
+ ;(assistant as { reasoning_content?: string }).reasoning_content = replayed
249
+ }
250
+ return assistant
251
+ })
252
+ }
253
+
254
+ export function toDeepSeekTools(params: ChatCompletionParams): ChatCompletionTool[] | undefined {
255
+ if (!params.tools || params.tools.length === 0) return undefined
256
+ // No `strict` branch: `enforceToolInputSchema` names tools whose schema
257
+ // should be enforced rather than suggested, and this wire has no field for
258
+ // it. Setting `strict: true` here would be accepted and ignored — the same
259
+ // defect `assertEffortUnsupported` refuses above — so the flag is left to
260
+ // the kernel's own post-call Zod validation, which runs either way.
261
+ return params.tools.map((t) => ({
262
+ type: 'function' as const,
263
+ function: {
264
+ name: t.function.name,
265
+ description: t.function.description ?? '',
266
+ parameters: (t.function.parameters ?? {}) as Record<string, unknown>,
267
+ },
268
+ }))
269
+ }
270
+
271
+ export class DeepSeekProvider implements LLMProvider {
272
+ readonly id = 'deepseek'
273
+ readonly name = 'DeepSeek'
274
+ readonly capabilities = DEEPSEEK_CAPABILITIES
275
+
276
+ private client: OpenAI
277
+ private defaultModel?: string
278
+ private samplingMode: 'refuse' | 'ignore'
279
+
280
+ constructor(config: DeepSeekConfig) {
281
+ if (!config.apiKey) {
282
+ throw new Error('DeepSeek API key is required. Set DEEPSEEK_API_KEY env variable.')
283
+ }
284
+ const clientOptions: ConstructorParameters<typeof OpenAI>[0] = {
285
+ apiKey: config.apiKey,
286
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
287
+ }
288
+ if (config.timeout !== undefined) clientOptions.timeout = config.timeout
289
+ clientOptions.defaultHeaders = { ...attributionHeaders(), ...(config.defaultHeaders ?? {}) }
290
+
291
+ this.client = new OpenAI(clientOptions)
292
+ this.defaultModel = config.model
293
+ this.samplingMode = config.samplingInThinkingMode ?? 'refuse'
294
+ }
295
+
296
+ private resolveModel(params: ChatCompletionParams): string {
297
+ const model = params.model || this.defaultModel
298
+ if (!model) {
299
+ throw new Error(
300
+ 'DeepSeekProvider: model is required. Pass `model` in config or on the chat call.',
301
+ )
302
+ }
303
+ return model
304
+ }
305
+
306
+ async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
307
+ const model = this.resolveModel(params)
308
+ assertEffortUnsupported(params)
309
+ assertSamplingUsable(params, this.samplingMode)
310
+
311
+ const sampling = thinkingEnabled(params.thinking)
312
+ ? {}
313
+ : {
314
+ temperature: params.temperature,
315
+ top_p: params.topP,
316
+ frequency_penalty: params.frequencyPenalty,
317
+ presence_penalty: params.presencePenalty,
318
+ }
319
+
320
+ // Built as its own object so `stream: true` keeps its literal type and the
321
+ // SDK's streaming overload is the one selected. Casting the whole
322
+ // argument — the obvious way to smuggle `thinking` past a type that does
323
+ // not know about it — widens `stream` to `boolean` and the call resolves
324
+ // to the non-streaming overload, whose result has no async iterator.
325
+ const body = {
326
+ model,
327
+ messages: toDeepSeekMessages(params.messages),
328
+ stream: true as const,
329
+ stream_options: { include_usage: true },
330
+ tools: toDeepSeekTools(params),
331
+ tool_choice: formatToolChoice(params.toolChoice),
332
+ max_tokens: params.maxTokens,
333
+ stop: params.stop,
334
+ response_format: params.responseFormat,
335
+ ...sampling,
336
+ }
337
+
338
+ // Sent only when the caller said something. Absent means the vendor's own
339
+ // default, which is thinking ON — see `thinkingEnabled`. The three
340
+ // variants are the vendor's, and they are the same three the SDK's
341
+ // `ThinkingConfig` declares, so this is a rename and not a mapping.
342
+ //
343
+ // Cast back to `typeof body` rather than to the SDK's parameter type: the
344
+ // extra key rides along at runtime, and the literal `stream: true`
345
+ // survives, which is what keeps the streaming overload selected.
346
+ const withThinking = (
347
+ params.thinking ? { ...body, thinking: { type: params.thinking.type } } : body
348
+ ) as typeof body
349
+
350
+ let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>
351
+ try {
352
+ stream = await this.client.chat.completions.create(withThinking, {
353
+ signal: params.signal,
354
+ })
355
+ } catch (err) {
356
+ if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
357
+ if (isProviderRequestError(err)) throw err
358
+ throw providerVendorError({ providerId: 'deepseek', error: err })
359
+ }
360
+
361
+ // One reasoning block per turn on this wire: the vendor streams
362
+ // `reasoning_content` as a flat run of deltas with no index and no
363
+ // block boundaries, so index 0 is the whole of it rather than a guess.
364
+ let reasoningOpen = false
365
+
366
+ try {
367
+ for await (const chunk of stream) {
368
+ params.signal?.throwIfAborted()
369
+ try {
370
+ const choice = chunk.choices[0]
371
+ const delta = choice?.delta as
372
+ | { content?: string | null; reasoning_content?: string | null; tool_calls?: unknown[] }
373
+ | undefined
374
+
375
+ const toolCalls = (
376
+ delta?.tool_calls as
377
+ | Array<{
378
+ index: number
379
+ id?: string
380
+ type?: 'function'
381
+ function?: { name?: string; arguments?: string }
382
+ }>
383
+ | undefined
384
+ )?.map((tc) => ({
385
+ index: tc.index,
386
+ id: tc.id,
387
+ type: tc.type,
388
+ function: tc.function
389
+ ? { name: tc.function.name, arguments: tc.function.arguments }
390
+ : undefined,
391
+ }))
392
+
393
+ const reasoningText = delta?.reasoning_content
394
+ const finishReason = choice?.finish_reason
395
+ ? mapFinishReason(choice.finish_reason)
396
+ : undefined
397
+ const usage = chunk.usage ? parseUsage(chunk.usage as RawDeepSeekUsage) : undefined
398
+
399
+ if (reasoningText) {
400
+ reasoningOpen = true
401
+ yield {
402
+ id: chunk.id,
403
+ delta: { reasoning: { index: 0, type: 'thinking', text: reasoningText } },
404
+ }
405
+ }
406
+
407
+ // The block closes when content or a tool call starts, which is
408
+ // the only boundary this wire gives. Without it a consumer's
409
+ // reasoning pane never receives `done` and stays open for the
410
+ // life of the run.
411
+ const contentStarting =
412
+ (delta?.content !== undefined && delta.content !== null) ||
413
+ (toolCalls !== undefined && toolCalls.length > 0)
414
+ if (reasoningOpen && contentStarting) {
415
+ reasoningOpen = false
416
+ yield { id: chunk.id, delta: { reasoning: { index: 0, done: true } } }
417
+ }
418
+
419
+ const hasDelta =
420
+ (delta?.content !== undefined && delta.content !== null) ||
421
+ (toolCalls !== undefined && toolCalls.length > 0)
422
+ if (!hasDelta && !finishReason && !usage) continue
423
+
424
+ yield {
425
+ id: chunk.id,
426
+ delta: { content: delta?.content ?? undefined, toolCalls },
427
+ finishReason,
428
+ usage,
429
+ }
430
+ } catch (parseErr) {
431
+ if (isProviderRequestError(parseErr)) throw parseErr
432
+ throw new ProviderRequestError({
433
+ kind: 'server',
434
+ providerId: 'deepseek',
435
+ detail: 'the provider stream returned malformed data',
436
+ })
437
+ }
438
+ }
439
+ // A turn that was all reasoning and no content — the model thought
440
+ // and then stopped — still has to close its block.
441
+ if (reasoningOpen) {
442
+ yield { id: 'deepseek-reasoning-close', delta: { reasoning: { index: 0, done: true } } }
443
+ }
444
+ } catch (err) {
445
+ if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
446
+ if (isProviderRequestError(err)) throw err
447
+ throw providerVendorError({ providerId: 'deepseek', error: err })
448
+ }
449
+ }
450
+
451
+ async probeCredential(): Promise<void> {
452
+ await this.client.models.list()
453
+ }
454
+
455
+ async listModels(): Promise<ModelInfo[]> {
456
+ const page = await this.client.models.list()
457
+ return page.data.map((m) => ({
458
+ id: m.id,
459
+ name: m.id,
460
+ inputPrice: 0,
461
+ outputPrice: 0,
462
+ supportsToolUse: true,
463
+ supportsStreaming: true,
464
+ }))
465
+ }
466
+
467
+ async healthCheck(): Promise<boolean> {
468
+ try {
469
+ await this.client.models.list()
470
+ return true
471
+ } catch {
472
+ return false
473
+ }
474
+ }
475
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { ProviderRegistry, type RegisterOptions } from '@namzu/sdk'
2
+ import { DEEPSEEK_CAPABILITIES, DeepSeekProvider } from './client.js'
3
+ import type { DeepSeekProviderConfig } from './types.js'
4
+
5
+ // Module augmentation: register deepseek's config type in the sdk's registry
6
+ // interface. This must live inside index.ts (not a .d.ts) so it executes when
7
+ // index.ts is imported.
8
+ declare module '@namzu/sdk' {
9
+ interface ProviderConfigRegistry {
10
+ deepseek: DeepSeekProviderConfig
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Register `DeepSeekProvider` under the `'deepseek'` type in `@namzu/sdk`'s
16
+ * `ProviderRegistry`. Call once at app startup before
17
+ * `ProviderRegistry.create({ type: 'deepseek', ... })`.
18
+ *
19
+ * Throws `DuplicateProviderError` if `'deepseek'` is already registered.
20
+ * Pass `{ replace: true }` to override.
21
+ */
22
+ export function registerDeepSeek(options?: RegisterOptions): void {
23
+ ProviderRegistry.register('deepseek', DeepSeekProvider, DEEPSEEK_CAPABILITIES, options)
24
+ }
25
+
26
+ export {
27
+ DEEPSEEK_CAPABILITIES,
28
+ DeepSeekProvider,
29
+ assertEffortUnsupported,
30
+ assertSamplingUsable,
31
+ thinkingEnabled,
32
+ toDeepSeekMessages,
33
+ toDeepSeekTools,
34
+ } from './client.js'
35
+ export type { DeepSeekConfig, DeepSeekProviderConfig } from './types.js'
package/src/types.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * DeepSeek-specific provider config shapes.
3
+ *
4
+ * `DeepSeekConfig` is the constructor input for `DeepSeekProvider` (no
5
+ * discriminator). `DeepSeekProviderConfig` is the shape the consumer passes to
6
+ * `ProviderRegistry.create({ type: 'deepseek', ... })` — it extends
7
+ * `DeepSeekConfig` with the `type: 'deepseek'` discriminator for the registry's
8
+ * generic narrowing.
9
+ */
10
+
11
+ export interface DeepSeekConfig {
12
+ apiKey: string
13
+ /** Default model. Can be overridden per-call via ChatCompletionParams.model. */
14
+ model?: string
15
+ /**
16
+ * Override the base URL.
17
+ *
18
+ * Defaults to the vendor's own endpoint. Set this for a gateway that
19
+ * fronts DeepSeek — but note that a gateway which strips unknown response
20
+ * fields will drop `reasoning_content`, and this driver's whole reason to
21
+ * exist is that field.
22
+ */
23
+ baseURL?: string
24
+ /** Request timeout in ms. */
25
+ timeout?: number
26
+ /** Custom headers appended to every request. */
27
+ defaultHeaders?: Record<string, string>
28
+ /**
29
+ * What to do when a caller sets a sampling parameter that thinking mode
30
+ * ignores — `temperature`, `topP`, `frequencyPenalty`, `presencePenalty`.
31
+ *
32
+ * The vendor accepts all four in thinking mode and applies none of them:
33
+ * no error, no warning, no effect. Measured against the live API on
34
+ * 2026-08-17, not read off the documentation, which says only that they
35
+ * are "not supported".
36
+ *
37
+ * `'refuse'` (the default) throws, because a caller who set `temperature:
38
+ * 0` and got sampling anyway believes they pinned something they did not
39
+ * — the exact shape of `refuse-do-not-degrade`. `'ignore'` sends them and
40
+ * lets the vendor discard them, for a host that would rather have one
41
+ * knob it can leave set across providers.
42
+ *
43
+ * Thinking is ON by default at the vendor, so this fires more often than
44
+ * you might expect. Disable thinking per call (`thinking: { type:
45
+ * 'disabled' }`) and the parameters are honoured normally.
46
+ */
47
+ samplingInThinkingMode?: 'refuse' | 'ignore'
48
+ }
49
+
50
+ export interface DeepSeekProviderConfig extends DeepSeekConfig {
51
+ type: 'deepseek'
52
+ }