| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- /**
- * Register a {@link DeepSeekAdapter} for the `deepseek-official` provider route on
- * `ctx.llm`, with connection facts resolved per request instead of frozen at
- * load: the plugin layers its `cordis.yml` entry config under the optional
- * `llm-deepseek` user-settings section (`ctx.settings`) and resolves the API
- * key through the optional credential seam (`ctx.credentials`), so a changed
- * base URL, catalog, or key reaches the very next request without restarting
- * anything, while an in-flight stream keeps the facts it started with. The
- * one registration-captured fact — the retry policy — re-registers the route
- * in place when it changes.
- * @module @deepseek-ai/dsh-llm-deepseek
- */
- import type { Context } from '@deepseek-ai/cordis'
- import z from '@deepseek-ai/schemastery'
- import { assertUsableApiKey, LlmError, resolveImageAttachmentAccess, resolveRetryPolicy, RetryPolicySchema } from '@deepseek-ai/dsh-llm'
- import type { ModelModality, RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
- import type {} from '@deepseek-ai/dsh-fs'
- import { credentialRef } from '@deepseek-ai/dsh-credentials'
- import { launchEnvironmentOf, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
- import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
- import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
- import { getOrCreateAnonymousUserId, type AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
- import {
- DEFAULT_CONTEXT_WINDOW,
- DEFAULT_FILE_EXPIRY_SECONDS,
- DEFAULT_FILE_QUOTA_CLEANUP_BATCH,
- DEFAULT_FILE_REFRESH_MARGIN_SECONDS,
- DEFAULT_FILES_API_TIMEOUT_MS,
- DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM,
- DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM,
- DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM,
- DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET,
- DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES,
- DEFAULT_MAX_IMAGES_PER_REQUEST,
- DEFAULT_MAX_REQUEST_FILES_BYTES,
- DEFAULT_MAX_TOKENS,
- DEFAULT_REQUEST_IMAGE_MAX_BYTES,
- DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
- DEFAULT_STREAM_IDLE_TIMEOUT_MS,
- DeepSeekAdapter,
- } from './adapter.ts'
- import type { DeepSeekCatalogModel, DeepSeekConnectionOptions } from './adapter.ts'
- export {
- DEFAULT_CONTEXT_WINDOW,
- DEFAULT_FILE_EXPIRY_SECONDS,
- DEFAULT_FILE_QUOTA_CLEANUP_BATCH,
- DEFAULT_FILE_REFRESH_MARGIN_SECONDS,
- DEFAULT_FILES_API_TIMEOUT_MS,
- DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM,
- DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM,
- DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM,
- DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET,
- DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES,
- DEFAULT_MAX_IMAGES_PER_REQUEST,
- DEFAULT_MAX_REQUEST_FILES_BYTES,
- DEFAULT_MAX_TOKENS,
- DEFAULT_REQUEST_IMAGE_MAX_BYTES,
- DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
- DEFAULT_STREAM_IDLE_TIMEOUT_MS,
- DeepSeekAdapter,
- } from './adapter.ts'
- export type { DeepSeekAdapterOptions, DeepSeekCatalogModel, DeepSeekConnectionOptions } from './adapter.ts'
- export { DeepSeekFileStore, MAX_CHAT_IMAGE_BYTES } from './file-store.ts'
- export type { DeepSeekFileConnection, DeepSeekFilePolicy, DeepSeekFileReference } from './file-store.ts'
- export { DeepSeekFilesClient, MAX_FILE_EXPIRY_SECONDS, MAX_FILE_UPLOAD_BYTES, MAX_STORED_FILE_BYTES, MAX_STORED_FILE_COUNT, MIN_FILE_EXPIRY_SECONDS } from './files-api.ts'
- export type { DeepSeekFileObject, DeepSeekFilePage } from './files-api.ts'
- export { DeepSeekFileId } from './file-id.ts'
- export type { DeepSeekFileId as DeepSeekFileIdType } from './file-id.ts'
- export { DeepSeekUploadIndex, deepSeekFileScope } from './upload-index.ts'
- export type { DeepSeekUploadRecord } from './upload-index.ts'
- export type { RequestDefaults } from './serialize.ts'
- export type * from './types.ts'
- export const name = 'llm-deepseek'
- export const inject = ['llm']
- const NS = settingsNamespace('llm-deepseek')
- const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY'
- /** The single provider route this plugin owns. */
- const PROVIDER = 'deepseek-official'
- const DEFAULT_MODELS: DeepSeekCatalogModel[] = [
- {
- id: 'deepseek-v4-flash',
- name: 'DeepSeek-V4-Flash',
- description: 'Fast, efficient, and economical; suited to focused, routine, or parallel tasks.',
- contextWindow: DEFAULT_CONTEXT_WINDOW,
- },
- {
- id: 'deepseek-v4-pro',
- name: 'DeepSeek-V4-Pro',
- description: 'Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.',
- contextWindow: DEFAULT_CONTEXT_WINDOW,
- },
- {
- id: 'deepseek-v4-flash-vision-exp',
- name: 'DeepSeek-V4-Flash-Vision-Exp',
- contextWindow: DEFAULT_CONTEXT_WINDOW,
- inputModalities: ['text', 'image'],
- imagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
- imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES,
- },
- ]
- const MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[]
- /**
- * Plugin config, validated by the same-named schemastery schema and doubling
- * as the `llm-deepseek` settings-section shape. Every field is optional in
- * yml: a missing API key resolves through {@link Config.apiKeyEnv} at each
- * request (a request without any key fails with `MISSING_CREDENTIAL`, not at
- * plugin load), omitted thinking mode uses the provider default, and omitted
- * reasoning effort resolves to `high`.
- */
- export interface Config {
- /** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
- apiKeyEnv?: string
- /** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
- baseURL?: string
- /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
- thinking?: 'enabled' | 'disabled'
- /** Default thinking effort (default `high`); `off` disables thinking per request. */
- reasoningEffort?: 'off' | 'low' | 'high' | 'max'
- /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
- maxTokens?: number
- /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
- defaultContextWindow?: number
- /** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
- models?: DeepSeekCatalogModel[]
- /** Maximum provider idle time while one stream read is outstanding (default five minutes). */
- streamIdleTimeoutMs?: number
- /** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
- maxRequestFilesBytes?: number
- /** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
- maxInlineRequestImageBytes?: number
- /** Maximum number of represented images per chat request (default 600). */
- maxImagesPerRequest?: number
- /** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
- imageOffloadByteQuantum?: number
- /** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
- inlineImageOffloadByteQuantum?: number
- /** Image-count removal step after the request exceeds its count bound (default 20). */
- imageOffloadCountQuantum?: number
- /** Maximum duration of one request-image Files API resolution (default one minute). */
- filesApiTimeoutMs?: number
- /** Explicit lifetime assigned to each uploaded image (default seven days). */
- fileExpiresAfterSeconds?: number
- /** Remaining lifetime below which an indexed file is replaced (default one hour). */
- fileRefreshMarginSeconds?: number
- /** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
- fileQuotaCleanupBatch?: number
- /** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
- retryPolicy?: RetryPolicyConfig
- }
- const catalogModel: z<DeepSeekCatalogModel> = z.object({
- id: z.string().required(),
- name: z.string(),
- description: z.string(),
- contextWindow: z.number().step(1).min(1),
- maxTokens: z.number().step(1).min(1),
- inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(['text']),
- imagePixelBudget: z.union([z.number().step(1).min(1), 'low']),
- imageMaxBytes: z.number().step(1).min(1),
- })
- export const Config: z<Config> = z.object({
- apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
- baseURL: z.string(),
- thinking: z.union(['enabled', 'disabled']),
- reasoningEffort: z.union(['off', 'low', 'high', 'max']),
- maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),
- defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
- models: z.array(catalogModel).default(DEFAULT_MODELS),
- streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
- maxRequestFilesBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_FILES_BYTES),
- maxInlineRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES),
- maxImagesPerRequest: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGES_PER_REQUEST),
- imageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM),
- inlineImageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM),
- imageOffloadCountQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM),
- filesApiTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_FILES_API_TIMEOUT_MS),
- fileExpiresAfterSeconds: z.number().step(1).min(3_600).max(2_592_000).default(DEFAULT_FILE_EXPIRY_SECONDS),
- fileRefreshMarginSeconds: z.number().step(1).min(0).default(DEFAULT_FILE_REFRESH_MARGIN_SECONDS),
- fileQuotaCleanupBatch: z.number().step(1).min(1).max(1_000).default(DEFAULT_FILE_QUOTA_CLEANUP_BATCH),
- retryPolicy: RetryPolicySchema,
- })
- /** Public API default; the internal endpoint comes from $DEEPSEEK_BASE_URL. */
- export const PUBLIC_BASE_URL = 'https://api.deepseek.com'
- /** Environment variable naming this provider's endpoint, honored only from trusted layers. */
- const BASE_URL_ENV = 'DEEPSEEK_BASE_URL'
- /**
- * One resolution's complete request facts. Connection and credential facts
- * are one value on purpose: a snapshot the resolver rejects keeps the whole
- * previous generation, so a request can never pair a stale endpoint with a
- * newer key.
- */
- export type ResolvedDeepSeekOptions = DeepSeekConnectionOptions
- /** Resolve, validate, and detach the advisory model catalog. */
- function resolveModels(models: readonly DeepSeekCatalogModel[] | undefined): DeepSeekCatalogModel[] {
- const seen = new Set<string>()
- return (models ?? DEFAULT_MODELS).map((model) => {
- if (Object.hasOwn(model, 'imageDetail')) {
- throw new Error('llm-deepseek: catalog model imageDetail is no longer supported; use imagePixelBudget')
- }
- if (model.id.length === 0) throw new Error('llm-deepseek: catalog model ids must be non-empty')
- if (model.name !== undefined && model.name.length === 0) {
- throw new Error(`llm-deepseek: catalog model "${model.id}" has an empty name`)
- }
- if (model.contextWindow !== undefined
- && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) {
- throw new Error(
- `llm-deepseek: catalog model "${model.id}" contextWindow must be a positive integer`,
- )
- }
- if (model.maxTokens !== undefined
- && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) {
- throw new Error(
- `llm-deepseek: catalog model "${model.id}" maxTokens must be a positive integer`,
- )
- }
- const inputModalities = model.inputModalities ?? ['text']
- if (inputModalities.length === 0) {
- throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must not be empty`)
- }
- if (inputModalities.some(modality => !MODEL_MODALITIES.includes(modality))) {
- throw new Error(
- `llm-deepseek: catalog model "${model.id}" inputModalities must contain only "text" and "image"`,
- )
- }
- if (new Set(inputModalities).size !== inputModalities.length) {
- throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must not contain duplicates`)
- }
- const hasImage = inputModalities.includes('image')
- if (!hasImage && (model.imagePixelBudget !== undefined || model.imageMaxBytes !== undefined)) {
- throw new Error(`llm-deepseek: text-only catalog model "${model.id}" cannot declare image request limits`)
- }
- if (model.imagePixelBudget !== undefined
- && model.imagePixelBudget !== 'low'
- && (!Number.isSafeInteger(model.imagePixelBudget) || model.imagePixelBudget <= 0)) {
- throw new Error(`llm-deepseek: catalog model "${model.id}" imagePixelBudget must be "low" or a positive safe integer`)
- }
- if (model.imageMaxBytes !== undefined
- && (!Number.isSafeInteger(model.imageMaxBytes) || model.imageMaxBytes <= 0)) {
- throw new Error(`llm-deepseek: catalog model "${model.id}" imageMaxBytes must be a positive safe integer`)
- }
- if (seen.has(model.id)) throw new Error(`llm-deepseek: duplicate catalog model "${model.id}"`)
- seen.add(model.id)
- return {
- id: model.id,
- ...model.name === undefined ? {} : { name: model.name },
- ...model.description === undefined ? {} : { description: model.description },
- ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
- ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
- inputModalities: [...inputModalities],
- ...hasImage
- ? {
- imagePixelBudget: model.imagePixelBudget === 'low'
- ? DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET
- : model.imagePixelBudget ?? DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
- imageMaxBytes: model.imageMaxBytes ?? DEFAULT_REQUEST_IMAGE_MAX_BYTES,
- }
- : {},
- }
- })
- }
- /**
- * The one explicit resolve step from raw config to validated connection
- * facts. Programmatic construction may bypass Schemastery normalization, so
- * every default and bound is re-judged here — for the composition entry at
- * load (fail loud) and for each settings snapshot at its first use.
- * @param config - raw plugin config or resolved settings snapshot.
- * @param environment - this run's environment layers, or `undefined` outside
- * the product CLI. Every layer may supply an endpoint: the product trusts the
- * project it is launched in, so a checkout can point its own agent at the
- * gateway that checkout is meant to use.
- * @returns validated connection facts plus the credential reference.
- */
- export function resolveAdapterOptions(config: Config, environment?: LaunchEnvironmentSnapshot): ResolvedDeepSeekOptions {
- if (config.thinking === 'disabled'
- && config.reasoningEffort !== undefined
- && config.reasoningEffort !== 'off') {
- throw new Error('llm-deepseek: only reasoningEffort "off" can be configured when thinking is disabled')
- }
- if (config.defaultContextWindow !== undefined
- && (!Number.isInteger(config.defaultContextWindow) || config.defaultContextWindow <= 0)) {
- throw new Error('llm-deepseek: defaultContextWindow must be a positive integer')
- }
- if (config.maxTokens !== undefined
- && (!Number.isSafeInteger(config.maxTokens) || config.maxTokens <= 0)) {
- throw new Error('llm-deepseek: maxTokens must be a positive safe integer')
- }
- const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
- if (!Number.isFinite(streamIdleTimeoutMs)
- || streamIdleTimeoutMs <= 0
- || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
- throw new Error(
- `llm-deepseek: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`,
- )
- }
- const maxRequestFilesBytes = config.maxRequestFilesBytes ?? DEFAULT_MAX_REQUEST_FILES_BYTES
- if (!Number.isSafeInteger(maxRequestFilesBytes) || maxRequestFilesBytes <= 0) {
- throw new Error('llm-deepseek: maxRequestFilesBytes must be a positive safe integer')
- }
- const maxInlineRequestImageBytes = config.maxInlineRequestImageBytes ?? DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES
- if (!Number.isSafeInteger(maxInlineRequestImageBytes) || maxInlineRequestImageBytes <= 0) {
- throw new Error('llm-deepseek: maxInlineRequestImageBytes must be a positive safe integer')
- }
- const maxImagesPerRequest = config.maxImagesPerRequest ?? DEFAULT_MAX_IMAGES_PER_REQUEST
- if (!Number.isSafeInteger(maxImagesPerRequest) || maxImagesPerRequest <= 0) {
- throw new Error('llm-deepseek: maxImagesPerRequest must be a positive safe integer')
- }
- const imageOffloadByteQuantum = config.imageOffloadByteQuantum ?? DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM
- if (!Number.isSafeInteger(imageOffloadByteQuantum) || imageOffloadByteQuantum <= 0) {
- throw new Error('llm-deepseek: imageOffloadByteQuantum must be a positive safe integer')
- }
- if (imageOffloadByteQuantum > maxRequestFilesBytes) {
- throw new Error('llm-deepseek: imageOffloadByteQuantum must not exceed maxRequestFilesBytes')
- }
- const inlineImageOffloadByteQuantum = config.inlineImageOffloadByteQuantum
- ?? DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM
- if (!Number.isSafeInteger(inlineImageOffloadByteQuantum) || inlineImageOffloadByteQuantum <= 0) {
- throw new Error('llm-deepseek: inlineImageOffloadByteQuantum must be a positive safe integer')
- }
- if (inlineImageOffloadByteQuantum > maxInlineRequestImageBytes) {
- throw new Error('llm-deepseek: inlineImageOffloadByteQuantum must not exceed maxInlineRequestImageBytes')
- }
- const imageOffloadCountQuantum = config.imageOffloadCountQuantum ?? DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM
- if (!Number.isSafeInteger(imageOffloadCountQuantum) || imageOffloadCountQuantum <= 0) {
- throw new Error('llm-deepseek: imageOffloadCountQuantum must be a positive safe integer')
- }
- if (imageOffloadCountQuantum > maxImagesPerRequest) {
- throw new Error('llm-deepseek: imageOffloadCountQuantum must not exceed maxImagesPerRequest')
- }
- const filesApiTimeoutMs = config.filesApiTimeoutMs ?? DEFAULT_FILES_API_TIMEOUT_MS
- if (!Number.isFinite(filesApiTimeoutMs)
- || filesApiTimeoutMs <= 0
- || filesApiTimeoutMs > MAX_TIMER_DELAY_MS) {
- throw new Error(
- `llm-deepseek: filesApiTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`,
- )
- }
- const fileExpiresAfterSeconds = config.fileExpiresAfterSeconds ?? DEFAULT_FILE_EXPIRY_SECONDS
- if (!Number.isSafeInteger(fileExpiresAfterSeconds)
- || fileExpiresAfterSeconds < 3_600
- || fileExpiresAfterSeconds > 2_592_000) {
- throw new Error('llm-deepseek: fileExpiresAfterSeconds must be an integer from 3600 through 2592000')
- }
- const fileRefreshMarginSeconds = config.fileRefreshMarginSeconds ?? DEFAULT_FILE_REFRESH_MARGIN_SECONDS
- if (!Number.isSafeInteger(fileRefreshMarginSeconds)
- || fileRefreshMarginSeconds < 0
- || fileRefreshMarginSeconds >= fileExpiresAfterSeconds) {
- throw new Error('llm-deepseek: fileRefreshMarginSeconds must be a non-negative integer below fileExpiresAfterSeconds')
- }
- const fileQuotaCleanupBatch = config.fileQuotaCleanupBatch ?? DEFAULT_FILE_QUOTA_CLEANUP_BATCH
- if (!Number.isSafeInteger(fileQuotaCleanupBatch)
- || fileQuotaCleanupBatch < 1
- || fileQuotaCleanupBatch > 1_000) {
- throw new Error('llm-deepseek: fileQuotaCleanupBatch must be an integer from 1 through 1000')
- }
- return {
- apiKeyEnv: credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
- baseURL: config.baseURL
- ?? environment?.get(BASE_URL_ENV)?.value
- ?? PUBLIC_BASE_URL,
- defaults: {
- thinking: config.thinking,
- reasoningEffort: config.reasoningEffort,
- },
- maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
- defaultContextWindow: config.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,
- models: resolveModels(config.models),
- streamIdleTimeoutMs,
- maxRequestFilesBytes,
- maxInlineRequestImageBytes,
- maxImagesPerRequest,
- imageOffloadByteQuantum,
- inlineImageOffloadByteQuantum,
- imageOffloadCountQuantum,
- filesApiTimeoutMs,
- filePolicy: {
- expiresAfterSeconds: fileExpiresAfterSeconds,
- refreshMarginSeconds: fileRefreshMarginSeconds,
- quotaCleanupBatch: fileQuotaCleanupBatch,
- },
- retryPolicy: resolveRetryPolicy(config.retryPolicy, 'llm-deepseek: retryPolicy'),
- }
- }
- export function apply(ctx: Context, config: Config): void {
- let current: () => Config = () => config
- let lastRaw: Config | undefined
- let lastGood: ResolvedDeepSeekOptions | undefined
- const options = (): ResolvedDeepSeekOptions => {
- const raw = current()
- if (raw === lastRaw && lastGood !== undefined) return lastGood
- try {
- const next = resolveAdapterOptions(raw, launchEnvironmentOf(ctx))
- lastRaw = raw
- lastGood = next
- return next
- } catch (error) {
- // Static composition resolves before anything registers, so this branch
- // only sees a live settings snapshot failing a beyond-schema bound:
- // keep serving the last good facts and say so once per bad snapshot.
- if (lastGood === undefined) throw error
- lastRaw = raw
- ctx.logger.error('llm-deepseek: keeping the last good configuration after an invalid settings section')
- ctx.logger.error(error)
- return lastGood
- }
- }
- options()
- const resolveApiKey = async (connection: ResolvedDeepSeekOptions): Promise<string> => {
- // Every credential fact comes from the caller's snapshot, so a rejected
- // settings generation cannot leak its key onto the previous endpoint.
- const ref = connection.apiKeyEnv
- const credentials = ctx.get('credentials')
- if (credentials !== undefined) {
- const hit = await credentials.resolve(ref)
- if (hit !== undefined) return assertUsableApiKey(hit.value, 'llm-deepseek', ref)
- } else {
- // Without the seam there is no managed store to rank against, so the
- // environment is the whole credential plane.
- const ambient = launchEnvironmentOf(ctx).get(ref)
- if (ambient !== undefined && ambient.value.length > 0) {
- return assertUsableApiKey(ambient.value, 'llm-deepseek', ref)
- }
- }
- throw new LlmError(
- `llm-deepseek: no API key for provider route "${PROVIDER}"; store ${ref} through the credentials`
- + ` service (the web Models page writes it), or export ${ref} in the launching environment`,
- 'MISSING_CREDENTIAL',
- )
- }
- let userId: AnonymousUserId | undefined
- const resolveUserId = (): AnonymousUserId => userId ??= getOrCreateAnonymousUserId()
- const adapter = new DeepSeekAdapter({
- options,
- resolveApiKey,
- resolveUserId,
- resolveAttachments: () => ctx.get('attachments'),
- resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(
- attachments,
- hostPath => ctx.get('fs')?.processPathFromHostPath(hostPath),
- ref,
- ),
- prepareExtensions: (request) => {
- const extensions = ctx.get('deepseekLlmApiExtensions')
- return extensions?.prepare(request)
- ?? Promise.resolve({ fields: {}, accept: () => Promise.resolve() })
- },
- })
- ctx.llm.registerConfigurableProviders([
- { provider: PROVIDER, displayName: 'DeepSeek', settingsNs: NS, settingsPath: [] },
- ])
- // Route effects bind to this apply fiber via the stable `ctx` reference,
- // even when a swap runs inside the scoped settings callback below.
- const registration = ctx.llm.registerAdapter([PROVIDER], adapter)
- let registeredPolicy = options().retryPolicy
- const ensureRegistrationFacts = (): void => {
- const policy = options().retryPolicy
- if (deepEqualJson(policy, registeredPolicy)) return
- // The registry captures the retry policy at registration, so it is the one
- // fact per-request resolution cannot refresh. `replace` re-reads it in one
- // synchronous registry section: disposing and re-registering instead would
- // publish an empty route set between the two, and an observer that reacted
- // to it would see this provider disappear and come back.
- registration.replace([PROVIDER])
- registeredPolicy = policy
- }
- installSettingsSection(ctx, NS, Config, config, {
- setSource: (source) => {
- current = source
- },
- onChange: ensureRegistrationFacts,
- })
- }
|