@volter/twin-deepseek 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +198 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +28 -0
  5. package/dist/src/deepseek-budget.d.ts +51 -0
  6. package/dist/src/deepseek-budget.js +152 -0
  7. package/dist/src/deepseek-cache.d.ts +56 -0
  8. package/dist/src/deepseek-cache.js +151 -0
  9. package/dist/src/deepseek-capabilities.d.ts +4 -0
  10. package/dist/src/deepseek-capabilities.js +1520 -0
  11. package/dist/src/deepseek-conformance.d.ts +14 -0
  12. package/dist/src/deepseek-conformance.js +473 -0
  13. package/dist/src/deepseek-connector.d.ts +168 -0
  14. package/dist/src/deepseek-connector.js +386 -0
  15. package/dist/src/deepseek-models.d.ts +30 -0
  16. package/dist/src/deepseek-models.js +38 -0
  17. package/dist/src/deepseek-scenario.d.ts +55 -0
  18. package/dist/src/deepseek-scenario.js +170 -0
  19. package/dist/src/deepseek-server.d.ts +16 -0
  20. package/dist/src/deepseek-server.js +191 -0
  21. package/dist/src/deepseek-stub.d.ts +75 -0
  22. package/dist/src/deepseek-stub.js +191 -0
  23. package/dist/src/deepseek-twin.d.ts +77 -0
  24. package/dist/src/deepseek-twin.js +1103 -0
  25. package/dist/src/deepseek-types.d.ts +172 -0
  26. package/dist/src/deepseek-types.js +26 -0
  27. package/dist/src/index.d.ts +15 -0
  28. package/dist/src/index.js +93 -0
  29. package/package.json +68 -0
  30. package/src/cli.ts +27 -0
  31. package/src/deepseek-budget.ts +178 -0
  32. package/src/deepseek-cache.ts +159 -0
  33. package/src/deepseek-capabilities.ts +1443 -0
  34. package/src/deepseek-conformance.ts +512 -0
  35. package/src/deepseek-connector.ts +440 -0
  36. package/src/deepseek-models.ts +65 -0
  37. package/src/deepseek-scenario.ts +188 -0
  38. package/src/deepseek-server.ts +201 -0
  39. package/src/deepseek-stub.ts +200 -0
  40. package/src/deepseek-twin.ts +1163 -0
  41. package/src/deepseek-types.ts +201 -0
  42. package/src/index.ts +133 -0
@@ -0,0 +1,172 @@
1
+ /** A chat message param as the caller sends it (content is a string OR a content-part array). */
2
+ export type DeepSeekMessageParam = {
3
+ role: 'system' | 'user' | 'assistant' | 'tool';
4
+ content?: string | Array<Record<string, unknown>> | null;
5
+ /** Optional participant name — supported on system, user and assistant messages only.
6
+ * `@ai-sdk/deepseek` warns `unsupported: message name on tool messages`. */
7
+ name?: string;
8
+ /** Beta-only: continue this assistant message's content instead of starting a new turn.
9
+ * Requires the `/beta` base URL and must be the FINAL message. */
10
+ prefix?: boolean;
11
+ /** The chain-of-thought DeepSeek returned on a previous assistant turn. When the request carries
12
+ * `tools`, DeepSeek requires previous turns to carry it back or answers 400
13
+ * (api-docs.deepseek.com/guides/thinking_mode). */
14
+ reasoning_content?: string | null;
15
+ tool_calls?: DeepSeekToolCall[];
16
+ tool_call_id?: string;
17
+ };
18
+ /** A function tool_call inside an assistant message (faithful shape). */
19
+ export type DeepSeekToolCall = {
20
+ id: string;
21
+ type: 'function';
22
+ function: {
23
+ name: string;
24
+ arguments: string;
25
+ };
26
+ };
27
+ /**
28
+ * The chat.completion `usage` object. `prompt_cache_hit_tokens` + `prompt_cache_miss_tokens` are
29
+ * DeepSeek's KV-cache accounting and always sum to `prompt_tokens`
30
+ * (api-docs.deepseek.com/guides/kv_cache: "the number of tokens in the input of this request that
31
+ * resulted in a cache hit" / "…that did not result in a cache hit"). `@ai-sdk/deepseek` reads both
32
+ * into `providerMetadata.deepseek.promptCacheHitTokens` / `…MissTokens` and derives
33
+ * `usage.inputTokens.cacheRead` from the hit count (src/chat/convert-to-deepseek-usage.ts).
34
+ */
35
+ export type DeepSeekUsage = {
36
+ prompt_tokens: number;
37
+ completion_tokens: number;
38
+ total_tokens: number;
39
+ prompt_cache_hit_tokens: number;
40
+ prompt_cache_miss_tokens: number;
41
+ prompt_tokens_details?: {
42
+ cached_tokens: number;
43
+ };
44
+ completion_tokens_details?: {
45
+ reasoning_tokens: number;
46
+ };
47
+ };
48
+ /**
49
+ * The assistant message DeepSeek returns. NOTE `reasoning_content` (no OpenAI counterpart) and the
50
+ * absence of OpenAI's `refusal` / `annotations`.
51
+ */
52
+ export type DeepSeekAssistantMessage = {
53
+ role: 'assistant';
54
+ content: string | null;
55
+ reasoning_content?: string | null;
56
+ tool_calls?: DeepSeekToolCall[];
57
+ };
58
+ /** One entry of a `logprobs.content` / `logprobs.reasoning_content` array. */
59
+ export type DeepSeekLogprob = {
60
+ token: string;
61
+ logprob: number;
62
+ bytes: number[] | null;
63
+ top_logprobs: Array<{
64
+ token: string;
65
+ logprob: number;
66
+ bytes: number[] | null;
67
+ }>;
68
+ };
69
+ /** DeepSeek splits logprobs by channel: generated content AND reasoning content. */
70
+ export type DeepSeekLogprobs = {
71
+ content?: DeepSeekLogprob[] | null;
72
+ reasoning_content?: DeepSeekLogprob[] | null;
73
+ };
74
+ /** `insufficient_system_resource` is DeepSeek's own value — `@ai-sdk/deepseek` maps it to the
75
+ * unified `error` finish reason (src/chat/map-deepseek-finish-reason.ts). */
76
+ export type DeepSeekFinishReason = 'stop' | 'length' | 'content_filter' | 'tool_calls' | 'insufficient_system_resource';
77
+ export type DeepSeekChoice = {
78
+ index: number;
79
+ message: DeepSeekAssistantMessage;
80
+ logprobs: DeepSeekLogprobs | null;
81
+ finish_reason: DeepSeekFinishReason;
82
+ };
83
+ /** The unary chat.completion response envelope (faithful shape). */
84
+ export type DeepSeekChatCompletion = {
85
+ id: string;
86
+ object: 'chat.completion';
87
+ created: number;
88
+ model: string;
89
+ choices: DeepSeekChoice[];
90
+ usage: DeepSeekUsage;
91
+ system_fingerprint: string;
92
+ };
93
+ /** `POST https://api.deepseek.com/beta/completions` — fill-in-the-middle. `object` is
94
+ * `text_completion` and the choice carries `text`, not a message. */
95
+ export type DeepSeekCompletionChoice = {
96
+ index: number;
97
+ text: string;
98
+ finish_reason: 'stop' | 'length' | 'content_filter' | 'insufficient_system_resource';
99
+ logprobs: {
100
+ tokens: string[];
101
+ token_logprobs: number[];
102
+ text_offset: number[];
103
+ top_logprobs: Array<Record<string, number>>;
104
+ } | null;
105
+ };
106
+ export type DeepSeekCompletion = {
107
+ id: string;
108
+ object: 'text_completion';
109
+ created: number;
110
+ model: string;
111
+ choices: DeepSeekCompletionChoice[];
112
+ usage: DeepSeekUsage;
113
+ system_fingerprint: string;
114
+ };
115
+ /**
116
+ * A DeepSeek model row. THREE keys, and that is the whole object: DeepSeek's own
117
+ * `GET /models` example response is `{"id": "...", "object": "model", "owned_by": "deepseek"}`
118
+ * (api-docs.deepseek.com/api/list-models, read 2026-08-31). OpenAI's row additionally carries
119
+ * `created`; DeepSeek's does not, and inventing one would be surface the vendor does not have.
120
+ */
121
+ export type DeepSeekModel = {
122
+ id: string;
123
+ object: 'model';
124
+ owned_by: string;
125
+ };
126
+ /** `GET /user/balance` (api-docs.deepseek.com/api/get-user-balance). Balances are STRINGS. */
127
+ export type DeepSeekBalanceInfo = {
128
+ currency: 'CNY' | 'USD';
129
+ total_balance: string;
130
+ granted_balance: string;
131
+ topped_up_balance: string;
132
+ };
133
+ export type DeepSeekBalance = {
134
+ is_available: boolean;
135
+ balance_infos: DeepSeekBalanceInfo[];
136
+ };
137
+ /** The file object (api-docs.deepseek.com/guides/files_api; validated key-for-key by
138
+ * `@ai-sdk/deepseek`'s `deepSeekFilesResponseSchema`). `expires_at` appears only when an
139
+ * expiration was requested. */
140
+ export type DeepSeekFile = {
141
+ id: string;
142
+ object: 'file';
143
+ bytes: number;
144
+ created_at: number;
145
+ filename: string;
146
+ purpose: 'user_data';
147
+ expires_at?: number;
148
+ };
149
+ /**
150
+ * DeepSeek's error envelope. `message` is the only REQUIRED key; `type`, `param` and `code` are
151
+ * nullish. This is not folklore from a rendered docs example — it is the zod schema
152
+ * `@ai-sdk/deepseek@3.0.37` actually decodes every failed response with
153
+ * (`deepSeekErrorSchema`, src/chat/deepseek-chat-api-types.ts), which is the stronger first-party
154
+ * oracle (ADDING_A_TWIN.md §6, "When first-party sources conflict").
155
+ */
156
+ export type DeepSeekError = {
157
+ error: {
158
+ message: string;
159
+ type?: string | null;
160
+ param?: unknown;
161
+ code?: string | number | null;
162
+ };
163
+ };
164
+ /** A single Server-Sent Event the streaming path emits (collected, never socketed in tests).
165
+ * `data` is the JSON payload; `[DONE]` is signalled with `done: true` (no data object). */
166
+ export type SseEvent = {
167
+ data?: Record<string, unknown>;
168
+ done?: boolean;
169
+ };
170
+ /** A sink the streaming path writes events into (an injected collector in tests / a real
171
+ * HTTP SSE writer in the server). NO real sockets or setTimeout in the handler. */
172
+ export type SseSink = (event: SseEvent) => void;
@@ -0,0 +1,26 @@
1
+ // Shared wire-shape types for the DeepSeek Platform API. These mirror the REAL vendor JSON shapes
2
+ // as published by DeepSeek's own API reference (api-docs.deepseek.com, read 2026-08-31) and by the
3
+ // zod schemas `@ai-sdk/deepseek@3.0.37` decodes responses with (src/chat/deepseek-chat-api-types.ts,
4
+ // src/files/deepseek-files-api.ts). The twin never imports the SDK at runtime; the SDK is exercised
5
+ // only in *.test.ts.
6
+ //
7
+ // DEEPSEEK IS OPENAI-COMPATIBLE, NOT OPENAI, and the differences below are load-bearing — they are
8
+ // exactly what a "copy the openai pack" twin gets wrong (ADDING_A_TWIN.md §0, the compatible-vendor
9
+ // warning). The REJECTIONS are the fidelity surface:
10
+ // • there is NO `/v1` path prefix — `base_url` is `https://api.deepseek.com` and the endpoint is
11
+ // `/chat/completions` (api-docs.deepseek.com "Your First API Call", read 2026-08-31);
12
+ // • the documented error table has NO 404 and DOES have 402 (Insufficient Balance) and
13
+ // 422 (Invalid Parameters) — OpenAI 400s where DeepSeek 422s
14
+ // (api-docs.deepseek.com/quick_start/error_codes);
15
+ // • `usage` carries the KV-cache split `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`
16
+ // (OpenAI reports `prompt_tokens_details.cached_tokens` instead);
17
+ // • the assistant message/delta carries `reasoning_content` (OpenAI has no such field);
18
+ // • `finish_reason` includes DeepSeek's own `insufficient_system_resource`;
19
+ // • the end-user identifier is `user_id` (regex + 512-char cap), NOT OpenAI's `user`;
20
+ // • `response_format` accepts ONLY `text` and `json_object` — `json_schema` is OpenAI-only;
21
+ // • `n`, `seed`, `logit_bias` and `top_k` are not DeepSeek parameters at all;
22
+ // • `frequency_penalty` / `presence_penalty` are DEPRECATED but accepted with NO effect (they
23
+ // must NOT be errors), as are `temperature` / `top_p` while thinking is enabled;
24
+ // • assistant-prefix completion and strict tool calls live only under the `/beta` base URL;
25
+ // • a `GET /models` row is `{id, object, owned_by}` — no `created`, no `context_window`.
26
+ export {};
@@ -0,0 +1,15 @@
1
+ export { handleDeepSeekTwinRequest, streamChat, buildChatCompletion, DEEPSEEK_BETA_PREFIX, DEEPSEEK_ANTHROPIC_PREFIX } from './deepseek-twin.js';
2
+ export type { DeepSeekRequest, DeepSeekResponseEnvelope } from './deepseek-twin.js';
3
+ export { createDeepSeekTwinFetch, createDeepSeekTwinServer, type DeepSeekTwinFetchOptions } from './deepseek-server.js';
4
+ export { DEEPSEEK_MODELS, FIM_MODELS, RETIRED_MODEL_IDS, VISION_MODELS, findModel, isThinkingModel } from './deepseek-models.js';
5
+ export { cacheHitTokens, canonicalMessage, prefixId, prefixKey, recordCachePrefixes } from './deepseek-cache.js';
6
+ export { buildUsage, contentToText, countPromptTokens, estimateTokens, fnv1a, lastUserText, messageTokens, stubAssistantText, stubFimText, stubFingerprint, stubJsonObject, stubReasoningText, stubToolArguments, stubToolCall, } from './deepseek-stub.js';
7
+ export type { DeepSeekAssistantMessage, DeepSeekBalance, DeepSeekBalanceInfo, DeepSeekChatCompletion, DeepSeekChoice, DeepSeekCompletion, DeepSeekCompletionChoice, DeepSeekError, DeepSeekFile, DeepSeekFinishReason, DeepSeekLogprob, DeepSeekLogprobs, DeepSeekMessageParam, DeepSeekModel, DeepSeekToolCall, DeepSeekUsage, SseEvent, SseSink, } from './deepseek-types.js';
8
+ export { createDeepSeekScenarioEngine, deepseekScenarioAdapter, loadDeepSeekScenarioDocument, realizeDeepSeekRespond } from './deepseek-scenario.js';
9
+ export type { DeepSeekScenarioEngine, DeepSeekScenarioRequest, DeepSeekScenarioRespond, ScenarioToolCall, ScriptedResult } from './deepseek-scenario.js';
10
+ export { fullSyncDeepSeek, externalIdFor, deepseekRequestForAction, liveDeepSeekExecute, mapBalance, mapFile, mapModel, pullDeepSeekState, pushDeepSeekAction, pushPendingDeepSeekActions, syncDeepSeekFromReal, unpushableReason, } from './deepseek-connector.js';
11
+ export type { DeepSeekExecute, LiveDeepSeekOptions } from './deepseek-connector.js';
12
+ export { DEEPSEEK_BUDGET_CEILING, DEEPSEEK_BUDGET_MAX_RETRY_AFTER_S, DEEPSEEK_BUDGET_WINDOW_MS, DEEPSEEK_CALL_WEIGHTS, DEEPSEEK_RATE_BUDGET, DeepSeekBudget, DeepSeekBudgetError, deepseekBudgetPath, deepseekCallWeight, } from './deepseek-budget.js';
13
+ export type { DeepSeekBudgetErrorKind, DeepSeekBudgetOptions, DeepSeekBudgetReservation, DeepSeekBudgetSnapshot } from './deepseek-budget.js';
14
+ import type { TwinPack } from '@volter/world-core';
15
+ export declare const pack: TwinPack;
@@ -0,0 +1,93 @@
1
+ // @volter/twin-deepseek — the DeepSeek twin (one vendor, one package), built on the shared
2
+ // @volter/world-core kernel. The DeepSeek Platform API: a vendor-faithful protocol envelope (chat
3
+ // completions/streaming/tool_calls, `reasoning_content`, DeepSeek's KV-cache-bearing `usage`), the
4
+ // published model catalog, the account balance, beta FIM completion, and a STATEFUL image Files API
5
+ // — over an event/action log. (DeepSeek is an API-first vendor: platform.deepseek.com is a
6
+ // keys/billing/usage console, not where the work happens — docs/contributing/architecture.md C1b — so this pack ships
7
+ // no mirror.)
8
+ //
9
+ // THE DETERMINISTIC STUB IS THE ANSWER: the twin runs no model, so POST /chat/completions returns a
10
+ // DETERMINISTIC STUB completion (clearly labeled), its `reasoning_content` is a labeled stub, and
11
+ // POST /beta/completions returns a labeled FIM stub — never pretending to be real inference.
12
+ // Everything around them — the wire protocol, and above all WHAT DEEPSEEK REFUSES — is faithful.
13
+ // (Conformance + capability tooling live in @volter/world-tooling, a dev dependency — NOT
14
+ // re-exported here, per E2.)
15
+ export { handleDeepSeekTwinRequest, streamChat, buildChatCompletion, DEEPSEEK_BETA_PREFIX, DEEPSEEK_ANTHROPIC_PREFIX } from "./deepseek-twin.js";
16
+ export { createDeepSeekTwinFetch, createDeepSeekTwinServer } from "./deepseek-server.js";
17
+ export { DEEPSEEK_MODELS, FIM_MODELS, RETIRED_MODEL_IDS, VISION_MODELS, findModel, isThinkingModel } from "./deepseek-models.js";
18
+ export { cacheHitTokens, canonicalMessage, prefixId, prefixKey, recordCachePrefixes } from "./deepseek-cache.js";
19
+ export { buildUsage, contentToText, countPromptTokens, estimateTokens, fnv1a, lastUserText, messageTokens, stubAssistantText, stubFimText, stubFingerprint, stubJsonObject, stubReasoningText, stubToolArguments, stubToolCall, } from "./deepseek-stub.js";
20
+ export { createDeepSeekScenarioEngine, deepseekScenarioAdapter, loadDeepSeekScenarioDocument, realizeDeepSeekRespond } from "./deepseek-scenario.js";
21
+ export { fullSyncDeepSeek, externalIdFor, deepseekRequestForAction, liveDeepSeekExecute, mapBalance, mapFile, mapModel, pullDeepSeekState, pushDeepSeekAction, pushPendingDeepSeekActions, syncDeepSeekFromReal, unpushableReason, } from "./deepseek-connector.js";
22
+ // The client-side rate budget — the fail-closed backstop `liveDeepSeekExecute` routes every live
23
+ // request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
24
+ // here is DeepSeek's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
25
+ // bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
26
+ // `DeepSeekBudgetError` by type; there is deliberately no export that disables the guard.
27
+ export { DEEPSEEK_BUDGET_CEILING, DEEPSEEK_BUDGET_MAX_RETRY_AFTER_S, DEEPSEEK_BUDGET_WINDOW_MS, DEEPSEEK_CALL_WEIGHTS, DEEPSEEK_RATE_BUDGET, DeepSeekBudget, DeepSeekBudgetError, deepseekBudgetPath, deepseekCallWeight, } from "./deepseek-budget.js";
28
+ import { DEEPSEEK_RATE_BUDGET as RATE_BUDGET } from "./deepseek-budget.js";
29
+ export const pack = {
30
+ vendor: 'deepseek',
31
+ // The SAME object deepseek-budget.ts declares at module load — one source of truth, so registering
32
+ // the pack and importing the connector can never arm two different ceilings.
33
+ rateBudget: RATE_BUDGET,
34
+ transport: 'rest',
35
+ archetype: 'generative',
36
+ bin: 'world-deepseek',
37
+ // R2 adopted-as-debt, legibly: declared types no replay can create, each with its reason.
38
+ resourcesUnreachable: {
39
+ 'model': 'the vendor catalog',
40
+ 'balance': 'the account ledger the vendor keeps; read-only',
41
+ },
42
+ resources: ['model', 'file', 'balance', 'cache_prefix'],
43
+ specSource: "DeepSeek Platform API — enumerated top-down from api-docs.deepseek.com (quick_start/{error_codes,rate_limit,pricing}, api/{create-chat-completion,create-completion,list-models,get-user-balance}, guides/{kv_cache,thinking_mode,files_api,anthropic_api,chat_prefix_completion}; read 2026-08-31) and cross-checked key-for-key against the zod schemas @ai-sdk/deepseek@3.0.37 encodes/decodes with (src/chat/deepseek-chat-api-types.ts, src/chat/deepseek-chat-language-model{,-options}.ts, src/chat/deepseek-prepare-tools.ts, src/files/deepseek-files{,-api,-options}.ts) plus the provider doc page shipped in that tarball (docs/30-deepseek.mdx). DeepSeek publishes no OpenAPI document. Envelope-faithful; model output, reasoning_content, FIM text and logprob values are labeled deterministic stubs.",
44
+ description: "DeepSeek Platform API twin — faithful protocol envelope (chat completions/streaming/tool_calls, reasoning_content, KV-cache-split usage), the published catalog, user balance, beta FIM + prefix completion + strict tools, and a stateful image Files API; generative output is a labeled stub and the OpenAI-divergent REFUSALS (402/422, no json_schema, no n/seed/logit_bias/user) are modeled.",
45
+ // DeepSeek's base_url is https://api.deepseek.com with NO version segment — the endpoint is
46
+ // `/chat/completions` (api-docs.deepseek.com "Your First API Call", read 2026-08-31). This is the
47
+ // single most likely thing to be copied wrong from an OpenAI-shaped exemplar.
48
+ browserRouting: { apiPathPrefix: '/', loaderHost: 'https://api.deepseek.com' },
49
+ // ADOPTION (adding-a-twin.md §3): how an app repo betrays that it talks to DeepSeek. Declared
50
+ // HERE, not in world-runtime's central SDK_TWINS/ENV_STEM_VENDORS maps — declaring in both throws.
51
+ //
52
+ // `@ai-sdk/deepseek` is the Vercel AI SDK provider and the ONLY first-party npm client of this
53
+ // surface — DeepSeek publishes no SDK of its own; its docs tell integrators to point the OpenAI
54
+ // or Anthropic SDK at api.deepseek.com instead ("The DeepSeek API uses an API format compatible
55
+ // with OpenAI/Anthropic"). That is exactly why `openai` is NOT claimed here: a repo depending on
56
+ // `openai` is an OpenAI repo unless its base URL says otherwise, and claiming the package would
57
+ // mis-attribute every OpenAI integration in the census to this pack. The 2026-08-31-b census
58
+ // bears this out — 5 of the 6 demanding repos signal `@ai-sdk/deepseek`; the sixth (nextchat)
59
+ // signals the bare `deepseek` stem via DEEPSEEK_API_KEY / DEEPSEEK_URL.
60
+ //
61
+ // `envStems: ['DEEPSEEK']` normalizes (covers.ts `normalizeId`: lowercase + strip
62
+ // non-alphanumerics) to `deepseek`, which is what DEEPSEEK_API_KEY — the one env
63
+ // `@ai-sdk/deepseek` reads (`loadApiKey({ environmentVariableName: 'DEEPSEEK_API_KEY' })`,
64
+ // src/deepseek-provider.ts) — and NextChat's DEEPSEEK_URL both reduce to.
65
+ //
66
+ // No `scopes`: `@ai-sdk/` is Vercel's scope shared by every provider package, not DeepSeek's.
67
+ adoption: {
68
+ // No official Python SDK: DeepSeek's own docs tell Python callers to use the `openai` client
69
+ // against api.deepseek.com, and that distribution belongs to the openai pack (the pypi list is
70
+ // the distribution; DEEPSEEK_BASE_URL is what routes it). The `deepseek` name on PyPI is an
71
+ // unaffiliated third-party wrapper with no adoption, deliberately not claimed.
72
+ pypi: [],
73
+ sdks: ['@ai-sdk/deepseek'], envStems: ['DEEPSEEK'],
74
+ },
75
+ // INTERCEPTION: the one host every client addresses. `@ai-sdk/deepseek` defaults its baseURL to
76
+ // `https://api.deepseek.com` and builds every URL as `${baseURL}${path}` for chat and
77
+ // `${baseURL}/files` for uploads (src/deepseek-provider.ts, src/files/deepseek-files.ts), so the
78
+ // standard surface, the `/beta` surface and the `/anthropic` surface all live on this one host.
79
+ // No second host exists anywhere in the SDK.
80
+ hosts: [{ host: 'api.deepseek.com' }],
81
+ // The injector intercepts api.deepseek.com (see `hosts`), so a world wires this pack through
82
+ // DEEPSEEK_TWIN_URL rather than an app-read base-URL var.
83
+ //
84
+ // THE GROUNDING FOR THE "NONE" HALF: `@ai-sdk/deepseek` reads exactly ONE environment variable —
85
+ // `DEEPSEEK_API_KEY`, via `loadApiKey` — and takes `baseURL` as a constructor option only
86
+ // (`DeepSeekProviderSettings.baseURL`, src/deepseek-provider.ts). There is no DEEPSEEK_BASE_URL
87
+ // in the SDK, in DeepSeek's docs, or anywhere in the tarball. NextChat's `DEEPSEEK_URL` is that
88
+ // one application's own convention, not a vendor-documented var, and injecting it would make
89
+ // `covers` report every DeepSeek world covered while the five repos on `@ai-sdk/deepseek` — which
90
+ // read no such var — still talked to the real vendor. That is precisely the class of lie
91
+ // ADDING_A_TWIN.md §7 point 12 forbids ("never invent a <VENDOR>_BASE_URL the app does not read").
92
+ endpointEnvNone: "no app-read endpoint env: @ai-sdk/deepseek — the only first-party npm client of this surface — reads exactly one environment variable (DEEPSEEK_API_KEY, via loadApiKey in src/deepseek-provider.ts) and takes baseURL as a constructor option, so there is no vendor-documented base-URL var to inject. The injector covers api.deepseek.com instead (see `hosts`). NextChat's DEEPSEEK_URL is one application's own convention, not vendor surface, and claiming it would report a world covered while the SDK-based majority still reached the real vendor.",
93
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@volter/twin-deepseek",
3
+ "version": "0.1.0",
4
+ "description": "Local DeepSeek twin — a faithful, stateful local DeepSeek Platform API your real `@ai-sdk/deepseek` talks to unmodified. The model is stubbed (deterministic), but the protocol envelope is vendor-faithful and so are DeepSeek's OpenAI-DIVERGENT REFUSALS: 422 for a bad parameter and 402 for a drained balance (not OpenAI's 400), no /v1 path segment, no json_schema/n/seed/logit_bias/user, beta-only prefix completion and strict tools, reasoning_content, and a real KV-cache-split usage ledger. Built on @volter/world-core.",
5
+ "keywords": [
6
+ "twin",
7
+ "local",
8
+ "mock",
9
+ "mirror",
10
+ "simulator",
11
+ "fixtures",
12
+ "testing",
13
+ "sdk",
14
+ "api",
15
+ "localstack",
16
+ "deepseek",
17
+ "llm",
18
+ "openai-compatible",
19
+ "reasoning"
20
+ ],
21
+ "author": "Volter (https://github.com/volter-ai)",
22
+ "license": "Apache-2.0",
23
+ "files": [
24
+ "src",
25
+ "README.md",
26
+ "LICENSE",
27
+ "!**/*.test.ts",
28
+ "!**/*.test.tsx",
29
+ "dist"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/volter-ai/twin.git",
34
+ "directory": "packages/twin/deepseek"
35
+ },
36
+ "homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/deepseek#readme",
37
+ "type": "module",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/src/index.d.ts",
41
+ "default": "./dist/src/index.js"
42
+ }
43
+ },
44
+ "bin": {
45
+ "world-deepseek": "dist/src/cli.js"
46
+ },
47
+ "scripts": {
48
+ "test": "bun test src/*.test.ts",
49
+ "typecheck": "tsc --noEmit",
50
+ "build": "node ../../../scripts/publish/build.mjs",
51
+ "prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
52
+ "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
53
+ },
54
+ "peerDependencies": {
55
+ "@volter/world-core": "2.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "@volter/world-core": "2.0.0",
59
+ "@volter/world-tooling": "0.1.0",
60
+ "@ai-sdk/deepseek": "^3.0.37",
61
+ "@types/bun": "^1.2.20",
62
+ "@types/node": "^24.0.0",
63
+ "typescript": "^5.9.0"
64
+ },
65
+ "engines": {
66
+ "node": ">=22.3"
67
+ }
68
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
3
+ // world-deepseek CLI: serve the DeepSeek API twin or run conformance. DeepSeek is an API-first vendor —
4
+ // its console is a keys/usage/playground dev console, not where the work happens
5
+ // (docs/contributing/architecture.md C1b) — so this pack ships no mirror.
6
+ import { hasFlag, optionValue } from '@volter/world-core/args';
7
+ import { createDeepSeekTwinServer } from './deepseek-server.ts';
8
+
9
+ const [cmd, ...rest] = process.argv.slice(2);
10
+ const port = Number(optionValue(rest, '--port', '0')) || undefined;
11
+ const root = optionValue(rest, '--root') || undefined;
12
+ const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
13
+ const scenario = optionValue(rest, '--scenario') || undefined; // scripted completions (JSON file)
14
+
15
+ if (cmd === 'serve') {
16
+ const s = await createDeepSeekTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}), ...(scenario ? { scenarioPath: scenario } : {}) });
17
+ process.stdout.write(`deepseek twin (DeepSeek Platform API, no /v1 segment; model output is a deterministic stub)${readOnly ? ' [read-only]' : ''}${scenario ? ` [scenario: ${scenario}]` : ''} at http://127.0.0.1:${s.port}\n`);
18
+ await keepProcessAlive();
19
+ } else if (cmd === 'conformance') {
20
+ // dev-only; lazy so the bin runs without @volter/world-tooling
21
+ const { checkDeepSeekConformance } = await import('./deepseek-conformance.ts');
22
+ const report = await checkDeepSeekConformance({ ...(root ? { root } : {}) });
23
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
24
+ if (!report.ok) process.exitCode = 1;
25
+ } else {
26
+ process.stdout.write('Usage: world-deepseek serve|conformance [--port N] [--root DIR] [--read-only] [--scenario FILE]\n');
27
+ }
@@ -0,0 +1,178 @@
1
+ // DeepSeek's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus the thin typed
2
+ // bindings `liveDeepSeekExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
3
+ // window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger —
4
+ // lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`). Read that module's
5
+ // header for the full rationale AND for the honest list of what the guard does not guarantee.
6
+ //
7
+ // ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────
8
+ // A real ~4.5-DAY vendor lockout (Figma, 2026-07-25) happened because raw API calls were made
9
+ // outside the pack's connector — no cache, no batching, no ceiling. Discipline only binds the code
10
+ // that follows it; a BUDGET binds the code that does not.
11
+ //
12
+ // ── HOW THE CEILING WAS CHOSEN (live-read, 2026-08-31) ──────────────────────────────────────
13
+ // https://api-docs.deepseek.com/quick_start/rate_limit (read 2026-08-31) publishes NO scalar
14
+ // per-minute or per-day request allowance at all. What it publishes is a CONCURRENCY table —
15
+ // deepseek-v4-pro 500, deepseek-v4-flash 2500, deepseek-v4-flash-vision-exp 2500 simultaneous
16
+ // connections per account — plus the note that "if you need higher concurrency, you can submit a
17
+ // capacity expansion request". Concurrency is not a rate: it bounds how many calls are in flight,
18
+ // not how many are made per minute, so nothing on that page can be turned into a window ceiling.
19
+ // (This figure was live-fetched for THIS build. The copied-exemplar hazard ADDING_A_TWIN.md §0.5
20
+ // warns about is real: the pack this one was cloned from asserted GroqCloud's eight-dimension
21
+ // RPM/RPD/TPM/ASH scheme, which DeepSeek does not have at all.)
22
+ //
23
+ // So — exactly as ADDING_A_TWIN.md directs when a vendor publishes no scalar limit ("say exactly
24
+ // that in `reason` and stay at or under the fallback") — this declaration is pinned at the kernel's
25
+ // undeclared fallback in every dimension: window 60s, ceiling 60, defaultWeight 2, i.e. 30
26
+ // calls/minute, EXACTLY `DEFAULT_RATE_BUDGET`, with no endpoint priced cheaper than the fallback
27
+ // would price it. What the declaration buys is RESOLUTION, and it buys it DOWNWARD only.
28
+ //
29
+ // It bounds the 60s AVERAGE; it does not pace (the kernel refuses, it never sleeps). The backstop
30
+ // for a sub-second burst is the cooldown: a `retry-after` read off a real 429 turns into a
31
+ // persisted refusal.
32
+ //
33
+ // ── HOW THE WEIGHTS WERE CHOSEN (and what is a judgement call) ───────────────────────────────
34
+ // • `/chat/completions`, `/beta/chat/completions` and `/beta/completions` cost 6. DeepSeek prices
35
+ // and meters these by TOKENS, not requests (api-docs.deepseek.com/quick_start/pricing bills
36
+ // input cache-hit / cache-miss / output tokens), so one inference request consumes far more of
37
+ // an account's real allowance than a file-list poll. 60/6 = at most 10 inference calls a window.
38
+ // That factor is a judgement call, not a vendor figure — it is deliberately the STRICT
39
+ // direction, which needs no vendor justification; being looser would, and there is none to have.
40
+ // • Everything else (files, models, balance) costs 2 — the fallback's own default.
41
+ //
42
+ // ── AN HONEST NOTE ON THE INFERENCE RULE ────────────────────────────────────────────────────
43
+ // This pack's connector pulls models, files and the account balance; it NEVER issues live
44
+ // inference, because a twin does not run the model anywhere — local or remote (CLAUDE.md,
45
+ // serve-path determinism). `liveDeepSeekExecute`'s path allowlist refuses `/chat/completions`
46
+ // outright. So the `inference: 6` rule is DEFENCE IN DEPTH, not a live pricing path, and the pack
47
+ // claims exactly that: `deepseek.rate_limits.inference_costs_more` asserts the pure pricing
48
+ // function, `deepseek.rate_limits.executor_path_allowlist` asserts the refusal, and no capability
49
+ // claims a live inference call was ever budgeted.
50
+ import {
51
+ declareRateBudget,
52
+ rateBudgetPath,
53
+ rateBudgetWeight,
54
+ RateBudget,
55
+ type RateBudgetDeclaration,
56
+ type RateBudgetOptions,
57
+ type RateBudgetReservation,
58
+ type RateBudgetSnapshot,
59
+ } from '@volter/world-core';
60
+
61
+ const VENDOR = 'deepseek';
62
+
63
+ /** Rolling window, in ms. Spend older than this is pruned. */
64
+ export const DEEPSEEK_BUDGET_WINDOW_MS = 60_000;
65
+
66
+ /**
67
+ * Weighted units allowed inside one window. 60/60s at `defaultWeight` 2 = 30 calls a minute —
68
+ * EXACTLY the kernel's undeclared fallback, because DeepSeek publishes no scalar that would justify
69
+ * more. See the header.
70
+ */
71
+ export const DEEPSEEK_BUDGET_CEILING = 60;
72
+
73
+ /** Seconds. A `retry-after` above this means the key is throttled hard — fail loudly, don't sleep. */
74
+ export const DEEPSEEK_BUDGET_MAX_RETRY_AFTER_S = 300;
75
+
76
+ /** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs. judged. */
77
+ export const DEEPSEEK_CALL_WEIGHTS = {
78
+ /** `/chat/completions`, `/beta/chat/completions`, `/beta/completions` — TOKEN-metered and
79
+ * token-billed, so one call spends far more of an account's real allowance than a list poll.
80
+ * At 60/6 that is at most 10 inference calls a window. */
81
+ inference: 6,
82
+ /** Everything else: files list/upload/retrieve/delete, models, user balance. */
83
+ other: 2,
84
+ } as const;
85
+
86
+ /** THE PACK'S DECLARATION — pure data, the only DeepSeek-specific thing in the whole budget. */
87
+ export const DEEPSEEK_RATE_BUDGET: RateBudgetDeclaration = {
88
+ windowMs: DEEPSEEK_BUDGET_WINDOW_MS,
89
+ ceiling: DEEPSEEK_BUDGET_CEILING,
90
+ defaultWeight: DEEPSEEK_CALL_WEIGHTS.other,
91
+ maxRetryAfterSeconds: DEEPSEEK_BUDGET_MAX_RETRY_AFTER_S,
92
+ rules: [
93
+ // Anchored on DeepSeek's REAL paths — there is no `/v1` segment on this vendor. The optional
94
+ // `/beta` prefix is part of the pattern because the beta base URL reaches the SAME priced
95
+ // inference endpoints; a rule that missed it would price a beta chat call as a 2-unit read.
96
+ { match: '^POST (/beta)?/chat/completions$', weight: DEEPSEEK_CALL_WEIGHTS.inference },
97
+ { match: '^POST /beta/completions$', weight: DEEPSEEK_CALL_WEIGHTS.inference },
98
+ ],
99
+ reason:
100
+ 'DeepSeek publishes NO scalar request-rate limit at all (api-docs.deepseek.com/quick_start/rate_limit, ' +
101
+ 'read 2026-08-31). The only figures on that page are CONCURRENCY limits — deepseek-v4-pro 500, ' +
102
+ 'deepseek-v4-flash 2500, deepseek-v4-flash-vision-exp 2500 simultaneous connections per account, ' +
103
+ 'with a capacity-expansion request available for more — and concurrency bounds calls in flight, ' +
104
+ 'not calls per minute, so it cannot be turned into a window ceiling. Because no published figure ' +
105
+ 'justifies going higher, the ceiling is pinned at the kernel fallback in every dimension: 60 units ' +
106
+ '/ 60s at defaultWeight 2 = 30 calls/min, identical to DEFAULT_RATE_BUDGET. The declaration buys ' +
107
+ 'resolution DOWNWARD, never headroom: inference (POST /chat/completions, POST /beta/chat/completions, ' +
108
+ 'POST /beta/completions) costs 6, so at most 10 land in a window, because DeepSeek bills those ' +
109
+ 'endpoints by token (input cache-hit / cache-miss / output) rather than by request. That factor is ' +
110
+ 'a judgement call in the strict direction, not a vendor figure. The window bounds the 60s AVERAGE ' +
111
+ 'and does not pace; a `retry-after` read off a real 429 is the persisted cooldown backstop.',
112
+ };
113
+
114
+ // Declared at module load, so merely importing this module (which `deepseek-connector.ts` does) is
115
+ // enough to arm the real ceiling.
116
+ declareRateBudget(VENDOR, DEEPSEEK_RATE_BUDGET);
117
+
118
+ /**
119
+ * Price one call. The key is `"<METHOD> <path>"` with the query string split off, so a rule can
120
+ * price by method (a write is not a read) without the kernel knowing anything about DeepSeek. An
121
+ * unclassified endpoint still costs `defaultWeight` — nothing is ever free.
122
+ */
123
+ export function deepseekCallWeight(method: string, path: string): number {
124
+ const { bare, query } = splitQuery(path);
125
+ // UPPER-CASE the method: `fetch` normalizes a known lowercase method before sending, so
126
+ // `execute('post', …)` really does issue a POST and must be priced as one.
127
+ return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
128
+ }
129
+
130
+ /**
131
+ * `/chat/completions?a=1` -> `{ bare: '/chat/completions', query: { a: '1' } }`. Rules match the path;
132
+ * `whenQuery*` the query. NORMALIZED, because the anchored rules are otherwise trivially evaded:
133
+ * `fetch` upper-cases a known method before sending, so `execute('post', …)` issues a real WRITE
134
+ * that a `^POST ` rule would price as a read; and a trailing slash makes a path miss a `$` anchor
135
+ * while most routers treat it as the same endpoint.
136
+ */
137
+ function splitQuery(path: string): { bare: string; query: Record<string, string> } {
138
+ const at = path.indexOf('?');
139
+ const query: Record<string, string> = {};
140
+ if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
141
+ // Collapse REPEATED slashes as well as a trailing one: `//chat/completions` reaches the same
142
+ // endpoint on most routers but misses a `^POST (/beta)?/chat/completions$` rule, which would
143
+ // price an inference call as a 2-unit read.
144
+ const raw = (at === -1 ? path : path.slice(0, at)).replace(/\/{2,}/g, '/');
145
+ const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
146
+ return { bare, query };
147
+ }
148
+
149
+ /** Where DeepSeek's ledger lives. Token-keyed and cwd-independent by default (DeepSeek's limits are per
150
+ * ORGANIZATION, i.e. per key, so a cwd-scoped ledger would hand the same key a fresh allowance in
151
+ * every checkout, worktree and CI matrix leg); pass `root` for world-scoped accounting. */
152
+ export function deepseekBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
153
+ const o = typeof opts === 'string' ? { root: opts } : opts;
154
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
155
+ // excess-property check only catches object literals) must not redirect this pack's ledger.
156
+ return rateBudgetPath({ ...o, vendor: VENDOR });
157
+ }
158
+
159
+ /** Construction options for DeepSeek's budget. The vendor is fixed; everything else may only TIGHTEN. */
160
+ export type DeepSeekBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
161
+
162
+ /**
163
+ * DeepSeek's budget — the shared kernel guard bound to this vendor's declaration. A real subclass, not
164
+ * an alias, so `budget instanceof DeepSeekBudget` in `liveDeepSeekExecute` means "a budget that accounts
165
+ * against DEEPSEEK's ledger under DEEPSEEK's ceiling".
166
+ */
167
+ export class DeepSeekBudget extends RateBudget {
168
+ constructor(opts: DeepSeekBudgetOptions = {}) {
169
+ super({ ...opts, vendor: VENDOR });
170
+ }
171
+ }
172
+
173
+ /** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
174
+ * which one refused, and `err.kind` says why. */
175
+ export { RateBudgetError as DeepSeekBudgetError } from '@volter/world-core';
176
+ export type { RateBudgetErrorKind as DeepSeekBudgetErrorKind } from '@volter/world-core';
177
+ export type DeepSeekBudgetReservation = RateBudgetReservation;
178
+ export type DeepSeekBudgetSnapshot = RateBudgetSnapshot;