DeepSeek Harness PI_AI_ERROR: no retry, misfires, 400 lock

TroubleshootingPublished 2026-10-17Author: DeepSeek Plugin Market
DeepSeek HarnessPI_AI_ERRORllm-pi-airetryableCodesretry policyINVALID_REQUESTopencode-go
DeepSeek Harness pi-ai routes: transient errors fall through to PI_AI_ERROR and skip retry, rate limits lose to a 401/403 regex, and no-body 400 locks sessions.

If you use a pi-ai-kind route in DeepSeek Harness (DSH) (Codex, OpenRouter, opencode-go, a self-hosted gateway, etc.), you will likely meet a failure where "nothing is broken but it just does not retry": a turn suddenly ends with a generic PI_AI_ERROR, and there is not even one llm/retry line in the log. The cause is not the network but the error classifier: classifyPiAiError is a chain of ordered, unanchored text regexes where the first match wins, and anything unrecognized falls through to the catch-all PI_AI_ERROR; the retry round only handles the five classes EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT. So the four kinds of upstream transient failures that recur in this batch of discussions all fall into the fallback: HTTP/2 stream interruption (upstream_http2_stream_error), Codex server overload and buffer limits (server_error / exceeded request buffer limit), rate limiting stolen by an auth regex (judged AUTH when 401 and 429 appear in the same sentence), and a no-body 400 being judged INVALID_REQUEST after which a long session enters a self-heal-less absorbing state (#9589, #9397, #9502, #9564, #9636, #9352).

DeepSeek Harness mechanism: a plain-text regex classifier + a five-class allowlist

In one sentence: classification depends on "what the text looks like", not "what code the upstream gave", so any upstream wording not in the regex table is classified as the fallback code PI_AI_ERROR, and the fallback code has no retry eligibility — that is the entire reason "transient failures are not retried". Two implementation details determine all the symptoms (#9589):

  1. The classifier: classifyPiAiError() is a chain of unanchored (no ^/$) text regexes, matched in order, returning on the first hit; at the end is the catch-all fallback PI_AI_ERROR;
  2. The retry round: it retries only the five classes EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT, and PI_AI_ERROR is not among them.

DeepSeek Harness specific gaps reported in this batch

Upstream returnsExpected classActual classCause
upstream_http2_stream_error: Upstream HTTP/2 stream failedTRANSPORTPI_AI_ERRORthe classifier only recognizes the old wording HTTP2 request did not get a response (#9564, #9508)
server_error: Our servers are currently overloaded…SERVERPI_AI_ERRORthe SERVER branch only recognizes \b5\d\d\b, while the Responses route generates ${code}: ${message}, with no digits (#9502)
exceeded request buffer limit while retrying upstreamSERVERPI_AI_ERRORas above, plain text with no status code (#9397)
Codex server overload (status-code-less wording)SERVERPI_AI_ERRORas above; also \bsocket\b cannot match WebSocket (no word boundary) (#9589)
One sentence containing both 401/403 and 429RATE_LIMITAUTHthe 401|403 entry comes before 429, and the first match wins (#9636)
400 status code (no body)——INVALID_REQUESTthe \b400\b branch asserts "request content illegal" about a string that has only a status code and no body (#9352)

The thread gives a telling quantification: about half of the pi-ai retryable wordings are invisible to the harness — that is, the "upstream thinks it is retryable" and "DSH thinks it is retryable" tables are drifting apart (#9397).

DeepSeek Harness criteria: how to confirm you hit this family

Two things are enough: whether turn/end carries an assistant/message, and whether there is an llm/retry event.

  1. A turn is interrupted by PI_AI_ERROR, and in the log turn/end shows an error end without an assistant output;
  2. There is no llm/retry event at all (meaning it never entered the retry round this time);
  3. When you switch to a TRANSPORT-class fault you can see records such as retry 5/5 — this comparison best shows the problem is in the classification, not the network (#9564).

DeepSeek Harness fix 1: add the missing codes to the retry allowlist (config layer)

In one sentence: retryableCodes has wholesale replace semantics, so when adding you must write the default five classes together. Write the config in the profile patch file matching the current session mode (cordis.patch.yml), not the outdated settings.yaml (#9694).

  1. First confirm which code to add. Look at turn/end's reason.error.code in the log and confirm whether it is PI_AI_ERROR or INVALID_REQUEST (#9694).
  2. Write the full list in the profile patch (list the default five together to avoid wholesale replacement squeezing them out):
yaml
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- patch:
    id: llm-pi-ai
    config:
      retryPolicy:
        retryableCodes:
          - EMPTY_RESPONSE
          - RATE_LIMIT
          - SERVER
          - TIMEOUT
          - TRANSPORT
          - PI_AI_ERROR        # the fallback code to add this time
  1. Restart DSH for the config to take effect, then re-run the request that failed before.
  2. Verify: an llm/retry event should appear in the log this time; if it still does not, the code is being intercepted earlier (see the 400 case in the next section) (#9694).
  3. Know the cost: PI_AI_ERROR is a fallback code, so adding it to the allowlist means "retry all unrecognized errors", including those that should not be retried. So this is a temporary mitigation, not a long-term answer (#9694).

A confusing point: why can you not find this option in the settings UI? Because the pi-ai adapter's retryPolicy is not marked as editable in the settings UI (only the deepseek adapter shows it), so you can only write the patch by hand (#9694).

DeepSeek Harness fix 2: why a no-body 400 is an "absorbing state"

In one sentence: INVALID_REQUEST is not in the default retryable set, while the automatic recovery from compaction overflow only recognizes CONTEXT_WINDOW_EXCEEDED, and the two do not match — so this state is neither retried nor automatically recovered, and the long session is locked from then on. The symptom is "suddenly every request returns 400 status code (no body)", after which even /compact cannot save it (#9352).

There is a related magnitude problem worth looking at together: some gateways' error bodies are only 31 bytes long, i.e. "a status code with no valid message", so DSH can only treat it as a "bare status code"; the real trigger on opencode-go is prompt_tokens + max_tokens > 2^20 — the request's max_tokens comes from the catalog-declared output cap (for example 384000), while the declared 1M window is larger than the upstream's actual 2^20 limit (#9434).

DeepSeek Harness three-step handling

  1. First bring the request volume down: explicitly squeeze maxTokens for that provider/model (one workable value given in the thread is 65536), or compact the history manually with /compact (#9434):
yaml
providers:
  <provider>:
    modelOverrides:
      <model>:
        maxTokens: 65536
  1. Add INVALID_REQUEST to retryableCodes (the same way as the previous section, noting the wholesale replace semantics), giving this kind of 400 a chance to retry successfully (#9352).
  2. Or use a community plugin that reclassifies a no-body 4xx as a retryable error (the thread mentions a similar plugin that reclassifies "no-body 4xx" as EMPTY_RESPONSE, letting the default policy take over) (#9352).
  3. Verify: replay the same request and expect this time either success or at least one retry record; if it still fails immediately, there is some other cause (for example a window declaration not matching reality), so go back to step 1 and keep squeezing the volume.

DeepSeek Harness fix 3: don't expect "opening up PI_AI_ERROR wholesale" to be a one-time cure

In one sentence: loosening the whole PI_AI_ERROR to retryable pulls in "errors that should not be retried" and retries them endlessly. The thread stresses this boundary repeatedly (#9397, #9502):

  1. The errors it drags along: some state-class errors (for example running, pending) should not be retried in the first place, and retrying only amplifies cost;
  2. A neighboring branch with the opposite meaning: the branch near 413 has the opposite meaning to "buffer over limit", and generalizing the match to buffer / too-large conflates the two (#9397);
  3. The more stable direction: narrow fixes — map a whole family of upstream code prefixes to TRANSPORT or SERVER (for example the whole upstream_http2_* family, and server_error as its own entry), and keep the branch order, moving the rate-limit branch before the auth branch (#9502, #9636);
  4. The upstream already has the capability: the thread mentions that pi-ai itself exports a retryable judgment (isRetryableAssistantError), and switching the DSH side to call it avoids maintaining a second wording table that drifts (#9589).

DSH plugin troubleshooting notes

The mnemonic for this family is: first "is there an llm/retry", then "what is the turn/end code", and only last look at the upstream text. Nine points:

  1. No llm/retry means it never entered the retry round, so do not spend effort on "not enough retries" (#9564).
  2. The literal meaning of PI_AI_ERROR is "classification failed", not a specific fault (#9589).
  3. The two retry tables are drifting: what upstream considers retryable is not equal to what DSH considers retryable, and the magnitude of difference can reach half (#9397).
  4. Rate limiting being misjudged as auth is very common; the criterion is "a clearly valid key reported as AUTH" (#9636).
  5. INVALID_REQUEST is an absorbing state: no retry + no automatic recovery, so manual intervention is required (#9352).
  6. When the error body is truncated (for example only 31 bytes remain), DSH can only treat it as a bare status code, making troubleshooting hard; first solve "why the error body was lost" (#9434).
  7. retryableCodes is a wholesale replacement: writing it requires writing the default five classes together (#9694).
  8. Not finding this option in the settings UI is normal: the pi-ai adapter's retryPolicy is not exposed to the UI (#9694).
  9. State the fix status honestly: the narrow fix of the classifier gaps, the retryability of INVALID_REQUEST, and the mapping of the summary to the upstream code are all unfixed in the versions the discussions refer to; everything given here is user-side config and plugin mitigation, not an official fix.

When troubleshooting this kind of error, ruling out the "plugin layer" variable first often saves half the time — after all, the retry policy, vision attachments, and pricing guard are all plugin-shaped. If you want to see the current environment's plugins and logs at a glance in a UI, rather than typing commands in various places, use a visual plugin marketplace such as DSH Plugin Hub: the installed list, install/uninstall history, and system logs are all in one panel.

System Logs

Source: Discussion #9589, Discussion #9397, Discussion #9502, Discussion #9564, Discussion #9508, Discussion #9636, Discussion #9434, Discussion #9352, Discussion #9694.

FAQ

On a pi-ai route in DeepSeek Harness a turn is interrupted by PI_AI_ERROR, and there is not even one llm/retry line in the log. Why?

In DeepSeek Harness, a PI_AI_ERROR with no retry happens because it is the fallback code when the classifier cannot recognize the error, and it is not in the default retryable set. The pi-ai adapter classifies errors with a chain of text regexes where the first match wins; once the upstream wording is outside the regex coverage it falls through to the catch-all PI_AI_ERROR at the end, and the retry round only handles the five classes EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT and TRANSPORT, so this turn ends immediately. The criterion is that turn/end has no assistant/message and there is no llm/retry event at all.

The upstream returns upstream_http2_stream_error or server_error. Why does DeepSeek Harness not retry automatically?

In DeepSeek Harness neither of these two is retried, because neither wording carries a numeric status code. The classifier's SERVER branch only recognizes three-digit numbers starting with 5, while the string the Responses route generates has the shape code: message with no digits, so it does not match; the HTTP/2 one also only recognizes the old wording, and the new code falls through to the fallback too. A workable mitigation is to add these codes to the retry allowlist explicitly, or map the whole family of upstream code prefixes to the TRANSPORT class, but be careful not to open up PI_AI_ERROR wholesale.

When the error message contains both 401 and 429, DeepSeek Harness says the key is invalid but the key is actually fine. How do you explain it?

In DeepSeek Harness this 401/429 key-invalid judgment is a regex-order problem. The classifier is all unanchored text regexes with the first match winning, and the 401|403 entry comes before 429, so when both appear it is judged AUTH rather than RATE_LIMIT, and AUTH is not in the default retryable set, so it neither retries nor gives the correct attribution. The fix is to move the rate-limit branch before the auth branch, or add the corresponding code to that route's retryableCodes.

In DeepSeek Harness a long session suddenly returns 400 status code (no body) on every request, and even compaction cannot save it. What do I do?

In DeepSeek Harness the every-turn no-body 400 is an absorbing state caused by a no-body 400 being judged INVALID_REQUEST: that code is not in the default retryable set, while the automatic recovery from compaction overflow only recognizes CONTEXT_WINDOW_EXCEEDED, so the two do not match and it never recovers. The available handling is to add INVALID_REQUEST explicitly to that profile's retryableCodes, or use a community plugin that reclassifies no-body 4xx as a retryable error.

In DeepSeek Harness where do I write PI_AI_ERROR to add it to retryPolicy.retryableCodes, and will it overwrite the default five classes?

In DeepSeek Harness, retryableCodes is written in the profile's patch file (the cordis.patch.yml layer), not the outdated settings.yaml. Note that retryableCodes is a wholesale replacement, not an append: if you only write PI_AI_ERROR, the default EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT and TRANSPORT are all dropped, so you must list every code you want. Also, the pi-ai adapter's retryPolicy is not marked as editable in the settings UI, so you cannot find this option on the settings page.

Related Terms

PI_AI_ERROR
PI_AI_ERROR is the fallback error code the DeepSeek Harness pi-ai adapter uses when its error classifier cannot recognize an upstream error; it means "classification failed" rather than a specific fault. It is not in the default retryable set, so a turn that hits it usually ends immediately.— https://github.com/deepseek-ai/deepseek-harness/discussions/9589
classifyPiAiError
classifyPiAiError is the function in the pi-ai adapter that maps upstream error text to an internal error code; it is implemented as a sequence of text regexes matched in order, returning on the first hit. It does not parse the upstream's stable error code, so a change in upstream wording causes classification drift and eventually falls through to the PI_AI_ERROR fallback.— https://github.com/deepseek-ai/deepseek-harness/discussions/9636
retryPolicy.retryableCodes
retryPolicy.retryableCodes is the list in the retry policy declaring which error codes are allowed automatic retry; it is plugin config. It has wholesale replace semantics: whatever the patch writes is all that remains, and codes not written lose their retry eligibility.— https://github.com/deepseek-ai/deepseek-harness/discussions/9694
upstream_http2_stream_error
upstream_http2_stream_error is the error code an upstream returns when an HTTP/2 stream is interrupted; it is a transient transport failure and semantically should be retryable. It is currently not classified into the TRANSPORT class, so it falls through to the PI_AI_ERROR fallback and loses its retry.— https://github.com/deepseek-ai/deepseek-harness/discussions/9564

Sources