dsh-llm-longcat
LongCat (LongCat-2.0) provider for DeepSeek Harness — 1M context, thinking mode, tool calling
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 20, 2026
- Updated
- Aug 20, 2026
Introduction
dsh-llm-longcat
LongCat adapter for the DeepSeek Harness LLM seam.
Adds LongCat-2.0 as a model provider: 1M context, thinking mode, tool calling.
Features
- Thinking mode — recognizes LongCat's
reasoning_contentfield and translates it into harnessReasoningBlocks - Tool calling — full function-calling support, with
argumentskept a raw JSON string end to end - Multi-turn — replays
reasoning_contenton tool-call turns, as thinking-mode passback requires - Streaming — SSE with the
usage-before-finishordering the harness relies on - Credential seam — the key resolves per request from
ctx.credentialsor the environment; no secret in any config file
Supported models
| Model | Context | Max output | Notes |
|---|---|---|---|
LongCat-2.0 | 1,048,576 | 131,072 | text-only; thinking + tool calling |
Facts from GET /openai/v1/models/LongCat-2.0, the only documented endpoint that
reports supported_parameters. Tool calling is not mentioned on the
chat-completions doc page and is only visible there.
Install
dsh plugin --profile default add github:ffyuuu/dsh-llm-longcat
export LONGCAT_API_KEY=... # create one at https://longcat.chat/platform/api_keys
Installing a bundle lets the package's install scripts run on your machine, outside the sandbox the agent runs under. Pin a commit so a later push cannot change what executes:
dsh plugin --profile default add github:ffyuuu/dsh-llm-longcat#3dcb3b1b5870ba52baab053453bdbb28826e5f13
Then pick LongCat-2.0 in the model selector. The key may also be stored through the Web UI's Models page instead of the environment.
Config
- id: llm-longcat
name: dsh-llm-longcat
config:
apiKeyEnv: LONGCAT_API_KEY # default; resolved per request, never a literal key
baseURL: https://api.longcat.chat/openai/v1 # optional; $LONGCAT_BASE_URL then the public API
thinking: enabled # optional deployment policy; `disabled` locks every request to off
reasoningEffort: high # optional; off | high — LongCat's switch is binary
maxTokens: 131072 # optional per-request output cap
defaultContextWindow: 1048576
streamIdleTimeoutMs: 300000 # optional; five-minute default
retryPolicy: # optional; omission uses bounded normal defaults
mode: normal
maxRetries: 3
models:
- id: LongCat-2.0
contextWindow: 1048576
A llm-longcat: section in $DSH_HOME/settings.yaml overrides any field
without a restart: base URL, catalog, request defaults, and idle budget all
take effect on the next request, while an in-flight stream keeps the facts it
started with.
Reasoning is binary, deliberately
LongCat controls thinking with thinking: {type: enabled|disabled} and does
not accept OpenAI's top-level reasoning_effort — its
supported_parameters lists the former and omits the latter. There is
therefore no low/medium/high gradient to map, and this adapter offers exactly
two levels rather than advertising controls that would collapse onto the same
two request bodies:
| Selected effort | Wire body |
|---|---|
high ("Thinking") | {"thinking": {"type": "enabled"}} |
off | {"thinking": {"type": "disabled"}} |
| (none named) | resolves from config; still explicit |
off serializes an explicit disabled rather than omitting the field —
omitting it would hand the decision to LongCat's server-side default, which is
not what selecting Off should mean. Requesting low, medium, or max fails
with UNSUPPORTED_REASONING_EFFORT before any network I/O.
Wire-format notes
- Tool-call deltas repeat
idandnameas explicitnull. LongCat sends them on the opening delta and thennull(not omitted) on every continuation, so a naive!== undefinedguard blanks the assembled call's name. Verified on live traffic; pinned by a regression test. - Streaming only, with
stream_options.include_usagealways on. Usage may arrive attached to the finish chunk or as a trailing usage-only chunk; both are deferred to[DONE]sousagealways precedesfinish. - The first thinking-mode delta can be an empty string — it must not open a reasoning block.
- Reasoning passback: on assistant turns that carried tool calls,
reasoning_contentis serialized back into history; on tool-call-free turns it is dropped (ignored anyway — saves tokens). - Assistant
contentis always a string, never null: the message is durable session history, and a null there would make later turns replay a body the endpoint can reject. - Cache accounting:
prompt_tokens_details.cached_tokensmaps tocacheReadTokensand is subtracted out ofinputTokensto keep the harness's disjoint-count convention.
Errors
Non-2xx responses throw LlmError with stable codes. LongCat documents a
dedicated 402 for exhausted token quota and puts insufficient_quota on
403, where most OpenAI-compatible providers use 429 — both are classified
as QUOTA before the auth and rate-limit buckets, so a depleted balance is
never reported as a bad key or retried as a transient rate limit.
| Condition | Code |
|---|---|
| 402, or quota detail at any status | QUOTA_EXCEEDED |
| 401 / 403 | AUTH |
| 429 | RATE_LIMIT |
| 400 with context-overflow detail | CONTEXT_WINDOW_EXCEEDED |
| other 400 | INVALID_REQUEST |
| 5xx | SERVER |
no [DONE] / bad JSON | STREAM_CLOSED / MALFORMED_RESPONSE |
A completed stream that opened no content blocks becomes a finish error with
EMPTY_RESPONSE, which the shipped retry policy treats as retryable.
Tests
npm run typecheck # against the published @deepseek-ai/dsh-llm types
npm test # 30 unit tests over serialize + translate
npm run build # emits lib/ and lib/types/
npm run test:e2e # real API, needs LONGCAT_API_KEY, spends a few hundred tokens
test:e2e drives the built adapter's own serialize → SSE → translate pipeline
against api.longcat.chat, so it verifies what the plugin actually sends
rather than a hand-written approximation. It is what caught the null-name
delta bug.
Limitations
- No image input. LongCat-2.0 reports
modality: text->text, so image content is refused before sending, naming the model. - No stop sequences.
stopis absent fromsupported_parameters; passing one fails withUNSUPPORTED_OPTIONrather than silently running past it. - Reasoning is binary — no low/medium/high gradient exists to map.
License
MIT