<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>API on Trevor's Blog</title><link>http://hitrevor.com/en/tags/api/</link><description>Recent content in API on Trevor's Blog</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Sat, 14 Nov 2026 17:06:15 +0800</lastBuildDate><atom:link href="http://hitrevor.com/en/tags/api/index.xml" rel="self" type="application/rss+xml"/><item><title>DeepSeek Opened the App's APIs. Harness Learned to Import History.</title><link>http://hitrevor.com/en/posts/dsh-api-import/</link><pubDate>Sat, 14 Nov 2026 17:06:15 +0800</pubDate><guid>http://hitrevor.com/en/posts/dsh-api-import/</guid><description>DeepSeek opened the App&amp;rsquo;s toolkit as API parameters: search, file parsing, speech. One sentence from me triggered three server-side searches, billed per tool call. Same night, dsh 0.2.2-rc.3 added chat import, and upgrading a profile disabled six plugins.</description><content:encoded><![CDATA[<p>On November 13, DeepSeek did two things that point in opposite directions.</p>
<p>The official release opened up the &ldquo;whole-suite app API.&rdquo; Web search, file parsing, speech — capabilities that until then existed only inside the App — now work from the API by declaring a parameter. The pitch line is &ldquo;reproduce the complete DeepSeek App, 1:1.&rdquo;</p>
<p>That same evening, DeepSeek Harness shipped v0.2.2-rc.3. It added exactly one thing: import the conversation history sitting in the DeepSeek App into your workspace and keep talking.</p>
<p>One side pushes capability out. The other pulls data in. Neither is big news alone; together they say something. The vendor is disassembling its own App while the community stitches its own history back together.</p>
<p>I ran both on my machine. Some numbers don&rsquo;t match the docs.</p>
<p><img alt="Two directions on the same day" loading="lazy" src="/images/dsh-api-import/fig1.png"></p>
<h2 id="first-what-they-actually-opened">First, What They Actually Opened</h2>
<p>What got opened is really two layers: parameter switches, and standalone endpoints.</p>
<p>Layer one is switches. In the existing Messages, Responses and Chat Completions APIs, declare the tool and it works: <code>deepseek_web_search</code> handles web search and deep-thinking search, <code>deepseek_read_url</code> reads a link, and file input and model safety each get a parameter.</p>
<p>The key is one line of documentation: search and fetching both happen server-side, the client executes no tools.</p>
<p>That line decides how you use it. You don&rsquo;t write your own scraper and wire it to the model; you send one request and the server walks the middle steps for you.</p>
<p>Layer two is standalone endpoints. Search, Fetch, OCR, ASR and TTS can each be called on their own, without going through a chat model. Take that route and you are the one assembling the workflow.</p>
<p>Together, the official positioning is &ldquo;turn the parameters on and you have the App.&rdquo; I checked how much of that holds up.</p>
<h2 id="one-call-three-searches-on-the-server">One Call, Three Searches on the Server</h2>
<p>I tried the simplest possible request against the Responses API: ask when Kotlin 2.5.0-RC ships and what&rsquo;s in it, answer in three sentences.</p>
<p>The request body had three fields: model <code>deepseek-flash</code>, tool <code>deepseek_web_search</code>, one line of input.</p>
<p>The response came back at 6,969 bytes in 4.2 seconds. Not one answer — five content blocks in sequence: reasoning, search call, reasoning, search call, answer.</p>
<p>The first search block says <code>kind: &quot;deep_search&quot;</code>. The model issued three queries on its own, mixing Chinese and English. One asked about the release date and features in Chinese, one used <code>site:kotlinlang.org</code> to pin results to the official site, and one went straight for the release announcement page.</p>
<p>Those three queries brought back 16 sources, each with url, title, snippet, site_name and published_at. I counted: official kotlinlang.org doc pages sitting alongside reposts on third-party forums.</p>
<p>That&rsquo;s the real cost of &ldquo;the server walks it for you.&rdquo; You say one sentence; it searches three times behind your back.</p>
<p><img alt="Content blocks and usage from a single request" loading="lazy" src="/images/dsh-api-import/fig2.png"></p>
<h2 id="you-pay-per-tool-execution">You Pay Per Tool Execution</h2>
<p>One field deserves its own section: <code>server_tool_use</code>. This call returned tool_search 3, tool_fetch 0, tool_image_search 0. Three searches, no fetches, no reverse image search.</p>
<p>The same usage block carries <code>input_tokens: 11799</code> (4,608 of them cache hits) and <code>output_tokens: 761</code> (433 of them reasoning tokens).</p>
<p>Of those eleven thousand input tokens, the overwhelming majority is the content of those 16 sources, not your question.</p>
<p>So when the docs say &ldquo;tool call fees are charged on top of token fees,&rdquo; the thing being counted is <code>server_tool_use</code>. You write one line of tool declaration in your code, the bill grows three lines of search fees, and you don&rsquo;t control how many times it searches.</p>
<h2 id="five-standalone-endpoints-tried-one-by-one">Five Standalone Endpoints, Tried One by One</h2>
<p>If your use case doesn&rsquo;t need conversation, calling the standalone endpoints is cheaper. I ran the three I could verify locally.</p>
<p>TTS was the cleanest. Send a Chinese paragraph plus a voice_id, get a wav back in 2.09 seconds. The default voice stella returns 276,524 bytes, 5.76 seconds, 24kHz; switching to tide gives 261,164 bytes, 5.44 seconds. Same text, male voice 0.32 seconds shorter than female.</p>
<p>ASR closed the loop on that wav. 1.2 seconds to return, <code>duration_ms: 5760</code>, matching the 5.76 seconds afinfo measured locally.</p>
<p>The transcript has a trap, though. What I said was &ldquo;多字段的 value class 在字节码里就是一个普通类，没有 box-impl&rdquo; — a multi-field value class is just an ordinary class in bytecode, no box-impl. What came back was &ldquo;多字段的Value Class在字节码里就是一个普通类，没有Box应用.&rdquo;</p>
<p><code>box-impl</code> came back as &ldquo;Box应用&rdquo; — Box app.</p>
<p>This isn&rsquo;t an ASR-specific defect. Anyone dropping English technical terms into Chinese speech recognition hits it: the model hears the sound and guesses the closest Chinese word. Write code comments by voice and that error walks straight into the repo.</p>
<p>For OCR I used a cover image I&rsquo;d made myself: 2.29 seconds, 481 input tokens for 131 output tokens. Layout like &ldquo;36.0 字节 / 次&rdquo; (36.0 bytes per call) and &ldquo;0 / 2 迁移开关&rdquo; (0 of 2 migration switches) — decimal points, slashes and all — came back accurate, right down to the middle-dot separator.</p>
<p><img alt="Measured numbers for the three standalone endpoints" loading="lazy" src="/images/dsh-api-import/fig3.png"></p>
<h2 id="harness-added-exactly-one-thing">Harness Added Exactly One Thing</h2>
<p>That same evening dsh v0.2.2-rc.3 landed, commit <code>38a6a5c</code>, by imccyu and winewill.</p>
<p>Two changes: a new experimental &ldquo;DeepSeek chat import&rdquo; plugin, and a fix for in-app updates failing on the Windows desktop build.</p>
<p>The second is a plain bug fix, nothing to unpack. The first is worth discussing, because it pairs with the API released that day: the official demo supports importing past conversations, and dsh turned that into a plugin.</p>
<p>The plugin is an optional layer — the release notes state plainly that shipped profiles don&rsquo;t include it. You mount it by hand:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">insert</span><span class="p">:</span><span class="w">  </span><span class="c"># appended to the end of the profile&#39;s patch layer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">deepseek-chat-import</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;@deepseek-ai/dsh-deepseek-chat-import&#39;</span><span class="w">
</span></span></span></code></pre></div><p>Usage: click the upload arrow next to &ldquo;New session,&rdquo; pick a <code>deepseek_data-*.zip</code>, then pick the workspace that should receive the conversations.</p>
<h2 id="the-upgrade-hit-a-wall">The Upgrade Hit a Wall</h2>
<p>I upgraded my everyday web profile first. <code>npm install -g @deepseek-ai/dsh@0.2.2-rc.3</code>, it installed 0.2.2-rc.3, I restarted.</p>
<p>The startup log flooded a screen:</p>
<pre tabindex="0"><code>disabling profile plugin row &#34;storage-domain&#34;: Plugin
@deepseek-ai/dsh-storage-domain@0.2.2-rc.1 is incompatible with dsh 0.2.2-rc.3
</code></pre><p>storage-domain, plugin-market, cua, ui-cua, tool-plugin-market, cua-provider-registry — all six disabled. Same reason every time: the profile has rc.1, the main package is rc.3, the versions don&rsquo;t line up.</p>
<p>Then the chain reaction: storageDomain can&rsquo;t start, so workspaceRegistry goes pending, session-controller follows it into pending, and the plugin I actually care about sits in the list waiting on workspaceRegistry.</p>
<p>Nothing was broken about the plugin. It was held up by upstream. This is the other face of the &ldquo;Everything is a Plugin&rdquo; philosophy: the finer the composition, the further one version mismatch cascades through the whole stack.</p>
<p><img alt="Six plugins disabled for version mismatch" loading="lazy" src="/images/dsh-api-import/fig5.png"></p>
<h2 id="a-clean-profile-and-it-runs">A Clean Profile and It Runs</h2>
<p>Rather than touch the dependencies of the profile I use daily, I spun up a fresh one from the official template: <code>dsh --profile chatimport --from-default-profile web --port 3081</code>, a different port so it wouldn&rsquo;t collide with the original.</p>
<p>The new profile carries just two bundles, <code>@deepseek-ai/dsh-base</code> and <code>@deepseek-ai/dsh-web-app</code>. Clean. Add that insert block, restart, and the log shows no disabling lines and nothing pending.</p>
<p>The UI gained a button. I dumped every button on the page through the accessibility tree; the second one is called Import DeepSeek chats, 38 pixels square, right next to New session.</p>
<p>So the plugin is alive. The question becomes what to feed it.</p>
<h2 id="import-test-three-two-one-skipped">Import Test: Three, Two, One Skipped</h2>
<p>I don&rsquo;t have a real export package, so I reverse-engineered the structure from the zod schema in the plugin source and built one.</p>
<p>A valid export ZIP needs three things: <code>manifest.json</code> with <code>export_format_version: 3</code>, a <code>chat_session_index.json</code> listing sessions, and one <code>chat_sessions/{id}.json</code> per session. The session file holds a message tree, each message carrying id, parent_id, role, status and fragments.</p>
<p>I built three sessions: a plain Q&amp;A, one with web search and multi-round reasoning, and one designed to test filtering.</p>
<p>In the first version I made &ldquo;content filtered&rdquo; mean message status <code>FINISHED</code> with a <code>CONTENT_FILTER</code> fragment stuffed in. The result dialog said, flatly: Imported 3 sessions into the smoke-test-connectivity-f0b666d1b2bb workspace.</p>
<p>All three came through, including the one that should have been dropped. I went into the conversion source and found the check reads exactly one field: <code>if (reply?.status === 'CONTENT_FILTER')</code> then <code>continue</code>, with the counter increment as its own separate step.</p>
<p>The filter looks at the message&rsquo;s own status, not the fragment type. I fixed that and rebuilt the package; the second dialog said Imported 2 sessions, followed by 1 conversation could not be imported.</p>
<p>Two in, one skipped. That matches the docs: &ldquo;a question whose answer was withdrawn by content filtering is omitted along with the answer, and sessions with no other turn are not imported.&rdquo;</p>
<p>Which confirmed something else: import the same package twice and the sessions duplicate. My workspace sidebar now shows five sessions, with the Kotlin one and the Flutter one each having two copies, and the filtered one still single. The importer keeps no record of what it has already imported.</p>
<p><img alt="The import funnel and the duplicates" loading="lazy" src="/images/dsh-api-import/fig4.png"></p>
<h2 id="import-writes-locally-only-chatting-touches-the-model">Import Writes Locally; Only Chatting Touches the Model</h2>
<p>Opening an imported session, the history is complete: my questions, my answers, the timestamps, rendered as 11/10 17:00, with 1 turns 1 steps at the bottom.</p>
<p>Then I sent a new question straight into that session to see whether it could keep talking. I got a red line: This turn failed, saying the deepseek-official provider route has no API key, to write one through the credentials service or export one in the startup environment, with a MISSING_CREDENTIAL code at the end.</p>
<p>The new profile has no credentials configured — that&rsquo;s my environment. But the error itself proves something: import never called the model at any point, or it would have stalled here first.</p>
<p>The red &ldquo;Injected context&rdquo; line in the UI says the same thing: the history is already in context, there&rsquo;s just nobody reading it. In the screenshot, that failed turn pushed the bottom stats to 2 turns 2 steps.</p>
<h2 id="two-roads-two-different-problems">Two Roads, Two Different Problems</h2>
<p>Put the two side by side and they answer different questions.</p>
<p>The whole-suite API answers &ldquo;how do I build something that behaves like the DeepSeek App.&rdquo; It wraps the dirty work — search, parsing, speech — and sells it to you: token fees plus tool fees, in exchange for not building your own retrieval and parsing.</p>
<p>Chat import answers &ldquo;how do I stop losing what I&rsquo;ve already talked through.&rdquo; It has nothing to do with the model — pure local read and write, splitting a ZIP into session files and writing them into a workspace.</p>
<p>On my machine the first produces a result in four seconds, the second puts three sessions into the sidebar in one. But the first asks you to trust that its server searched three times on your behalf, and the second asks you to accept that importing the same package five times will write five copies.</p>
<p>Two things shipped the same day: one pushing capability out, one pulling history in. If you have to pick one to start with, it comes down to whether you&rsquo;re short on capability or short on memory.</p>
]]></content:encoded></item></channel></rss>