返回源码地图

packages/llm/llm-deepseek/src/config.ts

main snapshot · da00f7f5358f · 正文引用章节 09;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/** Plugin configuration and complete request-local resolution for DeepSeek. */
2import type { Volatile } from '@deepseek-ai/cordis'
3
4import z from '@deepseek-ai/schemastery'
5import { isVolatile } from '@deepseek-ai/cosmokit'
6import { resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'
7import type { ModelModality, RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
8import type { LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
9import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
10import type { DeepSeekCatalogModel, DeepSeekConnectionOptions } from './types.ts'
11import { DEFAULT_MODELS } from './models.ts'
12import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES, DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM, DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM, DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM, DEFAULT_FILE_EXPIRY_SECONDS, DEFAULT_FILE_REFRESH_MARGIN_SECONDS, DEFAULT_FILE_QUOTA_CLEANUP_BATCH, DEFAULT_FILES_API_TIMEOUT_MS } from './defaults.ts'
13import { DEFAULT_MAX_IMAGES_PER_REQUEST, DEFAULT_MAX_REQUEST_FILES_BYTES, DEFAULT_REQUEST_IMAGE_MAX_BYTES } from './request-pricing.ts'
14
15const MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[]
16
17/** Shared Messages request configuration, without provider credential selection. */
18export interface Config {
19 /** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
20 baseURL: Volatile<string | undefined>
21 /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
22 thinking: Volatile<'enabled' | 'disabled' | undefined>
23 /** Default thinking effort (default `high`); `off` disables thinking per request. */
24 reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
25 /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
26 maxTokens: Volatile<number>
27 /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
28 defaultContextWindow: Volatile<number>
29 /** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
30 models: Volatile<DeepSeekCatalogModel[]>
31 /** Maximum provider idle time while one stream read is outstanding (default five minutes). */
32 streamIdleTimeoutMs: Volatile<number>
33 /** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
34 maxRequestFilesBytes: Volatile<number>
35 /** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
36 maxInlineRequestImageBytes: Volatile<number>
37 /** Maximum number of represented images per chat request (default 600). */
38 maxImagesPerRequest: Volatile<number>
39 /** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
40 imageOffloadByteQuantum: Volatile<number>
41 /** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
42 inlineImageOffloadByteQuantum: Volatile<number>
43 /** Image-count removal step after the request exceeds its count bound (default 20). */
44 imageOffloadCountQuantum: Volatile<number>
45 /** Maximum duration of one request-image Files API resolution (default one minute). */
46 filesApiTimeoutMs: Volatile<number>
47 /** Explicit lifetime assigned to each uploaded image (default seven days). */
48 fileExpiresAfterSeconds: Volatile<number>
49 /** Remaining lifetime below which an indexed file is replaced (default one hour). */
50 fileRefreshMarginSeconds: Volatile<number>
51 /** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
52 fileQuotaCleanupBatch: Volatile<number>
53 /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
54 retryPolicy: Volatile<RetryPolicyConfig | undefined>
55}
56
57/** Plain options accepted by the provider resolver. */
58export type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? Exclude<T, undefined> : never }
59
60/** Read the current value behind every reference of a validated Config.
61 * @param config Parsed plugin Config.
62 * @returns Plain options for the resolver.
63 */
64export function plainOptions(config: Config): Options {
65 return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, isVolatile(value) ? value.get() : value]))
66}
67
68const catalogModel: z<DeepSeekCatalogModel> = z.object({
69 id: z.string().required(),
70 name: z.string(),
71 description: z.string(),
72 contextWindow: z.number().step(1).min(1),
73 maxTokens: z.number().step(1).min(1),
74 inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(['text']),
75 imagePixelBudget: z.union([z.number().step(1).min(1), 'low']),
76 imageMaxBytes: z.number().step(1).min(1),
77 systemPromptUpdate: z.const('in-history'),
78 toolUpdate: z.union(['in-history', 'addition-only'] as const),
79})
80
81/** Shared schema fields for Messages protocol options. */
82export const deepSeekConfigFields = {
83 baseURL: z.string().volatile(),
84 thinking: z.union(['enabled', 'disabled']).volatile(),
85 reasoningEffort: z.union(['off', 'low', 'high', 'max']).volatile(),
86 maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS).volatile(),
87 defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW).volatile(),
88 models: z.array(catalogModel).default(DEFAULT_MODELS).volatile(),
89 streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS).volatile(),
90 maxRequestFilesBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_FILES_BYTES).volatile(),
91 maxInlineRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES).volatile(),
92 maxImagesPerRequest: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGES_PER_REQUEST).volatile(),
93 imageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM).volatile(),
94 inlineImageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM).volatile(),
95 imageOffloadCountQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM).volatile(),
96 filesApiTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_FILES_API_TIMEOUT_MS).volatile(),
97 fileExpiresAfterSeconds: z.number().step(1).min(3_600).max(2_592_000).default(DEFAULT_FILE_EXPIRY_SECONDS).volatile(),
98 fileRefreshMarginSeconds: z.number().step(1).min(0).default(DEFAULT_FILE_REFRESH_MARGIN_SECONDS).volatile(),
99 fileQuotaCleanupBatch: z.number().step(1).min(1).max(1_000).default(DEFAULT_FILE_QUOTA_CLEANUP_BATCH).volatile(),
100 retryPolicy: RetryPolicySchema.volatile(),
101}
102
103export const Config = z.object(deepSeekConfigFields)
104
105/** Public API default; the internal endpoint comes from $DEEPSEEK_BASE_URL. */
106export const PUBLIC_BASE_URL = 'https://api.deepseek.com/anthropic'
107
108/** Environment variable naming this provider's endpoint, honored only from trusted layers. */
109const BASE_URL_ENV = 'DEEPSEEK_BASE_URL'
110
111/** Complete protocol settings captured for one request operation. */
112export type ResolvedDeepSeekOptions = DeepSeekConnectionOptions
113
114/** Resolve, validate, and detach the advisory model catalog. */
115function resolveModels(models: readonly DeepSeekCatalogModel[] | undefined): DeepSeekCatalogModel[] {
116 const seen = new Set<string>()
117 return (models ?? DEFAULT_MODELS).map((model) => {
118 if (Object.hasOwn(model, 'imageDetail')) {
119 throw new Error('llm-deepseek: catalog model imageDetail is no longer supported; use imagePixelBudget')
120 }
121 if (model.id.length === 0) throw new Error('llm-deepseek: catalog model ids must be non-empty')
122 if (model.name !== undefined && model.name.length === 0) {
123 throw new Error(`llm-deepseek: catalog model "${model.id}" has an empty name`)
124 }
125 if (model.contextWindow !== undefined
126 && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {
127 throw new Error(
128 `llm-deepseek: catalog model "${model.id}" contextWindow must be a positive integer`,
129 )
130 }
131 if (model.maxTokens !== undefined
132 && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {
133 throw new Error(
134 `llm-deepseek: catalog model "${model.id}" maxTokens must be a positive integer`,
135 )
136 }
137 const inputModalities = model.inputModalities ?? ['text']
138 if (inputModalities.length === 0) {
139 throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must not be empty`)
140 }
141 if (inputModalities.some(modality => !MODEL_MODALITIES.includes(modality))) {
142 throw new Error(
143 `llm-deepseek: catalog model "${model.id}" inputModalities must contain only "text" and "image"`,
144 )
145 }
146 if (new Set(inputModalities).size !== inputModalities.length) {
147 throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must not contain duplicates`)
148 }
149 const hasImage = inputModalities.includes('image')
150 if (!hasImage && (model.imagePixelBudget !== undefined || model.imageMaxBytes !== undefined)) {
151 throw new Error(`llm-deepseek: text-only catalog model "${model.id}" cannot declare image request limits`)
152 }
153 if (model.imagePixelBudget !== undefined
154 && model.imagePixelBudget !== 'low'
155 && (!Number.isSafeInteger(model.imagePixelBudget) || model.imagePixelBudget <= 0)) {
156 throw new Error(`llm-deepseek: catalog model "${model.id}" imagePixelBudget must be "low" or a positive safe integer`)
157 }
158 if (model.imageMaxBytes !== undefined
159 && (!Number.isSafeInteger(model.imageMaxBytes) || model.imageMaxBytes <= 0)) {
160 throw new Error(`llm-deepseek: catalog model "${model.id}" imageMaxBytes must be a positive safe integer`)
161 }
162 // Widened: a dynamic config update reaches this check without schema validation.
163 const systemPromptUpdate: string | undefined = model.systemPromptUpdate
164 if (systemPromptUpdate !== undefined && systemPromptUpdate !== 'in-history') {
165 throw new Error(`llm-deepseek: catalog model "${model.id}" systemPromptUpdate must be "in-history" when present`)
166 }
167 const toolUpdate: string | undefined = model.toolUpdate
168 if (toolUpdate !== undefined && toolUpdate !== 'in-history' && toolUpdate !== 'addition-only') {
169 throw new Error(`llm-deepseek: catalog model "${model.id}" toolUpdate must be "in-history" or "addition-only" when present`)
170 }
171 if (seen.has(model.id)) throw new Error(`llm-deepseek: duplicate catalog model "${model.id}"`)
172 seen.add(model.id)
173 return {
174 id: model.id,
175 ...model.name === undefined ? {} : { name: model.name },
176 ...model.description === undefined ? {} : { description: model.description },
177 ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
178 ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
179 ...model.systemPromptUpdate === undefined ? {} : { systemPromptUpdate: model.systemPromptUpdate },
180 ...model.toolUpdate === undefined ? {} : { toolUpdate: model.toolUpdate },
181 inputModalities: [...inputModalities],
182 ...hasImage
183 ? {
184 ...model.imagePixelBudget === undefined ? {} : { imagePixelBudget: model.imagePixelBudget },
185 imageMaxBytes: model.imageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES,
186 }
187 : {},
188 }
189 })
190}
191
192/**
193 * The one explicit resolve step from raw config to validated protocol
194 * settings. Programmatic construction may bypass Schemastery normalization, so
195 * every default and bound is re-judged here — for the composition entry at
196 * load (fail loud) and for each settings snapshot at its first use.
197 * @param config - raw plugin config or resolved settings snapshot.
198 * @param environment - this run's environment layers, or `undefined` outside
199 * the product CLI. Every layer may supply an endpoint: the product trusts the
200 * project it is launched in, so a checkout can point its own agent at the
201 * gateway that checkout is meant to use.
202 * @returns validated protocol settings.
203 */
204export function resolveAdapterOptions(config: Options, environment?: LaunchEnvironmentSnapshot): ResolvedDeepSeekOptions {
205 // Settings updates can reach this resolver without schema validation.
206 if (Object.hasOwn(config, 'protocol')) {
207 throw new Error('llm-deepseek: protocol is not configurable; remove it and use a Messages-compatible baseURL')
208 }
209 if (config.thinking === 'disabled'
210 && config.reasoningEffort !== undefined
211 && config.reasoningEffort !== 'off') {
212 throw new Error('llm-deepseek: only reasoningEffort "off" can be configured when thinking is disabled')
213 }
214 if (config.defaultContextWindow !== undefined
215 && (!Number.isInteger(config.defaultContextWindow) || config.defaultContextWindow <= 0)) {
216 throw new Error('llm-deepseek: defaultContextWindow must be a positive integer')
217 }
218 if (config.maxTokens !== undefined
219 && (!Number.isSafeInteger(config.maxTokens) || config.maxTokens <= 0)) {
220 throw new Error('llm-deepseek: maxTokens must be a positive safe integer')
221 }
222 const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
223 if (!Number.isFinite(streamIdleTimeoutMs)
224 || streamIdleTimeoutMs <= 0
225 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
226 throw new Error(
227 `llm-deepseek: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`,
228 )
229 }
230 const maxRequestFilesBytes = config.maxRequestFilesBytes ?? DEFAULT_MAX_REQUEST_FILES_BYTES
231 if (!Number.isSafeInteger(maxRequestFilesBytes) || maxRequestFilesBytes <= 0) {
232 throw new Error('llm-deepseek: maxRequestFilesBytes must be a positive safe integer')
233 }
234 const maxInlineRequestImageBytes = config.maxInlineRequestImageBytes ?? DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES
235 if (!Number.isSafeInteger(maxInlineRequestImageBytes) || maxInlineRequestImageBytes <= 0) {
236 throw new Error('llm-deepseek: maxInlineRequestImageBytes must be a positive safe integer')
237 }
238 const maxImagesPerRequest = config.maxImagesPerRequest ?? DEFAULT_MAX_IMAGES_PER_REQUEST
239 if (!Number.isSafeInteger(maxImagesPerRequest) || maxImagesPerRequest <= 0) {
240 throw new Error('llm-deepseek: maxImagesPerRequest must be a positive safe integer')
241 }
242 const imageOffloadByteQuantum = config.imageOffloadByteQuantum ?? DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM
243 if (!Number.isSafeInteger(imageOffloadByteQuantum) || imageOffloadByteQuantum <= 0) {
244 throw new Error('llm-deepseek: imageOffloadByteQuantum must be a positive safe integer')
245 }
246 if (imageOffloadByteQuantum > maxRequestFilesBytes) {
247 throw new Error('llm-deepseek: imageOffloadByteQuantum must not exceed maxRequestFilesBytes')
248 }
249 const inlineImageOffloadByteQuantum = config.inlineImageOffloadByteQuantum
250 ?? DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM
251 if (!Number.isSafeInteger(inlineImageOffloadByteQuantum) || inlineImageOffloadByteQuantum <= 0) {
252 throw new Error('llm-deepseek: inlineImageOffloadByteQuantum must be a positive safe integer')
253 }
254 if (inlineImageOffloadByteQuantum > maxInlineRequestImageBytes) {
255 throw new Error('llm-deepseek: inlineImageOffloadByteQuantum must not exceed maxInlineRequestImageBytes')
256 }
257 const imageOffloadCountQuantum = config.imageOffloadCountQuantum ?? DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM
258 if (!Number.isSafeInteger(imageOffloadCountQuantum) || imageOffloadCountQuantum <= 0) {
259 throw new Error('llm-deepseek: imageOffloadCountQuantum must be a positive safe integer')
260 }
261 if (imageOffloadCountQuantum > maxImagesPerRequest) {
262 throw new Error('llm-deepseek: imageOffloadCountQuantum must not exceed maxImagesPerRequest')
263 }
264 const filesApiTimeoutMs = config.filesApiTimeoutMs ?? DEFAULT_FILES_API_TIMEOUT_MS
265 if (!Number.isFinite(filesApiTimeoutMs)
266 || filesApiTimeoutMs <= 0
267 || filesApiTimeoutMs > MAX_TIMER_DELAY_MS) {
268 throw new Error(
269 `llm-deepseek: filesApiTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`,
270 )
271 }
272 const fileExpiresAfterSeconds = config.fileExpiresAfterSeconds ?? DEFAULT_FILE_EXPIRY_SECONDS
273 if (!Number.isSafeInteger(fileExpiresAfterSeconds)
274 || fileExpiresAfterSeconds < 3_600
275 || fileExpiresAfterSeconds > 2_592_000) {
276 throw new Error('llm-deepseek: fileExpiresAfterSeconds must be an integer from 3600 through 2592000')
277 }
278 const fileRefreshMarginSeconds = config.fileRefreshMarginSeconds ?? DEFAULT_FILE_REFRESH_MARGIN_SECONDS
279 if (!Number.isSafeInteger(fileRefreshMarginSeconds)
280 || fileRefreshMarginSeconds < 0
281 || fileRefreshMarginSeconds >= fileExpiresAfterSeconds) {
282 throw new Error('llm-deepseek: fileRefreshMarginSeconds must be a non-negative integer below fileExpiresAfterSeconds')
283 }
284 const fileQuotaCleanupBatch = config.fileQuotaCleanupBatch ?? DEFAULT_FILE_QUOTA_CLEANUP_BATCH
285 if (!Number.isSafeInteger(fileQuotaCleanupBatch)
286 || fileQuotaCleanupBatch < 1
287 || fileQuotaCleanupBatch > 1_000) {
288 throw new Error('llm-deepseek: fileQuotaCleanupBatch must be an integer from 1 through 1000')
289 }
290 const baseURL = config.baseURL ?? environment?.get(BASE_URL_ENV)?.value ?? PUBLIC_BASE_URL
291 const parsed = new URL(baseURL)
292 if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
293 throw new Error('llm-deepseek: Messages baseURL must be an HTTP(S) root without credentials, query, or fragment')
294 }
295 return {
296 baseURL,
297 defaults: {
298 thinking: config.thinking,
299 reasoningEffort: config.reasoningEffort,
300 },
301 maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
302 defaultContextWindow: config.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,
303 models: resolveModels(config.models),
304 streamIdleTimeoutMs,
305 maxRequestFilesBytes,
306 maxInlineRequestImageBytes,
307 maxImagesPerRequest,
308 imageOffloadByteQuantum,
309 inlineImageOffloadByteQuantum,
310 imageOffloadCountQuantum,
311 filesApiTimeoutMs,
312 filePolicy: {
313 expiresAfterSeconds: fileExpiresAfterSeconds,
314 refreshMarginSeconds: fileRefreshMarginSeconds,
315 quotaCleanupBatch: fileQuotaCleanupBatch,
316 },
317 retryPolicy: resolveRetryPolicy(config.retryPolicy, 'llm-deepseek: retryPolicy'),
318 }
319}