Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Both support optional thinking mode, tool calling, and 1M token context.
| `deepseek-copilot.maxTokens` | `0` | Max output tokens (`0` = no limit). Useful for cost control |
| `deepseek-copilot.modelIdOverrides` | prefilled official ID map | API model IDs to send for DeepSeek V4 Flash / Pro. Change only for compatible third-party APIs with different model names |
| `deepseek-copilot.debugMode` | `minimal` | Diagnostic mode: `minimal` for token usage only, `metadata` for privacy-preserving logs, or `verbose` for full request dumps and pipeline snapshots under extension global storage. Full dumps may include sensitive prompt text, tool schemas, file snippets, and image descriptions. Use `DeepSeek: Open Request Dumps Folder` to open the dump location |
| `deepseek-copilot.visionModel` | *(auto)* | Which Copilot model to proxy images through |
| `deepseek-copilot.visionModel` | *(auto)* | VS Code vision model used to proxy images. Configure from `DeepSeek: Configure Vision Proxy`; new saves use `vendor/id`, while legacy bare model IDs are still read |
| `deepseek-copilot.visionPrompt` | *(built-in)* | Prompt used to describe image attachments |
| `deepseek-copilot.experimental.stabilizeToolList` | `false` | Experimental. Tries to pre-activate VS Code/Copilot virtual tools so the DeepSeek API `tools` parameter is more complete and stable across turns. May improve context-cache hit rate when enabled tools change between turns. Can increase input tokens because more function definitions may be included; cache-hit input tokens are cheaper but still count toward usage. Usually leave it off with 64 or fewer enabled tools unless the tool list still changes across turns; do not enable it with more than 128 enabled tools |

Expand Down
2 changes: 1 addition & 1 deletion README.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ API Key 存储在 VS Code 的 `SecretStorage` 中(macOS 钥匙串 / Windows
| `deepseek-copilot.maxTokens` | `0` | 最大输出 Token 数(`0` = 不限制)。可用于成本控制 |
| `deepseek-copilot.modelIdOverrides` | 预填官方 ID 映射 | DeepSeek V4 Flash / Pro 对应的 API 模型 ID。仅在使用模型名不同的兼容第三方 API 时修改 |
| `deepseek-copilot.debugMode` | `minimal` | 诊断模式:`minimal` 仅上报 token 用量,`metadata` 输出隐私安全日志,`verbose` 将完整请求 dump 和 pipeline snapshot 写入扩展 global storage。完整 dump 可能包含敏感提示词文本、工具定义、文件片段和图片描述。使用 `DeepSeek: 打开请求 Dump 目录` 打开 dump 位置 |
| `deepseek-copilot.visionModel` | *(自动)* | 用作视觉代理的 Copilot 模型 |
| `deepseek-copilot.visionModel` | *(自动)* | 用作图片代理的 VS Code 视觉模型。请通过 `DeepSeek: 配置视觉代理` 设置;新版保存为 `vendor/id`,旧版裸模型 ID 仍兼容读取 |
| `deepseek-copilot.visionPrompt` | *(内置)* | 用于描述图片附件的提示词 |
| `deepseek-copilot.experimental.stabilizeToolList` | `false` | 实验性设置。尝试预先激活 VS Code/Copilot 的虚拟工具,让传给 DeepSeek API 的 `tools` 参数在多轮对话中更完整、更稳定。当已启用工具跨轮次变化时,可能提高上下文缓存命中率。代价是 input tokens 可能增加;缓存命中的 input tokens 单价更低,但仍会计入用量。64 个或更少已启用工具时通常无需开启,除非工具列表仍在跨轮次变化;超过 128 个已启用工具时不建议开启 |

Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"deepseek-copilot.config.modelIdOverrides.description": "Override the API model ID sent for each DeepSeek model. Defaults are prefilled with official DeepSeek IDs; change them only when using a compatible third-party API that uses different model names.",
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-flash.description": "API model ID for DeepSeek V4 Flash",
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-pro.description": "API model ID for DeepSeek V4 Pro",
"deepseek-copilot.config.visionModel.description": "Compatibility setting managed by [Configure Vision Proxy](command:deepseek-copilot.setVisionModel). Stores the selected VS Code vision model when that source is active.",
"deepseek-copilot.config.visionModel.description": "Compatibility setting managed by [Configure Vision Proxy](command:deepseek-copilot.setVisionModel). Stores the selected VS Code vision model as `vendor/id` when that source is active. Legacy bare model IDs are read for compatibility.",
"deepseek-copilot.config.visionPrompt.description": "Prompt sent to the vision proxy model when describing image attachments before forwarding them to DeepSeek.\n\n[Configure Vision Proxy](command:deepseek-copilot.setVisionModel)"
}
2 changes: 1 addition & 1 deletion package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"deepseek-copilot.config.modelIdOverrides.description": "覆盖各 DeepSeek 模型实际使用的 API 模型 ID,默认使用官方 DeepSeek ID,仅在对接不同模型名称的第三方 API 时需要修改。",
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-flash.description": "DeepSeek V4 Flash 的 API 模型 ID。",
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-pro.description": "DeepSeek V4 Pro 的 API 模型 ID。",
"deepseek-copilot.config.visionModel.description": "由[配置视觉代理](command:deepseek-copilot.setVisionModel)管理的兼容设置;当选择 VS Code 视觉模型来源时,用于保存所选模型。",
"deepseek-copilot.config.visionModel.description": "由[配置视觉代理](command:deepseek-copilot.setVisionModel)管理的兼容设置;当选择 VS Code 视觉模型来源时,以 `vendor/id` 保存所选模型。旧版裸模型 ID 仍会兼容读取。",
"deepseek-copilot.config.visionPrompt.description": "在将图片附件转发给 DeepSeek 之前,发送给视觉代理模型的提示词。\n\n[配置视觉代理](command:deepseek-copilot.setVisionModel)"
}
105 changes: 105 additions & 0 deletions src/provider/vision/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type vscode from 'vscode';
import { t } from '../../i18n';
import { safeStringify } from '../../json';
import { logger } from '../../logger';
import { formatVisionProxyError } from './protocols/errors';
import { getVSCodeVisionTargetChatSessionType } from './sources/vscode/model';
import type { VisionProxyConfig } from './types';

export function showVisionLogs(): void {
logger.show();
}

export function logVSCodeVisionModelSelected(model: vscode.LanguageModelChat): void {
logger.info(
`${t('vision.proxyUsing', model.id)} selected=${formatVSCodeVisionModelIdentity(model)}`,
);
}

export function logVSCodeVisionModelNotFound(modelId: string): void {
logger.warn(t('vision.notFound', modelId));
}

export function logVisionApiEndpointSelected(modelId: string): void {
logger.info(`Vision proxy: ${modelId} source=api-endpoint`);
}

export function logInvalidVisionProxyApiEndpointConfig(
source: string | undefined,
explicitApiEndpointSource: boolean,
error: unknown,
): void {
logger.warn(
`Invalid vision proxy API endpoint configuration; source=${source ?? 'unset'} fallback=${explicitApiEndpointSource ? 'none' : 'vscode-lm'}`,
error,
);
}

export function logVisionProxyUnavailable(): void {
logger.warn(t('vision.unavailable'));
}

export function logVisionProxyDescribeFailed(error: unknown): void {
logger.error(t('vision.proxyError'), formatVisionProxyError(error));
}

export function logVisionProxyTestSucceeded(
config: VisionProxyConfig,
apiKey: string | undefined,
description: string,
): void {
logger.info(
'Vision proxy test succeeded:',
formatVisionProxyTestDiagnostics(config, apiKey, description),
);
}

export function logVisionProxyTestFailed(error: unknown): void {
logger.error('Vision proxy test failed:', formatVisionProxyError(error));
}

function formatVSCodeVisionModelIdentity(model: vscode.LanguageModelChat): string {
return [
formatLogField('id', model.id),
formatLogField('vendor', model.vendor),
formatLogField('name', model.name),
formatLogField('family', model.family),
formatLogField('version', model.version),
formatLogField('targetChatSessionType', getVSCodeVisionTargetChatSessionType(model)),
].join(' ');
}

function formatLogField(name: string, value: unknown): string {
return `${name}=${formatLogValue(value)}`;
}

function formatLogValue(value: unknown): string {
const text = asString(value);
return text ? JSON.stringify(text) : 'n/a';
}

function formatVisionProxyTestDiagnostics(
config: VisionProxyConfig,
apiKey: string | undefined,
description: string,
): string {
return joinDiagnosticParts(
`kind=vision`,
`phase=describe`,
`providerFamily=${safeStringify(config.providerFamily)}`,
`apiType=${safeStringify(config.apiType)}`,
`model=${safeStringify(config.modelId)}`,
`endpoint=${safeStringify(config.url)}`,
`hasApiKey=${Boolean(apiKey?.trim())}`,
`responseChars=${description.length}`,
config.headers ? `headerNames=${safeStringify(Object.keys(config.headers).sort())}` : undefined,
);
}

function joinDiagnosticParts(...parts: (string | undefined)[]): string {
return parts.filter(Boolean).join(' ');
}

function asString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
7 changes: 3 additions & 4 deletions src/provider/vision/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import vscode from 'vscode';
import { t } from '../../i18n';
import { toWellFormedString } from '../../json';
import { logger } from '../../logger';
import { parseFirstReplayMarker } from '../replay';
import { createVisionProxyFailureNotice, createVisionProxyMissingNotice } from '../tools/notices';
import {
formatVisionProxyError,
formatVisionProxyErrorCode,
getVisionProxyErrorDisplayCode,
isVisionProxyError,
Expand All @@ -22,6 +20,7 @@ import type {
VisionResolutionStats,
} from './types';
import { getVisionPrompt } from './sources/vscode';
import { logVisionProxyDescribeFailed, logVisionProxyUnavailable } from './log';

interface CurrentVisionResolution {
text: string;
Expand Down Expand Up @@ -238,7 +237,7 @@ async function resolveCurrentVisionText(
): Promise<CurrentVisionResolution> {
if (!visionDescriber || token.isCancellationRequested) {
if (!visionDescriber) {
logger.warn(t('vision.unavailable'));
logVisionProxyUnavailable();
}
stats.unavailableImageMessages += 1;
return { text: createVisionReplayText(IMAGE_DESCRIPTION_UNAVAILABLE, nonImageParts) };
Expand All @@ -262,7 +261,7 @@ async function resolveCurrentVisionText(
stats.generatedImageMessages += 1;
return { text: createVisionReplayText(createImageDescriptionText(description), nonImageParts) };
} catch (error) {
logger.error(t('vision.proxyError'), formatVisionProxyError(error));
logVisionProxyDescribeFailed(error);
stats.failedImageMessages += 1;
return createFailedVisionResolution(
getVisionProxyErrorDisplayCode(error),
Expand Down
11 changes: 4 additions & 7 deletions src/provider/vision/service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import vscode from 'vscode';
import { t } from '../../i18n';
import { logger } from '../../logger';
import { logInvalidVisionProxyApiEndpointConfig, logVisionApiEndpointSelected } from './log';
import { VISION_PROXY_API_KEY_SECRET, VisionProxyConfigStore } from './sources/endpoint/config';
import { createEndpointVisionDescriber } from './sources/endpoint';
import { openVisionProxyPanel } from './ui/panel';
Expand Down Expand Up @@ -55,15 +55,15 @@ export function createVisionService(context: vscode.ExtensionContext): {
}
const apiKey = await store.getApiKey();
const describer = createEndpointVisionDescriber(result.config, apiKey);
logger.info(`Vision proxy: ${describer.id} source=api-endpoint`);
logVisionApiEndpointSelected(describer.id);
return describer;
}

const result = getApiEndpointConfig(store, false);
if (result.config) {
const apiKey = await store.getApiKey();
const describer = createEndpointVisionDescriber(result.config, apiKey);
logger.info(`Vision proxy: ${describer.id} source=api-endpoint`);
logVisionApiEndpointSelected(describer.id);
return describer;
}
return vscodeLm.get();
Expand All @@ -84,10 +84,7 @@ function getApiEndpointConfig(
try {
return { config: store.getConfig() };
} catch (error) {
logger.warn(
`Invalid vision proxy API endpoint configuration; source=${store.getSource() ?? 'unset'} fallback=${explicitApiEndpointSource ? 'none' : 'vscode-lm'}`,
error,
);
logInvalidVisionProxyApiEndpointConfig(store.getSource(), explicitApiEndpointSource, error);
return { error };
}
}
Expand Down
38 changes: 4 additions & 34 deletions src/provider/vision/sources/endpoint/test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import vscode from 'vscode';
import { t } from '../../../../i18n';
import { safeStringify } from '../../../../json';
import { logger } from '../../../../logger';
import { logVisionProxyTestFailed, logVisionProxyTestSucceeded } from '../../log';
import { VisionProxyClient } from '../../protocols/client';
import {
formatVisionProxyError,
getVisionProxyErrorDisplayCode,
isVisionProxyError,
} from '../../protocols/errors';
import { getVisionProxyErrorDisplayCode, isVisionProxyError } from '../../protocols/errors';
import type { VisionProxyConfig } from '../../types';

export interface VisionProxyTestResult {
Expand Down Expand Up @@ -42,13 +37,10 @@ export async function testVisionProxyConnection(
],
token: tokenSource.token,
});
logger.info(
'Vision proxy test succeeded:',
formatVisionProxyTestDiagnostics(config, apiKey, description),
);
logVisionProxyTestSucceeded(config, apiKey, description);
return { ok: true, imageDataUrl: TEST_IMAGE_DATA_URL, response: description };
} catch (error) {
logger.error('Vision proxy test failed:', formatVisionProxyError(error));
logVisionProxyTestFailed(error);
if (isVisionProxyError(error)) {
return {
ok: false,
Expand All @@ -65,25 +57,3 @@ export async function testVisionProxyConnection(
tokenSource.dispose();
}
}

function formatVisionProxyTestDiagnostics(
config: VisionProxyConfig,
apiKey: string | undefined,
description: string,
): string {
return joinDiagnosticParts(
`kind=vision`,
`phase=describe`,
`providerFamily=${safeStringify(config.providerFamily)}`,
`apiType=${safeStringify(config.apiType)}`,
`model=${safeStringify(config.modelId)}`,
`endpoint=${safeStringify(config.url)}`,
`hasApiKey=${Boolean(apiKey?.trim())}`,
`responseChars=${description.length}`,
config.headers ? `headerNames=${safeStringify(Object.keys(config.headers).sort())}` : undefined,
);
}

function joinDiagnosticParts(...parts: (string | undefined)[]): string {
return parts.filter(Boolean).join(' ');
}
Loading
Loading