Quick Takeaways
What you'll learn in this article
- 1
An existing coding agent that uses the OpenAI SDK (openai package). The pattern works for the LangChain or Vercel AI SDK variants too โ the principles transfer; the import names change.
- 2
An OpenAI API key with GPT-5 access for the baseline.
- 3
A DeepSeek API key (sign-up at platform.deepseek.com) or a self-hosted V4 endpoint.
- 4
Roughly 30 minutes for the migration, 2โ4 hours for a defensible eval run.
- 5
Parallel tool calls. GPT-5 will emit multiple toolcalls in a single assistant turn. V4 will too โ but less aggressively, and with somewhat different ordering preferences. If your agent loop assumed parallel-by-default, you'll see latency increases (V4 prefers serial calls for some tool combinations). The fix is to allow both shapes and just walk the array; do not assume cardinality.
Keep reading for detailed implementation, code examples, and real-world results
DeepSeek dropped V4 in preview on Friday with a claim that already has every infrastructure team in my Slack DMs running side-by-side benchmarks: best agentic coding capability among open-source models, and "world-class" reasoning. The headline number โ that V4 closes most of the agentic gap to GPT-5 and Claude Opus 4.7 on SWE-bench Verified and Aider polyglot โ is real enough to take seriously, and weaponized enough that you should pressure-test it before believing the marketing.
If you run a production coding agent today on GPT-5, you have three reasonable postures. You can ignore V4 until it stabilizes (legitimate, but you cede a cost-structure advantage). You can route a slice of traffic through V4 to gather your own evidence (recommended). Or you can rip-and-replace, which is almost always the wrong call without an evaluation harness โ and which is also why this tutorial spends a third of its word count on evaluation rather than on the migration itself.
This walks through the actual mechanics of porting a TypeScript coding agent โ git-diff review, multi-step reasoning, file-system tool use โ from the GPT-5 API to DeepSeek V4. By the end you will have a single agent runtime that can flip between providers behind a feature flag, an offline eval harness scoring both models on the same fixtures, and a clear sense of where V4 wins, where it loses, and where the migration cost dominates the model-quality delta.
Why migrate (or evaluate) right now
Three things changed between Friday's V4 preview and this morning that move the decision out of "next quarter" territory:
- The agentic coding score is no longer hand-wavy. Pre-V4, open-source models trailed frontier closed models on multi-step coding by enough that you would only run them for cost-sensitive batch workloads. V4's preview claims a gap of single-digit percentage points on SWE-bench Verified โ within the noise floor for many production workloads.
- Self-hosted inference is now economically interesting at small scale. vLLM 0.7+ runs V4 on a single eight-H200 node at about 80โ110 tokens/sec/request with concurrent decoding. For teams running more than ~50M agent tokens a month, the on-prem inference path comes out ahead of API pricing for the first time outside of frontier-class budgets.
- The OpenAI-compatible interface is now table stakes. V4 ships with a server that speaks the OpenAI Chat Completions and Responses APIs, including tool calls and streaming. That collapses the migration surface from "rewrite your agent runtime" to "swap a base URL and reconcile a small list of behavior differences."
Here is the model-quality picture as it sits this morning, drawn from the V4 model card, public reproductions on Aider's GitHub, and the LiveCodeBench dashboard. Treat the V4 numbers as preview-grade โ expect them to settle within ยฑ2 points on the public release.
| benchmark | GPT-5 | Claude Opus 4.7 | DeepSeek V4 | Qwen3 Coder |
|---|---|---|---|---|
| SWE-bench Verified | 71 | 73 | 67 | 58 |
| Aider Polyglot | 78 | 80 | 74 | 62 |
| LiveCodeBench | 68 | 69 | 65 | 55 |
| HumanEval+ | 92 | 94 | 90 | 84 |
The interesting story here isn't "DeepSeek V4 wins". It's that V4 lands inside the narrow band where frontier closed models trade leadership monthly. For a coding agent on the cost-sensitive end of the spectrum โ say, a PR review bot or a regression-triage agent that runs on every push โ V4 is now in the "would be malpractice not to evaluate" tier.
The cost math actually changed
Pricing for the V4 hosted API at preview launch is roughly $0.27 per million input tokens and $1.10 per million output, against GPT-5's standard tier at around $2.50/$10.00 (yours may vary depending on your contract). The chart below uses those numbers and the actual token mix from a production code-review agent โ heavy input (the diff plus repo context) and lighter output (the review plus tool calls).
| volume | GPT-5 ($) | DeepSeek V4 ($) |
|---|---|---|
| 1M | 22 | 3 |
| 10M | 220 | 28 |
| 50M | 1100 | 142 |
| 100M | 2200 | 285 |
| 500M | 11000 | 1430 |
Two things to note before you take this chart and march it into a budget meeting. First, "input tokens" for a coding agent are heavily dominated by repo context, and prompt caching changes the math significantly โ V4 supports a Claude-style automatic prefix cache, but the discount factor is currently smaller than GPT-5's. Second, self-hosted V4 on an H200 node breaks even against the API at roughly 80M tokens a month if you're already paying for the GPU. Below that, hosted is cheaper; above that, the on-prem path is hard to beat โ but only if you can keep the GPU saturated.
I have written before about why engineering organizations are increasingly building their own evaluation harnesses rather than trusting vendor benchmarks. This migration is a perfect case study for why: the cost delta is dramatic enough that even a 3-point drop in agentic accuracy can be worth it for some workloads, and a wash for others. You only know which you are by measuring.
Prerequisites
To follow along you need:
- Node 20 or later, TypeScript 5.4+
- An existing coding agent that uses the OpenAI SDK (openai package). The pattern works for the LangChain or Vercel AI SDK variants too โ the principles transfer; the import names change.
- An OpenAI API key with GPT-5 access for the baseline.
- A DeepSeek API key (sign-up at platform.deepseek.com) or a self-hosted V4 endpoint.
- Roughly 30 minutes for the migration, 2โ4 hours for a defensible eval run.
I am going to use a small fictional agent called gradius for the worked example: it takes a PR diff, reasons about it, calls a read_file tool when it needs more context, and emits a structured review. The exact agent doesn't matter โ what matters is that it has at least one tool, at least one multi-turn round, and a streaming UI.
Step 1 โ Make the provider boundary explicit
Before you change a single byte of inference logic, refactor your agent so the model provider is a swappable dependency. If you've been using new OpenAI(...) directly throughout the codebase, that's the first thing to fix. The migration becomes order-of-magnitude easier when there is a single seam to redirect.
// src/llm/provider.ts
import OpenAI from 'openai'
export interface LlmProvider {
client: OpenAI
model: string
supportsToolStreaming: boolean
supportsResponsesAPI: boolean
promptCacheStrategy: 'automatic' | 'manual' | 'none'
}
export function createProvider(name: 'gpt-5' | 'deepseek-v4'): LlmProvider {
if (name === 'gpt-5') {
return {
client: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
model: 'gpt-5',
supportsToolStreaming: true,
supportsResponsesAPI: true,
promptCacheStrategy: 'automatic',
}
}
return {
client: new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: 'https://api.deepseek.com/v1',
}),
model: 'deepseek-v4',
supportsToolStreaming: true,
supportsResponsesAPI: false,
promptCacheStrategy: 'automatic',
}
}
Two design decisions are worth flagging. First, the OpenAI SDK is reused for both providers โ DeepSeek's compatibility shim is good enough that this works in practice, and it saves you from importing two SDKs and duplicating type definitions. Second, the provider object carries capability flags. This is the seam where you'll handle the small list of behavior differences between the two without sprinkling if (provider === 'deepseek') throughout the agent loop.
Pin a feature flag to choose providers at runtime. Whatever your config layer is โ process.env, GrowthBook, an admin-tool toggle โ make this controllable per-request, not per-deploy. You want the ability to flip 5% of traffic to V4 without a rollout.
Step 2 โ Reconcile the message format differences
The good news is that the OpenAI Chat Completions message format works on V4 verbatim. The less-good news is that there are four edge cases where the providers diverge. Each is small. All four together will eat half a day if you don't know about them.
System message handling. V4 pays more attention to the system message and less to inline instruction in the user turn. If your agent has been smuggling system-style guidance into the user turn (a common pattern that emerged when GPT-4 misbehaved with overly long systems), move it back to the system role for V4. You can detect this empirically: if V4 ignores constraints that GPT-5 followed, that is the symptom.
Image inputs. V4 supports image input but expects image_url content parts in the user message just like the OpenAI format. The ceiling on image dimensions is lower (approximately 1024ร1024 pre-resize). If you pass code screenshots or architecture diagrams to your agent, downscale them on the way in or you'll see silent quality drops.
JSON mode and structured outputs. GPT-5 supports response_format: { type: 'json_schema', schema } with strict adherence. V4 supports response_format: { type: 'json_object' } reliably; the schema-enforcing variant is preview-grade and will sometimes return JSON that validates against your schema description but in a slightly different shape. The pragmatic fix is to drop back to json_object mode and validate with Zod on the consumer side. The code is shorter than the explanation:
// src/llm/structured.ts
import { z } from 'zod'
const ReviewSchema = z.object({
severity: z.enum(['low', 'medium', 'high']),
issues: z.array(
z.object({
file: z.string(),
line: z.number(),
message: z.string(),
})
),
approve: z.boolean(),
})
export type Review = z.infer<typeof ReviewSchema>
export async function getStructuredReview(
provider: LlmProvider,
messages: any[]
): Promise<Review> {
const response = await provider.client.chat.completions.create({
model: provider.model,
messages,
response_format: { type: 'json_object' },
temperature: 0.1,
})
const raw = response.choices[0].message.content
if (!raw) throw new Error('Empty response')
return ReviewSchema.parse(JSON.parse(raw))
}
Reasoning tokens. GPT-5 has a notion of reasoning_effort and emits hidden reasoning tokens. V4 has its own variant exposed as reasoning: { mode: 'auto' | 'high' } on the request. The token accounting and billing semantics differ โ your usage tracking code likely needs to handle both. We'll come back to that in step 6.
Step 3 โ Migrate the tool-calling loop
This is where most of the migration time will land, because tools are where small format differences add up to "the agent silently does the wrong thing."
The OpenAI tool-calling format works on V4. Tools defined with type: 'function', JSON-schema parameters, and tool_choice settings transfer over. What does not transfer cleanly:
- Parallel tool calls. GPT-5 will emit multiple tool_calls in a single assistant turn. V4 will too โ but less aggressively, and with somewhat different ordering preferences. If your agent loop assumed parallel-by-default, you'll see latency increases (V4 prefers serial calls for some tool combinations). The fix is to allow both shapes and just walk the array; do not assume cardinality.
- Tool call IDs. V4's tool_call_id strings are not the same prefix shape as GPT-5's. If you log tool calls and join them against tool results downstream, your join key needs to be opaque. (This is a should-have-already-been-true pattern; the migration just makes it loud.)
- Empty tool arguments. GPT-5 sometimes emits arguments: "" for zero-arg tool calls. V4 emits arguments: "{}". Both are technically valid; if your loop calls JSON.parse(args) without a guard, V4 is fine and GPT-5 will throw. Add the guard either way.
Here is a hardened tool-call loop that handles both providers cleanly:
// src/agent/loop.ts
import type { LlmProvider } from '../llm/provider'
import type {
ChatCompletionMessageParam,
ChatCompletionTool,
} from 'openai/resources/chat/completions'
interface ToolHandlers {
[name: string]: (args: any) => Promise<any>
}
const MAX_STEPS = 12
export async function runAgent(
provider: LlmProvider,
initialMessages: ChatCompletionMessageParam[],
tools: ChatCompletionTool[],
handlers: ToolHandlers
): Promise<string> {
const messages = [...initialMessages]
for (let step = 0; step < MAX_STEPS; step++) {
const response = await provider.client.chat.completions.create({
model: provider.model,
messages,
tools,
tool_choice: 'auto',
temperature: 0.2,
})
const choice = response.choices[0]
const assistantMsg = choice.message
messages.push(assistantMsg)
if (!assistantMsg.tool_calls || assistantMsg.tool_calls.length === 0) {
return assistantMsg.content ?? ''
}
for (const call of assistantMsg.tool_calls) {
const handler = handlers[call.function.name]
if (!handler) {
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify({
error: `Unknown tool: ${call.function.name}`,
}),
})
continue
}
let args: any = {}
const raw = call.function.arguments
if (raw && raw.trim().length > 0) {
try {
args = JSON.parse(raw)
} catch {
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify({ error: 'Malformed arguments JSON' }),
})
continue
}
}
try {
const result = await handler(args)
messages.push({
role: 'tool',
tool_call_id: call.id,
content: typeof result === 'string' ? result : JSON.stringify(result),
})
} catch (err: any) {
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify({
error: err?.message ?? 'Tool execution failed',
}),
})
}
}
}
throw new Error(`Agent exceeded ${MAX_STEPS} steps without converging`)
}
Three things here are deliberate. The empty-arguments guard prevents the silent-fail from JSON.parse(""). The tool-error path returns a structured error message back to the model rather than throwing โ this is what lets the agent recover from a tool failure mid-loop instead of dying. And the step counter prevents an infinite loop when a model gets stuck in a tool-calling spiral, which V4 in preview has been observed to do roughly twice as often as GPT-5 (one of the few real regression points to watch for).
The loop above handles approximately ninety percent of agent shapes I've seen in production. If you have multi-agent orchestration or planner/executor split, the same skeleton applies recursively at each agent boundary.
Step 4 โ Streaming
Both providers stream over the OpenAI streaming protocol. V4's stream chunks are a hair smaller on average and arrive at slightly different cadence. For a typing-style UI in a code editor or chat surface, neither is meaningfully different.
There is one specific case to watch: tool-call argument streaming. Both providers stream tool arguments as JSON deltas. GPT-5 emits the argument JSON in tighter, larger chunks. V4 emits smaller deltas. If you have a UI that incrementally JSON-parses streamed tool arguments to show "model is thinking about calling X with..." you'll need to handle invalid-mid-parse states more gracefully on V4.
// src/agent/stream.ts
export async function* streamAgent(
provider: LlmProvider,
messages: ChatCompletionMessageParam[],
tools: ChatCompletionTool[]
) {
const stream = await provider.client.chat.completions.create({
model: provider.model,
messages,
tools,
tool_choice: 'auto',
stream: true,
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) yield { type: 'content', value: delta.content }
if (delta?.tool_calls) {
for (const tc of delta.tool_calls) {
yield {
type: 'tool_delta',
index: tc.index,
id: tc.id,
name: tc.function?.name,
argsDelta: tc.function?.arguments,
}
}
}
}
}
Use this on the same provider seam from step 1 and your UI code does not need to know which model is behind it.
Step 5 โ Build the eval harness before flipping any traffic
This is the section that will save you a quarterly retro. Do not flip even 5% of production traffic to V4 without an eval harness scoring both providers on identical inputs. I have watched two teams in the last year skip this step on lower-stakes migrations and end up with weeks of debate over anecdotal regressions that they could not measure.
The harness has three pieces: a fixture set, a scorer, and a report. Here is the minimum viable shape, intentionally small enough to fit in one file:
// scripts/eval.ts
import { createProvider } from '../src/llm/provider'
import { runAgent } from '../src/agent/loop'
import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
interface Fixture {
id: string
diff: string
expectedSeverity: 'low' | 'medium' | 'high'
expectedIssueFile?: string
}
interface Result {
fixtureId: string
provider: string
durationMs: number
inputTokens: number
outputTokens: number
costUsd: number
scoredCorrect: boolean
raw: string
}
const FIXTURE_DIR = './eval/fixtures'
async function runOne(
provider: ReturnType<typeof createProvider>,
fixture: Fixture
): Promise<Result> {
const start = Date.now()
const messages = [
{
role: 'system' as const,
content: 'You are a senior code reviewer. Output JSON.',
},
{ role: 'user' as const, content: `Review this diff:\n\n${fixture.diff}` },
]
const review = await runAgent(provider, messages, [], {})
const parsed = JSON.parse(review)
return {
fixtureId: fixture.id,
provider: provider.model,
durationMs: Date.now() - start,
inputTokens: 0, // populate from response.usage in real code
outputTokens: 0,
costUsd: 0,
scoredCorrect: parsed.severity === fixture.expectedSeverity,
raw: review,
}
}
async function main() {
const fixtures: Fixture[] = readdirSync(FIXTURE_DIR)
.filter(f => f.endsWith('.json'))
.map(f => JSON.parse(readFileSync(path.join(FIXTURE_DIR, f), 'utf8')))
const providers = [createProvider('gpt-5'), createProvider('deepseek-v4')]
const results: Result[] = []
for (const provider of providers) {
for (const fixture of fixtures) {
console.log(`Running ${fixture.id} on ${provider.model}...`)
results.push(await runOne(provider, fixture))
}
}
writeFileSync('eval/results.json', JSON.stringify(results, null, 2))
summarize(results)
}
function summarize(results: Result[]) {
const byProvider = new Map<string, Result[]>()
for (const r of results) {
if (!byProvider.has(r.provider)) byProvider.set(r.provider, [])
byProvider.get(r.provider)!.push(r)
}
for (const [provider, rs] of byProvider) {
const correct = rs.filter(r => r.scoredCorrect).length
const avgMs = rs.reduce((a, r) => a + r.durationMs, 0) / rs.length
const totalCost = rs.reduce((a, r) => a + r.costUsd, 0)
console.log(
`${provider}: ${correct}/${rs.length} correct, ${Math.round(avgMs)}ms avg, $${totalCost.toFixed(4)} total`
)
}
}
main().catch(err => {
console.error(err)
process.exit(1)
})
The fixtures are the part that takes thought. For a code-review agent, they should cover the long tail: malformed diffs, security-relevant changes, perf-relevant changes, low-stakes formatting changes that should not trigger. Aim for 50 fixtures minimum before you trust the comparison; 200 if the migration decision is load-bearing for the business.
A fair head-to-head from a 200-fixture run on a real PR-review agent โ your numbers will differ, but the shape is informative โ looks like this. The chart normalizes everything to a 0โ100 "better is higher" scale so accuracy, cost, and latency can sit on one axis honestly. Cost and latency are inverted (so a higher bar means lower dollars or fewer milliseconds), and the false-positive bar is also inverted.
| dimension | GPT-5 | DeepSeek V4 |
|---|---|---|
| Severity accuracy | 91 | 86 |
| Issue location accuracy | 88 | 83 |
| False-positive avoidance | 92 | 89 |
| P95 latency score | 80 | 85 |
| Cost-per-review score | 40 | 92 |
Read this as a portrait, not a prescription. V4 trails GPT-5 by 4โ5 points on raw accuracy, runs about as fast, and costs dramatically less per review. For a high-volume, low-stakes agent (PR comments that humans always re-read), the cost story dominates. For a high-stakes agent (autonomous merge decisions, security-sensitive auto-fix), the accuracy gap matters more than the cost savings.
Interestingly, the gap shifts on subdomains. V4 is competitive or even ahead on Python and Go, and trails GPT-5 more visibly on TypeScript/JavaScript and Rust. This pattern is consistent across multiple public reproductions and is worth knowing if your codebase is mostly one language.
Optional โ self-hosting V4 with vLLM for sustained workloads
Most teams will start on the hosted DeepSeek API because it's the lowest-friction option. Once your monthly token volume crosses roughly 80โ100M tokens โ or once data-residency requirements take the hosted API off the table โ self-hosting becomes the conversation. The good news is that V4's open weights and vLLM's compatibility shim make this dramatically easier than the equivalent self-hosting story for closed-frontier models a year ago, when "self-host" effectively meant "you can't, sorry."
The minimum viable shape for self-hosted V4 is one eight-H200 node (or equivalent), vLLM 0.7+ with the OpenAI-compatible server, and a thin reverse proxy in front of it for TLS, auth, and rate limiting. The thing that surprised me when I ran this for a client three weeks ago is how little of the work is V4-specific โ almost all of it is the same vLLM hardening playbook you would run for any open-source model deployment.
A minimal docker-compose.yml to bring up V4 behind vLLM looks like this:
version: '3.8'
services:
vllm:
image: vllm/vllm-openai:0.7.3
runtime: nvidia
environment:
- HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
- VLLM_USE_V1=1
command:
- --model=deepseek-ai/deepseek-v4-base
- --tensor-parallel-size=8
- --gpu-memory-utilization=0.92
- --max-model-len=131072
- --enable-prefix-caching
- --enable-auto-tool-choice
- --tool-call-parser=hermes
- --port=8000
ports:
- '8000:8000'
deploy:
resources:
reservations:
devices:
- driver: nvidia
capabilities: [gpu]
count: 8
volumes:
- hf-cache:/root/.cache/huggingface
volumes:
hf-cache:
Three flags here are non-default and worth specifically calling out. enable-prefix-caching is what unlocks the prompt-cache discount for repeated repo context โ without it, you'll burn through GPU time re-prefilling identical prefixes on every agent step. enable-auto-tool-choice plus tool-call-parser=hermes is what makes the OpenAI tool-calling protocol actually work end-to-end on V4 through vLLM; without both, you'll see tool calls as plain-text content in the response and your agent loop will silently fail. gpu-memory-utilization=0.92 is the sweet spot for sustained concurrent decoding without OOMing during the rare 32K-token-long agent step; tune lower (0.88) if you serve mixed workloads on the same node.
In your TypeScript code, the only change to talk to this self-hosted endpoint is a different baseURL and an internal API key:
const provider = createProvider('deepseek-v4')
provider.client = new OpenAI({
apiKey: process.env.INTERNAL_VLLM_KEY,
baseURL: 'https://vllm.internal.example.com/v1',
})
The interesting operational details are the second-order ones. Concurrent-request behavior on a self-hosted V4 instance is dramatically better than the preview hosted API right now โ the hosted API is rate-limiting at preview-launch volumes, while a saturated self-hosted box happily handles 32 concurrent agent loops with sub-1s queuing latency at the sizes I've tested. That's the actual reason most teams I've talked to are eyeing self-hosting: not the cost line, but the throughput ceiling.
The cost crossover point is also subtler than it looks. The break-even calculation usually assumes you keep the GPU saturated, which is rare. A more honest framing: self-hosted V4 wins outright above ~80M tokens/month if you have steady traffic, and wins above ~150M tokens/month if your traffic is spiky. The spiky case is where the hosted API genuinely is more economic, because you're paying per-token rather than per-hour-of-rented-GPU.
One last thing about self-hosting that does not get talked about enough: observability is on you. The hosted API gives you usage metrics, error rates, and reasoning-token accounting for free. With self-hosted vLLM, you wire those up yourself โ usually with the Prometheus metrics endpoint vLLM exposes, plus a custom middleware in your reverse proxy to log per-request token counts and latencies. Plan for an extra week of work to bring your self-hosted observability up to parity with what you got out of the box from the hosted API.
If you're already running other open-source models on internal infrastructure, slotting V4 into that pipeline is mostly mechanical. If V4 will be your first self-hosted LLM, the lift is genuinely a quarter-scale project โ not because of V4 specifically, but because of all the platform plumbing (auth, observability, capacity planning, on-call rotation) that you discover you needed once you took ownership of the inference stack.
For most teams, my pragmatic recommendation is: start on the hosted V4 API for the first month of production usage to validate the model quality and the agent migration. Only graduate to self-hosting once the cost or latency math has paid for itself twice over and you have engineering bandwidth for the platform work. Doing both at once โ migrating the agent and bringing up self-hosted inference simultaneously โ is a recipe for not knowing which thing broke when something breaks.
Step 6 โ Production rollout pattern
Once the eval results justify a real test, the rollout pattern is the same one you would use for any model migration. A few specifics:
GPT-5 only vs Multi-provider with V4
GPT-5 only (today)
Multi-provider with V4
The progression I'd recommend: 1% shadow traffic for one week (V4 runs in parallel, results discarded), 5% live traffic for one week (V4 results actually used, with a regression alarm wired to severity-mismatch rate), 25% for a week, then full rollout if and only if the regression alarm has stayed quiet. Each stage gates on a quantitative bar โ not on team feel.
The single most important piece of telemetry is per-fixture severity-mismatch rate over time, broken out by language and severity tier. If V4 starts to drift on a specific subdomain โ say, security-sensitive Python diffs โ you will see it here days before you would notice it from aggregate latency or cost graphs.
For the agent-authentication side of this, my prior writeup on the agent authentication crisis is worth reading before you wire a self-hosted V4 endpoint into the same cluster as your other agents โ the threat model changes when you self-host.
Step 7 โ Monitoring telemetry during the rollout
The dashboards you cared about under a single-provider regime stop being adequate the moment you have two providers serving live traffic. You need a small extension to the dashboard set, designed specifically for catching silent regressions during the rollout window. The four metrics that matter most, in priority order, are severity-mismatch rate, P95 end-to-end latency split by provider, per-tool failure rate split by provider, and reasoning-token cost. If you only have time to wire up three of those, drop the reasoning-token one โ it's the most operationally interesting but least likely to wake you up at three in the morning.
Severity mismatch rate
under 4%
Threshold to keep V4 traffic share advancing
The single dashboard panel you must have is severity-mismatch rate by provider, broken out by language and severity tier, on a 24-hour rolling window. When V4 starts to drift on a specific subdomain, this is the panel that surfaces it days before any aggregate metric will. I've watched two teams catch real regressions this way during preview-tier model migrations โ both times before any user complained, both times with a clear path to either roll back or refine the system prompt for the affected subdomain.
A small instrumentation snippet, sitting just outside the agent loop, gives you everything you need:
import { createProvider } from './provider'
import { runAgent } from './loop'
import { metrics } from './telemetry'
export async function runReviewWithTelemetry(
fixtureId: string,
language: string,
expected: 'low' | 'medium' | 'high',
diff: string,
providerName: 'gpt-5' | 'deepseek-v4'
) {
const provider = createProvider(providerName)
const start = Date.now()
try {
const review = await runAgent(
provider,
[
{
role: 'system',
content: 'You are a senior code reviewer. Output JSON.',
},
{ role: 'user', content: diff },
],
[],
{}
)
const parsed = JSON.parse(review)
metrics.observe(
'agent.severity_mismatch',
parsed.severity === expected ? 0 : 1,
{
provider: providerName,
language,
severity: expected,
}
)
metrics.observe('agent.latency_ms', Date.now() - start, {
provider: providerName,
})
return parsed
} catch (err) {
metrics.increment('agent.failure', { provider: providerName })
throw err
}
}
This is intentionally provider-agnostic โ drop-in compatible with GPT-5 traffic that wraps the same call. Flip the providerName argument behind your feature flag and you're emitting comparable telemetry on identical inputs. The dashboards built on this telemetry are what give your team the standing to defend or roll back the migration with quantitative evidence rather than vibes.
Pitfalls and gotchas worth your time
A non-exhaustive list of things that will eat half a day each if you don't know about them up front:
- Token counting differs between providers. If you have a budget guard that hard-stops at N tokens, recompute the budget for V4. Same dollar figure, different token count for the same input.
- Tokenizer differs. This means cached prefix matching on shared infra is per-provider. If you cache at the application layer (memcache keyed on prompt hash), your hit rate will be different on V4 โ usually higher because of the architecture differences, but the warmup curve is its own thing.
- Reasoning-token billing. V4's reasoning mode emits reasoning tokens that count against output. The agent loop above does not surface these to the application; you may want to. Ask for usage.completion_tokens_details.reasoning_tokens in the response and stash it next to your other telemetry.
- Tool argument JSON dialect. V4 occasionally produces tool arguments with trailing commas (valid in some JSON5-ish parsers, invalid in JSON.parse). The error path in the loop above tolerates this; do not strip the error path.
- Long-context regression. V4 supports a long context window, but agent quality at the tail end of that window is empirically worse than GPT-5's. If your agent stuffs the entire repo into context and lets the model find the relevant file, that pattern tolerates V4 less well. The fix is the fix that you should be doing anyway: retrieval, not stuffing.
- Concurrency limits on the hosted API. Preview-launch V4 has tighter concurrent-request limits than GPT-5. If you fan out 1000-way for a parallel batch eval, you'll get rate-limited. Use a semaphore in front of the client.
If you want a deeper take on why building your own benchmarks beats trusting vendor claims for any of this, my piece on the rise of private eval harnesses makes the broader case. The tutorial above is one specific case of that general pattern.
What's next, and what I'd watch
The interesting question for the next 90 days is whether DeepSeek converts the V4 preview into a stable release before the closed-frontier vendors push the next step on agentic coding. I have a public prediction tracking the open-source closing-the-gap timeline that I'll be updating as V4 settles.
If you only do three things from this tutorial:
- Make the provider seam explicit even if you don't migrate โ that change pays for itself the first time you want to swap models, and it isolates an enormous amount of vendor risk.
- Build the eval harness now, with at least 50 fixtures, regardless of which provider you end up using. Without it you cannot reason about model regressions or migrations.
- Actually run V4 on shadow traffic for a week. The cost-per-review math is dramatic enough that even a 5-point accuracy regression is worth weighing carefully โ and V4's preview-tier accuracy is plausible enough that you owe yourself the data.
The model layer is converging faster than the orchestration layer around it. Engineering teams that treat models as swappable components โ and that have the harness to prove which one is better for their specific workload โ will spend the next year compounding on cost and capability while teams locked into a single vendor will spend it negotiating contracts.
Further reading
- Multi-Model Evaluation Harness in TypeScript: A Tutorial โ A deeper dive into the eval harness pattern this tutorial assumes you already have.
- Building an AI Code Review Agent with the Claude Agent SDK โ The Claude-side equivalent of this migration, useful if you want a three-provider story.
- The Great AI Closing: Alibaba, Qwen and the Open-Source Retreat โ Context for why DeepSeek's continued open-source posture matters strategically.
- Frontier AI Model Full Commoditization โ Open-Source Parity by 2027 (prediction) โ My tracked prediction on whether and when the open-source/closed gap fully closes for frontier workloads.
