@este.systems/dsc 1.1.1 → 1.2.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/CHANGELOG.md +18 -1
- package/README.md +53 -4
- package/dist/agent.js +3 -2
- package/dist/agent.js.map +1 -1
- package/dist/api.js +508 -14
- package/dist/api.js.map +1 -1
- package/dist/history.js +15 -0
- package/dist/history.js.map +1 -1
- package/dist/prompt.js +2 -0
- package/dist/prompt.js.map +1 -1
- package/dist/slash_dispatch.js +742 -0
- package/dist/slash_dispatch.js.map +1 -0
- package/dist/tools.js +184 -2
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +79 -817
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"deepseek-v4-
|
|
1
|
+
export const DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
|
|
2
|
+
// Registry of every model dsc knows. The active model is the routing key: its
|
|
3
|
+
// `provider` selects the transport, the API key, and the cost table. Adding a
|
|
4
|
+
// provider is two steps — register its models here, and register the Provider
|
|
5
|
+
// in PROVIDERS below. v4-pro figures are discounted (valid through 2026-05-31).
|
|
6
|
+
export const MODEL_REGISTRY = {
|
|
7
|
+
"deepseek-v4-pro": {
|
|
8
|
+
id: "deepseek-v4-pro",
|
|
9
|
+
provider: "deepseek",
|
|
10
|
+
rates: { in_hit: 0.0034e-6, in_miss: 0.414e-6, out: 0.828e-6 },
|
|
11
|
+
contextWindow: 1_000_000,
|
|
12
|
+
},
|
|
13
|
+
"deepseek-v4-flash": {
|
|
14
|
+
id: "deepseek-v4-flash",
|
|
15
|
+
provider: "deepseek",
|
|
16
|
+
rates: { in_hit: 0.0028e-6, in_miss: 0.138e-6, out: 0.276e-6 },
|
|
17
|
+
contextWindow: 1_000_000,
|
|
18
|
+
},
|
|
19
|
+
// Anthropic. Pricing: $3/M input, $15/M output, $0.30/M cache read.
|
|
20
|
+
// Requires ANTHROPIC_API_KEY (env) or providers.anthropic.api_key (config).
|
|
21
|
+
"claude-sonnet-4-6": {
|
|
22
|
+
id: "claude-sonnet-4-6",
|
|
23
|
+
provider: "anthropic",
|
|
24
|
+
rates: { in_hit: 0.30e-6, in_miss: 3e-6, out: 15e-6 },
|
|
25
|
+
contextWindow: 200_000,
|
|
26
|
+
maxTokens: 8192,
|
|
27
|
+
},
|
|
8
28
|
};
|
|
29
|
+
export const DEFAULT_MODEL = "deepseek-v4-pro";
|
|
30
|
+
/** Model ids dsc will offer. Phase 1 lists every registered model (all
|
|
31
|
+
* DeepSeek today); provider-key availability filtering arrives with the
|
|
32
|
+
* multi-provider config work. Order is registry insertion order. */
|
|
33
|
+
export const AVAILABLE_MODELS = Object.keys(MODEL_REGISTRY);
|
|
34
|
+
/** Resolve a model id to its spec, falling back to the default for unknown
|
|
35
|
+
* ids (mirrors history.ts's load-time model guard). */
|
|
36
|
+
export function modelSpec(model) {
|
|
37
|
+
return MODEL_REGISTRY[model] ?? MODEL_REGISTRY[DEFAULT_MODEL];
|
|
38
|
+
}
|
|
9
39
|
export class DeepSeekError extends Error {
|
|
10
40
|
status;
|
|
11
41
|
body;
|
|
@@ -201,6 +231,88 @@ export function apiKeySource() {
|
|
|
201
231
|
return null;
|
|
202
232
|
}
|
|
203
233
|
}
|
|
234
|
+
/** Per-provider key metadata for `/api-key` and onboarding messages. */
|
|
235
|
+
export const PROVIDER_KEY_INFO = {
|
|
236
|
+
deepseek: {
|
|
237
|
+
label: "DeepSeek",
|
|
238
|
+
envVar: "DEEPSEEK_API_KEY",
|
|
239
|
+
signup: "https://platform.deepseek.com/api_keys",
|
|
240
|
+
},
|
|
241
|
+
anthropic: {
|
|
242
|
+
label: "Anthropic",
|
|
243
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
244
|
+
signup: "https://console.anthropic.com/settings/keys",
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
/** Read a non-DeepSeek provider's key from `providers.<id>.api_key`. */
|
|
248
|
+
function configProviderKey(provider) {
|
|
249
|
+
const cfg = getConfig();
|
|
250
|
+
const providers = cfg && typeof cfg.providers === "object" && cfg.providers
|
|
251
|
+
? cfg.providers
|
|
252
|
+
: null;
|
|
253
|
+
const p = providers?.[provider];
|
|
254
|
+
const k = p && typeof p === "object" ? p.api_key : undefined;
|
|
255
|
+
return typeof k === "string" && k.length ? k : null;
|
|
256
|
+
}
|
|
257
|
+
/** Where a provider's key comes from, without revealing it. */
|
|
258
|
+
export function providerKeySource(provider) {
|
|
259
|
+
// DeepSeek has the richer legacy reader (env var, top-level api_key, env block).
|
|
260
|
+
if (provider === "deepseek")
|
|
261
|
+
return apiKeySource();
|
|
262
|
+
const info = PROVIDER_KEY_INFO[provider];
|
|
263
|
+
if (info && process.env[info.envVar])
|
|
264
|
+
return "env";
|
|
265
|
+
try {
|
|
266
|
+
return configProviderKey(provider) ? "file" : null;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Save a provider's API key to the config file, preserving other fields.
|
|
274
|
+
* DeepSeek writes the top-level `api_key` (back-compat with getApiKey);
|
|
275
|
+
* every other provider writes `providers.<id>.api_key`.
|
|
276
|
+
*/
|
|
277
|
+
export async function saveProviderKey(provider, key) {
|
|
278
|
+
const trimmed = key.trim();
|
|
279
|
+
if (!trimmed)
|
|
280
|
+
throw new DeepSeekError("api key is empty");
|
|
281
|
+
if (provider === "deepseek")
|
|
282
|
+
return saveApiKey(trimmed);
|
|
283
|
+
const p = configPath();
|
|
284
|
+
await fsp.mkdir(nodePath.dirname(p), { recursive: true });
|
|
285
|
+
let existing = {};
|
|
286
|
+
try {
|
|
287
|
+
let txt = await fsp.readFile(p, "utf8");
|
|
288
|
+
if (txt.charCodeAt(0) === 0xfeff)
|
|
289
|
+
txt = txt.slice(1);
|
|
290
|
+
const parsed = JSON.parse(txt);
|
|
291
|
+
if (parsed && typeof parsed === "object")
|
|
292
|
+
existing = parsed;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// missing or unparseable — start fresh
|
|
296
|
+
}
|
|
297
|
+
const providers = existing.providers && typeof existing.providers === "object"
|
|
298
|
+
? existing.providers
|
|
299
|
+
: {};
|
|
300
|
+
const sub = providers[provider] && typeof providers[provider] === "object"
|
|
301
|
+
? providers[provider]
|
|
302
|
+
: {};
|
|
303
|
+
sub.api_key = trimmed;
|
|
304
|
+
providers[provider] = sub;
|
|
305
|
+
existing.providers = providers;
|
|
306
|
+
await fsp.writeFile(p, JSON.stringify(existing, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
307
|
+
try {
|
|
308
|
+
await fsp.chmod(p, 0o600);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// POSIX-only; ignore on Windows
|
|
312
|
+
}
|
|
313
|
+
_cachedConfig = undefined;
|
|
314
|
+
return p;
|
|
315
|
+
}
|
|
204
316
|
/**
|
|
205
317
|
* Merge `key` into the config file at `configPath()`, creating the file +
|
|
206
318
|
* parent directory if needed. Preserves any other fields already in the
|
|
@@ -330,8 +442,389 @@ export async function saveSearchKey(provider, key) {
|
|
|
330
442
|
_cachedConfig = undefined;
|
|
331
443
|
return p;
|
|
332
444
|
}
|
|
445
|
+
function openAICompatProvider(o) {
|
|
446
|
+
return {
|
|
447
|
+
id: o.id,
|
|
448
|
+
resolveKey: o.resolveKey,
|
|
449
|
+
chat: (opts) => openAICompatChat(o.url, o.requireKey(), opts),
|
|
450
|
+
chatStream: (opts) => openAICompatStreamOnce(o.url, o.requireKey(), opts),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const deepseekProvider = openAICompatProvider({
|
|
454
|
+
id: "deepseek",
|
|
455
|
+
url: DEEPSEEK_API_URL,
|
|
456
|
+
resolveKey: () => {
|
|
457
|
+
try {
|
|
458
|
+
return getApiKey();
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
// getApiKey throws the existing "set DEEPSEEK_API_KEY / config" guidance.
|
|
465
|
+
requireKey: getApiKey,
|
|
466
|
+
});
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Anthropic provider
|
|
469
|
+
//
|
|
470
|
+
// The Messages API differs from the OpenAI shape in several ways, so this
|
|
471
|
+
// provider owns a translation layer in both directions:
|
|
472
|
+
// - system prompt is a top-level field, not a message
|
|
473
|
+
// - content is an array of blocks (text / tool_use / tool_result / thinking)
|
|
474
|
+
// - tool *calls* are assistant `tool_use` blocks; tool *results* go in a
|
|
475
|
+
// following user message as `tool_result` blocks (consecutive tool results
|
|
476
|
+
// coalesce into one user turn)
|
|
477
|
+
// - SSE is event-typed (message_start / content_block_delta / message_delta)
|
|
478
|
+
// - usage splits cached vs. fresh input tokens differently
|
|
479
|
+
// The normalized request in / ChatResponse out matches every other provider,
|
|
480
|
+
// so the agent loop never sees the difference.
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
export const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
|
|
483
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
484
|
+
const ANTHROPIC_DEFAULT_MAX_TOKENS = 8192;
|
|
485
|
+
function anthropicKey() {
|
|
486
|
+
const env = process.env.ANTHROPIC_API_KEY;
|
|
487
|
+
if (env)
|
|
488
|
+
return env;
|
|
489
|
+
return configProviderKey("anthropic");
|
|
490
|
+
}
|
|
491
|
+
function requireAnthropicKey() {
|
|
492
|
+
const k = anthropicKey();
|
|
493
|
+
if (!k) {
|
|
494
|
+
throw new DeepSeekError(`No Anthropic API key. Set ANTHROPIC_API_KEY, or add providers.anthropic.api_key to ${configPath()}.`);
|
|
495
|
+
}
|
|
496
|
+
return k;
|
|
497
|
+
}
|
|
498
|
+
/** Translate the normalized message list into Anthropic's (system, messages)
|
|
499
|
+
* pair. Runs of `tool` messages coalesce into a single user turn of
|
|
500
|
+
* `tool_result` blocks, which must follow the assistant `tool_use` turn. */
|
|
501
|
+
export function toAnthropic(messages) {
|
|
502
|
+
let system;
|
|
503
|
+
const out = [];
|
|
504
|
+
let pendingResults = [];
|
|
505
|
+
const flush = () => {
|
|
506
|
+
if (pendingResults.length) {
|
|
507
|
+
out.push({ role: "user", content: pendingResults });
|
|
508
|
+
pendingResults = [];
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
for (const m of messages) {
|
|
512
|
+
if (m.role === "system") {
|
|
513
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
514
|
+
if (text)
|
|
515
|
+
system = system ? `${system}\n\n${text}` : text;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (m.role === "tool") {
|
|
519
|
+
pendingResults.push({
|
|
520
|
+
type: "tool_result",
|
|
521
|
+
tool_use_id: m.tool_call_id ?? "",
|
|
522
|
+
content: typeof m.content === "string" ? m.content : "",
|
|
523
|
+
});
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
flush();
|
|
527
|
+
if (m.role === "user") {
|
|
528
|
+
out.push({ role: "user", content: typeof m.content === "string" ? m.content : "" });
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const blocks = [];
|
|
532
|
+
if (typeof m.content === "string" && m.content)
|
|
533
|
+
blocks.push({ type: "text", text: m.content });
|
|
534
|
+
for (const tc of m.tool_calls ?? []) {
|
|
535
|
+
let input = {};
|
|
536
|
+
try {
|
|
537
|
+
input = JSON.parse(tc.function.arguments || "{}");
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
input = {};
|
|
541
|
+
}
|
|
542
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.function.name, input });
|
|
543
|
+
}
|
|
544
|
+
// Anthropic rejects empty assistant content; guard (shouldn't happen).
|
|
545
|
+
if (blocks.length === 0)
|
|
546
|
+
blocks.push({ type: "text", text: " " });
|
|
547
|
+
out.push({ role: "assistant", content: blocks });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
flush();
|
|
551
|
+
return { system, messages: out };
|
|
552
|
+
}
|
|
553
|
+
export function toAnthropicTools(tools) {
|
|
554
|
+
if (!tools || !tools.length)
|
|
555
|
+
return undefined;
|
|
556
|
+
return tools.map((t) => ({
|
|
557
|
+
name: t.function.name,
|
|
558
|
+
description: t.function.description,
|
|
559
|
+
input_schema: t.function.parameters,
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
export function mapAnthropicStop(s) {
|
|
563
|
+
if (s === "tool_use")
|
|
564
|
+
return "tool_calls";
|
|
565
|
+
if (s === "end_turn" || s === "stop_sequence")
|
|
566
|
+
return "stop";
|
|
567
|
+
if (s === "max_tokens")
|
|
568
|
+
return "length";
|
|
569
|
+
return s ?? undefined;
|
|
570
|
+
}
|
|
571
|
+
export function anthropicUsage(u) {
|
|
572
|
+
if (!u)
|
|
573
|
+
return undefined;
|
|
574
|
+
const input = u.input_tokens ?? 0;
|
|
575
|
+
const cacheRead = u.cache_read_input_tokens ?? 0;
|
|
576
|
+
const cacheCreate = u.cache_creation_input_tokens ?? 0;
|
|
577
|
+
const output = u.output_tokens ?? 0;
|
|
578
|
+
const prompt = input + cacheRead + cacheCreate;
|
|
579
|
+
return {
|
|
580
|
+
prompt_tokens: prompt,
|
|
581
|
+
completion_tokens: output,
|
|
582
|
+
total_tokens: prompt + output,
|
|
583
|
+
// Map cached reads to "hit"; fresh input + cache writes bill at "miss".
|
|
584
|
+
prompt_cache_hit_tokens: cacheRead,
|
|
585
|
+
prompt_cache_miss_tokens: input + cacheCreate,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function anthropicRequestBody(opts, spec, stream) {
|
|
589
|
+
const { system, messages } = toAnthropic(opts.messages);
|
|
590
|
+
const body = {
|
|
591
|
+
model: opts.model,
|
|
592
|
+
max_tokens: spec.maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
593
|
+
messages,
|
|
594
|
+
stream,
|
|
595
|
+
};
|
|
596
|
+
if (system)
|
|
597
|
+
body.system = system;
|
|
598
|
+
const tools = toAnthropicTools(opts.tools);
|
|
599
|
+
if (tools)
|
|
600
|
+
body.tools = tools;
|
|
601
|
+
return body;
|
|
602
|
+
}
|
|
603
|
+
function anthropicHeaders(apiKey, stream) {
|
|
604
|
+
const h = {
|
|
605
|
+
"content-type": "application/json",
|
|
606
|
+
"x-api-key": apiKey,
|
|
607
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
608
|
+
};
|
|
609
|
+
if (stream)
|
|
610
|
+
h["accept"] = "text/event-stream";
|
|
611
|
+
return h;
|
|
612
|
+
}
|
|
613
|
+
async function anthropicChat(opts, spec) {
|
|
614
|
+
const apiKey = requireAnthropicKey();
|
|
615
|
+
const res = await fetch(ANTHROPIC_API_URL, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers: anthropicHeaders(apiKey, false),
|
|
618
|
+
body: JSON.stringify(anthropicRequestBody(opts, spec, false)),
|
|
619
|
+
signal: opts.signal,
|
|
620
|
+
});
|
|
621
|
+
const text = await res.text();
|
|
622
|
+
if (!res.ok)
|
|
623
|
+
throw new DeepSeekError(`HTTP ${res.status}`, res.status, text);
|
|
624
|
+
let data;
|
|
625
|
+
try {
|
|
626
|
+
data = JSON.parse(text);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
throw new DeepSeekError(`Invalid JSON response: ${text.slice(0, 200)}`);
|
|
630
|
+
}
|
|
631
|
+
const blocks = Array.isArray(data.content) ? data.content : [];
|
|
632
|
+
let content = "";
|
|
633
|
+
let reasoning = "";
|
|
634
|
+
const toolCalls = [];
|
|
635
|
+
for (const b of blocks) {
|
|
636
|
+
if (b.type === "text")
|
|
637
|
+
content += b.text ?? "";
|
|
638
|
+
else if (b.type === "thinking")
|
|
639
|
+
reasoning += b.thinking ?? "";
|
|
640
|
+
else if (b.type === "tool_use")
|
|
641
|
+
toolCalls.push({
|
|
642
|
+
id: b.id,
|
|
643
|
+
type: "function",
|
|
644
|
+
function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) },
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
choices: [
|
|
649
|
+
{
|
|
650
|
+
message: {
|
|
651
|
+
role: "assistant",
|
|
652
|
+
content: content || null,
|
|
653
|
+
tool_calls: toolCalls.length ? toolCalls : undefined,
|
|
654
|
+
reasoning_content: reasoning || undefined,
|
|
655
|
+
},
|
|
656
|
+
finish_reason: mapAnthropicStop(data.stop_reason),
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
usage: anthropicUsage(data.usage),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function anthropicStreamOnce(opts, spec) {
|
|
663
|
+
const apiKey = requireAnthropicKey();
|
|
664
|
+
const res = await fetch(ANTHROPIC_API_URL, {
|
|
665
|
+
method: "POST",
|
|
666
|
+
headers: anthropicHeaders(apiKey, true),
|
|
667
|
+
body: JSON.stringify(anthropicRequestBody(opts, spec, true)),
|
|
668
|
+
signal: opts.signal,
|
|
669
|
+
});
|
|
670
|
+
if (!res.ok) {
|
|
671
|
+
const text = await res.text();
|
|
672
|
+
throw new DeepSeekError(`HTTP ${res.status}`, res.status, text);
|
|
673
|
+
}
|
|
674
|
+
if (!res.body)
|
|
675
|
+
throw new DeepSeekError("No response body for stream");
|
|
676
|
+
let content = "";
|
|
677
|
+
let reasoning = "";
|
|
678
|
+
let stopReason;
|
|
679
|
+
const blocks = {};
|
|
680
|
+
let uInput = 0;
|
|
681
|
+
let uCacheRead = 0;
|
|
682
|
+
let uCacheCreate = 0;
|
|
683
|
+
let uOutput = 0;
|
|
684
|
+
const reader = res.body.getReader();
|
|
685
|
+
const decoder = new TextDecoder("utf-8");
|
|
686
|
+
let buf = "";
|
|
687
|
+
while (true) {
|
|
688
|
+
const { done, value } = await reader.read();
|
|
689
|
+
if (done)
|
|
690
|
+
break;
|
|
691
|
+
buf += decoder.decode(value, { stream: true });
|
|
692
|
+
let nl;
|
|
693
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
694
|
+
const rawLine = buf.slice(0, nl).replace(/\r$/, "");
|
|
695
|
+
buf = buf.slice(nl + 1);
|
|
696
|
+
// Anthropic sends "event: <type>" + "data: <json>"; the type is also in
|
|
697
|
+
// the JSON payload, so we route on that and ignore the event: lines.
|
|
698
|
+
if (!rawLine || rawLine.startsWith(":") || !rawLine.startsWith("data:"))
|
|
699
|
+
continue;
|
|
700
|
+
const data = rawLine.slice(5).trim();
|
|
701
|
+
let evt;
|
|
702
|
+
try {
|
|
703
|
+
evt = JSON.parse(data);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
switch (evt.type) {
|
|
709
|
+
case "message_start": {
|
|
710
|
+
const u = evt.message?.usage;
|
|
711
|
+
if (u) {
|
|
712
|
+
uInput = u.input_tokens ?? 0;
|
|
713
|
+
uCacheRead = u.cache_read_input_tokens ?? 0;
|
|
714
|
+
uCacheCreate = u.cache_creation_input_tokens ?? 0;
|
|
715
|
+
}
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
case "content_block_start": {
|
|
719
|
+
const idx = evt.index ?? 0;
|
|
720
|
+
const cb = evt.content_block ?? {};
|
|
721
|
+
blocks[idx] = { type: cb.type, id: cb.id, name: cb.name, args: "" };
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "content_block_delta": {
|
|
725
|
+
const idx = evt.index ?? 0;
|
|
726
|
+
const d = evt.delta ?? {};
|
|
727
|
+
if (d.type === "text_delta" && d.text) {
|
|
728
|
+
content += d.text;
|
|
729
|
+
opts.onContent?.(d.text);
|
|
730
|
+
}
|
|
731
|
+
else if (d.type === "thinking_delta" && d.thinking) {
|
|
732
|
+
reasoning += d.thinking;
|
|
733
|
+
opts.onReasoning?.(d.thinking);
|
|
734
|
+
}
|
|
735
|
+
else if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
|
|
736
|
+
if (blocks[idx])
|
|
737
|
+
blocks[idx].args += d.partial_json;
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
case "message_delta": {
|
|
742
|
+
if (evt.delta?.stop_reason)
|
|
743
|
+
stopReason = evt.delta.stop_reason;
|
|
744
|
+
if (evt.usage?.output_tokens != null)
|
|
745
|
+
uOutput = evt.usage.output_tokens;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
// content_block_stop / message_stop / ping: nothing to accumulate.
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const toolCalls = Object.keys(blocks)
|
|
753
|
+
.map(Number)
|
|
754
|
+
.sort((a, b) => a - b)
|
|
755
|
+
.map((i) => blocks[i])
|
|
756
|
+
.filter((b) => b.type === "tool_use")
|
|
757
|
+
.map((b, i) => ({
|
|
758
|
+
id: b.id ?? `call_${i}`,
|
|
759
|
+
type: "function",
|
|
760
|
+
function: { name: b.name ?? "", arguments: b.args || "{}" },
|
|
761
|
+
}));
|
|
762
|
+
return {
|
|
763
|
+
choices: [
|
|
764
|
+
{
|
|
765
|
+
message: {
|
|
766
|
+
role: "assistant",
|
|
767
|
+
content: content || null,
|
|
768
|
+
tool_calls: toolCalls.length ? toolCalls : undefined,
|
|
769
|
+
reasoning_content: reasoning || undefined,
|
|
770
|
+
},
|
|
771
|
+
finish_reason: mapAnthropicStop(stopReason),
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
usage: anthropicUsage({
|
|
775
|
+
input_tokens: uInput,
|
|
776
|
+
cache_read_input_tokens: uCacheRead,
|
|
777
|
+
cache_creation_input_tokens: uCacheCreate,
|
|
778
|
+
output_tokens: uOutput,
|
|
779
|
+
}),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const anthropicProvider = {
|
|
783
|
+
id: "anthropic",
|
|
784
|
+
resolveKey: anthropicKey,
|
|
785
|
+
chat: anthropicChat,
|
|
786
|
+
chatStream: anthropicStreamOnce,
|
|
787
|
+
};
|
|
788
|
+
const PROVIDERS = {
|
|
789
|
+
deepseek: deepseekProvider,
|
|
790
|
+
anthropic: anthropicProvider,
|
|
791
|
+
};
|
|
792
|
+
/** Whether a model id is registered (regardless of key availability). Used by
|
|
793
|
+
* the session loader so resuming a session keyed to any known model works. */
|
|
794
|
+
export function isKnownModel(model) {
|
|
795
|
+
return model in MODEL_REGISTRY;
|
|
796
|
+
}
|
|
797
|
+
/** Whether a model can actually be used right now — its provider has a key.
|
|
798
|
+
* The default provider (DeepSeek) is always considered available so the
|
|
799
|
+
* first-launch "set your key" flow still surfaces a model to select. */
|
|
800
|
+
export function modelAvailable(model) {
|
|
801
|
+
const spec = MODEL_REGISTRY[model];
|
|
802
|
+
if (!spec)
|
|
803
|
+
return false;
|
|
804
|
+
if (spec.provider === MODEL_REGISTRY[DEFAULT_MODEL].provider)
|
|
805
|
+
return true;
|
|
806
|
+
const p = PROVIDERS[spec.provider];
|
|
807
|
+
return !!p && p.resolveKey() !== null;
|
|
808
|
+
}
|
|
809
|
+
/** Model ids dsc will offer right now — registered AND usable (provider key
|
|
810
|
+
* present, or the default provider). This is the list `/model` and `--help`
|
|
811
|
+
* should show; `AVAILABLE_MODELS` remains the full registry for validation. */
|
|
812
|
+
export function availableModels() {
|
|
813
|
+
return AVAILABLE_MODELS.filter(modelAvailable);
|
|
814
|
+
}
|
|
815
|
+
/** The Provider that serves a given model, via the registry. */
|
|
816
|
+
export function providerFor(model) {
|
|
817
|
+
const spec = modelSpec(model);
|
|
818
|
+
const p = PROVIDERS[spec.provider];
|
|
819
|
+
if (!p) {
|
|
820
|
+
throw new DeepSeekError(`No provider registered for '${spec.provider}' (model '${model}')`);
|
|
821
|
+
}
|
|
822
|
+
return p;
|
|
823
|
+
}
|
|
333
824
|
export async function chat(opts) {
|
|
334
|
-
|
|
825
|
+
return providerFor(opts.model).chat(opts, modelSpec(opts.model));
|
|
826
|
+
}
|
|
827
|
+
async function openAICompatChat(url, apiKey, opts) {
|
|
335
828
|
const body = {
|
|
336
829
|
model: opts.model,
|
|
337
830
|
messages: opts.messages,
|
|
@@ -339,7 +832,7 @@ export async function chat(opts) {
|
|
|
339
832
|
};
|
|
340
833
|
if (opts.tools && opts.tools.length)
|
|
341
834
|
body.tools = opts.tools;
|
|
342
|
-
const res = await fetch(
|
|
835
|
+
const res = await fetch(url, {
|
|
343
836
|
method: "POST",
|
|
344
837
|
headers: {
|
|
345
838
|
"Content-Type": "application/json",
|
|
@@ -392,9 +885,11 @@ function sleep(ms, signal) {
|
|
|
392
885
|
});
|
|
393
886
|
}
|
|
394
887
|
export async function chatStream(opts) {
|
|
888
|
+
const provider = providerFor(opts.model);
|
|
889
|
+
const spec = modelSpec(opts.model);
|
|
395
890
|
for (let attempt = 1;; attempt++) {
|
|
396
891
|
try {
|
|
397
|
-
return await
|
|
892
|
+
return await provider.chatStream(opts, spec);
|
|
398
893
|
}
|
|
399
894
|
catch (e) {
|
|
400
895
|
if (isAbortError(e))
|
|
@@ -410,8 +905,7 @@ export async function chatStream(opts) {
|
|
|
410
905
|
}
|
|
411
906
|
}
|
|
412
907
|
}
|
|
413
|
-
async function
|
|
414
|
-
const apiKey = getApiKey();
|
|
908
|
+
async function openAICompatStreamOnce(url, apiKey, opts) {
|
|
415
909
|
const body = {
|
|
416
910
|
model: opts.model,
|
|
417
911
|
messages: opts.messages,
|
|
@@ -420,7 +914,7 @@ async function streamOnce(opts) {
|
|
|
420
914
|
};
|
|
421
915
|
if (opts.tools && opts.tools.length)
|
|
422
916
|
body.tools = opts.tools;
|
|
423
|
-
const res = await fetch(
|
|
917
|
+
const res = await fetch(url, {
|
|
424
918
|
method: "POST",
|
|
425
919
|
headers: {
|
|
426
920
|
"Content-Type": "application/json",
|
|
@@ -551,7 +1045,7 @@ export function recordUsage(stats, usage) {
|
|
|
551
1045
|
stats.cache_miss_tokens += usage.prompt_cache_miss_tokens ?? 0;
|
|
552
1046
|
}
|
|
553
1047
|
export function computeCostUsd(stats, model) {
|
|
554
|
-
const rates =
|
|
1048
|
+
const rates = modelSpec(model).rates;
|
|
555
1049
|
const hit = stats.cache_hit_tokens;
|
|
556
1050
|
const miss = stats.cache_miss_tokens;
|
|
557
1051
|
const out = stats.completion_tokens;
|