Skip to content

Commit 1d7b668

Browse files
authored
fix(vision): disambiguate VS Code LM model selection (#161)
Store VS Code vision model selections as provider-qualified vendor/id keys so duplicate model IDs from Copilot, Copilot CLI, and Claude Code do not collide. Also filter unsupported Claude Code/Copilot CLI candidates from the vision proxy picker, keep legacy bare model IDs readable, and normalize future saves to canonical keys. Fixes #149
1 parent de85319 commit 1d7b668

14 files changed

Lines changed: 259 additions & 105 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Both support optional thinking mode, tool calling, and 1M token context.
103103
| `deepseek-copilot.maxTokens` | `0` | Max output tokens (`0` = no limit). Useful for cost control |
104104
| `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 |
105105
| `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 |
106-
| `deepseek-copilot.visionModel` | *(auto)* | Which Copilot model to proxy images through |
106+
| `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 |
107107
| `deepseek-copilot.visionPrompt` | *(built-in)* | Prompt used to describe image attachments |
108108
| `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 |
109109

README.zh-cn.md

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

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@
2828
"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.",
2929
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-flash.description": "API model ID for DeepSeek V4 Flash",
3030
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-pro.description": "API model ID for DeepSeek V4 Pro",
31-
"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.",
31+
"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.",
3232
"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)"
3333
}

package.nls.zh-cn.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@
2828
"deepseek-copilot.config.modelIdOverrides.description": "覆盖各 DeepSeek 模型实际使用的 API 模型 ID,默认使用官方 DeepSeek ID,仅在对接不同模型名称的第三方 API 时需要修改。",
2929
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-flash.description": "DeepSeek V4 Flash 的 API 模型 ID。",
3030
"deepseek-copilot.config.modelIdOverrides.deepseek-v4-pro.description": "DeepSeek V4 Pro 的 API 模型 ID。",
31-
"deepseek-copilot.config.visionModel.description": "由[配置视觉代理](command:deepseek-copilot.setVisionModel)管理的兼容设置;当选择 VS Code 视觉模型来源时,用于保存所选模型",
31+
"deepseek-copilot.config.visionModel.description": "由[配置视觉代理](command:deepseek-copilot.setVisionModel)管理的兼容设置;当选择 VS Code 视觉模型来源时,以 `vendor/id` 保存所选模型。旧版裸模型 ID 仍会兼容读取",
3232
"deepseek-copilot.config.visionPrompt.description": "在将图片附件转发给 DeepSeek 之前,发送给视觉代理模型的提示词。\n\n[配置视觉代理](command:deepseek-copilot.setVisionModel)"
3333
}

src/provider/vision/log.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type vscode from 'vscode';
2+
import { t } from '../../i18n';
3+
import { safeStringify } from '../../json';
4+
import { logger } from '../../logger';
5+
import { formatVisionProxyError } from './protocols/errors';
6+
import { getVSCodeVisionTargetChatSessionType } from './sources/vscode/model';
7+
import type { VisionProxyConfig } from './types';
8+
9+
export function showVisionLogs(): void {
10+
logger.show();
11+
}
12+
13+
export function logVSCodeVisionModelSelected(model: vscode.LanguageModelChat): void {
14+
logger.info(
15+
`${t('vision.proxyUsing', model.id)} selected=${formatVSCodeVisionModelIdentity(model)}`,
16+
);
17+
}
18+
19+
export function logVSCodeVisionModelNotFound(modelId: string): void {
20+
logger.warn(t('vision.notFound', modelId));
21+
}
22+
23+
export function logVisionApiEndpointSelected(modelId: string): void {
24+
logger.info(`Vision proxy: ${modelId} source=api-endpoint`);
25+
}
26+
27+
export function logInvalidVisionProxyApiEndpointConfig(
28+
source: string | undefined,
29+
explicitApiEndpointSource: boolean,
30+
error: unknown,
31+
): void {
32+
logger.warn(
33+
`Invalid vision proxy API endpoint configuration; source=${source ?? 'unset'} fallback=${explicitApiEndpointSource ? 'none' : 'vscode-lm'}`,
34+
error,
35+
);
36+
}
37+
38+
export function logVisionProxyUnavailable(): void {
39+
logger.warn(t('vision.unavailable'));
40+
}
41+
42+
export function logVisionProxyDescribeFailed(error: unknown): void {
43+
logger.error(t('vision.proxyError'), formatVisionProxyError(error));
44+
}
45+
46+
export function logVisionProxyTestSucceeded(
47+
config: VisionProxyConfig,
48+
apiKey: string | undefined,
49+
description: string,
50+
): void {
51+
logger.info(
52+
'Vision proxy test succeeded:',
53+
formatVisionProxyTestDiagnostics(config, apiKey, description),
54+
);
55+
}
56+
57+
export function logVisionProxyTestFailed(error: unknown): void {
58+
logger.error('Vision proxy test failed:', formatVisionProxyError(error));
59+
}
60+
61+
function formatVSCodeVisionModelIdentity(model: vscode.LanguageModelChat): string {
62+
return [
63+
formatLogField('id', model.id),
64+
formatLogField('vendor', model.vendor),
65+
formatLogField('name', model.name),
66+
formatLogField('family', model.family),
67+
formatLogField('version', model.version),
68+
formatLogField('targetChatSessionType', getVSCodeVisionTargetChatSessionType(model)),
69+
].join(' ');
70+
}
71+
72+
function formatLogField(name: string, value: unknown): string {
73+
return `${name}=${formatLogValue(value)}`;
74+
}
75+
76+
function formatLogValue(value: unknown): string {
77+
const text = asString(value);
78+
return text ? JSON.stringify(text) : 'n/a';
79+
}
80+
81+
function formatVisionProxyTestDiagnostics(
82+
config: VisionProxyConfig,
83+
apiKey: string | undefined,
84+
description: string,
85+
): string {
86+
return joinDiagnosticParts(
87+
`kind=vision`,
88+
`phase=describe`,
89+
`providerFamily=${safeStringify(config.providerFamily)}`,
90+
`apiType=${safeStringify(config.apiType)}`,
91+
`model=${safeStringify(config.modelId)}`,
92+
`endpoint=${safeStringify(config.url)}`,
93+
`hasApiKey=${Boolean(apiKey?.trim())}`,
94+
`responseChars=${description.length}`,
95+
config.headers ? `headerNames=${safeStringify(Object.keys(config.headers).sort())}` : undefined,
96+
);
97+
}
98+
99+
function joinDiagnosticParts(...parts: (string | undefined)[]): string {
100+
return parts.filter(Boolean).join(' ');
101+
}
102+
103+
function asString(value: unknown): string | undefined {
104+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
105+
}

src/provider/vision/resolve.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import vscode from 'vscode';
22
import { t } from '../../i18n';
33
import { toWellFormedString } from '../../json';
4-
import { logger } from '../../logger';
54
import { parseFirstReplayMarker } from '../replay';
65
import { createVisionProxyFailureNotice, createVisionProxyMissingNotice } from '../tools/notices';
76
import {
8-
formatVisionProxyError,
97
formatVisionProxyErrorCode,
108
getVisionProxyErrorDisplayCode,
119
isVisionProxyError,
@@ -22,6 +20,7 @@ import type {
2220
VisionResolutionStats,
2321
} from './types';
2422
import { getVisionPrompt } from './sources/vscode';
23+
import { logVisionProxyDescribeFailed, logVisionProxyUnavailable } from './log';
2524

2625
interface CurrentVisionResolution {
2726
text: string;
@@ -238,7 +237,7 @@ async function resolveCurrentVisionText(
238237
): Promise<CurrentVisionResolution> {
239238
if (!visionDescriber || token.isCancellationRequested) {
240239
if (!visionDescriber) {
241-
logger.warn(t('vision.unavailable'));
240+
logVisionProxyUnavailable();
242241
}
243242
stats.unavailableImageMessages += 1;
244243
return { text: createVisionReplayText(IMAGE_DESCRIPTION_UNAVAILABLE, nonImageParts) };
@@ -262,7 +261,7 @@ async function resolveCurrentVisionText(
262261
stats.generatedImageMessages += 1;
263262
return { text: createVisionReplayText(createImageDescriptionText(description), nonImageParts) };
264263
} catch (error) {
265-
logger.error(t('vision.proxyError'), formatVisionProxyError(error));
264+
logVisionProxyDescribeFailed(error);
266265
stats.failedImageMessages += 1;
267266
return createFailedVisionResolution(
268267
getVisionProxyErrorDisplayCode(error),

src/provider/vision/service.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import vscode from 'vscode';
22
import { t } from '../../i18n';
3-
import { logger } from '../../logger';
3+
import { logInvalidVisionProxyApiEndpointConfig, logVisionApiEndpointSelected } from './log';
44
import { VISION_PROXY_API_KEY_SECRET, VisionProxyConfigStore } from './sources/endpoint/config';
55
import { createEndpointVisionDescriber } from './sources/endpoint';
66
import { openVisionProxyPanel } from './ui/panel';
@@ -55,15 +55,15 @@ export function createVisionService(context: vscode.ExtensionContext): {
5555
}
5656
const apiKey = await store.getApiKey();
5757
const describer = createEndpointVisionDescriber(result.config, apiKey);
58-
logger.info(`Vision proxy: ${describer.id} source=api-endpoint`);
58+
logVisionApiEndpointSelected(describer.id);
5959
return describer;
6060
}
6161

6262
const result = getApiEndpointConfig(store, false);
6363
if (result.config) {
6464
const apiKey = await store.getApiKey();
6565
const describer = createEndpointVisionDescriber(result.config, apiKey);
66-
logger.info(`Vision proxy: ${describer.id} source=api-endpoint`);
66+
logVisionApiEndpointSelected(describer.id);
6767
return describer;
6868
}
6969
return vscodeLm.get();
@@ -84,10 +84,7 @@ function getApiEndpointConfig(
8484
try {
8585
return { config: store.getConfig() };
8686
} catch (error) {
87-
logger.warn(
88-
`Invalid vision proxy API endpoint configuration; source=${store.getSource() ?? 'unset'} fallback=${explicitApiEndpointSource ? 'none' : 'vscode-lm'}`,
89-
error,
90-
);
87+
logInvalidVisionProxyApiEndpointConfig(store.getSource(), explicitApiEndpointSource, error);
9188
return { error };
9289
}
9390
}

src/provider/vision/sources/endpoint/test.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import vscode from 'vscode';
22
import { t } from '../../../../i18n';
3-
import { safeStringify } from '../../../../json';
4-
import { logger } from '../../../../logger';
3+
import { logVisionProxyTestFailed, logVisionProxyTestSucceeded } from '../../log';
54
import { VisionProxyClient } from '../../protocols/client';
6-
import {
7-
formatVisionProxyError,
8-
getVisionProxyErrorDisplayCode,
9-
isVisionProxyError,
10-
} from '../../protocols/errors';
5+
import { getVisionProxyErrorDisplayCode, isVisionProxyError } from '../../protocols/errors';
116
import type { VisionProxyConfig } from '../../types';
127

138
export interface VisionProxyTestResult {
@@ -42,13 +37,10 @@ export async function testVisionProxyConnection(
4237
],
4338
token: tokenSource.token,
4439
});
45-
logger.info(
46-
'Vision proxy test succeeded:',
47-
formatVisionProxyTestDiagnostics(config, apiKey, description),
48-
);
40+
logVisionProxyTestSucceeded(config, apiKey, description);
4941
return { ok: true, imageDataUrl: TEST_IMAGE_DATA_URL, response: description };
5042
} catch (error) {
51-
logger.error('Vision proxy test failed:', formatVisionProxyError(error));
43+
logVisionProxyTestFailed(error);
5244
if (isVisionProxyError(error)) {
5345
return {
5446
ok: false,
@@ -65,25 +57,3 @@ export async function testVisionProxyConnection(
6557
tokenSource.dispose();
6658
}
6759
}
68-
69-
function formatVisionProxyTestDiagnostics(
70-
config: VisionProxyConfig,
71-
apiKey: string | undefined,
72-
description: string,
73-
): string {
74-
return joinDiagnosticParts(
75-
`kind=vision`,
76-
`phase=describe`,
77-
`providerFamily=${safeStringify(config.providerFamily)}`,
78-
`apiType=${safeStringify(config.apiType)}`,
79-
`model=${safeStringify(config.modelId)}`,
80-
`endpoint=${safeStringify(config.url)}`,
81-
`hasApiKey=${Boolean(apiKey?.trim())}`,
82-
`responseChars=${description.length}`,
83-
config.headers ? `headerNames=${safeStringify(Object.keys(config.headers).sort())}` : undefined,
84-
);
85-
}
86-
87-
function joinDiagnosticParts(...parts: (string | undefined)[]): string {
88-
return parts.filter(Boolean).join(' ');
89-
}

0 commit comments

Comments
 (0)