melchizedek-agents 1.10.2 → 1.11.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 (49) hide show
  1. package/.env.example +10 -0
  2. package/AGENT_SETUP.md +2 -1
  3. package/CHANGELOG.md +44 -0
  4. package/DOCUMENTATION.md +43 -7
  5. package/ONBOARDING.md +7 -4
  6. package/QUICKSTART.md +3 -3
  7. package/README.md +5 -5
  8. package/config/agents/examples/{augustin.yaml → current_events.yaml} +22 -50
  9. package/config/agents/examples/model_zoo.yaml +21 -9
  10. package/config/agents/syndicate.schema.json +58 -0
  11. package/config/agents/syndicateSchema.yaml +9 -5
  12. package/db/migrations/0019_deepseek_kimi_pricing.sql +48 -0
  13. package/dist/lib/config.d.ts +11 -0
  14. package/dist/lib/config.js +11 -0
  15. package/dist/lib/doctor.js +2 -0
  16. package/dist/lib/loadSyndicate.d.ts +1 -1
  17. package/dist/lib/model.d.ts +2 -0
  18. package/dist/lib/model.js +1 -0
  19. package/dist/lib/models/adapterResolver.js +3 -1
  20. package/dist/lib/models/capabilities.js +11 -0
  21. package/dist/lib/models/chatCompletionsAdapter.d.ts +24 -1
  22. package/dist/lib/models/chatCompletionsAdapter.js +42 -5
  23. package/dist/lib/models/contract.d.ts +1 -1
  24. package/dist/lib/models/deepseekAdapter.d.ts +127 -0
  25. package/dist/lib/models/deepseekAdapter.js +157 -0
  26. package/dist/lib/models/endpoints.d.ts +2 -1
  27. package/dist/lib/models/endpoints.js +5 -1
  28. package/dist/lib/models/gateway.d.ts +3 -2
  29. package/dist/lib/models/gateway.js +5 -4
  30. package/dist/lib/models/gatewayAdapter.d.ts +1 -1
  31. package/dist/lib/models/gatewayAdapter.js +1 -1
  32. package/dist/lib/models/providerMap.d.ts +3 -2
  33. package/dist/lib/models/providerMap.js +5 -1
  34. package/dist/lib/models/reasoning.d.ts +1 -1
  35. package/dist/lib/models/reasoning.js +4 -2
  36. package/dist/lib/models/registry.js +4 -1
  37. package/dist/lib/observability/tracer.d.ts +1 -1
  38. package/dist/lib/onboarding.js +3 -1
  39. package/dist/lib/syndicateSchema.d.ts +16 -0
  40. package/dist/lib/syndicateSchema.js +1 -0
  41. package/dist/lib/toolRegistry.js +1 -1
  42. package/dist/lib/tools/credentialEnv.js +1 -1
  43. package/package.json +2 -2
  44. package/skills/briefs/melchizedek-author.md +1 -1
  45. package/skills/briefs/melchizedek-models.md +7 -6
  46. package/skills/briefs/melchizedek.md +5 -5
  47. package/skills/melchizedek/SKILL.md +2 -2
  48. package/skills/melchizedek-onboard/SKILL.md +1 -1
  49. package/skills/melchizedek-onboard-keys/SKILL.md +2 -2
@@ -269,6 +269,7 @@ orchestrator:
269
269
  # gpt-* /o* → OpenAI (OPENAI_API_KEY)
270
270
  # grok-* → xAI (XAI_API_KEY)
271
271
  # kimi-* → Moonshot Kimi (MOONSHOT_API_KEY)
272
+ # deepseek-* → DeepSeek (DEEPSEEK_API_KEY)
272
273
  # ollama/* → local Ollama (keyless)
273
274
  # Subagents inherit this if not set on their own config.
274
275
  model: "gemini-3.5-flash-lite"
@@ -443,14 +444,16 @@ orchestrator:
443
444
  # that effort; none is the model's off switch,
444
445
  # or low effort on Opus 5 and 5.5, Fable and
445
446
  # Mythos (ADR 0049)
446
- # gpt-*/o*, grok-*, kimi-*, ollama/*, a gateway
447
+ # gpt-*/o*, grok-*, kimi-*, deepseek-*, ollama/*, a gateway
447
448
  # → reasoningEffort (an effort word; where the
448
- # provider lacks one, its nearest setting above)
449
+ # provider lacks one, its nearest setting above;
450
+ # deepseek-* beside thinking enabled, and none
451
+ # as thinking disabled)
449
452
  # A budget is sent as-is to Gemini and to Claude 4.6 and earlier, and as
450
453
  # the smallest level that covers it everywhere else. Unset, each provider
451
454
  # keeps its default.
452
455
  # xhigh and max (ADR 0117) reach the models that take them: both on later
453
- # claude-*, max on kimi-k3 (xhigh rounds up to it), xhigh on gpt-5.2 and
456
+ # claude-*, max on kimi-k3 and deepseek-* (xhigh rounds up to it), xhigh on gpt-5.2 and
454
457
  # later and grok-4.7. Elsewhere the model's highest setting is sent and
455
458
  # the span carries llm.reasoning.weakened; a gateway gets high.
456
459
  # Cannot be combined with generateContentConfig.thinkingConfig or
@@ -466,7 +469,8 @@ orchestrator:
466
469
  # Sampling temperature. Lower = more deterministic. Range: [0.0, 2.0]
467
470
  # ⚠ Do NOT set this on current Gemini models — they are tuned for their
468
471
  # own default sampling and the knob is not part of their contract.
469
- # Set it only for claude-*, gpt-*, grok-*, kimi-*, or ollama/* agents.
472
+ # Set it only for claude-*, gpt-*, grok-*, kimi-*, or ollama/* agents
473
+ # (DeepSeek ignores it while thinking, its default).
470
474
  # temperature: 0.7
471
475
 
472
476
  # Nucleus sampling. Tokens sampled until cumulative prob = top_p.
@@ -504,7 +508,7 @@ orchestrator:
504
508
  # ── model_overrides ───────────────────────────────────────
505
509
  # [optional] A prompt nuance for one provider (ADR 0115). Keys are the
506
510
  # providers a model id routes to: gemini, anthropic, openai, xai,
507
- # moonshot, ollama. Each holds ONE of:
511
+ # moonshot, deepseek, ollama. Each holds ONE of:
508
512
  # instruction: replaces the instruction on that provider
509
513
  # instruction_append: added after the instruction, after a blank line
510
514
  # Applied for the provider of this agent's model, before the skills
@@ -0,0 +1,48 @@
1
+ -- ============================================================================
2
+ -- 0019_deepseek_kimi_pricing — price rows for DeepSeek and Moonshot Kimi
3
+ -- (extends ADR 0136 and ADR 0139)
4
+ -- ============================================================================
5
+ -- Rows only, in 0017's column layout (USD per million tokens):
6
+ -- input_per_mtok an input token that is not a cache hit
7
+ -- output_per_mtok an output token (thinking included: NULL thinking
8
+ -- rate, so thinking prices at the output rate)
9
+ -- cache_read_per_mtok an input token read from the provider's cache
10
+ -- cache_write_per_mtok NULL: neither vendor bills a cache write, so a
11
+ -- written token prices as input
12
+ --
13
+ -- DeepSeek (lib/models/deepseekAdapter.ts): the PEAK rates. DeepSeek bills
14
+ -- off-peak at half, and peak (01:00–04:00 and 06:00–10:00 UTC on weekdays)
15
+ -- at exactly double the off-peak list; a flat row cannot say the hour, so
16
+ -- it holds the higher rate and a budget never underestimates a call.
17
+ -- Off-peak per 1M: deepseek-flash $0.15 in / $0.003 cache hit / $0.60 out;
18
+ -- deepseek-v4-pro $0.66 / $0.022 / $1.98. Checked 2026-10-22 against
19
+ -- api-docs.deepseek.com/quick_start/pricing. The retired deepseek-v4-flash
20
+ -- and deepseek-v4-flash-vision-exp ids are still accepted, served by
21
+ -- V4.1-Flash and billed at Flash's price, so they get Flash's row.
22
+ --
23
+ -- Moonshot Kimi (lib/models/kimiAdapter.ts): no row until now. The rates
24
+ -- are the family table in the adapter's header (platform.moonshot.ai ›
25
+ -- Model list, October 2026); verified_at stays NULL because this migration
26
+ -- did not re-check them against the vendor's list (0015's convention: spend
27
+ -- built on the row is a steering estimate until someone does).
28
+ --
29
+ -- New rows never overwrite a row that exists: an operator's own rates
30
+ -- survive. Nothing secret, nothing personal. Idempotent; safe to re-run.
31
+ -- ============================================================================
32
+
33
+ INSERT INTO model_pricing (model, input_per_mtok, output_per_mtok, cache_read_per_mtok, cache_write_per_mtok, verified_at, source, note) VALUES
34
+ -- DeepSeek, peak rates (double the off-peak list).
35
+ ('deepseek-flash', 0.30, 1.20, 0.006, NULL, '2026-10-22', 'api-docs.deepseek.com/quick_start/pricing', 'DeepSeek first-party, checked 2026-10-22; PEAK rate (01:00-04:00 and 06:00-10:00 UTC weekdays), double the off-peak $0.15 / $0.003 / $0.60, held so budgets never underestimate'),
36
+ ('deepseek-v4-pro', 1.32, 3.96, 0.044, NULL, '2026-10-22', 'api-docs.deepseek.com/quick_start/pricing', 'DeepSeek first-party, checked 2026-10-22; PEAK rate (01:00-04:00 and 06:00-10:00 UTC weekdays), double the off-peak $0.66 / $0.022 / $1.98, held so budgets never underestimate'),
37
+ ('deepseek-v4-flash', 0.30, 1.20, 0.006, NULL, '2026-10-22', 'api-docs.deepseek.com/quick_start/pricing', 'DeepSeek first-party, checked 2026-10-22; retired id served by V4.1-Flash and billed at Flash price; PEAK rate, double the off-peak $0.15 / $0.003 / $0.60'),
38
+ ('deepseek-v4-flash-vision-exp', 0.30, 1.20, 0.006, NULL, '2026-10-22', 'api-docs.deepseek.com/quick_start/pricing', 'DeepSeek first-party, checked 2026-10-22; retired id served by V4.1-Flash and billed at Flash price; PEAK rate, double the off-peak $0.15 / $0.003 / $0.60'),
39
+ -- Moonshot Kimi, from the family table in lib/models/kimiAdapter.ts.
40
+ ('kimi-k3', 3.00, 15.00, 0.30, NULL, NULL, 'platform.moonshot.ai (Model list, October 2026), via the family table in lib/models/kimiAdapter.ts', 'Moonshot first-party; not re-checked by this migration'),
41
+ ('kimi-k2.6', 0.95, 4.00, 0.16, NULL, NULL, 'platform.moonshot.ai (Model list, October 2026), via the family table in lib/models/kimiAdapter.ts', 'Moonshot first-party; not re-checked by this migration'),
42
+ ('kimi-k2.7-code', 0.95, 4.00, 0.19, NULL, NULL, 'platform.moonshot.ai (Model list, October 2026), via the family table in lib/models/kimiAdapter.ts', 'Moonshot first-party; not re-checked by this migration'),
43
+ ('kimi-k2.7-code-highspeed', 1.90, 8.00, 0.38, NULL, NULL, 'platform.moonshot.ai (Model list, October 2026), via the family table in lib/models/kimiAdapter.ts', 'Moonshot first-party; not re-checked by this migration')
44
+ ON CONFLICT (model) DO NOTHING;
45
+
46
+ INSERT INTO melchizedek_schema_version (version, name)
47
+ VALUES (19, '0019_deepseek_kimi_pricing')
48
+ ON CONFLICT (version) DO NOTHING;
@@ -77,6 +77,17 @@ export declare const DEFAULT_KIMI_MODEL = "kimi-k3";
77
77
  * override. Source: platform.moonshot.ai › Guides › Reasoning effort.
78
78
  */
79
79
  export declare const DEFAULT_KIMI_REASONING_EFFORT = "high";
80
+ /**
81
+ * Default DeepSeek model used when a deepseek-* model is requested but no
82
+ * specific identifier is provided. Requires DEEPSEEK_API_KEY
83
+ * (lib/models/deepseekAdapter.ts). deepseek-flash (DeepSeek-V4.1-Flash) is
84
+ * the cheap tier with vision, at $0.15 / $0.60 per 1M off-peak;
85
+ * deepseek-v4-pro is the deeper, text-only tier at about four times that
86
+ * (the family table and peak pricing are in the adapter's header).
87
+ * No reasoning effort is pinned: DeepSeek's own default is `high`, the
88
+ * middle of its three levels (low | high | max), not its slowest.
89
+ */
90
+ export declare const DEFAULT_DEEPSEEK_MODEL = "deepseek-flash";
80
91
  /**
81
92
  * Model calls one turn may make, subagents included, when the syndicate sets
82
93
  * no `max_steps`. A runaway tool loop stops here instead of at the provider's
@@ -78,6 +78,17 @@ export const DEFAULT_KIMI_MODEL = 'kimi-k3';
78
78
  * override. Source: platform.moonshot.ai › Guides › Reasoning effort.
79
79
  */
80
80
  export const DEFAULT_KIMI_REASONING_EFFORT = 'high';
81
+ /**
82
+ * Default DeepSeek model used when a deepseek-* model is requested but no
83
+ * specific identifier is provided. Requires DEEPSEEK_API_KEY
84
+ * (lib/models/deepseekAdapter.ts). deepseek-flash (DeepSeek-V4.1-Flash) is
85
+ * the cheap tier with vision, at $0.15 / $0.60 per 1M off-peak;
86
+ * deepseek-v4-pro is the deeper, text-only tier at about four times that
87
+ * (the family table and peak pricing are in the adapter's header).
88
+ * No reasoning effort is pinned: DeepSeek's own default is `high`, the
89
+ * middle of its three levels (low | high | max), not its slowest.
90
+ */
91
+ export const DEFAULT_DEEPSEEK_MODEL = 'deepseek-flash';
81
92
  /**
82
93
  * Model calls one turn may make, subagents included, when the syndicate sets
83
94
  * no `max_steps`. A runaway tool loop stops here instead of at the provider's
@@ -178,6 +178,7 @@ export const CONSOLE_URLS = {
178
178
  openai: 'https://platform.openai.com/api-keys',
179
179
  xai: 'https://console.x.ai',
180
180
  moonshot: 'https://platform.moonshot.ai',
181
+ deepseek: 'https://platform.deepseek.com/api_keys',
181
182
  ollama: 'https://ollama.com',
182
183
  };
183
184
  // ── Discovery ────────────────────────────────────────────────────────────────
@@ -603,6 +604,7 @@ const PROBE_ID = {
603
604
  openai: 'gpt-x',
604
605
  xai: 'grok-x',
605
606
  moonshot: 'kimi-x',
607
+ deepseek: 'deepseek-x',
606
608
  ollama: 'ollama/x',
607
609
  };
608
610
  /**
@@ -149,7 +149,7 @@ export interface AgentYamlConfig {
149
149
  * appended after a blank line, for the provider of the model the agent runs
150
150
  * on. Applied at compile time, before the skills index; not for fallback_model.
151
151
  */
152
- model_overrides?: Partial<Record<'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'ollama', {
152
+ model_overrides?: Partial<Record<'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'deepseek' | 'ollama', {
153
153
  instruction?: string;
154
154
  instruction_append?: string;
155
155
  }>>;
@@ -30,6 +30,8 @@ export { ChatCompletionsAdapter } from './models/chatCompletionsAdapter.ts';
30
30
  export type { ChatCompletionsAdapterOptions } from './models/chatCompletionsAdapter.ts';
31
31
  export { KimiAdapter } from './models/kimiAdapter.ts';
32
32
  export type { KimiAdapterOptions } from './models/kimiAdapter.ts';
33
+ export { DeepSeekAdapter } from './models/deepseekAdapter.ts';
34
+ export type { DeepSeekAdapterOptions } from './models/deepseekAdapter.ts';
33
35
  export { OllamaAdapter } from './models/ollamaAdapter.ts';
34
36
  export type { OllamaAdapterOptions } from './models/ollamaAdapter.ts';
35
37
  export { GatewayAdapter } from './models/gatewayAdapter.ts';
package/dist/lib/model.js CHANGED
@@ -23,6 +23,7 @@ export { GptAdapter } from './models/gptAdapter.js';
23
23
  export { GrokAdapter } from './models/grokAdapter.js';
24
24
  export { ChatCompletionsAdapter } from './models/chatCompletionsAdapter.js';
25
25
  export { KimiAdapter } from './models/kimiAdapter.js';
26
+ export { DeepSeekAdapter } from './models/deepseekAdapter.js';
26
27
  export { OllamaAdapter } from './models/ollamaAdapter.js';
27
28
  export { GatewayAdapter } from './models/gatewayAdapter.js';
28
29
  export { GeminiAdapter } from './models/geminiAdapter.js';
@@ -25,6 +25,7 @@ import { GeminiAdapter } from './geminiAdapter.js';
25
25
  import { GptAdapter } from './gptAdapter.js';
26
26
  import { GrokAdapter } from './grokAdapter.js';
27
27
  import { KimiAdapter } from './kimiAdapter.js';
28
+ import { DeepSeekAdapter } from './deepseekAdapter.js';
28
29
  import { OllamaAdapter } from './ollamaAdapter.js';
29
30
  import { providerForModel } from './providerMap.js';
30
31
  /**
@@ -53,7 +54,7 @@ export function scopedKey(model, apiKey, keyProvider) {
53
54
  /** A provider name as the A2A X-Provider header gives it; anything unknown reads as gemini. */
54
55
  export function normalizeProvider(provider) {
55
56
  const p = provider?.toLowerCase();
56
- if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai' || p === 'moonshot') {
57
+ if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai' || p === 'moonshot' || p === 'deepseek') {
57
58
  return p;
58
59
  }
59
60
  return 'gemini';
@@ -84,6 +85,7 @@ const CONTRACT_ADAPTER = {
84
85
  : new GptAdapter({ model: r.model, apiKey: r.apiKey, endpoint: r.endpoint }),
85
86
  xai: (r) => new GrokAdapter({ model: r.model, apiKey: r.apiKey }),
86
87
  moonshot: (r) => new KimiAdapter({ model: r.model, apiKey: r.apiKey, baseUrl: r.endpoint?.baseURL }),
88
+ deepseek: (r) => new DeepSeekAdapter({ model: r.model, apiKey: r.apiKey, baseUrl: r.endpoint?.baseURL }),
87
89
  };
88
90
  /** A resolver over the one prefix table. A Gemini id gets GeminiAdapter (ADR 0100, ADR 0107). */
89
91
  export function adapterResolver() {
@@ -193,6 +193,16 @@ export const CAPABILITY_MATRIX = {
193
193
  vision: ok('test', 'user-turn images only, sent as base64 (Moonshot takes no public image URLs)'),
194
194
  native_search: unsupported("Moonshot's model-side $web_search retires 2026-10-20 and its successor is a REST call, not a request field; the web_search sentinel is dropped (use web_extract)"),
195
195
  },
196
+ deepseek: {
197
+ delegation: ok(),
198
+ memory_tools: ok(),
199
+ structured_output: degraded('JSON mode (response_format json_object), not a strict schema: the schema is asked for in the system message and the engine validates the reply'),
200
+ structured_output_with_tools: degraded(setModelResponseNote('DeepSeek takes JSON mode only, no strict schema')),
201
+ thinking_with_tools: ok('test', "reasoning_content is sent back on every assistant message of a request with tools, earlier turns included, as DeepSeek requires (else a 400): the same model's stored state, an empty string where none is stored (ADR 0046); effort travels as reasoning_effort beside a thinking switch"),
202
+ streaming: ok(),
203
+ vision: ok('test', 'user-turn images only, sent as base64; deepseek-flash only (deepseek-v4-pro is text-only)'),
204
+ native_search: unsupported("DeepSeek's Search and Fetch are separate per-request REST APIs, not a chat request field; the web_search sentinel is dropped (use web_extract)"),
205
+ },
196
206
  ollama: {
197
207
  delegation: ok(),
198
208
  memory_tools: ok(),
@@ -239,6 +249,7 @@ export const REASONING_ABOVE_HIGH = {
239
249
  openai: 'xhigh as asked on GPT-5.2 and later, and max goes as xhigh there, weakened; high, weakened, on the o-series, GPT-5 and GPT-5.1',
240
250
  xai: 'xhigh as asked on grok-4.7, and max goes as xhigh there, weakened; high, weakened, on grok-4.5 and grok-4.6',
241
251
  moonshot: 'max as asked on kimi-k3, and xhigh rounds up to max; the K2 generation takes a thinking switch, which these leave on',
252
+ deepseek: 'max as asked, and xhigh rounds up to max (DeepSeek has low | high | max), each beside thinking enabled',
242
253
  ollama: 'reasoning_effort high, weakened',
243
254
  gateway: 'reasoning_effort high, weakened, whatever the id: the words above high are not accepted across gateways',
244
255
  };
@@ -36,7 +36,10 @@
36
36
  * (replaysReasoningContent: Kimi) writes the response's reasoning_content
37
37
  * as providerState on the part it preceded and sends it back on that
38
38
  * assistant message within the current turn's tool loop, for the same
39
- * provider and model only (ADR 0046).
39
+ * provider and model only (ADR 0046). An adapter whose provider asks for
40
+ * it on every past assistant message of a request with tools
41
+ * (replaysReasoningOnEveryTurn: DeepSeek) sends it on all of them, an
42
+ * empty string where none of its own is stored.
40
43
  * - A turn that thought, or ran out of tokens, without a reply or a tool
41
44
  * call is an error final (`<ID>_MAX_TOKENS`, `<ID>_EMPTY_RESPONSE`, ADR
42
45
  * 0027), carrying its usage. An adapter that opts in
@@ -230,6 +233,24 @@ export declare abstract class ChatCompletionsAdapter implements ModelAdapter {
230
233
  * provider asks for the field back on a tool loop turns it on (Kimi).
231
234
  */
232
235
  protected replaysReasoningContent(_model: string): boolean;
236
+ /**
237
+ * Whether, on a request that carries `tools`, EVERY assistant message in
238
+ * the history (earlier turns' included, not only this turn's tool loop)
239
+ * carries `reasoning_content`: this adapter's own stored state for the
240
+ * same model where there is some, else an empty string, so the field is
241
+ * never missing. Default: never. Only meaningful beside
242
+ * replaysReasoningContent (DeepSeek, whose API returns 400 on a tool
243
+ * request missing a past turn's reasoning_content).
244
+ */
245
+ protected replaysReasoningOnEveryTurn(_model: string): boolean;
246
+ /**
247
+ * Whether the provider enforces a strict `json_schema` response_format.
248
+ * Default: true. Where it takes JSON mode only (DeepSeek), an outputSchema
249
+ * goes as `response_format: json_object` and the schema rides in the
250
+ * system message instead; the engine validates the reply against it
251
+ * (lib/runtime/native/agentLoop.ts saveOutput).
252
+ */
253
+ protected takesStrictJsonSchema(_model: string): boolean;
233
254
  /** A precondition the call cannot be made without (e.g. no API key), or undefined when ready. */
234
255
  protected missingRequirement(): ChatFailure | undefined;
235
256
  /** The failure for a non-2xx HTTP status. */
@@ -263,4 +284,6 @@ export declare abstract class ChatCompletionsAdapter implements ModelAdapter {
263
284
  /** The function tools this adapter sends for a request: none under `toolChoice: 'none'`. */
264
285
  toolsFor(request: ModelRequest): Array<Record<string, unknown>>;
265
286
  }
287
+ /** The system line asking for the schema where the provider takes JSON mode only (takesStrictJsonSchema). */
288
+ export declare function schemaInstruction(schema: unknown): string;
266
289
  export {};
@@ -36,7 +36,10 @@
36
36
  * (replaysReasoningContent: Kimi) writes the response's reasoning_content
37
37
  * as providerState on the part it preceded and sends it back on that
38
38
  * assistant message within the current turn's tool loop, for the same
39
- * provider and model only (ADR 0046).
39
+ * provider and model only (ADR 0046). An adapter whose provider asks for
40
+ * it on every past assistant message of a request with tools
41
+ * (replaysReasoningOnEveryTurn: DeepSeek) sends it on all of them, an
42
+ * empty string where none of its own is stored.
40
43
  * - A turn that thought, or ran out of tokens, without a reply or a tool
41
44
  * call is an error final (`<ID>_MAX_TOKENS`, `<ID>_EMPTY_RESPONSE`, ADR
42
45
  * 0027), carrying its usage. An adapter that opts in
@@ -382,6 +385,28 @@ export class ChatCompletionsAdapter {
382
385
  replaysReasoningContent(_model) {
383
386
  return false;
384
387
  }
388
+ /**
389
+ * Whether, on a request that carries `tools`, EVERY assistant message in
390
+ * the history (earlier turns' included, not only this turn's tool loop)
391
+ * carries `reasoning_content`: this adapter's own stored state for the
392
+ * same model where there is some, else an empty string, so the field is
393
+ * never missing. Default: never. Only meaningful beside
394
+ * replaysReasoningContent (DeepSeek, whose API returns 400 on a tool
395
+ * request missing a past turn's reasoning_content).
396
+ */
397
+ replaysReasoningOnEveryTurn(_model) {
398
+ return false;
399
+ }
400
+ /**
401
+ * Whether the provider enforces a strict `json_schema` response_format.
402
+ * Default: true. Where it takes JSON mode only (DeepSeek), an outputSchema
403
+ * goes as `response_format: json_object` and the schema rides in the
404
+ * system message instead; the engine validates the reply against it
405
+ * (lib/runtime/native/agentLoop.ts saveOutput).
406
+ */
407
+ takesStrictJsonSchema(_model) {
408
+ return true;
409
+ }
385
410
  /** A precondition the call cannot be made without (e.g. no API key), or undefined when ready. */
386
411
  missingRequirement() {
387
412
  return undefined;
@@ -684,7 +709,7 @@ export class ChatCompletionsAdapter {
684
709
  ...this.reasoningFields(model, request.reasoning),
685
710
  ...(tools.length > 0 ? { tools } : {}),
686
711
  ...(tools.length > 0 ? this.#toolChoice(choice, model, request.reasoning) : {}),
687
- ...this.#responseFormat(request),
712
+ ...this.#responseFormat(request, model),
688
713
  ...this.extraBodyFields(request),
689
714
  };
690
715
  this.#nativeTools(request.nativeTools ?? [], body, model);
@@ -713,7 +738,9 @@ export class ChatCompletionsAdapter {
713
738
  * `outputFormat: 'json'` without a schema is JSON mode (ADR 0061); each
714
739
  * of Ollama, Kimi and the gateway takes it.
715
740
  */
716
- #responseFormat(request) {
741
+ #responseFormat(request, model) {
742
+ if (request.outputSchema && !this.takesStrictJsonSchema(model))
743
+ return { response_format: { type: 'json_object' } };
717
744
  if (request.outputSchema) {
718
745
  return { response_format: { type: 'json_schema', json_schema: { name: 'response', strict: true, schema: toStrictJsonSchema(request.outputSchema) } } };
719
746
  }
@@ -752,7 +779,13 @@ export class ChatCompletionsAdapter {
752
779
  // span providers ask for it on. Earlier turns' state is left out: the
753
780
  // stored history is not what the model saw (tool payloads are elided
754
781
  // before storage), and replaying it bills it again as input (ADR 0046).
755
- const turnStart = this.replaysReasoningContent(model) ? currentTurnStart(request.messages) : Infinity;
782
+ // An adapter whose provider wants it on every past assistant message of
783
+ // a tool request (DeepSeek) replays from the first, and fills the field.
784
+ const everyTurn = this.replaysReasoningContent(model) && this.replaysReasoningOnEveryTurn(model) && this.toolsFor(request).length > 0;
785
+ const turnStart = everyTurn ? -1 : this.replaysReasoningContent(model) ? currentTurnStart(request.messages) : Infinity;
786
+ // JSON mode only: the schema the provider cannot hold the reply to is asked for in the system message.
787
+ if (request.outputSchema && !this.takesStrictJsonSchema(model))
788
+ system.push(schemaInstruction(request.outputSchema));
756
789
  request.messages.forEach((message, index) => {
757
790
  switch (message.role) {
758
791
  case 'system': {
@@ -817,7 +850,7 @@ export class ChatCompletionsAdapter {
817
850
  messages.push({
818
851
  role: 'assistant',
819
852
  content: text || null,
820
- ...(reasoning.length > 0 ? { reasoning_content: reasoning.join('') } : {}),
853
+ ...(reasoning.length > 0 ? { reasoning_content: reasoning.join('') } : everyTurn ? { reasoning_content: '' } : {}),
821
854
  ...(calls.length > 0 ? { tool_calls: calls } : {}),
822
855
  });
823
856
  }
@@ -840,6 +873,10 @@ export class ChatCompletionsAdapter {
840
873
  console.warn(message);
841
874
  }
842
875
  }
876
+ /** The system line asking for the schema where the provider takes JSON mode only (takesStrictJsonSchema). */
877
+ export function schemaInstruction(schema) {
878
+ return `Reply with one JSON object, and nothing else, that matches this JSON Schema:\n${JSON.stringify(schema)}`;
879
+ }
843
880
  /** A tool declaration as a function tool: the schema as written, or its strict form with `strict: true`. */
844
881
  function functionTool(tool) {
845
882
  return {
@@ -291,7 +291,7 @@ export interface Grounding {
291
291
  /** Configuration and installation failures any adapter reports before it calls. */
292
292
  type SetupErrorCode = 'MISSING_API_KEY' | 'ENDPOINT_MISCONFIGURED' | 'SDK_NOT_INSTALLED';
293
293
  /** The chat-completions failures, under the provider's id (the gateway names the upstream provider). */
294
- type ChatErrorCode = `${'MOONSHOT' | 'OLLAMA' | 'GEMINI' | 'ANTHROPIC' | 'OPENAI' | 'XAI'}_${'MAX_TOKENS' | 'EMPTY_RESPONSE' | 'UNREACHABLE'}` | 'MOONSHOT_MISSING_KEY' | 'MOONSHOT_HTTP_ERROR' | 'OLLAMA_HTTP_ERROR' | 'GATEWAY_NOT_CONFIGURED' | 'GATEWAY_KEY_MISSING' | 'GATEWAY_HTTP_ERROR';
294
+ type ChatErrorCode = `${'MOONSHOT' | 'DEEPSEEK' | 'OLLAMA' | 'GEMINI' | 'ANTHROPIC' | 'OPENAI' | 'XAI'}_${'MAX_TOKENS' | 'EMPTY_RESPONSE' | 'UNREACHABLE'}` | 'MOONSHOT_MISSING_KEY' | 'MOONSHOT_HTTP_ERROR' | 'DEEPSEEK_MISSING_KEY' | 'DEEPSEEK_HTTP_ERROR' | 'OLLAMA_HTTP_ERROR' | 'GATEWAY_NOT_CONFIGURED' | 'GATEWAY_KEY_MISSING' | 'GATEWAY_HTTP_ERROR';
295
295
  /**
296
296
  * Gemini's codes: a candidate's finish reason, or the prompt's block reason,
297
297
  * is the code itself, as ADK's adapter reported it. GEMINI_ERROR is a failed
@@ -0,0 +1,127 @@
1
+ /**
2
+ * lib/models/deepseekAdapter.ts — DeepSeek behind the engine's own model
3
+ * contract (lib/models/contract.ts, ADR 0048).
4
+ *
5
+ * WHY this file exists:
6
+ * Model optionality is a primary driver of this framework: the agent YAML
7
+ * declares `model`, and the registry routes it to the right provider.
8
+ * DeepSeek V4 is the cheapest capable model with tool calling and a 1M
9
+ * context on a first-party API (Flash at $0.15 / $0.60 per 1M off-peak),
10
+ * and its thinking mode needs request-side care the gateway cannot give
11
+ * it (reasoning_content back on every tool request, below), so it earns a
12
+ * direct adapter rather than only the gateway path; any `deepseek-*` id
13
+ * resolves to it (resolveAdapter, lib/models/adapterResolver.ts).
14
+ *
15
+ * WHY the chat-completions base (lib/models/chatCompletionsAdapter.ts):
16
+ * DeepSeek's API is OpenAI Chat Completions at https://api.deepseek.com
17
+ * (`/chat/completions`; it also serves a Responses API and an
18
+ * Anthropic-format API, neither adding a capability the chat dialect lacks
19
+ * here). The base provides tool calling, JSON mode, SSE streaming with
20
+ * usage, image parts as base64 data URIs, `reasoning_content` surfaced as
21
+ * thinking, and retries.
22
+ *
23
+ * HOW TO ENABLE:
24
+ * 1. Create a key at https://platform.deepseek.com/api_keys and put it in
25
+ * .env as DEEPSEEK_API_KEY.
26
+ * 2. Set model: "deepseek-flash" (or any deepseek-* id) in your YAML.
27
+ * The adapter is available when the key is set (logProviderStatuses()
28
+ * reports it).
29
+ * Optional: DEEPSEEK_BASE_URL for a proxy that speaks the same dialect.
30
+ *
31
+ * THE FAMILY (api-docs.deepseek.com › Models & Pricing, checked 2026-10-22;
32
+ * USD per 1M tokens, off-peak; peak is exactly double):
33
+ *
34
+ * model context max out in (miss) / cache hit / out use it for
35
+ * ─────────────────────────────────────────────────────────────────────────────
36
+ * deepseek-flash 1M 384K $0.15 / $0.003 / $0.60 DEFAULT — DeepSeek-V4.1-Flash-1017:
37
+ * vision, tools, thinking
38
+ * deepseek-v4-pro 1M 384K $0.66 / $0.022 / $1.98 DeepSeek-V4-Pro-0813: the deeper
39
+ * tier; text only, no vision
40
+ *
41
+ * Peak hours are 01:00–04:00 and 06:00–10:00 UTC on weekdays. The price
42
+ * rows (db/migrations/0019_deepseek_kimi_pricing.sql) carry the PEAK
43
+ * rates, so a budget never underestimates a call.
44
+ * Retired but still accepted: `deepseek-v4-flash` and
45
+ * `deepseek-v4-flash-vision-exp`, both served by V4.1-Flash and billed at
46
+ * Flash's price. The pre-V4 `deepseek-chat` and `deepseek-reasoner` were
47
+ * retired after 2026-07-24 (third-party reports); the 404 hint names the
48
+ * current ids. With no direct key, MODEL_GATEWAY serves an id as
49
+ * `deepseek/<id>` (lib/models/gateway.ts), the gateways' own catalog
50
+ * permitting.
51
+ *
52
+ * REASONING (ADR 0047; api-docs.deepseek.com › Thinking Mode):
53
+ * Both models think by default, at effort `high`. The switch is
54
+ * `thinking: { type: "enabled" | "disabled" }` in the body, the depth
55
+ * `reasoning_effort` (low | high | max). A request with no `reasoning`
56
+ * sends neither: the vendor's default (`high`, the middle of three) is not
57
+ * its slowest, so no effort is pinned in lib/config.ts. `none` (or a
58
+ * budget of 0) sends `thinking: disabled` and no effort; any other level
59
+ * sends `thinking: enabled` and its DeepSeek word: `medium` → `high`,
60
+ * `xhigh` → `max` (a missing word rounds up, as on Kimi K3, ADR 0117;
61
+ * DeepSeek's own server-side mapping would take `xhigh` as `high`), a
62
+ * budget → the level that covers it. In thinking mode temperature,
63
+ * presence_penalty and frequency_penalty are ignored without an error.
64
+ *
65
+ * THINKING WITH TOOLS (the one rule this adapter exists for):
66
+ * When a request carries `tools`, the `reasoning_content` of ALL previous
67
+ * assistant messages must be sent back, or the API returns 400; without
68
+ * tools it is ignored. The adapter turns on the base's
69
+ * replaysReasoningContent and replaysReasoningOnEveryTurn: the field rides
70
+ * as providerState on the part that followed it (ADR 0046) and goes back
71
+ * on every assistant message of a tool request, earlier turns included,
72
+ * for the same model; where none of its own is stored (an earlier turn's
73
+ * state dropped by the transcript projection, another model's turn) it is
74
+ * sent as an empty string, so the field is never missing. Whether DeepSeek
75
+ * accepts an empty string there is not verified live. The replayed text is
76
+ * billed again as input, mostly at the cache-hit price.
77
+ *
78
+ * TOOL CHOICE: auto and none. Whether thinking mode honours a forced
79
+ * `tool_choice` is not documented on the thinking-mode page and not
80
+ * checked live, so `required` and a named tool are weakened to auto and
81
+ * marked on the span (llm.tool_choice.weakened).
82
+ *
83
+ * LIMITATIONS:
84
+ * - Structured output is JSON mode (`response_format: json_object`), not a
85
+ * strict schema: an outputSchema goes as JSON mode with the schema asked
86
+ * for in the system message (takesStrictJsonSchema), and the engine
87
+ * validates the reply. Beside tools the schema goes as set_model_response.
88
+ * - web_search: DeepSeek's Search and Fetch are separate per-request REST
89
+ * APIs, not a chat request field, so the sentinel is dropped with a
90
+ * warning, as on Kimi; `web_extract` works.
91
+ * - Vision on deepseek-flash only; an image sent to deepseek-v4-pro is
92
+ * refused by the API.
93
+ * - Not run live: no DeepSeek key was available when this adapter was
94
+ * written (2026-10-22). Every cell of its capability row is asserted
95
+ * against the request body the adapter sends
96
+ * (tests/capabilityMatrix.test.ts), and the reasoning replay in
97
+ * tests/models.test.ts, all offline.
98
+ */
99
+ import type { ReasoningSetting } from './contract.ts';
100
+ import { ChatCompletionsAdapter } from './chatCompletionsAdapter.ts';
101
+ import type { ChatFailure } from './chatCompletionsAdapter.ts';
102
+ export declare const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
103
+ export interface DeepSeekAdapterOptions {
104
+ /** The model id as the YAML names it: "deepseek-*". */
105
+ model: string;
106
+ /** A caller's own key; default DEEPSEEK_API_KEY, read at call time. */
107
+ apiKey?: string;
108
+ /** Default: DEEPSEEK_BASE_URL in the environment, else https://api.deepseek.com. */
109
+ baseUrl?: string;
110
+ }
111
+ export declare class DeepSeekAdapter extends ChatCompletionsAdapter {
112
+ #private;
113
+ readonly provider = "deepseek";
114
+ constructor({ model, apiKey, baseUrl }: DeepSeekAdapterOptions);
115
+ protected endpointUrl(): string;
116
+ protected headers(): Record<string, string>;
117
+ /** reasoning_content goes back on the tool loop (see the header). */
118
+ protected replaysReasoningContent(_model: string): boolean;
119
+ /** ...and on every earlier assistant message of a request with tools, or DeepSeek returns 400. */
120
+ protected replaysReasoningOnEveryTurn(_model: string): boolean;
121
+ /** JSON mode only: no strict json_schema response_format. */
122
+ protected takesStrictJsonSchema(_model: string): boolean;
123
+ protected missingRequirement(): ChatFailure | undefined;
124
+ protected httpError(status: number, detail: string, model: string): ChatFailure;
125
+ /** The thinking switch and its effort (see the header). */
126
+ protected reasoningFields(model: string, setting: ReasoningSetting | undefined): Record<string, unknown>;
127
+ }