Calling DeepSeek V3 and R1 from TypeScript and Bun
When you run coding agents or automated pull-request reviews in a loop, token pricing quickly becomes the ceiling on how many iterations you can afford to run.
| Model | Input Price (USD per million tokens) | Output Price (USD per million tokens) |
|---|---|---|
| GPT-4.5 | $10 | $30 |
| Claude 3.7 | $3 | $15 |
| DeepSeek V3 | $0.07 | $1.10 |
DeepSeek V3 prices input at $0.07 per million tokens and output at $1.10 per million tokens, and cuts both rates by 50% during off-peak hours between 16:30 and 00:30 UTC. You can check the full rate table in the DeepSeek Pricing Details and the model changes in the V3 Release Notes.
Set up the API client in Bun
Bun includes fetch and automatic .env loading out of the box, so you can call DeepSeek's /chat/completions endpoint without installing an external SDK or dotenv. Write your key to .env:
# Create .env file with your API key
echo "DEEPSEEK_API_KEY=your_api_key_here" > .env
The endpoint accepts two model strings: deepseek-chat routes to DeepSeek V3 for fast code generation and interactive queries, while deepseek-reasoner routes to DeepSeek R1 when a task needs multi-step reasoning and analytical verification. The response payload returns the generated message alongside token usage counts and model metadata:
// No need to import fetch or dotenv as they're built into Bun
const apiKey = process.env.DEEPSEEK_API_KEY;
export async function callDeepSeekAPI(
model: "deepseek-chat" | "deepseek-reasoner",
message: string
) {
try {
// Make the API request
const response = await fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: model,
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: message },
],
stream: false,
}),
});
// Handle HTTP errors
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Parse and return the JSON response
const data = await response.json();
return data;
} catch (error) {
// Handle any errors that occur during the API call
console.error("Error calling DeepSeek API:", error);
throw error;
}
}
Generate a typed React component
For single-pass UI generation, call deepseek-chat and read the source string from response.choices[0].message.content:
import { callDeepSeekAPI } from "../services/deepseekService";
async function generateTaskComponent() {
const message = `Create a React component for a task list with:
1. Completion checkbox
2. Task text
3. Delete button
4. State management using hooks
5. Proper TypeScript types
6. Accessibility features`;
const response = await callDeepSeekAPI("deepseek-chat", message);
// The complete component code
return response.choices[0].message.content;
}
// Usage
const componentCode = await generateTaskComponent();
console.log("Generated Component:", componentCode);
Parse server-sent event streams
Waiting for a full completion stalls interactive CLI tools and chat UIs on long responses. Passing stream: true switches the HTTP response to server-sent events, where each decoded chunk contains one or more data: lines followed by a terminal data: [DONE] marker. Make sure your loop skips [DONE] before calling JSON.parse, or the stream parser will throw an exception on the final frame:
export async function callDeepSeekAPIStream(
model: "deepseek-chat" | "deepseek-reasoner",
message: string,
onChunk: (chunk: string) => void,
onComplete: () => void,
onError: (error: Error) => void
) {
try {
const response = await fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`,
},
body: JSON.stringify({
model: model,
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: message },
],
stream: true,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Process the streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
if (value) {
const chunk = decoder.decode(value);
// Parse and process each chunk
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data:')) {
const data = line.substring(5).trim();
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content || '';
if (content) onChunk(content);
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
}
onComplete();
} catch (error) {
onError(error);
}
}
// Example usage
export async function runStreamDemo() {
const message = "Explain quantum computing in simple terms";
let fullResponse = "";
console.log("Starting real-time explanation...");
await callDeepSeekAPIStream(
"deepseek-chat",
message,
(chunk) => {
fullResponse += chunk;
process.stdout.write(chunk);
},
() => console.log("\nExplanation complete"),
(error) => console.error("Stream error:", error)
);
return fullResponse;
}
Reuse context and track token spend
When sending multi-turn requests that share a system prompt or codebase prefix, forward the X-Conversation-ID header returned by the API so repeated prefixes hit the context cache instead of billing full input rates on every call:
export async function callDeepSeekAPIWithCache(
model: "deepseek-chat" | "deepseek-reasoner",
message: string,
conversationId?: string
) {
// If we have a conversation ID, we can use it for caching
const headers: HeadersInit = {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`,
};
if (conversationId) {
headers["X-Conversation-ID"] = conversationId;
}
const response = await fetch("https://api.deepseek.com/chat/completions", {
method: "POST",
headers,
body: JSON.stringify({
model: model,
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: message },
],
stream: false,
}),
});
// Get the conversation ID from the response for future use
const newConversationId = response.headers.get("X-Conversation-ID");
const data = await response.json();
return {
data,
conversationId: newConversationId || conversationId
};
}
Every completion response exposes response.usage.prompt_tokens and response.usage.completion_tokens. Comparing a four-characters-per-token preflight estimate against the actual billed tokens lets you catch runaway prompt templates early:
function estimateTokens(text: string): number {
// Rough estimate: 1 token ≈ 4 chars in English
return Math.ceil(text.length / 4);
}
async function trackTokenUsage(
model: "deepseek-chat" | "deepseek-reasoner",
message: string
) {
const estimatedInputTokens = estimateTokens(message);
console.log(`Estimated input tokens: ${estimatedInputTokens}`);
const response = await callDeepSeekAPI(model, message);
const actualInputTokens = response.usage.prompt_tokens;
const outputTokens = response.usage.completion_tokens;
const totalCost = (
(actualInputTokens * (model === "deepseek-chat" ? 0.07 : 0.09)) +
(outputTokens * (model === "deepseek-chat" ? 1.10 : 1.40))
) / 1000000; // Convert to dollars
console.log(`Actual input tokens: ${actualInputTokens}`);
console.log(`Output tokens: ${outputTokens}`);
console.log(`Estimated cost: $${totalCost.toFixed(6)}`);
return response;
}
Split workloads between R1 and V3
Sending every task to the reasoning model adds unnecessary latency and cost. Reserve deepseek-reasoner for planning and code review where you want the model to trace bug risks, performance bottlenecks, and security issues step by step:
async function reviewCode(codeString: string) {
const prompt = `Review this code for potential issues, optimizations, and best practices:
\`\`\`
${codeString}
\`\`\`
Provide feedback in the following categories:
1. Bug risks
2. Performance optimizations
3. Readability improvements
4. Security concerns
5. Best practice suggestions`;
const response = await callDeepSeekAPI("deepseek-reasoner", prompt);
return response.choices[0].message.content;
}
Keep mechanical jobs like generating function documentation on deepseek-chat, and schedule non-urgent batch runs inside the 16:30 to 00:30 UTC window to take the 50% off-peak discount:
async function generateDocs(functionCode: string) {
const prompt = `Generate comprehensive API documentation for this function:
\`\`\`
${functionCode}
\`\`\`
Include:
1. Function purpose
2. Parameter descriptions with types
3. Return value details
4. Example usage
5. Potential error cases`;
const response = await callDeepSeekAPI("deepseek-chat", prompt);
return response.choices[0].message.content;
}