diff --git a/package-lock.json b/package-lock.json index 96e1ac5..0e104c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -430,9 +430,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -450,9 +447,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -470,9 +464,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -490,9 +481,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 717c098..3aa70ef 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,20 @@ "minimum": 0, "description": "%deepseek-copilot.config.maxTokens.description%" }, + "deepseek-copilot.contextSize": { + "type": "number", + "default": 1000000, + "enum": [200000, 1000000], + "enumItemLabels": [ + "%deepseek-copilot.config.contextSize.200k.label%", + "%deepseek-copilot.config.contextSize.1m.label%" + ], + "markdownEnumDescriptions": [ + "%deepseek-copilot.config.contextSize.200k.description%", + "%deepseek-copilot.config.contextSize.1m.description%" + ], + "markdownDescription": "%deepseek-copilot.config.contextSize.description%" + }, "deepseek-copilot.experimental.stabilizeToolList": { "type": "boolean", "default": false, diff --git a/package.nls.json b/package.nls.json index 31ad075..23971da 100644 --- a/package.nls.json +++ b/package.nls.json @@ -17,6 +17,11 @@ "deepseek-copilot.config.title": "DeepSeek Copilot", "deepseek-copilot.config.baseUrl.description": "DeepSeek API base URL. Defaults to official DeepSeek API endpoint.", "deepseek-copilot.config.maxTokens.description": "Maximum number of output tokens per request. Set to 0 to use the API default (no limit). Useful for controlling costs.", + "deepseek-copilot.config.contextSize.description": "Input context window size reported to VS Code. Larger context allows longer conversations without compaction but may increase cost.", + "deepseek-copilot.config.contextSize.200k.label": "200K", + "deepseek-copilot.config.contextSize.200k.description": "200K tokens — safe default for most sessions.", + "deepseek-copilot.config.contextSize.1m.label": "1M", + "deepseek-copilot.config.contextSize.1m.description": "1M tokens — longer sessions without compaction.", "deepseek-copilot.config.experimental.stabilizeToolList.description": "**Experimental**: improve DeepSeek context-cache hit rate by pre-activating available tools.\n- When the enabled tools list changes across turns, this may improve DeepSeek context-cache hit rate.\n- Requests will include more function definitions, so input tokens may increase. Cache-hit input tokens are billed at a lower price, but still count toward usage.\n- This may add internal preflight tool calls to the current Copilot chat history. If you switch to another model in the same conversation, that model provider may reject or mishandle the replayed history. Start a new chat if model switching behaves unexpectedly.\n\nUse [Configure Tools](command:workbench.action.chat.configureTools) to **view and manage** your tool list:\n\n- 64 or fewer enabled tools: usually no need to enable this unless the tool list still changes across turns.\n- More than 128 enabled tools: not recommended. DeepSeek supports at most 128 functions in one `tools` request. Consider disabling tools you rarely use.", "deepseek-copilot.config.debugMode.description": "Controls what diagnostic information DeepSeek Copilot writes. Token usage is always reported to Copilot regardless of this setting.\n\n- **Minimal** — Token usage only. No diagnostic logs or request dumps.\n- **Metadata** — Privacy-safe diagnostic metadata (request hashes, prefix overlap, tool schema changes). Does not contain prompt text — safe to share in public issue reports. View with [`DeepSeek: Show Logs`](command:deepseek-copilot.showLogs).\n- **Verbose** — Complete request payloads written to disk for local debugging. **Warning: contains sensitive prompt content.** View with [`DeepSeek: Open Request Dumps Folder`](command:deepseek-copilot.openRequestDumpsFolder).", "deepseek-copilot.config.debugMode.minimal.label": "Minimal", diff --git a/src/config.ts b/src/config.ts index 477ca09..472a3a8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,6 +36,15 @@ export function getMaxTokens(): number | undefined { return value > 0 ? value : undefined; } +/** + * Get the configured context window size in tokens. + * Returns the value set by the user (200K or 1M), defaulting to 1M. + */ +export function getContextSize(): number { + const config = vscode.workspace.getConfiguration(CONFIG_SECTION); + return config.get('contextSize', 1000000); +} + /** * Diagnostic mode. `verbose` also enables metadata logs. * diff --git a/src/i18n.ts b/src/i18n.ts index 847eac4..d95f377 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -42,6 +42,13 @@ const zh: Translations = { 'thinking.max': '深度', 'thinking.max.desc': '深度推理,适合复杂任务', + // Context Size — model picker dropdown + 'contextSize.title': '上下文窗口', + 'contextSize.200k': '200K', + 'contextSize.200k.desc': '200K token 上下文(更快)', + 'contextSize.1m': '1M', + 'contextSize.1m.desc': '1M token 上下文(更大容量)', + // Vision 'vision.proxyUsing': '视觉代理:{0}', 'vision.notFound': '未找到视觉模型 "{0}"', @@ -232,6 +239,13 @@ const en: Translations = { 'thinking.max': 'Max', 'thinking.max.desc': 'Maximum reasoning depth for complex agent tasks', + // Context Size — model picker dropdown + 'contextSize.title': 'Context Window', + 'contextSize.200k': '200K', + 'contextSize.200k.desc': '200K token context (faster)', + 'contextSize.1m': '1M', + 'contextSize.1m.desc': '1M token context (larger capacity)', + // Vision // NOTE: vision.unableToDescribe has been moved to consts.ts as // IMAGE_DESCRIPTION_UNAVAILABLE — it is prompt content, not UI text. diff --git a/src/provider/index.ts b/src/provider/index.ts index 0456a58..258e2f4 100644 --- a/src/provider/index.ts +++ b/src/provider/index.ts @@ -1,7 +1,7 @@ import vscode from 'vscode'; import { AuthManager } from '../auth'; -import { getStabilizeToolListEnabled } from '../config'; -import { MODELS } from '../consts'; +import { getContextSize, getStabilizeToolListEnabled } from '../config'; +import { CONFIG_SECTION, MODELS } from '../consts'; import { t } from '../i18n'; import { logger } from '../logger'; import { createCacheDiagnosticsRecorder, dumpProviderInput } from './debug'; @@ -133,10 +133,11 @@ export class DeepSeekChatProvider implements vscode.LanguageModelChatProvider { const hasKey = await this.authManager.hasApiKey(); const pricingCurrency = this.balanceCurrencyResolver.getDisplayCurrency(); + const contextSize = getContextSize(); if (hasKey) { this.balanceCurrencyResolver.refreshInBackground(); } - return MODELS.map((model) => toChatInfo(model, hasKey, pricingCurrency)); + return MODELS.map((model) => toChatInfo(model, hasKey, pricingCurrency, contextSize)); } async provideLanguageModelChatResponse( @@ -184,6 +185,17 @@ export class DeepSeekChatProvider implements vscode.LanguageModelChatProvider { getVisionDescriber: () => this.vision.get(), }); + // Sync context size back to VS Code setting when user changes it via the + // model-picker dropdown. The updated maxInputTokens takes effect on the + // next request after Copilot Chat re-queries model information. + const currentContextSize = getContextSize(); + if (prepared.configuredContextSize !== currentContextSize) { + await vscode.workspace + .getConfiguration(CONFIG_SECTION) + .update('contextSize', prepared.configuredContextSize, vscode.ConfigurationTarget.Global); + this.onDidChangeLanguageModelChatInformationEmitter.fire(); + } + return streamChatCompletion({ prepared, progress, diff --git a/src/provider/models.ts b/src/provider/models.ts index 010e30b..a7a406b 100644 --- a/src/provider/models.ts +++ b/src/provider/models.ts @@ -16,25 +16,28 @@ import { toModelCostInfo, type ModelCostInformation } from './pricing/costs'; export type ThinkingEffort = 'none' | 'high' | 'max'; +export type ContextSize = 200000 | 1000000; + export type ModelConfigurationOptions = vscode.ProvideLanguageModelChatResponseOptions & { readonly modelConfiguration?: Record; readonly configuration?: Record; }; -type ThinkingEffortConfigurationSchema = ReturnType; +type ModelConfigurationSchema = ReturnType; export type ModelPickerChatInformation = vscode.LanguageModelChatInformation & ModelCostInformation & { readonly isUserSelectable: boolean; readonly isBYOK: true; readonly statusIcon?: vscode.ThemeIcon; - readonly configurationSchema?: ThinkingEffortConfigurationSchema; + readonly configurationSchema?: ModelConfigurationSchema; }; export function toChatInfo( m: ModelDefinition, hasApiKey: boolean, pricingCurrency?: PricingCurrency, + contextSize?: number, ): ModelPickerChatInformation { const modelDetail = resolveModelText(m, 'detail') ?? m.detail; const modelTooltip = resolveModelText(m, 'tooltip'); @@ -46,8 +49,7 @@ export function toChatInfo( detail: hasApiKey ? modelDetail : t('auth.apiKeyRequiredDetail'), tooltip: hasApiKey ? modelTooltip : t('auth.apiKeyRequiredDetail'), statusIcon: hasApiKey ? undefined : new vscode.ThemeIcon('warning'), - maxInputTokens: m.maxInputTokens, - maxOutputTokens: m.maxOutputTokens, + ...resolveContextWindow(m, contextSize), isBYOK: true, isUserSelectable: true, capabilities: { @@ -55,7 +57,7 @@ export function toChatInfo( imageInput: m.capabilities.imageInput, }, ...toModelCostInfo(m, pricingCurrency), - ...(m.capabilities.thinking ? { configurationSchema: buildThinkingEffortSchema() } : {}), + ...(hasApiKey ? { configurationSchema: buildModelConfigurationSchema(m) } : {}), }; } @@ -74,24 +76,77 @@ export function getConfiguredThinkingEffort(options: ModelConfigurationOptions): return configuredEffort === 'max' ? 'max' : 'high'; } -function buildThinkingEffortSchema() { - return { - properties: { - reasoningEffort: { - type: 'string', - title: t('status.thinking'), - enum: ['none', 'high', 'max'], - enumItemLabels: [t('thinking.none'), t('thinking.high'), t('thinking.max')], - enumDescriptions: [ - t('thinking.none.desc'), - t('thinking.high.desc'), - t('thinking.max.desc'), - ], - default: 'high', - group: 'navigation', - }, - }, - } as const; +/** + * Token split for the selectable 200K context window. + * + * VS Code/Copilot derives the displayed context window from + * `maxInputTokens + maxOutputTokens`, so each selectable window must split its + * *total* budget into input + output. The default 1M window keeps the + * accounting fixed in #71 (655,360 + 393,216 = 1,048,576 = DeepSeek's official + * combined input+output limit). The 200K option mirrors that same 5:3 + * input:output reservation, scaled to a 200,000-token total, so the reported + * window stays honest (~200K) instead of input + a separate output reservation. + */ +const CONTEXT_WINDOW_200K = { maxInputTokens: 125000, maxOutputTokens: 75000 } as const; + +/** + * Resolve the (input, output) token split for the selected context window. + * Unknown / unset values fall back to the model's own metadata, which encodes + * DeepSeek's official 1M (input + output) window. + */ +function resolveContextWindow( + m: ModelDefinition, + contextSize?: number, +): { maxInputTokens: number; maxOutputTokens: number } { + if (contextSize === 200000) { + return { ...CONTEXT_WINDOW_200K }; + } + return { maxInputTokens: m.maxInputTokens, maxOutputTokens: m.maxOutputTokens }; +} + +/** + * Read the context size selected by the user via the model-picker dropdown. + * Falls back to the VS Code setting when the dropdown hasn't been used yet. + */ +export function getConfiguredContextSize(options: ModelConfigurationOptions): ContextSize { + const configured = + options.modelConfiguration?.contextSize ?? options.configuration?.contextSize; + if (configured === 200000) { + return 200000; + } + return 1000000; +} + +function buildModelConfigurationSchema(m: ModelDefinition) { + const properties: Record = {}; + + if (m.capabilities.thinking) { + properties.reasoningEffort = { + type: 'string', + title: t('status.thinking'), + enum: ['none', 'high', 'max'], + enumItemLabels: [t('thinking.none'), t('thinking.high'), t('thinking.max')], + enumDescriptions: [ + t('thinking.none.desc'), + t('thinking.high.desc'), + t('thinking.max.desc'), + ], + default: 'high', + group: 'navigation', + }; + } + + properties.contextSize = { + type: 'number', + title: t('contextSize.title'), + enum: [200000, 1000000], + enumItemLabels: [t('contextSize.200k'), t('contextSize.1m')], + enumDescriptions: [t('contextSize.200k.desc'), t('contextSize.1m.desc')], + default: 1000000, + group: 'tokens', + }; + + return { properties } as const; } function resolveModelText(m: ModelDefinition, field: 'detail' | 'tooltip'): string | undefined { diff --git a/src/provider/request.ts b/src/provider/request.ts index c143325..84c811f 100644 --- a/src/provider/request.ts +++ b/src/provider/request.ts @@ -8,13 +8,13 @@ import { t } from '../i18n'; import type { DeepSeekRequest } from '../types'; import { convertMessages, countMessageChars } from './convert'; import { - dumpDeepSeekRequest, - type CacheDiagnosticsRecorder, - type CacheDiagnosticsRun, + dumpDeepSeekRequest, + type CacheDiagnosticsRecorder, + type CacheDiagnosticsRun, } from './debug'; -import { getConfiguredThinkingEffort, type ModelConfigurationOptions } from './models'; -import { classifyDeepSeekRequest, shouldForceThinkingNone, type RequestKind } from './routing'; +import { getConfiguredContextSize, getConfiguredThinkingEffort, type ContextSize, type ModelConfigurationOptions } from './models'; import type { ReplayMarkerMetadata } from './replay'; +import { classifyDeepSeekRequest, shouldForceThinkingNone, type RequestKind } from './routing'; import type { ConversationSegment } from './segment'; import { collectTrailingToolResultIds, prepareRequestTools } from './tools/request'; import { resolveImageMessages, type VisionDescriber } from './vision'; @@ -31,6 +31,8 @@ export interface PreparedChatRequest { replayMarkerMetadata: ReplayMarkerMetadata; visionMarkerTextChars?: number; initialResponseNotice?: string; + /** The context size selected via the model-picker dropdown (if any). */ + configuredContextSize: ContextSize; } export interface PrepareChatRequestOptions { @@ -88,6 +90,7 @@ export async function prepareChatRequest({ const configuredThinkingEffort = getConfiguredThinkingEffort( options as ModelConfigurationOptions, ); + const configuredContextSize = getConfiguredContextSize(options as ModelConfigurationOptions); // Only force helper requests into disabled thinking on the official API. // Custom endpoints keep their configured effort to preserve pre-#137 request shape. const forceNoneThinking = @@ -147,5 +150,6 @@ export async function prepareChatRequest({ replayMarkerMetadata: visionResolution.replayMarkerMetadata, visionMarkerTextChars: visionResolution.stats.markerVisionTextChars || undefined, initialResponseNotice: visionResolution.initialResponseNotice, + configuredContextSize, }; }