@volter/twin-deepseek 0.1.0 → 0.1.1

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.
package/README.md CHANGED
@@ -54,9 +54,9 @@ capability with a failable negative verify, grounded in
54
54
  | usage cache fields | **`prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`** | `prompt_tokens_details.cached_tokens` |
55
55
  | reasoning | **`reasoning_content`** on the message and the delta | `reasoning` on o-series responses only |
56
56
  | finish reasons | `stop`, `length`, `content_filter`, `tool_calls`, **`insufficient_system_resource`** | no `insufficient_system_resource` |
57
- | end-user id | **`user_id`** (`^[a-zA-Z0-9_-]+$`, ≤512) | `user` |
57
+ | end-user id | **`user_id`** (`^[a-zA-Z0-9_-]+$`, ≤512); OpenAI's `user` is accepted and ignored (the docs are silent; ruling in `deepseek-twin.ts`, `OPENAI_USER`) | `user` |
58
58
  | structured output (chat) | `text`, `json_object` — **no `json_schema` on `/chat/completions`** (its Responses API's `text.format` does take one) | `text`, `json_object`, `json_schema` |
59
- | not parameters at all | **`n`, `seed`, `logit_bias`, `top_k`, `user`, `max_completion_tokens`, `service_tier`, `parallel_tool_calls`, `functions`** — each a 422 by name | all accepted |
59
+ | not parameters at all | **`n`, `seed`, `logit_bias`, `top_k`, `max_completion_tokens`, `service_tier`, `parallel_tool_calls`, `functions`** — each a 422 by name | all accepted |
60
60
  | deprecated-but-accepted | `frequency_penalty`, `presence_penalty` — **no effect, never an error** | still honoured |
61
61
  | ignored-but-accepted | `temperature`, `top_p` while thinking is on — **no effect, never an error** | honoured |
62
62
  | beta-only | assistant `prefix: true`, `strict: true` tools, FIM `/beta/completions` | n/a |
@@ -210,12 +210,12 @@ export const DEEPSEEK_CAPABILITIES = [
210
210
  const sixteen = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ stop: Array.from({ length: 16 }, (_, i) => `s${i}`) }) });
211
211
  return many.status === 422 && ok(sixteen);
212
212
  })),
213
- done('deepseek.chat.rejects_openai_only_params', 'chat', "The OpenAI parameters DeepSeek's closed table does not declare (n, seed, logit_bias, top_k, user, max_completion_tokens, service_tier, parallel_tool_calls, functions) are refused BY NAME with 422", 'api', 'core', () => withRoot(async (h) => {
213
+ done('deepseek.chat.rejects_openai_only_params', 'chat', "The OpenAI parameters DeepSeek's closed table does not declare (n, seed, logit_bias, top_k, max_completion_tokens, service_tier, parallel_tool_calls, functions) are refused BY NAME with 422", 'api', 'core', () => withRoot(async (h) => {
214
214
  // THE flagship OpenAI-divergence check. A twin copied from an OpenAI-shaped exemplar serves
215
215
  // every one of these happily, which is the inherited-permissiveness false-green
216
216
  // ADDING_A_TWIN.md §0 names. Each must be 422 — DeepSeek's "Invalid Parameters" — not 400.
217
217
  for (const [key, value] of [
218
- ['n', 2], ['seed', 42], ['logit_bias', { '1': 1 }], ['top_k', 5], ['user', 'u1'],
218
+ ['n', 2], ['seed', 42], ['logit_bias', { '1': 1 }], ['top_k', 5],
219
219
  ['max_completion_tokens', 32], ['service_tier', 'flex'], ['parallel_tool_calls', false],
220
220
  ['functions', [{ name: 'f' }]], ['store', true], ['metadata', { a: 'b' }],
221
221
  ]) {
@@ -285,6 +285,17 @@ export const DEEPSEEK_CAPABILITIES = [
285
285
  const atCap = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user_id: 'a'.repeat(512) }) });
286
286
  return ok(good) && bad.status === 422 && long.status === 422 && ok(atCap);
287
287
  })),
288
+ done('deepseek.chat.openai_user_ignored', 'chat', "OpenAI's `user` (which OpenAI-compatible software such as LibreChat sends) is accepted and has no effect: it is not DeepSeek's `user_id`, and a non-string is 422", 'api', 'common', () => withRoot(async (h) => {
289
+ const plain = await h({ m: 'POST', p: CHAT_PATH, b: CHAT() });
290
+ // a value user_id's charset refuses: accepted here, so it is not being read as user_id
291
+ const withUser = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user: 'sam@friends.test' }) });
292
+ const both = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user: 'u1', user_id: 'bad id' }) });
293
+ const typed = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user: 42 }) });
294
+ const answer = (r) => JSON.stringify([r.body.choices?.[0]?.message?.content, r.body.usage?.prompt_tokens]);
295
+ return ok(plain) && ok(withUser) && answer(withUser) === answer(plain)
296
+ && both.status === 422 && msg(both).includes("'user_id'")
297
+ && typed.status === 422 && msg(typed).includes("'user'");
298
+ })),
288
299
  done('deepseek.chat.message_name', 'chat', 'Participant `name` is accepted on system/user/assistant turns and counted into prompt tokens', 'api', 'niche', () => withRoot(async (h) => {
289
300
  const named = await h({ m: 'POST', p: CHAT_PATH, b: { model: MODEL, messages: [{ role: 'user', content: 'hi', name: 'customer_alpha' }] } });
290
301
  const plain = await h({ m: 'POST', p: CHAT_PATH, b: { model: MODEL, messages: [{ role: 'user', content: 'hi' }] } });
@@ -195,7 +195,8 @@ const REJECTIONS = [
195
195
  { name: 'seed', method: 'POST', path: '/chat/completions', body: { ...CHAT, seed: 42 }, status: 422, why: "'seed' is not a DeepSeek parameter" },
196
196
  { name: 'logit_bias', method: 'POST', path: '/chat/completions', body: { ...CHAT, logit_bias: { '1': 1 } }, status: 422, why: "'logit_bias' is not a DeepSeek parameter" },
197
197
  { name: 'top_k', method: 'POST', path: '/chat/completions', body: { ...CHAT, top_k: 5 }, status: 422, why: "'top_k' is not a DeepSeek parameter" },
198
- { name: 'user', method: 'POST', path: '/chat/completions', body: { ...CHAT, user: 'u1' }, status: 422, why: "DeepSeek's end-user identifier is 'user_id', not OpenAI's 'user'" },
198
+ // OpenAI's `user` as a string is accepted and ignored (deepseek-twin.ts, OPENAI_USER); only a non-string is refused
199
+ { name: 'user (not a string)', method: 'POST', path: '/chat/completions', body: { ...CHAT, user: 42 }, status: 422, why: "OpenAI's 'user' is taken and ignored, but only as a string" },
199
200
  { name: 'max_completion_tokens', method: 'POST', path: '/chat/completions', body: { ...CHAT, max_completion_tokens: 10 }, status: 422, why: "DeepSeek declares 'max_tokens' only" },
200
201
  { name: 'service_tier', method: 'POST', path: '/chat/completions', body: { ...CHAT, service_tier: 'flex' }, status: 422, why: 'DeepSeek has no service tiers' },
201
202
  { name: 'parallel_tool_calls', method: 'POST', path: '/chat/completions', body: { ...CHAT, parallel_tool_calls: false }, status: 422, why: 'not in DeepSeek\'s parameter table' },
@@ -25,15 +25,17 @@
25
25
  // includes two OpenAI does not use: 402 Insufficient Balance and 422 Invalid Parameters
26
26
  // (api-docs.deepseek.com/quick_start/error_codes). OpenAI 400s a bad parameter; DeepSeek 422s
27
27
  // it. 400 is reserved for "Invalid request body format".
28
- // • `n`, `seed`, `logit_bias`, `top_k`, `user`, `max_completion_tokens`, `service_tier`,
28
+ // • `n`, `seed`, `logit_bias`, `top_k`, `max_completion_tokens`, `service_tier`,
29
29
  // `parallel_tool_calls`, `functions` are NOT DeepSeek parameters. The accepted set is a closed
30
30
  // documented table (api-docs.deepseek.com/api/create-chat-completion), which §6 licenses as a
31
- // literal-allowlist oracle — so anything outside CHAT_PARAMS is 422 by name.
31
+ // literal-allowlist oracle — so anything outside CHAT_PARAMS is 422 by name. OpenAI's `user` is
32
+ // the one exception, accepted and ignored (OPENAI_USER below).
32
33
  // • `response_format` accepts `text` and `json_object` ONLY. `json_schema` is an OpenAI feature;
33
34
  // `@ai-sdk/deepseek` confirms it by INJECTING the schema into a system message instead of
34
35
  // sending `response_format.json_schema` (src/chat/convert-to-deepseek-chat-messages.ts).
35
- // • `user_id`, not `user` — with a regex and a 512-character cap the SDK enforces client-side and
36
- // the vendor documents server-side.
36
+ // • `user_id` is DeepSeek's end-user identifier — with a regex and a 512-character cap the SDK
37
+ // enforces client-side and the vendor documents server-side. OpenAI's `user` is taken and has
38
+ // no effect (OPENAI_USER).
37
39
  // • Assistant-prefix completion and strict tool calls exist ONLY under the `/beta` base URL.
38
40
  // • `frequency_penalty` / `presence_penalty` are DEPRECATED but MUST NOT ERROR, and
39
41
  // `temperature` / `top_p` are IGNORED (not rejected) while thinking is enabled: "setting these
@@ -226,13 +228,31 @@ function parseJson(body) {
226
228
  return { error: invalidFormat('the request body is not valid JSON') };
227
229
  }
228
230
  }
231
+ /**
232
+ * OpenAI's `user`, taken and IGNORED — where DeepSeek's documentation stops and the twin decides
233
+ * (read 2026-09-28). The reference table (api-docs.deepseek.com/api/create-chat-completion) does not
234
+ * list `user` (its end-user field is `user_id`) and says nothing of a key it does not list. Against
235
+ * that silence: DeepSeek states "The DeepSeek API uses an API format compatible with OpenAI/Anthropic"
236
+ * and invites "the OpenAI/Anthropic SDK or softwares compatible with the OpenAI/Anthropic API"
237
+ * (api-docs.deepseek.com, "Your First API Call"); its Anthropic-compatible surface answers the fields
238
+ * it does not use as "Ignored" rather than refusing them (api-docs.deepseek.com/guides/anthropic_api:
239
+ * metadata's "user_id is supported, others are ignored"); and OpenAI-compatible software sends `user`
240
+ * to DeepSeek as a matter of course — LibreChat's DeepSeek endpoint, through LangChain's ChatOpenAI,
241
+ * sends it on every chat (cookbook/librechat, finding: the twin's 422 was retried into a hang), which
242
+ * could not be DeepSeek's supported configuration if the real API refused it. So the twin accepts a
243
+ * string `user` and gives it no effect: it is NOT `user_id` (no isolation, no content-safety
244
+ * identity) and is not validated against `user_id`'s charset. A non-string `user` is 422, as OpenAI's
245
+ * declared type is a string. The rest of the table stays closed: this ruling is evidence for `user`
246
+ * alone, and none has been found for `n`, `seed` and the others.
247
+ */
248
+ const OPENAI_USER = 'user';
229
249
  /**
230
250
  * THE CLOSED PARAMETER TABLE for `POST /chat/completions`
231
251
  * (api-docs.deepseek.com/api/create-chat-completion, read 2026-08-31) plus the two streaming keys
232
252
  * and the top-level `reasoning_effort` `@ai-sdk/deepseek` actually sends.
233
253
  *
234
254
  * §6 licenses a literal allowlist as an ORACLE precisely when the vendor documents a closed set,
235
- * and this is one — which is what gives `n`, `seed`, `logit_bias`, `top_k`, `user`,
255
+ * and this is one — which is what gives `n`, `seed`, `logit_bias`, `top_k`,
236
256
  * `max_completion_tokens`, `service_tier` and `parallel_tool_calls` a *checkable* refusal instead
237
257
  * of the silent acceptance an OpenAI-copied twin gives them.
238
258
  *
@@ -244,7 +264,7 @@ function parseJson(body) {
244
264
  const CHAT_PARAMS = new Set([
245
265
  'messages', 'model', 'thinking', 'max_tokens', 'response_format', 'stop', 'stream',
246
266
  'stream_options', 'temperature', 'top_p', 'tools', 'tool_choice', 'logprobs', 'top_logprobs',
247
- 'user_id', 'frequency_penalty', 'presence_penalty', 'reasoning_effort',
267
+ 'user_id', 'frequency_penalty', 'presence_penalty', 'reasoning_effort', OPENAI_USER,
248
268
  ]);
249
269
  /** The closed table for `POST /beta/completions` (api-docs.deepseek.com/api/create-completion). */
250
270
  const FIM_PARAMS = new Set([
@@ -432,7 +452,11 @@ function validateChat(params, beta) {
432
452
  return { error: invalidParameters("'top_logprobs' requires 'logprobs' to be true") };
433
453
  topLogprobs = v;
434
454
  }
435
- // (10) user_id — DeepSeek's end-user identifier. NOT OpenAI's `user` (which CHAT_PARAMS refuses).
455
+ // (10) user_id — DeepSeek's end-user identifier. NOT OpenAI's `user`, which is taken and ignored
456
+ // (OPENAI_USER): only its type is checked.
457
+ if (params[OPENAI_USER] !== undefined && params[OPENAI_USER] !== null && typeof params[OPENAI_USER] !== 'string') {
458
+ return { error: invalidParameters(`'${OPENAI_USER}' must be a string`) };
459
+ }
436
460
  let userId;
437
461
  if (params.user_id !== undefined && params.user_id !== null) {
438
462
  const v = params.user_id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-deepseek",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
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
5
  "keywords": [
6
6
  "twin",
@@ -52,10 +52,10 @@
52
52
  "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
53
53
  },
54
54
  "peerDependencies": {
55
- "@volter/world-core": "2.0.0"
55
+ "@volter/world-core": "2.0.1"
56
56
  },
57
57
  "devDependencies": {
58
- "@volter/world-core": "2.0.0",
58
+ "@volter/world-core": "2.0.1",
59
59
  "@volter/world-tooling": "0.1.0",
60
60
  "@ai-sdk/deepseek": "^3.0.37",
61
61
  "@types/bun": "^1.2.20",
@@ -225,12 +225,12 @@ export const DEEPSEEK_CAPABILITIES: CapabilitySpec[] = [
225
225
  return many.status === 422 && ok(sixteen);
226
226
  })),
227
227
 
228
- done('deepseek.chat.rejects_openai_only_params', 'chat', "The OpenAI parameters DeepSeek's closed table does not declare (n, seed, logit_bias, top_k, user, max_completion_tokens, service_tier, parallel_tool_calls, functions) are refused BY NAME with 422", 'api', 'core', () => withRoot(async (h) => {
228
+ done('deepseek.chat.rejects_openai_only_params', 'chat', "The OpenAI parameters DeepSeek's closed table does not declare (n, seed, logit_bias, top_k, max_completion_tokens, service_tier, parallel_tool_calls, functions) are refused BY NAME with 422", 'api', 'core', () => withRoot(async (h) => {
229
229
  // THE flagship OpenAI-divergence check. A twin copied from an OpenAI-shaped exemplar serves
230
230
  // every one of these happily, which is the inherited-permissiveness false-green
231
231
  // ADDING_A_TWIN.md §0 names. Each must be 422 — DeepSeek's "Invalid Parameters" — not 400.
232
232
  for (const [key, value] of [
233
- ['n', 2], ['seed', 42], ['logit_bias', { '1': 1 }], ['top_k', 5], ['user', 'u1'],
233
+ ['n', 2], ['seed', 42], ['logit_bias', { '1': 1 }], ['top_k', 5],
234
234
  ['max_completion_tokens', 32], ['service_tier', 'flex'], ['parallel_tool_calls', false],
235
235
  ['functions', [{ name: 'f' }]], ['store', true], ['metadata', { a: 'b' }],
236
236
  ] as Array<[string, unknown]>) {
@@ -297,6 +297,19 @@ export const DEEPSEEK_CAPABILITIES: CapabilitySpec[] = [
297
297
  return ok(good) && bad.status === 422 && long.status === 422 && ok(atCap);
298
298
  })),
299
299
 
300
+ done('deepseek.chat.openai_user_ignored', 'chat', "OpenAI's `user` (which OpenAI-compatible software such as LibreChat sends) is accepted and has no effect: it is not DeepSeek's `user_id`, and a non-string is 422", 'api', 'common', () => withRoot(async (h) => {
301
+ const plain = await h({ m: 'POST', p: CHAT_PATH, b: CHAT() });
302
+ // a value user_id's charset refuses: accepted here, so it is not being read as user_id
303
+ const withUser = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user: 'sam@friends.test' }) });
304
+ const both = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user: 'u1', user_id: 'bad id' }) });
305
+ const typed = await h({ m: 'POST', p: CHAT_PATH, b: CHAT({ user: 42 }) });
306
+ type Chat = { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number } };
307
+ const answer = (r: { body: unknown }) => JSON.stringify([(r.body as Chat).choices?.[0]?.message?.content, (r.body as Chat).usage?.prompt_tokens]);
308
+ return ok(plain) && ok(withUser) && answer(withUser) === answer(plain)
309
+ && both.status === 422 && msg(both).includes("'user_id'")
310
+ && typed.status === 422 && msg(typed).includes("'user'");
311
+ })),
312
+
300
313
  done('deepseek.chat.message_name', 'chat', 'Participant `name` is accepted on system/user/assistant turns and counted into prompt tokens', 'api', 'niche', () => withRoot(async (h) => {
301
314
  const named = await h({ m: 'POST', p: CHAT_PATH, b: { model: MODEL, messages: [{ role: 'user', content: 'hi', name: 'customer_alpha' }] } });
302
315
  const plain = await h({ m: 'POST', p: CHAT_PATH, b: { model: MODEL, messages: [{ role: 'user', content: 'hi' }] } });
@@ -251,7 +251,8 @@ const REJECTIONS: Array<{ name: string; method: string; path: string; body: unkn
251
251
  { name: 'seed', method: 'POST', path: '/chat/completions', body: { ...CHAT, seed: 42 }, status: 422, why: "'seed' is not a DeepSeek parameter" },
252
252
  { name: 'logit_bias', method: 'POST', path: '/chat/completions', body: { ...CHAT, logit_bias: { '1': 1 } }, status: 422, why: "'logit_bias' is not a DeepSeek parameter" },
253
253
  { name: 'top_k', method: 'POST', path: '/chat/completions', body: { ...CHAT, top_k: 5 }, status: 422, why: "'top_k' is not a DeepSeek parameter" },
254
- { name: 'user', method: 'POST', path: '/chat/completions', body: { ...CHAT, user: 'u1' }, status: 422, why: "DeepSeek's end-user identifier is 'user_id', not OpenAI's 'user'" },
254
+ // OpenAI's `user` as a string is accepted and ignored (deepseek-twin.ts, OPENAI_USER); only a non-string is refused
255
+ { name: 'user (not a string)', method: 'POST', path: '/chat/completions', body: { ...CHAT, user: 42 }, status: 422, why: "OpenAI's 'user' is taken and ignored, but only as a string" },
255
256
  { name: 'max_completion_tokens', method: 'POST', path: '/chat/completions', body: { ...CHAT, max_completion_tokens: 10 }, status: 422, why: "DeepSeek declares 'max_tokens' only" },
256
257
  { name: 'service_tier', method: 'POST', path: '/chat/completions', body: { ...CHAT, service_tier: 'flex' }, status: 422, why: 'DeepSeek has no service tiers' },
257
258
  { name: 'parallel_tool_calls', method: 'POST', path: '/chat/completions', body: { ...CHAT, parallel_tool_calls: false }, status: 422, why: 'not in DeepSeek\'s parameter table' },
@@ -25,15 +25,17 @@
25
25
  // includes two OpenAI does not use: 402 Insufficient Balance and 422 Invalid Parameters
26
26
  // (api-docs.deepseek.com/quick_start/error_codes). OpenAI 400s a bad parameter; DeepSeek 422s
27
27
  // it. 400 is reserved for "Invalid request body format".
28
- // • `n`, `seed`, `logit_bias`, `top_k`, `user`, `max_completion_tokens`, `service_tier`,
28
+ // • `n`, `seed`, `logit_bias`, `top_k`, `max_completion_tokens`, `service_tier`,
29
29
  // `parallel_tool_calls`, `functions` are NOT DeepSeek parameters. The accepted set is a closed
30
30
  // documented table (api-docs.deepseek.com/api/create-chat-completion), which §6 licenses as a
31
- // literal-allowlist oracle — so anything outside CHAT_PARAMS is 422 by name.
31
+ // literal-allowlist oracle — so anything outside CHAT_PARAMS is 422 by name. OpenAI's `user` is
32
+ // the one exception, accepted and ignored (OPENAI_USER below).
32
33
  // • `response_format` accepts `text` and `json_object` ONLY. `json_schema` is an OpenAI feature;
33
34
  // `@ai-sdk/deepseek` confirms it by INJECTING the schema into a system message instead of
34
35
  // sending `response_format.json_schema` (src/chat/convert-to-deepseek-chat-messages.ts).
35
- // • `user_id`, not `user` — with a regex and a 512-character cap the SDK enforces client-side and
36
- // the vendor documents server-side.
36
+ // • `user_id` is DeepSeek's end-user identifier — with a regex and a 512-character cap the SDK
37
+ // enforces client-side and the vendor documents server-side. OpenAI's `user` is taken and has
38
+ // no effect (OPENAI_USER).
37
39
  // • Assistant-prefix completion and strict tool calls exist ONLY under the `/beta` base URL.
38
40
  // • `frequency_penalty` / `presence_penalty` are DEPRECATED but MUST NOT ERROR, and
39
41
  // `temperature` / `top_p` are IGNORED (not rejected) while thinking is enabled: "setting these
@@ -283,13 +285,32 @@ type ToolChoice = 'auto' | 'none' | 'required' | { name: string };
283
285
  type ResponseFormat = 'text' | 'json_object';
284
286
  type ReasoningEffort = 'low' | 'high' | 'max';
285
287
 
288
+ /**
289
+ * OpenAI's `user`, taken and IGNORED — where DeepSeek's documentation stops and the twin decides
290
+ * (read 2026-09-28). The reference table (api-docs.deepseek.com/api/create-chat-completion) does not
291
+ * list `user` (its end-user field is `user_id`) and says nothing of a key it does not list. Against
292
+ * that silence: DeepSeek states "The DeepSeek API uses an API format compatible with OpenAI/Anthropic"
293
+ * and invites "the OpenAI/Anthropic SDK or softwares compatible with the OpenAI/Anthropic API"
294
+ * (api-docs.deepseek.com, "Your First API Call"); its Anthropic-compatible surface answers the fields
295
+ * it does not use as "Ignored" rather than refusing them (api-docs.deepseek.com/guides/anthropic_api:
296
+ * metadata's "user_id is supported, others are ignored"); and OpenAI-compatible software sends `user`
297
+ * to DeepSeek as a matter of course — LibreChat's DeepSeek endpoint, through LangChain's ChatOpenAI,
298
+ * sends it on every chat (cookbook/librechat, finding: the twin's 422 was retried into a hang), which
299
+ * could not be DeepSeek's supported configuration if the real API refused it. So the twin accepts a
300
+ * string `user` and gives it no effect: it is NOT `user_id` (no isolation, no content-safety
301
+ * identity) and is not validated against `user_id`'s charset. A non-string `user` is 422, as OpenAI's
302
+ * declared type is a string. The rest of the table stays closed: this ruling is evidence for `user`
303
+ * alone, and none has been found for `n`, `seed` and the others.
304
+ */
305
+ const OPENAI_USER = 'user';
306
+
286
307
  /**
287
308
  * THE CLOSED PARAMETER TABLE for `POST /chat/completions`
288
309
  * (api-docs.deepseek.com/api/create-chat-completion, read 2026-08-31) plus the two streaming keys
289
310
  * and the top-level `reasoning_effort` `@ai-sdk/deepseek` actually sends.
290
311
  *
291
312
  * §6 licenses a literal allowlist as an ORACLE precisely when the vendor documents a closed set,
292
- * and this is one — which is what gives `n`, `seed`, `logit_bias`, `top_k`, `user`,
313
+ * and this is one — which is what gives `n`, `seed`, `logit_bias`, `top_k`,
293
314
  * `max_completion_tokens`, `service_tier` and `parallel_tool_calls` a *checkable* refusal instead
294
315
  * of the silent acceptance an OpenAI-copied twin gives them.
295
316
  *
@@ -301,7 +322,7 @@ type ReasoningEffort = 'low' | 'high' | 'max';
301
322
  const CHAT_PARAMS = new Set([
302
323
  'messages', 'model', 'thinking', 'max_tokens', 'response_format', 'stop', 'stream',
303
324
  'stream_options', 'temperature', 'top_p', 'tools', 'tool_choice', 'logprobs', 'top_logprobs',
304
- 'user_id', 'frequency_penalty', 'presence_penalty', 'reasoning_effort',
325
+ 'user_id', 'frequency_penalty', 'presence_penalty', 'reasoning_effort', OPENAI_USER,
305
326
  ]);
306
327
 
307
328
  /** The closed table for `POST /beta/completions` (api-docs.deepseek.com/api/create-completion). */
@@ -497,7 +518,11 @@ function validateChat(params: Record<string, unknown>, beta: boolean): { args: C
497
518
  topLogprobs = v;
498
519
  }
499
520
 
500
- // (10) user_id — DeepSeek's end-user identifier. NOT OpenAI's `user` (which CHAT_PARAMS refuses).
521
+ // (10) user_id — DeepSeek's end-user identifier. NOT OpenAI's `user`, which is taken and ignored
522
+ // (OPENAI_USER): only its type is checked.
523
+ if (params[OPENAI_USER] !== undefined && params[OPENAI_USER] !== null && typeof params[OPENAI_USER] !== 'string') {
524
+ return { error: invalidParameters(`'${OPENAI_USER}' must be a string`) };
525
+ }
501
526
  let userId: string | undefined;
502
527
  if (params.user_id !== undefined && params.user_id !== null) {
503
528
  const v = params.user_id;