melchizedek-agents 1.10.3 → 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.
- package/.env.example +10 -0
- package/AGENT_SETUP.md +2 -1
- package/CHANGELOG.md +29 -0
- package/DOCUMENTATION.md +43 -7
- package/ONBOARDING.md +4 -1
- package/QUICKSTART.md +2 -2
- package/README.md +4 -4
- package/config/agents/examples/model_zoo.yaml +21 -9
- package/config/agents/syndicate.schema.json +58 -0
- package/config/agents/syndicateSchema.yaml +9 -5
- package/db/migrations/0019_deepseek_kimi_pricing.sql +48 -0
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +11 -0
- package/dist/lib/doctor.js +2 -0
- package/dist/lib/loadSyndicate.d.ts +1 -1
- package/dist/lib/model.d.ts +2 -0
- package/dist/lib/model.js +1 -0
- package/dist/lib/models/adapterResolver.js +3 -1
- package/dist/lib/models/capabilities.js +11 -0
- package/dist/lib/models/chatCompletionsAdapter.d.ts +24 -1
- package/dist/lib/models/chatCompletionsAdapter.js +42 -5
- package/dist/lib/models/contract.d.ts +1 -1
- package/dist/lib/models/deepseekAdapter.d.ts +127 -0
- package/dist/lib/models/deepseekAdapter.js +157 -0
- package/dist/lib/models/endpoints.d.ts +2 -1
- package/dist/lib/models/endpoints.js +5 -1
- package/dist/lib/models/gateway.d.ts +3 -2
- package/dist/lib/models/gateway.js +5 -4
- package/dist/lib/models/gatewayAdapter.d.ts +1 -1
- package/dist/lib/models/gatewayAdapter.js +1 -1
- package/dist/lib/models/providerMap.d.ts +3 -2
- package/dist/lib/models/providerMap.js +5 -1
- package/dist/lib/models/reasoning.d.ts +1 -1
- package/dist/lib/models/reasoning.js +4 -2
- package/dist/lib/models/registry.js +4 -1
- package/dist/lib/observability/tracer.d.ts +1 -1
- package/dist/lib/onboarding.js +3 -1
- package/dist/lib/syndicateSchema.d.ts +16 -0
- package/dist/lib/syndicateSchema.js +1 -0
- package/dist/lib/tools/credentialEnv.js +1 -1
- package/package.json +1 -1
- package/skills/briefs/melchizedek-author.md +1 -1
- package/skills/briefs/melchizedek-models.md +7 -6
- package/skills/briefs/melchizedek.md +2 -2
- package/skills/melchizedek-onboard/SKILL.md +1 -1
- package/skills/melchizedek-onboard-keys/SKILL.md +2 -2
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 { ChatCompletionsAdapter, reasonsNotAtAll } from './chatCompletionsAdapter.js';
|
|
100
|
+
import { trimTrailingSlashes } from './urls.js';
|
|
101
|
+
import { credentialValue } from './credentialSource.js';
|
|
102
|
+
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com';
|
|
103
|
+
export class DeepSeekAdapter extends ChatCompletionsAdapter {
|
|
104
|
+
provider = 'deepseek';
|
|
105
|
+
#apiKey;
|
|
106
|
+
#baseUrl;
|
|
107
|
+
constructor({ model, apiKey, baseUrl }) {
|
|
108
|
+
super({ model });
|
|
109
|
+
this.#apiKey = apiKey;
|
|
110
|
+
this.#baseUrl = trimTrailingSlashes(baseUrl || process.env.DEEPSEEK_BASE_URL || DEEPSEEK_BASE_URL);
|
|
111
|
+
}
|
|
112
|
+
endpointUrl() {
|
|
113
|
+
return `${this.#baseUrl}/chat/completions`;
|
|
114
|
+
}
|
|
115
|
+
#key() {
|
|
116
|
+
return this.#apiKey ?? credentialValue('DEEPSEEK_API_KEY');
|
|
117
|
+
}
|
|
118
|
+
headers() {
|
|
119
|
+
return { Authorization: `Bearer ${this.#key() ?? ''}` };
|
|
120
|
+
}
|
|
121
|
+
/** reasoning_content goes back on the tool loop (see the header). */
|
|
122
|
+
replaysReasoningContent(_model) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/** ...and on every earlier assistant message of a request with tools, or DeepSeek returns 400. */
|
|
126
|
+
replaysReasoningOnEveryTurn(_model) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/** JSON mode only: no strict json_schema response_format. */
|
|
130
|
+
takesStrictJsonSchema(_model) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
missingRequirement() {
|
|
134
|
+
if (this.#key())
|
|
135
|
+
return undefined;
|
|
136
|
+
return { code: 'DEEPSEEK_MISSING_KEY', message: 'DEEPSEEK_API_KEY is not set in environment.' };
|
|
137
|
+
}
|
|
138
|
+
httpError(status, detail, model) {
|
|
139
|
+
const hint = status === 401
|
|
140
|
+
? ' Check DEEPSEEK_API_KEY (platform.deepseek.com/api_keys).'
|
|
141
|
+
: status === 404
|
|
142
|
+
? ` Is "${model}" a current DeepSeek id? The current ids are deepseek-flash and deepseek-v4-pro; deepseek-chat and deepseek-reasoner were retired after 2026-07-24.`
|
|
143
|
+
: '';
|
|
144
|
+
return {
|
|
145
|
+
code: 'DEEPSEEK_HTTP_ERROR',
|
|
146
|
+
message: `DeepSeek returned ${status}: ${detail.slice(0, 4000)}.${hint}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The thinking switch and its effort (see the header). */
|
|
150
|
+
reasoningFields(model, setting) {
|
|
151
|
+
if (setting === undefined)
|
|
152
|
+
return {}; // DeepSeek's default: thinking on, effort high
|
|
153
|
+
if (reasonsNotAtAll(setting))
|
|
154
|
+
return { thinking: { type: 'disabled' } };
|
|
155
|
+
return { thinking: { type: 'enabled' }, reasoning_effort: this.effortFor(model, setting) };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* gemini direct | vertex Google Application Default Credentials
|
|
12
12
|
* anthropic direct | bedrock | vertex AWS credential chain / Google ADC
|
|
13
13
|
* openai direct | azure AZURE_OPENAI_API_KEY, else Entra ID
|
|
14
|
-
* xai, moonshot, ollama direct only (moonshot:
|
|
14
|
+
* xai, moonshot, deepseek, ollama direct only (moonshot, deepseek:
|
|
15
|
+
* MOONSHOT_BASE_URL, DEEPSEEK_BASE_URL for a proxy)
|
|
15
16
|
*
|
|
16
17
|
* Configuration comes from the environment (`endpointFromEnv`) or, per
|
|
17
18
|
* request, from the server's `credentials` plug point, which may return an
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* gemini direct | vertex Google Application Default Credentials
|
|
12
12
|
* anthropic direct | bedrock | vertex AWS credential chain / Google ADC
|
|
13
13
|
* openai direct | azure AZURE_OPENAI_API_KEY, else Entra ID
|
|
14
|
-
* xai, moonshot, ollama direct only (moonshot:
|
|
14
|
+
* xai, moonshot, deepseek, ollama direct only (moonshot, deepseek:
|
|
15
|
+
* MOONSHOT_BASE_URL, DEEPSEEK_BASE_URL for a proxy)
|
|
15
16
|
*
|
|
16
17
|
* Configuration comes from the environment (`endpointFromEnv`) or, per
|
|
17
18
|
* request, from the server's `credentials` plug point, which may return an
|
|
@@ -38,6 +39,7 @@ export const PLATFORMS_FOR = {
|
|
|
38
39
|
openai: ['direct', 'azure'],
|
|
39
40
|
xai: ['direct'],
|
|
40
41
|
moonshot: ['direct'],
|
|
42
|
+
deepseek: ['direct'],
|
|
41
43
|
ollama: ['direct'],
|
|
42
44
|
};
|
|
43
45
|
const PLATFORM_ENV = {
|
|
@@ -104,6 +106,8 @@ export function endpointFromEnv(provider, env = credentialEnv()) {
|
|
|
104
106
|
return { ...base, ...(trimmed(env.OPENAI_BASE_URL) ? { baseURL: trimmed(env.OPENAI_BASE_URL) } : {}) };
|
|
105
107
|
case 'moonshot/direct':
|
|
106
108
|
return { ...base, ...(trimmed(env.MOONSHOT_BASE_URL) ? { baseURL: trimmed(env.MOONSHOT_BASE_URL) } : {}) };
|
|
109
|
+
case 'deepseek/direct':
|
|
110
|
+
return { ...base, ...(trimmed(env.DEEPSEEK_BASE_URL) ? { baseURL: trimmed(env.DEEPSEEK_BASE_URL) } : {}) };
|
|
107
111
|
case 'openai/azure': {
|
|
108
112
|
const endpoint = trimmed(env.AZURE_OPENAI_ENDPOINT);
|
|
109
113
|
const key = trimmed(env.AZURE_OPENAI_API_KEY);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* WHY this file exists:
|
|
5
5
|
* A newcomer should be able to run every starter-pack syndicate with ONE
|
|
6
6
|
* credential when they choose to. A hosted gateway (Vercel AI Gateway,
|
|
7
|
-
* OpenRouter) fronts OpenAI, Anthropic, Google, xAI and
|
|
7
|
+
* OpenRouter) fronts OpenAI, Anthropic, Google, xAI, Moonshot and DeepSeek behind a single
|
|
8
8
|
* OpenAI-compatible endpoint, so a model id whose direct key is absent can
|
|
9
9
|
* still be served. This module owns that decision and nothing else.
|
|
10
10
|
*
|
|
@@ -81,7 +81,8 @@ export declare function gatewayUsable(): boolean;
|
|
|
81
81
|
* Gateways prefix every id with the upstream provider's slug and spell
|
|
82
82
|
* Anthropic's version segment with a dot ("claude-sonnet-4-6" is
|
|
83
83
|
* "anthropic/claude-sonnet-4.6" on both Vercel and OpenRouter). Gemini,
|
|
84
|
-
* GPT, Grok and
|
|
84
|
+
* GPT, Grok, Kimi and DeepSeek ids already carry dots (or need none), so only the
|
|
85
|
+
* prefix is added. An id
|
|
85
86
|
* the mapper gets wrong is fixed in MODEL_GATEWAY_MODEL_MAP, not in code.
|
|
86
87
|
*/
|
|
87
88
|
export declare function gatewayWireModel(model: string, gateway: GatewayInfo): string;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* WHY this file exists:
|
|
5
5
|
* A newcomer should be able to run every starter-pack syndicate with ONE
|
|
6
6
|
* credential when they choose to. A hosted gateway (Vercel AI Gateway,
|
|
7
|
-
* OpenRouter) fronts OpenAI, Anthropic, Google, xAI and
|
|
7
|
+
* OpenRouter) fronts OpenAI, Anthropic, Google, xAI, Moonshot and DeepSeek behind a single
|
|
8
8
|
* OpenAI-compatible endpoint, so a model id whose direct key is absent can
|
|
9
9
|
* still be served. This module owns that decision and nothing else.
|
|
10
10
|
*
|
|
@@ -52,14 +52,14 @@ export const GATEWAYS = {
|
|
|
52
52
|
label: 'Vercel AI Gateway',
|
|
53
53
|
baseUrl: 'https://ai-gateway.vercel.sh/v1',
|
|
54
54
|
consoleUrl: 'https://vercel.com/ai-gateway',
|
|
55
|
-
slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'xai', moonshot: 'moonshotai' },
|
|
55
|
+
slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'xai', moonshot: 'moonshotai', deepseek: 'deepseek' },
|
|
56
56
|
},
|
|
57
57
|
openrouter: {
|
|
58
58
|
id: 'openrouter',
|
|
59
59
|
label: 'OpenRouter',
|
|
60
60
|
baseUrl: 'https://openrouter.ai/api/v1',
|
|
61
61
|
consoleUrl: 'https://openrouter.ai/keys',
|
|
62
|
-
slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'x-ai', moonshot: 'moonshotai' },
|
|
62
|
+
slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'x-ai', moonshot: 'moonshotai', deepseek: 'deepseek' },
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
/**
|
|
@@ -124,7 +124,8 @@ function modelMapFromEnv() {
|
|
|
124
124
|
* Gateways prefix every id with the upstream provider's slug and spell
|
|
125
125
|
* Anthropic's version segment with a dot ("claude-sonnet-4-6" is
|
|
126
126
|
* "anthropic/claude-sonnet-4.6" on both Vercel and OpenRouter). Gemini,
|
|
127
|
-
* GPT, Grok and
|
|
127
|
+
* GPT, Grok, Kimi and DeepSeek ids already carry dots (or need none), so only the
|
|
128
|
+
* prefix is added. An id
|
|
128
129
|
* the mapper gets wrong is fixed in MODEL_GATEWAY_MODEL_MAP, not in code.
|
|
129
130
|
*/
|
|
130
131
|
export function gatewayWireModel(model, gateway) {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* WHAT STAYS TRUE THROUGH THE GATEWAY:
|
|
14
14
|
* - Attribution. `provider` is the YAML id's provider (anthropic, openai,
|
|
15
|
-
* gemini, xai, moonshot), never "gateway", so the ledger and any
|
|
15
|
+
* gemini, xai, moonshot, deepseek), never "gateway", so the ledger and any
|
|
16
16
|
* per-agent cost view keep the same provider column (ADR 0009). The
|
|
17
17
|
* transport is a separate span attribute: llm.transport = gateway:<id>.
|
|
18
18
|
* - Tool calling, structured output, reasoning (as `reasoning_effort`, the
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* WHAT STAYS TRUE THROUGH THE GATEWAY:
|
|
14
14
|
* - Attribution. `provider` is the YAML id's provider (anthropic, openai,
|
|
15
|
-
* gemini, xai, moonshot), never "gateway", so the ledger and any
|
|
15
|
+
* gemini, xai, moonshot, deepseek), never "gateway", so the ledger and any
|
|
16
16
|
* per-agent cost view keep the same provider column (ADR 0009). The
|
|
17
17
|
* transport is a separate span attribute: llm.transport = gateway:<id>.
|
|
18
18
|
* - Tool calling, structured output, reasoning (as `reasoning_effort`, the
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* Model-name conventions (see DOCUMENTATION.md §7):
|
|
12
12
|
* claude-* → anthropic gpt-* / o<digit>* → openai
|
|
13
13
|
* grok-* → xai ollama/<model> → ollama (local, keyless)
|
|
14
|
-
* kimi-* → moonshot
|
|
14
|
+
* kimi-* → moonshot deepseek-* → deepseek
|
|
15
|
+
* everything else → gemini (the default)
|
|
15
16
|
*/
|
|
16
|
-
export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'ollama';
|
|
17
|
+
export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'moonshot' | 'deepseek' | 'ollama';
|
|
17
18
|
interface ProviderInfo {
|
|
18
19
|
/** Env var holding the API key; null = keyless (local). */
|
|
19
20
|
keyEnv: string | null;
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Model-name conventions (see DOCUMENTATION.md §7):
|
|
12
12
|
* claude-* → anthropic gpt-* / o<digit>* → openai
|
|
13
13
|
* grok-* → xai ollama/<model> → ollama (local, keyless)
|
|
14
|
-
* kimi-* → moonshot
|
|
14
|
+
* kimi-* → moonshot deepseek-* → deepseek
|
|
15
|
+
* everything else → gemini (the default)
|
|
15
16
|
*/
|
|
16
17
|
import { credentialValue } from './credentialSource.js';
|
|
17
18
|
export const PROVIDERS = {
|
|
@@ -20,6 +21,7 @@ export const PROVIDERS = {
|
|
|
20
21
|
openai: { keyEnv: 'OPENAI_API_KEY', label: 'OpenAI GPT' },
|
|
21
22
|
xai: { keyEnv: 'XAI_API_KEY', label: 'xAI Grok' },
|
|
22
23
|
moonshot: { keyEnv: 'MOONSHOT_API_KEY', label: 'Moonshot Kimi' },
|
|
24
|
+
deepseek: { keyEnv: 'DEEPSEEK_API_KEY', label: 'DeepSeek' },
|
|
23
25
|
ollama: { keyEnv: null, label: 'Ollama (local)' },
|
|
24
26
|
};
|
|
25
27
|
/** Maps a model id to its provider. Unknown ids default to gemini, as
|
|
@@ -35,6 +37,8 @@ export function providerForModel(model) {
|
|
|
35
37
|
return 'xai';
|
|
36
38
|
if (/^kimi-/.test(model))
|
|
37
39
|
return 'moonshot';
|
|
40
|
+
if (/^deepseek-/.test(model))
|
|
41
|
+
return 'deepseek';
|
|
38
42
|
return 'gemini';
|
|
39
43
|
}
|
|
40
44
|
/** True when the provider's key is present (always true for local Ollama). */
|