Skip to content

Commit 0ccd91e

Browse files
authored
fix(settings): make API key and base URL actions discoverable (#276)
Expose API key actions and a scoped Base URL reset from Settings. Always preview reset targets, confirm API key removal, and link missing-key chat errors to the existing configuration flow. Closes #270
1 parent 61dd50f commit 0ccd91e

9 files changed

Lines changed: 152 additions & 11 deletions

File tree

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060
"command": "deepseek-copilot.clearApiKey",
6161
"title": "%deepseek-copilot.command.clearApiKey%"
6262
},
63+
{
64+
"command": "deepseek-copilot.resetBaseUrl",
65+
"title": "%deepseek-copilot.command.resetBaseUrl%"
66+
},
6367
{
6468
"command": "deepseek-copilot.setVisionModel",
6569
"title": "%deepseek-copilot.command.setVisionModel%"
@@ -119,7 +123,7 @@
119123
"deepseek-copilot.baseUrl": {
120124
"type": "string",
121125
"default": "https://api.deepseek.com",
122-
"description": "%deepseek-copilot.config.baseUrl.description%"
126+
"markdownDescription": "%deepseek-copilot.config.baseUrl.description%"
123127
},
124128
"deepseek-copilot.maxTokens": {
125129
"type": "number",

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"deepseek-copilot.command.setApiKey": "DeepSeek: Set API Key",
33
"deepseek-copilot.command.getApiKey": "DeepSeek: Get API Key",
44
"deepseek-copilot.command.clearApiKey": "DeepSeek: Clear API Key",
5+
"deepseek-copilot.command.resetBaseUrl": "DeepSeek: Reset Base URL",
56
"deepseek-copilot.command.setVisionModel": "DeepSeek: Configure Vision Proxy",
67
"deepseek-copilot.command.openSettings": "DeepSeek: Open Settings",
78
"deepseek-copilot.command.showLogs": "DeepSeek: Show Logs",
@@ -15,7 +16,7 @@
1516
"deepseek-copilot.walkthrough.advancedSettings.title": "Try advanced settings",
1617
"deepseek-copilot.walkthrough.advancedSettings.description": "Customize providers and model names, configure the vision proxy, enable experimental settings, and more in the extension settings.\n[Open settings](command:deepseek-copilot.openSettings)",
1718
"deepseek-copilot.config.title": "DeepSeek Copilot",
18-
"deepseek-copilot.config.baseUrl.description": "DeepSeek API base URL. Defaults to official DeepSeek API endpoint.",
19+
"deepseek-copilot.config.baseUrl.description": "DeepSeek API base URL. Defaults to the official DeepSeek API endpoint. [Reset to Default](command:deepseek-copilot.resetBaseUrl)\n\n[Set / Update API Key](command:deepseek-copilot.setApiKey) · [Clear API Key](command:deepseek-copilot.clearApiKey)",
1920
"deepseek-copilot.config.requestHeaders.description": "Custom headers for chat completions. [Learn more](https://github.com/Vizards/deepseek-v4-for-copilot/blob/main/docs/settings/request-headers.en.md).",
2021
"deepseek-copilot.config.maxTokens.description": "Maximum number of output tokens per request. Set to 0 to use the API default. Useful for controlling costs.",
2122
"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.",

package.nls.zh-cn.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"deepseek-copilot.command.setApiKey": "DeepSeek: 设置 API Key",
33
"deepseek-copilot.command.getApiKey": "DeepSeek: 获取 API Key",
44
"deepseek-copilot.command.clearApiKey": "DeepSeek: 清除 API Key",
5+
"deepseek-copilot.command.resetBaseUrl": "DeepSeek: 重置 Base URL",
56
"deepseek-copilot.command.setVisionModel": "DeepSeek: 配置视觉代理",
67
"deepseek-copilot.command.openSettings": "DeepSeek: 打开设置",
78
"deepseek-copilot.command.showLogs": "DeepSeek: 显示日志",
@@ -15,7 +16,7 @@
1516
"deepseek-copilot.walkthrough.advancedSettings.title": "尝试高级设置",
1617
"deepseek-copilot.walkthrough.advancedSettings.description": "可以在插件设置中自定义提供方与模型名称、自定义视觉代理、开启实验性设置等。\n[打开设置](command:deepseek-copilot.openSettings)",
1718
"deepseek-copilot.config.title": "DeepSeek 助手",
18-
"deepseek-copilot.config.baseUrl.description": "DeepSeek API 基础 URL,默认为官方 DeepSeek API 端点。",
19+
"deepseek-copilot.config.baseUrl.description": "DeepSeek API 基础 URL,默认为官方 DeepSeek API 端点。[恢复默认](command:deepseek-copilot.resetBaseUrl)\n\n[设置/更新 API Key](command:deepseek-copilot.setApiKey) · [清除 API Key](command:deepseek-copilot.clearApiKey)",
1920
"deepseek-copilot.config.requestHeaders.description": "聊天补全请求的自定义请求头。[了解更多](https://github.com/Vizards/deepseek-v4-for-copilot/blob/main/docs/settings/request-headers.zh.md)。",
2021
"deepseek-copilot.config.maxTokens.description": "每次请求的最大输出 Token 数,设为 0 时使用 API 默认值,可用于控制成本。",
2122
"deepseek-copilot.config.experimental.stabilizeToolList.description": "**实验性功能**:通过预先激活可用的工具来提升 DeepSeek 上下文缓存命中率。\n- 当已启用工具列表跨轮次变化时,这可能提高 DeepSeek 上下文缓存命中率;\n- 请求中将包含更多函数工具定义,input tokens 可能增加,虽然缓存命中的 input tokens 单价更低,但仍会计入用量;\n- 此设置可能会在当前 Copilot 对话历史中加入内部预检工具调用。在同一对话中切换到其他模型时,部分模型提供方可能无法正确重放这段历史;如果切换模型后请求异常,请新建对话后重试。\n\n 通过 [配置工具](command:workbench.action.chat.configureTools) **查看和管理**工具列表:\n\n- 64 个或更少已启用工具时通常无需开启,除非工具列表仍在跨轮次变化;\n- 超过 128 个已启用工具时不建议开启:DeepSeek 单次 `tools` 请求最多支持 128 个 functions。考虑禁用部分不常用工具。",

src/client/error/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ export function createUserFacingError(error: Error): Error {
158158
return displayError;
159159
}
160160

161+
export function createApiKeyNotConfiguredError(): Error {
162+
return createUserFacingError(
163+
new Error(
164+
formatMarkdownMessage(
165+
t('auth.notConfigured'),
166+
getConfigureApiKeyActions(errorActionUrlStore.get()),
167+
),
168+
),
169+
);
170+
}
171+
161172
function getHttpErrorMessage(status: number, createApiKeyUrl?: string): string {
162173
switch (status) {
163174
case 400:
@@ -253,8 +264,12 @@ function getUniversalHttpErrorActions(
253264
status: number,
254265
actionUrls: ErrorActionUrls,
255266
): readonly ErrorActionLink[] {
267+
return status === 401 ? getConfigureApiKeyActions(actionUrls) : [];
268+
}
269+
270+
function getConfigureApiKeyActions(actionUrls: ErrorActionUrls): readonly ErrorActionLink[] {
256271
const url = actionUrls.configureApiKey;
257-
return status === 401 && url ? [{ labelKey: 'error.action.setApiKey', url }] : [];
272+
return url ? [{ labelKey: 'error.action.setApiKey', url }] : [];
258273
}
259274

260275
function getProviderHttpErrorActions(status: number, baseUrl: string): readonly ErrorActionLink[] {

src/client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { DeepSeekClient } from './core';
22
export {
3+
createApiKeyNotConfiguredError,
34
createHttpError,
45
createUserFacingError,
56
DeepSeekRequestError,

src/i18n.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,21 @@ const zh: Translations = {
4949
'auth.emptyValidation': 'API Key 不能为空',
5050
'auth.saved': 'API Key 已安全保存。',
5151
'auth.removed': 'API Key 已移除。',
52-
'auth.notConfigured': 'API Key 未配置,请在命令面板运行 "DeepSeek: 设置 API Key"。',
52+
'auth.clearConfirm': '清除已保存的 API Key?',
53+
'auth.clearDetail': '清除后,再次使用时可能需要重新设置 API Key。',
54+
'auth.clearAction': '清除 API Key',
55+
'auth.notConfigured': '尚未配置 API Key,请先设置 API Key 后重试。',
56+
57+
// Settings
58+
'settings.resetBaseUrl.title': '恢复 DeepSeek Base URL 默认配置',
59+
'settings.resetBaseUrl.chooseScope': '选择要重置的范围;仅移除该范围的自定义地址',
60+
'settings.resetBaseUrl.user': '用户设置',
61+
'settings.resetBaseUrl.workspace': '当前工作区',
62+
'settings.resetBaseUrl.current': '当前配置:{0}',
63+
'settings.resetBaseUrl.notConfigured': '未自定义,无需重置',
64+
'settings.resetBaseUrl.after': '重置后生效地址:{0}',
65+
'settings.resetBaseUrl.afterInherited': '重置后继承其余配置,实际生效地址请查看设置',
66+
'settings.resetBaseUrl.failed': '重置 Base URL 失败,请运行 "DeepSeek: 显示日志" 查看详情。',
5367

5468
// Thinking Effort — short labels for model picker dropdown
5569
'status.thinking': '思考模式',
@@ -261,8 +275,23 @@ const en: Translations = {
261275
'auth.emptyValidation': 'API key cannot be empty',
262276
'auth.saved': 'DeepSeek API key saved.',
263277
'auth.removed': 'DeepSeek API key removed.',
264-
'auth.notConfigured':
265-
'DeepSeek API key not configured. Run "DeepSeek: Set API Key" from the Command Palette.',
278+
'auth.clearConfirm': 'Clear the saved API Key?',
279+
'auth.clearDetail': 'You may need to set an API Key again before using the extension.',
280+
'auth.clearAction': 'Clear API Key',
281+
'auth.notConfigured': 'DeepSeek API key is not configured. Set an API key and try again.',
282+
283+
// Settings
284+
'settings.resetBaseUrl.title': 'Reset DeepSeek Base URL',
285+
'settings.resetBaseUrl.chooseScope': 'Choose a scope; only its custom URL will be removed',
286+
'settings.resetBaseUrl.user': 'User Settings',
287+
'settings.resetBaseUrl.workspace': 'Current Workspace',
288+
'settings.resetBaseUrl.current': 'Configured URL: {0}',
289+
'settings.resetBaseUrl.notConfigured': 'Not customized; no reset needed',
290+
'settings.resetBaseUrl.after': 'Effective URL after reset: {0}',
291+
'settings.resetBaseUrl.afterInherited':
292+
'Inherits remaining configuration after reset; check Settings for the effective URL',
293+
'settings.resetBaseUrl.failed':
294+
'Failed to reset Base URL. Run "DeepSeek: Show Logs" for details.',
266295

267296
// Thinking Effort
268297
'status.thinking': 'Thinking Effort',

src/provider/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ export class DeepSeekChatProvider implements vscode.LanguageModelChatProvider {
9090
}
9191

9292
async clearApiKey(): Promise<void> {
93+
const clearAction = t('auth.clearAction');
94+
const selected = await vscode.window.showWarningMessage(
95+
t('auth.clearConfirm'),
96+
{ modal: true, detail: t('auth.clearDetail') },
97+
clearAction,
98+
);
99+
if (selected !== clearAction) {
100+
return;
101+
}
102+
93103
await this.authManager.deleteApiKey();
94104
this.invalidateCurrencyAndRefreshModels();
95105
vscode.window.showInformationMessage(t('auth.removed'));

src/provider/request.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import vscode from 'vscode';
22
import { AuthManager } from '../auth';
3-
import { DeepSeekClient } from '../client';
3+
import { createApiKeyNotConfiguredError, DeepSeekClient } from '../client';
44
import { getApiModelId, getBaseUrl, getMaxTokens, getRequestHeaders } from '../config';
55
import { MODELS } from '../consts';
66
import { isOfficialDeepSeekBaseUrl } from '../endpoint';
7-
import { t } from '../i18n';
87
import type { DeepSeekRequest } from '../types';
98
import { convertMessages, countMessageChars } from './convert';
109
import {
@@ -66,7 +65,7 @@ export async function prepareChatRequest({
6665
}: PrepareChatRequestOptions): Promise<PreparedChatRequest> {
6766
const apiKey = await authManager.getApiKey();
6867
if (!apiKey) {
69-
throw new Error(t('auth.notConfigured'));
68+
throw createApiKeyNotConfiguredError();
7069
}
7170

7271
const baseUrl = getBaseUrl();

src/runtime/commands.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import vscode from 'vscode';
2-
import { EXTERNAL_URLS } from '../consts';
2+
import { CONFIG_SECTION, EXTERNAL_URLS } from '../consts';
33
import { t } from '../i18n';
44
import { logger } from '../logger';
55
import { ensureRequestDumpRoot } from '../provider/debug';
66

7+
interface ResetTarget extends vscode.QuickPickItem {
8+
target: vscode.ConfigurationTarget;
9+
}
10+
711
export function registerCommands(context: vscode.ExtensionContext): void {
812
context.subscriptions.push(
13+
vscode.commands.registerCommand('deepseek-copilot.resetBaseUrl', resetBaseUrl),
914
vscode.commands.registerCommand('deepseek-copilot.showLogs', () => logger.show()),
1015
vscode.commands.registerCommand('deepseek-copilot.openRequestDumpsFolder', () =>
1116
openRequestDumpsFolder(context),
@@ -19,6 +24,82 @@ export function registerCommands(context: vscode.ExtensionContext): void {
1924
);
2025
}
2126

27+
async function resetBaseUrl(): Promise<void> {
28+
try {
29+
const config = vscode.workspace.getConfiguration(CONFIG_SECTION);
30+
const inspection = config.inspect<string>('baseUrl');
31+
const hasWorkspace = Boolean(
32+
vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.length,
33+
);
34+
const userValue = inspection?.globalValue;
35+
const workspaceValue = inspection?.workspaceValue;
36+
const targets: ResetTarget[] = [
37+
{
38+
label: t('settings.resetBaseUrl.user'),
39+
target: vscode.ConfigurationTarget.Global,
40+
description:
41+
userValue === undefined
42+
? t('settings.resetBaseUrl.notConfigured')
43+
: t('settings.resetBaseUrl.current', userValue),
44+
// Public inspection merges local and remote user values, hiding a possible fallback.
45+
detail:
46+
userValue === undefined
47+
? undefined
48+
: vscode.env.remoteName && workspaceValue === undefined
49+
? t('settings.resetBaseUrl.afterInherited')
50+
: t(
51+
'settings.resetBaseUrl.after',
52+
(workspaceValue ?? inspection?.defaultValue) || 'https://api.deepseek.com',
53+
),
54+
},
55+
// baseUrl has window scope: folder overrides do not apply in a multi-root workspace.
56+
...(hasWorkspace
57+
? [
58+
{
59+
label: t('settings.resetBaseUrl.workspace'),
60+
target: vscode.ConfigurationTarget.Workspace,
61+
description:
62+
workspaceValue === undefined
63+
? t('settings.resetBaseUrl.notConfigured')
64+
: t('settings.resetBaseUrl.current', workspaceValue),
65+
detail:
66+
workspaceValue === undefined
67+
? undefined
68+
: t(
69+
'settings.resetBaseUrl.after',
70+
(userValue ?? inspection?.defaultValue) || 'https://api.deepseek.com',
71+
),
72+
},
73+
]
74+
: []),
75+
];
76+
77+
// Markdown command links do not carry the active Settings tab's scope.
78+
const selected = await vscode.window.showQuickPick(targets, {
79+
title: t('settings.resetBaseUrl.title'),
80+
placeHolder: t('settings.resetBaseUrl.chooseScope'),
81+
});
82+
if (!selected) {
83+
return;
84+
}
85+
86+
const current = vscode.workspace.getConfiguration(CONFIG_SECTION).inspect<string>('baseUrl');
87+
const selectedValue =
88+
selected.target === vscode.ConfigurationTarget.Global
89+
? current?.globalValue
90+
: current?.workspaceValue;
91+
if (selectedValue === undefined) {
92+
return;
93+
}
94+
95+
// Global uses VS Code's user-setting routing, including remote user overrides.
96+
await config.update('baseUrl', undefined, selected.target);
97+
} catch (error) {
98+
logger.warn('Failed to reset DeepSeek base URL', error);
99+
void vscode.window.showErrorMessage(t('settings.resetBaseUrl.failed'));
100+
}
101+
}
102+
22103
async function openRequestDumpsFolder(context: vscode.ExtensionContext): Promise<void> {
23104
try {
24105
const root = await ensureRequestDumpRoot(context.globalStorageUri);

0 commit comments

Comments
 (0)