Integrating DeepSeek-R1 with VS Code: HTTP API Extension Development


- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
Most development teams now use AI-assisted coding tools, but DeepSeek-R1 stands apart from the majority. This tutorial walks through building a fully functional VS Code extension from scratch that sends editor context to DeepSeek-R1's HTTP API and returns intelligent code completions and chat responses directly inside the editor.
How to Integrate DeepSeek-R1 with VS Code via a Custom Extension
- Scaffold a new TypeScript VS Code extension using the Yeoman generator (
yo code). - Configure
package.jsonwith activation events, commands, keybindings, and the minimum VS Code engine version. - Build a DeepSeek-R1 API client module with native
fetch, timeout handling, and SSE streaming support. - Store the API key securely using VS Code's
SecretStorageAPI backed by the OS credential store. - Implement a Webview-based chat panel that streams tokens progressively from the DeepSeek-R1 API.
- Register a debounced inline completion provider that sends editor context as prefix/suffix to the API.
- Add a context-menu command that sends selected code for explanation and displays results in an output channel.
- Package the extension as a VSIX using
vsceand publish it to the VS Code Marketplace.
Table of Contents
- Why Build a Custom DeepSeek-R1 VS Code Extension?
- Understanding the Architecture
- Scaffolding the VS Code Extension Project
- Building the DeepSeek-R1 API Service Layer
- Implementing Core Extension Features
- Error Handling, Security, and Best Practices
- Testing and Debugging the Extension
- Packaging and Publishing
- Implementation Checklist
- Next Steps
Why Build a Custom DeepSeek-R1 VS Code Extension?
Most development teams now use AI-assisted coding tools, but DeepSeek-R1 stands apart from the majority. It ships with open weights and chain-of-thought reasoning, and it ranks in the top tier on benchmarks like SWE-bench Lite for coding tasks. These properties make it a strong fit for developer tooling. Building a custom VS Code extension that integrates DeepSeek-R1 via its HTTP API gives developers direct control over prompts, context windows, and the user experience in ways that generic AI plugins cannot.
Off-the-shelf AI extensions impose limitations: rigid prompt templates (GitHub Copilot, for example, does not expose its prompt structure for editing), opaque data handling that raises privacy concerns, and vendor lock-in that couples the workflow to a single provider's ecosystem. A custom extension sidesteps all of these constraints.
Building a custom VS Code extension that integrates DeepSeek-R1 via its HTTP API gives developers direct control over prompts, context windows, and the user experience in ways that generic AI plugins cannot.
This tutorial walks through building a fully functional VS Code extension from scratch. The extension sends editor context to DeepSeek-R1's HTTP API and returns intelligent code completions and chat responses directly inside the editor. By the end, the extension supports a chat panel via Webview, inline code completions, and a context menu for code explanations.
You will need: Node.js 18+ (note: native fetch is experimental in some Node.js 18 patch versions and may require the --experimental-fetch flag; it is stable in Node.js 21+), VS Code 1.85+, a DeepSeek API key (obtained from the DeepSeek platform), basic familiarity with TypeScript and VS Code extension APIs, and Git for version control.
Understanding the Architecture
How VS Code Extensions Work
VS Code extensions run inside a dedicated extension host process, isolated from the main editor process. Extensions declare activation events that determine when they load, and contribution points in package.json that define commands, menus, keybindings, configuration, and other UI surface area.
The key APIs relevant to this project include vscode.commands for registering executable commands, vscode.languages for providing completions, vscode.window for creating Webview panels and showing notifications, and vscode.workspace for reading configuration settings.
DeepSeek-R1 HTTP API Overview
The DeepSeek-R1 API follows an OpenAI-compatible format. The primary endpoint is /v1/chat/completions, hosted at https://api.deepseek.com. Requests use a JSON body with key parameters: model (set to "deepseek-reasoner" for R1), messages (an array of role/content objects), max_tokens, and stream (a boolean that enables Server-Sent Events for progressive output). Note: temperature is not supported by deepseek-reasoner; omit it when using this model. Authentication uses a bearer token passed in the Authorization header.
Developers should account for rate limits and per-token pricing when designing the extension's request patterns. Unbounded inline completion triggers, for example, can generate hundreds of API calls per hour during active editing. Check per-token pricing at the DeepSeek platform and consider providing a user-facing toggle in settings to enable or disable inline completions so that developers can control costs.
Data Flow
The architecture follows a straightforward pipeline: extension logic captures editor context, transforms it into an HTTP request with appropriate prompt formatting, sends it to the DeepSeek-R1 endpoint, and renders the parsed response back into the VS Code UI through a Webview panel, inline completion items, or an output channel.
Scaffolding the VS Code Extension Project
Generating the Extension Boilerplate
The Yeoman generator for VS Code extensions provides the fastest path to a working project structure. The following commands install the necessary tools and scaffold a TypeScript extension:
npm install -g yo generator-code
yo code
When prompted, select New Extension (TypeScript), provide a name such as deepseek-r1-assistant, and accept the defaults. Once scaffolding completes:
cd deepseek-r1-assistant
code .
Press F5 inside VS Code to launch the Extension Development Host and verify the default "Hello World" command works.
The generated project structure includes src/extension.ts (the entry point), package.json (manifest and contribution points), and tsconfig.json. The package.json needs to be updated to register the project's custom commands, keybindings, and activation events. The following snippet shows the fields to merge into the generated package.json (do not replace the entire file):
{
"engines": {
"vscode": "^1.85.0"
},
"activationEvents": [
"onCommand:deepseek.askQuestion",
"onCommand:deepseek.explainCode"
],
"contributes": {
"commands": [
{
"command": "deepseek.askQuestion",
"title": "DeepSeek: Ask Question"
},
{
"command": "deepseek.explainCode",
"title": "DeepSeek: Explain Selection"
}
],
"keybindings": [
{
"command": "deepseek.askQuestion",
"key": "ctrl+shift+d",
"mac": "cmd+shift+d"
}
]
}
}
The activationEvents array tells VS Code when to load the extension. Without it, registered commands will not appear in the Command Palette. The engines.vscode field declares the minimum VS Code version and is required for packaging and publishing.
Installing Dependencies
Node.js 18+ includes native fetch, so an external HTTP library is not strictly required. The extension reads its API key from VS Code settings or SecretStorage (see "Secure API Key Storage" below). For local development only, you can use dotenv to load the key from a .env file, but you must explicitly import it in your entry point:
npm install dotenv
npm install --save-dev @types/node
Create a .env file at the project root (and add it to .gitignore immediately):
DEEPSEEK_API_KEY=your_api_key_here
Then add the following line at the very top of src/extension.ts so that dotenv actually loads the file during local development:
import 'dotenv/config';
Important: Do not use the dotenv approach for published extensions. For distribution, use VS Code's SecretStorage as described in the security section below. The .env approach is strictly a local development convenience.
Building the DeepSeek-R1 API Service Layer
Creating the API Client Module
Encapsulating all HTTP logic in a dedicated module keeps the codebase maintainable. Create src/deepseekClient.ts:
import * as vscode from 'vscode';
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface ChatCompletionOptions {
max_tokens?: number;
}
interface ChatCompletionResponse {
id: string;
choices: {
message: { role: string; content: string };
finish_reason: string;
}[];
usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
}
const BASE_URL = 'https://api.deepseek.com';
const TIMEOUT_MS = 60000;
let _secretStorage: vscode.SecretStorage | undefined;
export function initSecretStorage(secrets: vscode.SecretStorage) {
_secretStorage = secrets;
}
export async function getApiKey(): Promise<string> {
// Prefer SecretStorage (OS credential store)
if (_secretStorage) {
const secret = await _secretStorage.get('deepseekR1.apiKey');
if (secret) return secret;
}
// Fallback: legacy plaintext setting (deprecated path)
const config = vscode.workspace.getConfiguration('deepseekR1');
const key = config.get<string>('apiKey');
if (key) return key;
throw new Error(
'DeepSeek API key not configured. Run "DeepSeek: Set API Key" to configure it.'
);
}
export async function sendChatCompletion(
messages: ChatMessage[],
options: ChatCompletionOptions = {}
): Promise<ChatCompletionResponse> {
const apiKey = await getApiKey();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'deepseek-reasoner',
messages,
max_tokens: options.max_tokens ?? 2048,
stream: false,
}),
signal: controller.signal,
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`DeepSeek API error ${response.status}: ${errorBody}`);
}
let data: ChatCompletionResponse;
try {
data = (await response.json()) as ChatCompletionResponse;
} catch {
throw new Error('DeepSeek API returned malformed JSON in the response body.');
}
return data;
} catch (error: unknown) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`DeepSeek API request timed out after ${TIMEOUT_MS}ms`);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
This module handles network failures, HTTP 4xx/5xx responses, and request timeouts using AbortController. Malformed JSON in the response body causes a descriptive error rather than a generic SyntaxError. The getApiKey() function checks SecretStorage first and falls back to the plaintext configuration setting for backward compatibility.
Handling Streaming Responses
Streaming matters for UX. Without it, users stare at a blank panel for 5 to 15 seconds (depending on prompt complexity) while DeepSeek-R1 completes its chain-of-thought reasoning. Server-Sent Events let the extension render tokens progressively.
Add a streaming function to the same module:
export async function* streamChatCompletion(
messages: ChatMessage[],
options: ChatCompletionOptions = {}
): AsyncGenerator<string> {
const apiKey = await getApiKey();
const controller = new AbortController();
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
// Per-chunk inactivity timeout: resets on each received chunk.
// If no data arrives within this window, the connection is aborted.
const CHUNK_TIMEOUT_MS = 30_000;
let chunkTimer: ReturnType<typeof setTimeout>;
const resetTimer = () => {
clearTimeout(chunkTimer);
chunkTimer = setTimeout(() => controller.abort(), CHUNK_TIMEOUT_MS);
};
resetTimer(); // Start timer before initial connection
try {
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'deepseek-reasoner',
messages,
max_tokens: options.max_tokens ?? 2048,
stream: true,
}),
signal: controller.signal,
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`DeepSeek API error ${response.status}: ${errorBody}`);
}
if (!response.body) {
throw new Error(
'DeepSeek API response body is null. ' +
'Ensure your Node.js version supports streaming fetch (21+ recommended).'
);
}
reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
resetTimer(); // Reset on each received chunk
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('
');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('data: ')) continue;
const data = trimmed.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) yield content;
} catch {
// Skip malformed SSE chunks
}
}
}
} catch (error: unknown) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(
`DeepSeek stream inactive for ${CHUNK_TIMEOUT_MS}ms — connection aborted.`
);
}
throw error;
} finally {
clearTimeout(chunkTimer!);
try {
if (reader) await reader.cancel();
} catch {
/* ignore */
}
}
}
Node.js compatibility note: response.body.getReader() relies on Web Streams support in the Node.js fetch implementation. If you encounter issues on certain Node.js 18 patch versions, consider using for await (const chunk of response.body) as a more portable alternative, or upgrade to Node.js 21+.
This async generator parses SSE chunks line by line, extracts the delta.content field from each JSON payload, and yields tokens incrementally. The per-chunk inactivity timeout resets on every received chunk, so slow but active streams stay alive while completely stalled connections abort after 30 seconds. The finally block ensures the stream reader is cancelled and the underlying connection is released, even if the generator is abandoned mid-stream.
Configuration Management
Rather than relying on .env files in production, the extension should declare its settings in package.json and read them through the VS Code configuration API. For the API key specifically, use SecretStorage as described in the security section below. The following configuration block handles non-secret settings. Merge this into the contributes section of your package.json:
{
"contributes": {
"configuration": {
"title": "DeepSeek R1",
"properties": {
"deepseekR1.maxTokens": {
"type": "number",
"default": 2048,
"description": "Maximum tokens in API responses"
}
}
}
}
}
Security note: The API key is stored exclusively via VS Code's SecretStorage API (see below), which uses the OS credential store. It is not declared as a configuration property to avoid accidental exposure in settings.json or Settings Sync.
The getApiKey() helper shown earlier in the API client checks SecretStorage first and falls back to the legacy configuration setting for backward compatibility.
Implementing Core Extension Features
Feature 1: Chat Panel (Webview)
The chat panel provides a conversational interface. Create src/chatPanel.ts:
import * as vscode from 'vscode';
import { streamChatCompletion } from './deepseekClient';
const MAX_INPUT_LENGTH = 32_000;
export function registerChatPanel(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('deepseek.askQuestion', () => {
const panel = vscode.window.createWebviewPanel(
'deepseekChat',
'DeepSeek R1 Chat',
vscode.ViewColumn.Beside,
{ enableScripts: true, retainContextWhenHidden: true }
);
const nonce = getNonce();
panel.webview.html = getChatHtml(nonce);
const messageSubscription = panel.webview.onDidReceiveMessage(async (message) => {
if (message.type === 'prompt') {
const text = typeof message.text === 'string'
? message.text.slice(0, MAX_INPUT_LENGTH)
: '';
if (!text) return;
try {
let fullResponse = '';
const stream = streamChatCompletion([
{ role: 'system', content: 'You are a helpful coding assistant.' },
{ role: 'user', content: text },
]);
for await (const token of stream) {
fullResponse += token;
panel.webview.postMessage({ type: 'token', content: token });
}
panel.webview.postMessage({ type: 'done' });
} catch (error: unknown) {
panel.webview.postMessage({
type: 'error',
content: error instanceof Error ? error.message : String(error),
});
}
}
});
panel.onDidDispose(() => messageSubscription.dispose());
})
);
}
function getNonce(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('');
}
function getChatHtml(nonce: string): string {
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline';">
<style>
body { font-family: var(--vscode-font-family); padding: 10px; }
#output { white-space: pre-wrap; margin-bottom: 10px; min-height: 100px;
border: 1px solid var(--vscode-input-border); padding: 8px; }
#input { width: 80%; padding: 6px; }
button { padding: 6px 12px; }
</style>
</head>
<body>
<div id="output"></div>
<input id="input" placeholder="Ask DeepSeek R1..." />
<button id="sendBtn">Send</button>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const output = document.getElementById('output');
function send() {
const input = document.getElementById('input');
output.textContent = '';
vscode.postMessage({ type: 'prompt', text: input.value });
input.value = '';
}
document.getElementById('sendBtn').addEventListener('click', send);
document.getElementById('input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') send();
});
window.addEventListener('message', (e) => {
const msg = e.data;
if (msg.type === 'token') output.textContent += msg.content;
if (msg.type === 'error') output.textContent = 'Error: ' + msg.content;
});
</script>
</body>
</html>`;
}
The Webview posts user messages to the extension host, which streams responses back token by token. The retainContextWhenHidden: true option preserves chat history when the user switches tabs (at the cost of additional memory). The Content Security Policy restricts script execution to the nonced inline script, mitigating XSS risks. The nonce is generated using crypto.getRandomValues to ensure it is cryptographically unpredictable. Input from the Webview is length-capped before being forwarded to the API. The message listener subscription is disposed when the panel closes to prevent resource leaks.
Note: The chat panel uses textContent for rendering, which is safe from XSS but means Markdown and code formatting in DeepSeek responses will not be rendered. For a richer experience, consider integrating a sanitized Markdown renderer such as marked with DOMPurify.
Feature 2: Inline Code Completion Provider
Create src/completionProvider.ts to register an InlineCompletionItemProvider:
import * as vscode from 'vscode';
import { sendChatCompletion } from './deepseekClient';
export class DeepSeekCompletionProvider implements vscode.InlineCompletionItemProvider {
private debounceTimer: NodeJS.Timeout | undefined;
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
_context: vscode.InlineCompletionContext,
token: vscode.CancellationToken
): Promise<vscode.InlineCompletionItem[]> {
// Debounce: wait 500ms after last keystroke.
// The promise resolves to `true` if the timer completed,
// or `false` if cancellation fired first.
const debounced = await new Promise<boolean>((resolve) => {
if (this.debounceTimer) clearTimeout(this.debounceTimer);
const onCancel = token.onCancellationRequested(() => {
clearTimeout(this.debounceTimer);
onCancel.dispose();
resolve(false);
});
this.debounceTimer = setTimeout(() => {
onCancel.dispose();
resolve(true);
}, 500);
});
if (!debounced || token.isCancellationRequested) return [];
const prefix = document.getText(
new vscode.Range(new vscode.Position(Math.max(0, position.line - 50), 0), position)
);
const suffixEndLine = Math.min(position.line + 10, document.lineCount - 1);
const suffixEndChar = document.lineAt(suffixEndLine).range.end.character;
const suffix = document.getText(
new vscode.Range(
position,
new vscode.Position(suffixEndLine, suffixEndChar)
)
);
try {
const response = await sendChatCompletion(
[
{
role: 'system',
content: `You are a code completion engine. Given the code prefix and suffix, output ONLY the code that should be inserted at the cursor. No explanations. Language: ${document.languageId}`,
},
{
role: 'user',
content: `PREFIX:
${prefix}
SUFFIX:
${suffix}
COMPLETION:`,
},
],
{ max_tokens: 256 }
);
const text = response.choices[0]?.message?.content?.trim();
if (!text) return [];
return [new vscode.InlineCompletionItem(text, new vscode.Range(position, position))];
} catch {
return [];
}
}
}
The provider extracts up to 50 lines of prefix and 10 lines of suffix from the cursor position, constructs a system prompt tuned for code completion, and returns the result as an InlineCompletionItem. The debounce logic uses a promise that resolves to a boolean: true when the timer fires normally, false when cancellation is requested. This ensures that a cancelled debounce causes the method to return early immediately, rather than falling through to the API call. The suffix range now extends to the end of the last line rather than column 0, ensuring the full context is captured.
Without it, users stare at a blank panel for 5 to 15 seconds (depending on prompt complexity) while DeepSeek-R1 completes its chain-of-thought reasoning. Server-Sent Events let the extension render tokens progressively.
Feature 3: Code Explanation via Context Menu
Add a context menu item by updating package.json:
{
"contributes": {
"menus": {
"editor/context": [
{
"command": "deepseek.explainCode",
"when": "editorHasSelection",
"group": "navigation"
}
]
}
}
}
Then register the command in src/extension.ts:
import * as vscode from 'vscode';
import { registerChatPanel } from './chatPanel';
import { DeepSeekCompletionProvider } from './completionProvider';
import { sendChatCompletion, initSecretStorage } from './deepseekClient';
export function activate(context: vscode.ExtensionContext) {
// Initialize SecretStorage for secure API key retrieval
initSecretStorage(context.secrets);
registerChatPanel(context);
context.subscriptions.push(
vscode.languages.registerInlineCompletionItemProvider(
{ pattern: '**' },
new DeepSeekCompletionProvider()
)
);
// Create the output channel once and reuse it across invocations
const explanationChannel = vscode.window.createOutputChannel('DeepSeek Explanation');
context.subscriptions.push(explanationChannel);
context.subscriptions.push(
vscode.commands.registerCommand('deepseek.explainCode', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const selection = editor.document.getText(editor.selection);
if (!selection) return;
explanationChannel.clear();
explanationChannel.show();
explanationChannel.appendLine('Explaining selected code...
');
try {
const response = await sendChatCompletion([
{ role: 'system', content: 'Explain the following code clearly and concisely.' },
{ role: 'user', content: selection },
]);
explanationChannel.appendLine(
response.choices[0]?.message?.content || 'No response.'
);
} catch (error: unknown) {
explanationChannel.appendLine(
`Error: ${error instanceof Error ? error.message : String(error)}`
);
}
})
);
}
export function deactivate() {}
Error Handling, Security, and Best Practices
Secure API Key Storage
For production distribution, storing the API key in VS Code's SecretStorage is far preferable to plaintext settings or .env files. SecretStorage uses the operating system's native credential store:
// Store
await context.secrets.store('deepseekR1.apiKey', apiKeyValue);
// Retrieve
const apiKey = await context.secrets.get('deepseekR1.apiKey');
if (!apiKey) {
vscode.window.showErrorMessage('No DeepSeek API key found. Please configure it.');
}
When shipping the extension, prompt users to enter their key via an input box on first activation and persist it through SecretStorage. The getApiKey() function in the API client module checks SecretStorage first, falling back to the configuration setting for backward compatibility. The initSecretStorage() call in activate() wires this up.
Graceful Degradation
Add timeout handling to every API call (already implemented via AbortController in the client). For transient errors, retry logic with exponential backoff prevents cascading failures. For example, wrap the fetch call in a loop that retries up to three times on 429 or 5xx responses, doubling the delay each time from an initial 1-second wait. Surface user-facing errors through vscode.window.showErrorMessage with actionable text rather than letting them fail silently.
Performance Considerations
The inline completion provider's 500ms debounce is critical. Without it, every keystroke could trigger an API call. Cache recent completions for identical context strings to avoid redundant requests. Monitor your DeepSeek API usage closely: each inline completion consumes roughly a few thousand tokens, and during active editing you may fire dozens of completions per hour. Check the DeepSeek pricing page for current per-token rates.
The inline completion provider's 500ms debounce is critical. Without it, every keystroke could trigger an API call.
Testing and Debugging the Extension
Running in the Extension Development Host
Pressing F5 in VS Code launches a new window with the extension loaded. The Debug Console in the original window displays console.log output and exceptions from the extension host process. This is the primary feedback loop during development.
Writing Unit Tests for the API Client
Mocking HTTP responses allows testing prompt construction and response parsing without hitting the live API. The vscode module is not available in a plain Node.js Jest environment, so it must be mocked. Create a Jest configuration that maps the module, and mock both fetch and the vscode API:
First, add the following to your jest.config.js (or the "jest" section of package.json):
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^vscode$': '<rootDir>/__mocks__/vscode.ts',
},
};
Create __mocks__/vscode.ts:
export const workspace = {
getConfiguration: jest.fn(() => ({
get: jest.fn(() => 'test-api-key'),
})),
};
export const window = {
showErrorMessage: jest.fn(),
createOutputChannel: jest.fn(),
};
Then write the tests:
import { sendChatCompletion } from '../src/deepseekClient';
describe('sendChatCompletion', () => {
const mockFetch = jest.fn();
beforeEach(() => {
(global as any).fetch = mockFetch;
});
afterEach(() => {
mockFetch.mockReset();
delete (global as any).fetch;
});
it('returns parsed response on success', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
id: 'test-id',
choices: [
{ message: { role: 'assistant', content: 'Hello' }, finish_reason: 'stop' },
],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}),
});
const result = await sendChatCompletion([{ role: 'user', content: 'Hi' }]);
expect(result.choices[0].message.content).toBe('Hello');
});
it('throws on HTTP error', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 429,
text: async () => 'Rate limited',
});
await expect(
sendChatCompletion([{ role: 'user', content: 'Hi' }])
).rejects.toThrow('DeepSeek API error 429');
});
});
Run the tests with:
npm install --save-dev jest ts-jest @types/jest
npx jest
Packaging and Publishing
Building the VSIX Package
The vsce tool packages extensions for distribution. Before packaging, ensure:
.envis excluded via.vscodeignore(add the line.envto that file)- The
engines.vscodefield inpackage.jsonis set to"^1.85.0"(or your target version) - All dependencies are production-ready
npm install -g @vscode/vsce
vsce package
This produces a .vsix file you can share directly or upload to the marketplace.
Publishing to the VS Code Marketplace
Create a publisher account on the Visual Studio Marketplace. Generate a Personal Access Token from Azure DevOps scoped to Marketplace > Manage, following the current vsce publishing guide as this process is subject to change. Then run vsce publish. Include a clear README, screenshots of the chat panel and inline completions, and a changelog so users can find and evaluate your extension.
Implementation Checklist
- [ ] Install Node.js 18+ and VS Code 1.85+
- [ ] Scaffold the extension with
yo code - [ ] Add
activationEventsandengines.vscodetopackage.json - [ ] Obtain a DeepSeek API key and store it in VS Code SecretStorage
- [ ] Build the API client module with error handling and streaming support
- [ ] Wire up the chat Webview panel (with CSP and
retainContextWhenHidden) - [ ] Register and debounce the inline completion provider
- [ ] Connect the context-menu "Explain" command
- [ ] Pass unit tests for the API client (with
vscodemodule mock) - [ ] Test the extension in the Development Host
- [ ] Confirm
.vscodeignoreexcludes.envand development files - [ ] Package the VSIX and publish to the Marketplace
Next Steps
This tutorial covered building a VS Code extension that integrates DeepSeek-R1 through its HTTP API, including a streaming chat panel, inline code completions with debouncing, a context menu for code explanations, secure API key storage, and testing strategies. Before publishing, complete the security hardening steps (SecretStorage, Webview CSP) and validate in the Extension Development Host.
From here, consider adding multi-model support so users can switch between DeepSeek-R1 and other OpenAI-compatible providers. You could also implement retrieval-augmented generation by indexing workspace files for richer context, or build a prompt templates system that adapts to different coding tasks. Start with the DeepSeek API documentation and the VS Code Extension API reference.
Matt MickiewiczMatt is the co-founder of SitePoint, 99designs and Flippa. He lives in Vancouver, Canada.