How I Integrated DeepSeek Into NanoAgent and Why I Built a Repair Layer for Real Coding Agents
When I started building NanoAgent, my goal was not to create another AI chat interface. I wanted a practical coding agent that could live inside a real developer workflow: a terminal, a desktop app, an editor, and eventually CI. NanoAgent was built to understand repositories, inspect files, plan changes, edit code, run validation, review diffs, and keep the human developer in control. The README still captures that core idea: NanoAgent is a local AI coding agent for desktop, terminal, editor, and CI workflows, designed to help developers work inside real repositories instead of disconnected chat sandboxes.
DeepSeek became an important part of that story.
Adding DeepSeek was not just about adding one more provider name to a dropdown. A coding agent is different from a chatbot. In a chatbot, a model can answer with imperfect formatting and the user can still understand it. In a coding agent, the model has to call tools. It has to produce JSON arguments, file paths, patch text, tool-call history, and sometimes reasoning metadata in exactly the shape the runtime expects. A small formatting mistake can stop the entire workflow.
That is why I treated DeepSeek support as a real engineering integration, not a cosmetic provider entry.
The First Step: Making DeepSeek a First-Class Provider
The first DeepSeek commit added it as an official provider inside NanoAgent. That work introduced DeepSeek into provider selection, onboarding, environment configuration, and provider display logic. The repository now lists DeepSeek among the supported providers, alongside OpenAI, Anthropic, GitHub Copilot, OpenRouter, Cerebras, Groq, Ollama, LM Studio, Ollama Cloud, and OpenAI-compatible providers.
At the code level, the provider integration is straightforward. NanoAgent maps provider kinds to display names and managed base URLs. DeepSeek is now represented as its own provider kind and points to the DeepSeek API base URL:
private const string DeepSeekBaseUrl = "https://api.deepseek.com/v1";
public static string ToDisplayName(this ProviderKind providerKind)
{
return providerKind switch
{
ProviderKind.DeepSeek => "DeepSeek",
ProviderKind.OpenAiCompatible => "OpenAI-compatible provider",
_ => providerKind.ToString()
};
}
public static string? GetManagedBaseUrl(this ProviderKind providerKind)
{
return providerKind switch
{
ProviderKind.DeepSeek => DeepSeekBaseUrl,
_ => null
};
}
The real implementation includes DeepSeek in the same provider mapping table as the other supported providers.
That gave users a clean setup path: choose DeepSeek, provide an API key, select a model, and start using NanoAgent. But once DeepSeek was running inside real tool-heavy workflows, I saw the more interesting problem.
The Real Problem: Tool Calls Are Fragile
Coding agents depend on tool calls. For example, if the model wants to read a file, it might call a tool like this:
{
"path": "README.md"
}
If it wants to write a file, it may call something like:
{
"path": "docs/intro.md",
"content": "# IntroductionnnHello from NanoAgent."
}
And if it wants to run a more complex tool, it may need arrays, object arrays, optional fields, booleans, or structured parameters:
{
"tasks": [
{
"task": "review",
"context": "Check the README for outdated setup instructions."
}
]
}
The problem is that models sometimes produce arguments that are semantically understandable but structurally invalid. For example, a schema may require an array:
{
"items": ["alpha", "beta"]
}
But the model may send the array as a string:
{
"items": "["alpha", "beta"]"
}
Or the schema may require an array, but the model sends one bare value:
{
"items": "alpha"
}
Or the schema may require an array of objects, but the model sends a single object:
{
"tasks": {
"task": "review",
"context": null
}
}
A strict tool runtime would reject those calls. Technically, rejection is correct. But from a product perspective, it is painful. The model clearly intended to call the right tool with the right information. The runtime can safely repair some of these cases because it has the tool schema.
That is why I added a DeepSeek-focused tool-argument repair layer.
The Repair Layer: Fix the Obvious, Reject the Dangerous
The commit 658b9cdc005fced3a10a4e6eea2c9fdebb2cb1f8 added the DeepSeek-focused repair layer. In NanoAgent, the repairer runs after the raw tool arguments are parsed and before the tool executes. The invocation pipeline parses the arguments, then passes them through ToolArgumentRepairer.RepairIfNeeded(...) with the tool schema and active session.
The important part is that repair is scoped. I did not want a global “silently mutate every model’s tool calls” behavior. The repairer only activates when the provider is DeepSeek or when the active model ID contains deepseek:
private static bool ShouldRepair(ReplSessionContext session)
{
return session.ProviderProfile.ProviderKind == ProviderKind.DeepSeek ||
session.ActiveModelId.Contains("deepseek", StringComparison.OrdinalIgnoreCase);
}
That mirrors the implementation in ToolArgumentRepairer.
The repair process is schema-driven. The repairer parses the tool schema, inspects expected property types, and only applies targeted transformations. At a high level, the flow looks like this:
public static JsonElement RepairIfNeeded(
JsonElement arguments,
string schemaJson,
ReplSessionContext session)
{
if (!ShouldRepair(session))
return arguments;
using JsonDocument schema = JsonDocument.Parse(schemaJson);
JsonNode? parsed = JsonNode.Parse(arguments.GetRawText());
if (parsed is not JsonObject obj)
return arguments;
bool changed = RepairObject(obj, schema.RootElement);
if (!changed)
return arguments;
using JsonDocument repaired = JsonDocument.Parse(obj.ToJsonString());
return repaired.RootElement.Clone();
}
The production implementation follows this same shape: check whether repair should run, parse the schema, parse arguments into a mutable JSON node, repair the object, and return cloned JSON only if something changed.
Example 1: Repairing a Stringified Array
Suppose a tool schema says this:
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["items"],
"additionalProperties": false
}
The valid argument is:
{
"items": ["alpha", "beta"]
}
But a model may produce:
{
"items": "["alpha", "beta"]"
}
The repair layer detects that items is supposed to be an array. If the value is a string that itself contains a JSON array, NanoAgent parses it and replaces the string with the actual array.
Simplified repair logic:
if (expectedType == "array" && propertyNode is JsonValue value)
{
string? text = value.GetValue<string>();
if (text.Trim().StartsWith("[") && text.Trim().EndsWith("]"))
{
JsonArray? parsedArray = JsonNode.Parse(text) as JsonArray;
parentObject[propertyName] = parsedArray;
}
}
The real code does this through TryRepairArrayNode(...) and TryParseStringifiedArray(...). It first checks whether the value is a string, then attempts to parse it as a JSON array. If parsing succeeds, it replaces the bad string with the parsed array.
Before repair:
{
"items": "["alpha", "beta"]"
}
After repair:
{
"items": ["alpha", "beta"]
}
This turns a failed tool call into a successful one without changing the user’s intent.
Example 2: Turning a Bare String Into a One-Item Array
Sometimes the model sends a single value where the schema expects an array:
{
"items": "alpha"
}
If the schema says items must be an array, NanoAgent can safely interpret this as a one-item array:
{
"items": ["alpha"]
}
This is especially useful for coding tools that accept multiple paths, multiple search terms, or multiple tasks. If the model sends one item, the intent is obvious.
A simplified version:
if (expectedType == "array" && propertyNode is JsonValue singleValue)
{
parentObject[propertyName] = new JsonArray(singleValue.GetValue<string>());
}
In the production implementation, if a property expects an array and the node is a string but not a JSON array string, the repairer wraps it in a new JsonArray.
Example 3: Wrapping a Single Object Into an Object Array
Another common pattern is object arrays. For example, a planning or task tool may expect this:
{
"tasks": [
{
"task": "review",
"context": "Check the README."
}
]
}
But the model may send this:
{
"tasks": {
"task": "review",
"context": null
}
}
If the schema says tasks is an array and the array items are objects, NanoAgent can repair the call by wrapping the object:
{
"tasks": [
{
"task": "review"
}
]
}
Notice the optional context: null can also be removed when it is not required. The repairer reads required fields from the schema and removes optional null values. The implementation builds a set of required properties from the schema’s required array, then removes optional properties whose value is null.
This matters because many tool schemas use additionalProperties: false. A null value for an optional field can still cause validation or execution trouble if the runtime expects the field to be absent. NanoAgent’s repair layer turns “present but null” into “not supplied” when the schema allows it.
Example 4: Cleaning Markdown Links From File Paths
One surprising issue I saw was path-like arguments containing Markdown links.
For example, a model might send this:
{
"path": "docs/[notes.md](http://notes.md)",
"overwrite": null
}
A human can see the intended path is:
docs/notes.md
But a file tool cannot safely use the Markdown link as a path. NanoAgent detects path-like properties and unwraps degenerate Markdown auto-links when the label and destination refer to the same value.
Simplified example:
string input = "docs/[notes.md](http://notes.md)";
string repaired = "docs/notes.md";
The production code treats names like path, workingDirectory, and properties ending in Path as path-like. It then uses a Markdown link regex and normalizes destinations by removing schemes like http:// before deciding whether the link can safely be unwrapped.
Before repair:
{
"path": "docs/[notes.md](http://notes.md)",
"overwrite": null
}
After repair:
{
"path": "docs/notes.md"
}
Again, the repair is narrow. It does not guess arbitrary paths. It only unwraps obvious degenerate Markdown links for path-like fields.
Why I Did Not Make Repair Global
A repair layer is powerful, but it can also become dangerous if it is too broad. If an agent silently changes tool arguments in surprising ways, the user loses trust. That is why I designed this layer around a few rules:
First, repair is provider-scoped. It runs for DeepSeek sessions or DeepSeek model IDs, not for every provider by default.
Second, repair is schema-driven. The repairer does not blindly rewrite JSON. It uses the tool schema to understand whether a property expects an array, object, or string.
Third, repair is conservative. If NanoAgent cannot clearly repair the arguments, it leaves them alone and lets the normal validation path reject them.
Fourth, repair happens before tool execution, not after. That means tools still receive structured JSON in the shape their schemas describe. The rest of the system does not need special DeepSeek branches.
This is the key design decision: keep provider-specific tolerance near the boundary, so the rest of NanoAgent remains clean.
Fixing Patch Application for DeepSeek
Tool arguments were one part of the DeepSeek integration. Patch application was another.
Coding agents often edit files by producing patch text. NanoAgent has to parse that patch text and apply it to the workspace. When a model produces a malformed hunk header, even a good code change can fail.
A normal patch hunk header looks like this:
@@ -10,7 +10,8 @@
But models sometimes produce malformed variants. In the commit 5c493ecc4a62a1c6087086d4a709bc8a327d1204, I added logic to tolerate malformed apply_patch hunk headers for DeepSeek-oriented flows. The repair function checks whether a line is a valid unified diff hunk header, converts it when possible, or splits malformed locator content into a safer form.
A simplified version of the idea:
private static IEnumerable<string> RepairApplyPatchHunkHeader(string line)
{
if (IsUnifiedDiffHunkHeader(line))
{
yield return ConvertUnifiedDiffHunkHeader(line);
yield break;
}
if (line == "@@" || !line.StartsWith("@@ "))
{
yield return line;
yield break;
}
string locator = line[3..];
if (locator.StartsWith("+") || locator.StartsWith("-") || locator.StartsWith(" "))
{
yield return "@@";
yield return locator;
yield break;
}
yield return line;
}
The production implementation follows that same approach. It detects proper unified diff hunk headers using a range regex and repairs update hunks while processing patch text.
For a developer, this means NanoAgent can recover from small patch-format mistakes instead of failing the whole edit. That is a big deal in long coding sessions. The user cares whether the change is correct and reviewable, not whether the model used perfect patch syntax every time.
DeepSeek Thinking Mode and Reasoning Metadata
Another issue I fixed was related to DeepSeek thinking-mode behavior. Reasoning-capable models can return extra metadata, and some providers expect that metadata to be preserved when previous assistant messages are replayed in a continuing conversation.
If an agent loses that metadata, the next request may not behave correctly. The model may fail, repeat itself, or lose continuity around tool calls.
The fix was to preserve reasoning fields through the conversation pipeline. Conceptually, assistant messages can carry normal content plus provider-specific reasoning metadata:
public sealed record ConversationResponse(
string? AssistantMessage,
IReadOnlyList<ConversationToolCall> ToolCalls,
string? ResponseId,
int? CompletionTokens = null,
int? PromptTokens = null,
int? TotalTokens = null,
int? CachedPromptTokens = null,
string? ReasoningContent = null,
string? ReasoningDetailsJson = null);
Then, when NanoAgent reconstructs conversation history for the next provider call, it can replay the assistant message with the reasoning information the provider expects.
This is one of those invisible fixes that users only notice when it is missing. If reasoning-mode history is not preserved, the agent feels unstable. If it is preserved, the session continues naturally.
Extending DeepSeek Model Metadata
Provider integration also required model metadata work. NanoAgent tries to discover available models and understand their context windows. Different providers expose this data in different shapes: context_length, contextWindow, max_context_tokens, nested metadata fields, and so on.
NanoAgent already reads multiple possible context-window property names. The model parser checks several names such as context_length, contextWindow, context_window_tokens, maxContextTokens, and related variants.
For DeepSeek V4 models, I added fallback context windows:
private static readonly IReadOnlyDictionary<string, int> ContextWindowFallbacks =
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["deepseek-v4-pro"] = 1_000_000,
["deepseek-v4-flash"] = 1_000_000
};
The implementation includes those DeepSeek V4 fallbacks directly in the OpenAI-compatible model provider client.
When parsing models, NanoAgent first tries to read the context window from provider metadata. If that is missing, it falls back to known values:
models.Add(new AvailableModel(
normalizedId,
TryGetContextWindowTokens(item) ??
TryGetFallbackContextWindowTokens(normalizedId)));
That is how the real parser works: normalize the model ID, read context-window tokens if available, otherwise apply the fallback. The fallback lookup also handles some free-model suffix variations before matching the known model IDs.
This may seem small, but it improves the user experience. If NanoAgent knows a model’s context window, it can make better decisions about prompt construction, history, and context management.
The Bigger Pattern: Runtime Quality Matters as Much as Model Quality
DeepSeek is powerful, but the model is only one half of the experience. The other half is the agent runtime.
A coding agent needs:
- Provider adapters
- Tool schemas
- Permission checks
- Patch handling
- File mutation tracking
- Undo and redo
- Conversation history
- Model metadata
- Git integration
- Editor and terminal surfaces
- CI automation
NanoAgent’s README emphasizes that it works across interactive terminal sessions, desktop workflows, VS Code and Visual Studio, ACP-compatible editor integrations, and CI automation. It also emphasizes human control through profiles, permissions, approval prompts, tracked edits, and local workspace behavior.
That is the environment DeepSeek has to run inside.
When a model produces slightly malformed JSON, the runtime should not collapse. When a patch hunk is almost correct, the runtime should try to repair it safely. When a reasoning model needs metadata replay, the runtime should preserve it. When provider metadata is incomplete, the runtime should apply known fallbacks.
That is the difference between a demo and a tool developers can use every day.
Example: A DeepSeek Tool Call Before and After NanoAgent Repair
Here is a complete example of the kind of DeepSeek tool-call issue NanoAgent now handles.
A file operation tool may expect this schema:
{
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string"
}
},
"workingDirectory": {
"type": "string"
},
"overwrite": {
"type": "boolean"
}
},
"required": ["paths"],
"additionalProperties": false
}
The model might produce:
{
"paths": "["README.md", "docs/[setup.md](http://setup.md)"]",
"workingDirectory": "[repo](http://repo)",
"overwrite": null
}
NanoAgent can repair this into:
{
"paths": ["README.md", "docs/setup.md"],
"workingDirectory": "repo"
}
The repair steps are:
pathsexpects an array, so NanoAgent parses the stringified JSON array.- Each path-like string can be checked for degenerate Markdown links.
workingDirectoryis path-like, so[repo](http://repo)becomesrepo.overwriteis optional and null, so it is removed.
The result is not a guess. It is a schema-guided cleanup of an obvious model-formatting mistake.
Example: A Bad Patch Header Before and After Repair
A model may produce a patch like this:
*** Begin Patch
*** Update File: README.md
@@ -20,6 +20,8 @@
+New DeepSeek setup notes.
*** End Patch
That is valid. But sometimes a malformed hunk line appears:
*** Begin Patch
*** Update File: README.md
@@ +New DeepSeek setup notes.
*** End Patch
Instead of immediately failing, NanoAgent can split the malformed locator-like line into a safer hunk boundary and content line:
*** Begin Patch
*** Update File: README.md
@@
+New DeepSeek setup notes.
*** End Patch
That is the purpose of the hunk-header repair logic. It detects lines beginning with @@, determines whether they are valid unified diff headers, and repairs the common malformed shape when the locator starts like patch content.
What I Learned From Integrating DeepSeek
The biggest lesson is that model integration is not finished when the HTTP request succeeds.
A real provider integration has multiple layers:
First, the provider must be selectable and configurable.
Second, the model list must be discoverable and useful.
Third, usage metadata such as token counts and cache hits must be parsed correctly.
Fourth, tool calls must survive real-world model output.
Fifth, patch application must be robust enough for coding workflows.
Sixth, reasoning-mode metadata must survive multi-turn tool interactions.
DeepSeek pushed NanoAgent to improve in all of these areas. That is a good thing. Every provider exposes slightly different behavior, and every model family teaches the runtime something new.
Why This Matters for Developers
Developers do not want an agent that works only when the model emits perfect JSON. They want an agent that can help them ship code.
That means the agent should be strict where safety matters and forgiving where formatting is obviously recoverable.
NanoAgent still asks for approval when actions need human control. It still keeps workspace changes local and reviewable. It still tracks edits so users can undo and redo. But when DeepSeek sends "items": "alpha" instead of "items": ["alpha"], NanoAgent should not waste the developer’s time. It should repair the call and continue.
That is the philosophy behind this work.
Conclusion
DeepSeek support in NanoAgent started as a provider integration, but it became a runtime-quality improvement.
I added DeepSeek as a first-class provider. I extended model metadata handling. I fixed reasoning-mode behavior. I added schema-guided tool-argument repair. I made patch application more tolerant of malformed hunk headers. Together, these changes make DeepSeek more useful inside real coding-agent workflows.
The important point is simple: the future of coding agents is not just better models. It is better systems around the models.
NanoAgent’s job is to turn model output into safe, reviewable, productive engineering work. DeepSeek gives NanoAgent another powerful model option. The repair and compatibility layers make that option practical.
That is why I built the DeepSeek integration this way: not as a checkbox, but as a real developer experience.
Attribution: What Came From the Reference and What Came From My Own Debugging
The DeepSeek tool-argument repair work started from a very specific observation: DeepSeek models could produce tool calls that were semantically correct but structurally malformed. The commit 658b9cdc005fced3a10a4e6eea2c9fdebb2cb1f8, titled “Add: DeepSeek-focused tool-argument repair layer,” explicitly references Ahmad Awais’s X post at https://x.com/MrAhmadAwais/status/2050956678502420612. That post became the reference point for improving how NanoAgent handles DeepSeek tool-call arguments.
The next commit, 5c493ecc4a62a1c6087086d4a709bc8a327d1204, was my own finding from testing DeepSeek inside NanoAgent. After the argument-repair layer, I found another failure mode: DeepSeek could produce malformed apply_patch hunk headers. The model’s intended edit was often clear, but the patch parser could not apply it because the hunk header was not in the expected format. That led me to add a targeted patch repair path in NanoAgent, released as “Fix: Tolerate malformed apply_patch hunk headers (DeepSeek).”
So the history is:
658b9cdc005fced3a10a4e6eea2c9fdebb2cb1f8
→ DeepSeek tool-argument repair layer
→ Reference: Ahmad Awais’s X post
5c493ecc4a62a1c6087086d4a709bc8a327d1204
→ Malformed apply_patch hunk-header repair
→ My own finding while testing DeepSeek in NanoAgent
That distinction matters because these two commits solve related but different problems. The first one focuses on malformed tool arguments: arrays returned as strings, single objects where arrays are expected, optional null values, and Markdown-style links inside paths. The second one focuses on malformed patch text: cases where DeepSeek produces an edit that is understandable but not immediately acceptable to NanoAgent’s patch parser.
Contribute to the Project:
NanoAgent is an open-source project available on GitHub at https://github.com/rizwan3d/NanoAgent. If you find this tool valuable and helpful, consider giving it a star on GitHub. Your support encourages the continuous improvement of NanoAgent.