@toddzheng024/dscode-bundle 0.7.2 → 0.7.3

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 (35) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/cordis.patch.yml +4 -0
  3. package/package.json +4 -2
  4. package/plugins/code-review/git.mjs +24 -2
  5. package/plugins/code-review/index.mjs +13 -6
  6. package/plugins/credentials/index.mjs +7 -5
  7. package/plugins/exec/cli.mjs +63 -0
  8. package/plugins/exec/index.mjs +1 -1
  9. package/plugins/memory/index.mjs +17 -12
  10. package/plugins/providers/catalog.mjs +134 -0
  11. package/plugins/session-cards/index.mjs +11 -8
  12. package/plugins/session-cards/manager.mjs +1 -1
  13. package/plugins/session-metrics/attribution.mjs +19 -0
  14. package/plugins/session-metrics/balance.mjs +41 -22
  15. package/plugins/session-metrics/index.mjs +26 -14
  16. package/plugins/session-metrics/pricing.mjs +24 -4
  17. package/plugins/session-metrics/view.mjs +5 -3
  18. package/plugins/tui-tools/doctor.mjs +9 -6
  19. package/plugins/tui-tools/index.mjs +2 -2
  20. package/plugins/ultra/policy.mjs +16 -0
  21. package/vendor/pi-ai/LICENSE +21 -0
  22. package/vendor/pi-ai/index.js +2702 -0
  23. package/vendor/pi-ai/types/adapter.d.ts +105 -0
  24. package/vendor/pi-ai/types/auth.d.ts +60 -0
  25. package/vendor/pi-ai/types/catalog.d.ts +355 -0
  26. package/vendor/pi-ai/types/config.d.ts +208 -0
  27. package/vendor/pi-ai/types/context.d.ts +42 -0
  28. package/vendor/pi-ai/types/discovery.d.ts +43 -0
  29. package/vendor/pi-ai/types/index.d.ts +69 -0
  30. package/vendor/pi-ai/types/login.d.ts +21 -0
  31. package/vendor/pi-ai/types/provider.d.ts +59 -0
  32. package/vendor/pi-ai/types/replay.d.ts +63 -0
  33. package/vendor/pi-ai/types/stream.d.ts +43 -0
  34. package/vendor/tui/dscode-providers/catalog.mjs +134 -0
  35. package/vendor/tui/index.mjs +146 -59
@@ -1,9 +1,14 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { appendMetric } from './store.mjs';
3
- import { estimateCost, PRICE_VERSION } from './pricing.mjs';
3
+ import { estimateCost, priceVersionFor } from './pricing.mjs';
4
4
  import { setMetricSource } from './view.mjs';
5
- import { refreshBalance } from './balance.mjs';
5
+ import { BALANCE_PROVIDERS, refreshBalance } from './balance.mjs';
6
+ import { providerSpec } from '../providers/catalog.mjs';
6
7
  import { createWindowRate } from './rate.mjs';
8
+ import { currentCharge } from './attribution.mjs';
9
+
10
+ // Chunks that carry generated output; the first one marks time to first token.
11
+ const OUTPUT_CHUNKS = new Set(['text-delta', 'reasoning-delta', 'tool-call-delta']);
7
12
  export const name = 'dscode-session-metrics';
8
13
  export const inject = ['llm', 'agents', 'tokenMeter', 'sessionProjections'];
9
14
  export function apply(ctx) {
@@ -26,42 +31,49 @@ export function apply(ctx) {
26
31
  // inject the credentials service, so a missing one cannot fail startup), keep
27
32
  // the request on a five-minute cache, and never let it reach the render path.
28
33
  const credentials = ctx.get?.('credentials');
29
- const refresh = async () => {
34
+ const refresh = () => Promise.all(BALANCE_PROVIDERS.map(async provider => {
30
35
  try {
31
- const resolved = await credentials?.resolve?.('DEEPSEEK_API_KEY');
36
+ const ref = providerSpec(provider).credentialRef;
37
+ const resolved = await credentials?.resolve?.(ref);
32
38
  const key = typeof resolved === 'string' ? resolved : resolved?.value;
33
- await refreshBalance({ key: key ?? process.env.DEEPSEEK_API_KEY });
39
+ await refreshBalance({ provider, key: key ?? process.env[ref] });
34
40
  } catch {
35
41
  /* balance stays unknown */
36
42
  }
37
- };
43
+ }));
38
44
  refresh();
39
45
  const balanceTimer = setInterval(refresh, 5 * 60 * 1000);
40
46
  if (typeof balanceTimer.unref === 'function') balanceTimer.unref();
41
47
  ctx.effect(() => () => clearInterval(balanceTimer));
42
48
  const record = (id, entry) => { try { appendMetric(home, id, entry); } catch { ctx.logger.warn('Session cost telemetry could not be saved.'); } };
43
49
  ctx.on('llm/stream', async function* (options, next) {
44
- if (!options.sessionId || !home) { yield* next(); return; }
50
+ // Plugin calls made for a session carry no sessionId on the wire; they are charged through the async context.
51
+ const charge = options.sessionId ? undefined : currentCharge();
52
+ const sessionId = options.sessionId ?? charge?.sessionId;
53
+ if (!sessionId || !home) { yield* next(); return; }
45
54
  const id = randomUUID(), time = Date.now();
46
- const recipients = new Set([options.sessionId]);
47
- let child = ctx.agents.get(options.sessionId);
55
+ const purpose = options.purpose ?? charge?.purpose ?? 'agent';
56
+ const recipients = new Set([sessionId]);
57
+ let child = ctx.agents.get(sessionId);
48
58
  while (child?.session.header.origin === 'subagent' && child.session.header.parentSession && !recipients.has(child.session.header.parentSession)) {
49
59
  recipients.add(child.session.header.parentSession);
50
60
  child = ctx.agents.get(child.session.header.parentSession);
51
61
  }
52
- const save = entry => { for (const recipient of recipients) record(recipient, { ...entry, sessionId: options.sessionId }); };
53
- save( { kind: 'start', id, time, provider: options.provider, model: options.model, purpose: options.purpose ?? 'agent' });
54
- let usage;
55
- const liveSession = !options.purpose || options.purpose === 'agent' ? ctx.agents.get(options.sessionId)?.session : undefined;
62
+ const save = entry => { for (const recipient of recipients) record(recipient, { ...entry, sessionId }); };
63
+ save({ kind: 'start', id, time, provider: options.provider, model: options.model, purpose });
64
+ let usage, firstTokenTime;
65
+ const liveSession = purpose === 'agent' ? ctx.agents.get(sessionId)?.session : undefined;
56
66
  try {
57
67
  for await (const chunk of next()) {
58
68
  if (chunk.type === 'usage') usage = chunk.usage;
69
+ else if (firstTokenTime === undefined && OUTPUT_CHUNKS.has(chunk.type)) firstTokenTime = Date.now();
59
70
  if (liveSession) liveRate.add(liveSession, chunk);
60
71
  yield chunk;
61
72
  }
62
73
  } finally {
63
74
  if (liveSession) liveRate.calibrate(liveSession, usage?.outputTokens);
64
- save({ kind: 'end', id, time, usage: usage ?? null, cost: estimateCost(options.provider, options.model, usage, time), priceVersion: PRICE_VERSION });
75
+ // `time` stays the start (it prices the call); `endTime` and `firstTokenTime` time it.
76
+ save({ kind: 'end', id, time, endTime: Date.now(), ...(firstTokenTime === undefined ? {} : { firstTokenTime }), usage: usage ?? null, cost: estimateCost(options.provider, options.model, usage, time), priceVersion: priceVersionFor(options.provider) });
65
77
  }
66
78
  });
67
79
  }
@@ -2,18 +2,38 @@
2
2
  // https://api-docs.deepseek.com/quick_start/pricing/
3
3
  export const PRICE_SOURCE = 'https://api-docs.deepseek.com/quick_start/pricing/';
4
4
  export const PRICE_VERSION = 'deepseek-2026-09-11';
5
+ // OpenRouter list prices for the DeepSeek models `/provider openrouter` declares,
6
+ // from the pinned pi-ai 0.85.1 catalog (its OpenRouter /models snapshot).
7
+ // OpenRouter bills no peak window. [cache read, input, output].
8
+ export const OPENROUTER_PRICE_VERSION = 'openrouter-pi-ai-0.85.1';
9
+ const OPENROUTER_PRICES = {
10
+ 'deepseek/deepseek-v4-flash': [0.017052, 0.08526, 0.17052],
11
+ 'deepseek/deepseek-v4-pro': [0.074196, 0.890358, 1.780716],
12
+ 'deepseek/deepseek-v4-flash-vision-exp': [0.007, 0.22, 0.66],
13
+ };
14
+
15
+ /** The price table a provider's ledger entries are estimated with. */
16
+ export function priceVersionFor(provider) {
17
+ return provider === 'openrouter' ? OPENROUTER_PRICE_VERSION : PRICE_VERSION;
18
+ }
19
+
5
20
  export function estimateCost(provider, model, usage, time) {
6
- if (provider !== 'deepseek-official' || !usage || !Number.isFinite(time)) return null;
21
+ if (!usage || !Number.isFinite(time)) return null;
22
+ if (provider === 'openrouter') return OPENROUTER_PRICES[model] ? charge(usage, OPENROUTER_PRICES[model]) : null;
23
+ if (provider !== 'deepseek-official') return null;
7
24
  // Earlier requests require an older price table; never back-price them at today's rate.
8
25
  if (time < Date.UTC(2026, 8, 11)) return null;
9
26
  const flash = ['deepseek-flash', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp'].includes(model)
10
27
  || model === 'deepseek-v4-pro' && time >= Date.UTC(2026, 8, 14, 4);
11
28
  if (!flash && model !== 'deepseek-v4-pro') return null;
12
- const peak = isPeak(time);
13
- const [read, input, output] = flash ? [0.003, 0.15, 0.6] : [0.022, 0.66, 1.98];
29
+ const cost = charge(usage, flash ? [0.003, 0.15, 0.6] : [0.022, 0.66, 1.98]);
30
+ return cost === null ? null : cost * (isPeak(time) ? 2 : 1);
31
+ }
32
+
33
+ function charge(usage, [read, input, output]) {
14
34
  const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens ?? 0, usage.cacheWriteTokens ?? 0];
15
35
  if (!values.every(n => Number.isFinite(n) && n >= 0) || values[3] !== 0) return null;
16
- return (values[0] * input + values[1] * output + values[2] * read) * (peak ? 2 : 1) / 1e6;
36
+ return (values[0] * input + values[1] * output + values[2] * read) / 1e6;
17
37
  }
18
38
 
19
39
  /**
@@ -3,6 +3,7 @@ import { t } from '../i18n/messages.mjs';
3
3
  import { estimateCost, peakEmoji } from './pricing.mjs';
4
4
  import { balanceNow, trustedNow } from './balance.mjs';
5
5
  import { sessionAverageTps } from './rate.mjs';
6
+ import { providerOfHeader } from '../providers/catalog.mjs';
6
7
  let source;
7
8
  export function setMetricSource(next) { source = next; return () => { if (source === next) source = undefined; }; }
8
9
  export function summarize(rows, events = [], corrupt = false) {
@@ -44,13 +45,14 @@ export function displayWidth(text) {
44
45
  for (const char of text) width += /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6\u2600-\u27bf\u{1f300}-\u{1faff}]/u.test(char) ? 2 : 1;
45
46
  return width;
46
47
  }
47
- export function formatFooter(metrics, context, columns = 80, rates, locale = 'en', header = '') {
48
+ export function formatFooter(metrics, context, columns = 80, rates, locale = 'en', header = '', provider = providerOfHeader(header) ?? 'deepseek-official') {
48
49
  const label = key => t(locale, key);
49
50
  const ctx = Number.isFinite(context) ? `${Math.round(context)}%` : '--';
50
51
  const cache = metrics.cache === null ? '--' : `${metrics.cache.toFixed(1)}%`;
51
- const balance = balanceNow();
52
+ // The balance belongs to the provider the header names; only DeepSeek's official route bills by a peak window.
53
+ const balance = balanceNow(provider);
52
54
  const spend = metrics.unknown && metrics.cost === 0 ? '--' : `$${metrics.cost.toFixed(2)}${metrics.unknown ? '+' : ''}${metrics.pending ? '…' : ''}`;
53
- const dollars = `${spend} / ${balance === null ? '$--' : '$' + balance.toFixed(2)} ${peakEmoji(trustedNow())}`;
55
+ const dollars = `${spend} / ${balance === null ? '$--' : '$' + balance.toFixed(2)}${provider === 'deepseek-official' ? ' ' + peakEmoji(trustedNow()) : ''}`;
54
56
  const base = rates ? [
55
57
  `${label('footer.current')}: ${Number.isFinite(rates.current) ? '~' + rates.current.toFixed(1) : '--'} tps`,
56
58
  `${label('footer.average')}: ${Number.isFinite(rates.average) ? rates.average.toFixed(1) : '--'} tps`,
@@ -4,6 +4,7 @@ import { Logger } from '@deepseek-ai/cordis';
4
4
  import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
5
5
  import { redact } from '../auto-review/policy.mjs';
6
6
  import { t, readLanguage } from '../i18n/messages.mjs';
7
+ import { chargeTo } from '../session-metrics/attribution.mjs';
7
8
  const L = (key, params) => t(readLanguage(), key, params);
8
9
 
9
10
  const MAX_LOG_BYTES = 1024 * 1024;
@@ -118,7 +119,7 @@ export function localDoctorReport(evidence) {
118
119
  return `${L('doctor.evidence', { traces: evidence.traces.length, logs: evidence.logs.length })}${evidence.logs.length ? '' : evidence.logCoverage ?? ''}\n${local.length ? local.slice(-8).join('\n') : L('doctor.noFindings')}\n${near300.length >= 2 ? `${L('doctor.nearTimeout', { count: near300.length })}\n` : ''}${evidence.logs.slice(-5).map(l => `${time(l.time)} ${l.level} ${l.source}: ${l.detail}`).join('\n')}`;
119
120
  }
120
121
 
121
- export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { model = true } = {}) {
122
+ export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { model = true, sessionId } = {}) {
122
123
  const fallback = localDoctorReport(evidence);
123
124
  if (!model) return fallback;
124
125
  if (!route?.provider || !route?.model) return `${fallback}\n${L('doctor.noRoute')}`;
@@ -126,11 +127,13 @@ export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { mode
126
127
  const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(45000)]);
127
128
  try {
128
129
  let finished = false;
129
- for await (const chunk of ctx.llm.stream({ provider: route.provider, model: route.model, reasoningEffort: 'low', maxTokens: 4096, system: SYSTEM,
130
- messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'plugin', plugin: 'dscode-doctor' } })], signal: deadline })) {
131
- deadline.throwIfAborted(); assembler.push(chunk);
132
- if (chunk.type === 'finish') finished = true;
133
- }
130
+ await chargeTo(sessionId, 'doctor', async () => {
131
+ for await (const chunk of ctx.llm.stream({ provider: route.provider, model: route.model, reasoningEffort: 'low', maxTokens: 4096, system: SYSTEM,
132
+ messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'plugin', plugin: 'dscode-doctor' } })], signal: deadline })) {
133
+ deadline.throwIfAborted(); assembler.push(chunk);
134
+ if (chunk.type === 'finish') finished = true;
135
+ }
136
+ });
134
137
  if (!finished || assembler.finish.kind !== 'stop') throw Error(`model response was incomplete (${safe(JSON.stringify(assembler.finish ?? { kind: 'no finish' }))})`);
135
138
  const blocks = assembler.blocks();
136
139
  if (blocks.some(b => !['text', 'reasoning'].includes(b.type))) throw Error('model returned unexpected tool output');
@@ -92,7 +92,7 @@ export function apply(ctx) {
92
92
  `Session: ${session.id}`, `Workspace: ${session.header.cwd ?? process.cwd()}`,
93
93
  `Agent: ${agent.status} | preset: ${session.header.agentPreset ?? 'standard'}`,
94
94
  `Model: ${route?.provider ?? 'default'} / ${route?.model ?? 'default'}`,
95
- `Effort: ${route?.reasoningEffort ?? 'model default'}${route?.reasoningEffort === 'ultra' ? ' (DeepSeek wire: max; collaboration enabled)' : ''}`,
95
+ `Effort: ${route?.reasoningEffort ?? 'model default'}${route?.reasoningEffort === 'ultra' ? ` (${route.provider === 'openrouter' ? 'OpenRouter wire: xhigh' : 'DeepSeek wire: max'}; collaboration enabled)` : ''}`,
96
96
  `Permission: ${show(ctx.permissionPresets.current(session))}`,
97
97
  `Tokens: ${show(usage ?? 'no provider usage yet')}`,
98
98
  `Context: ${pressure?.pressureTokens ?? pressure?.surfaceTokens ?? '?'} / ${pressure?.contextWindow ?? '?'} tokens`,
@@ -120,7 +120,7 @@ export function apply(ctx) {
120
120
  const evidence = await collectDoctorEvidence(ctx, { agent, signal });
121
121
  if (action === 'preview') return ok(JSON.stringify(evidence, null, 2));
122
122
  const route = agent.session.requestHeader()?.config ?? agent.options;
123
- return ok(`${health}\n\n${await analyzeDoctorEvidence(ctx, evidence, route, signal, { model: action !== 'local' })}`);
123
+ return ok(`${health}\n\n${await analyzeDoctorEvidence(ctx, evidence, route, signal, { model: action !== 'local', sessionId: agent.session.id })}`);
124
124
  });
125
125
  register('mcp', 'MCP list, tools <id>, enable/disable/reconnect <id>', async ({ agent, rawInput }) => {
126
126
  const [action = 'list', id, extra] = rawInput.trim().split(/\s+/).filter(Boolean);
@@ -16,6 +16,22 @@ export function flashRequest(options, messages) {
16
16
  return copy;
17
17
  }
18
18
 
19
+ /**
20
+ * The same shaping for pi-ai routes (OpenRouter), on harness-format options:
21
+ * Ultra sends max and adds the collaboration policy; below Ultra the delegation
22
+ * tools are not offered. Returns a copy; the logged input is never mutated.
23
+ */
24
+ export function piAiRequest(options) {
25
+ const ultra = options.reasoningEffort === 'ultra';
26
+ const hidden = ultra ? ['workflow', 'ralph'] : ['subagent', 'subagent_fork', 'workflow', 'ralph'];
27
+ const next = { ...options, ...(options.tools ? { tools: options.tools.filter(tool => !hidden.includes(tool.name)) } : {}), ...(ultra ? { reasoningEffort: 'max' } : {}) };
28
+ if (!ultra || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return next;
29
+ if (typeof next.system === 'string') return { ...next, system: next.system + '\n\n' + ULTRA_POLICY };
30
+ const [first, ...rest] = next.messages ?? [];
31
+ if (first?.role === 'system' && Array.isArray(first.content)) return { ...next, messages: [{ ...first, content: [...first.content, { type: 'text', text: '\n\n' + ULTRA_POLICY }] }, ...rest] };
32
+ return { ...next, messages: [{ role: 'system', content: [{ type: 'text', text: ULTRA_POLICY }] }, ...(next.messages ?? [])] };
33
+ }
34
+
19
35
  export function ultraRequest(options, messages) {
20
36
  if (options.reasoningEffort !== 'ultra' || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return messages;
21
37
  const copy = messages.map(m => ({ ...m }));
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.