@toddzheng024/dscode-bundle 0.7.5 → 0.7.7

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 (55) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +26 -5
  3. package/package.json +4 -4
  4. package/plugins/auto-review/index.mjs +6 -1
  5. package/plugins/code-review/index.mjs +9 -4
  6. package/plugins/compaction/tetris.mjs +65 -0
  7. package/plugins/compaction/threshold.mjs +46 -0
  8. package/plugins/credentials/index.mjs +2 -2
  9. package/plugins/dscode/index.mjs +4 -10
  10. package/plugins/exec/cli.mjs +3 -2
  11. package/plugins/exec/index.mjs +6 -1
  12. package/plugins/i18n/messages.mjs +18 -0
  13. package/plugins/memory/index.mjs +7 -3
  14. package/plugins/openrouter/adapter.mjs +157 -0
  15. package/plugins/openrouter/index.mjs +112 -0
  16. package/plugins/openrouter/models.mjs +151 -0
  17. package/plugins/openrouter/search.mjs +109 -0
  18. package/plugins/openrouter/wire.mjs +413 -0
  19. package/plugins/providers/catalog.mjs +28 -26
  20. package/plugins/providers/effort.mjs +35 -0
  21. package/plugins/providers/openrouter-account.mjs +171 -0
  22. package/plugins/session-cards/index.mjs +5 -1
  23. package/plugins/session-metrics/balance.mjs +29 -19
  24. package/plugins/session-metrics/index.mjs +19 -6
  25. package/plugins/session-metrics/pricing.mjs +45 -14
  26. package/plugins/session-metrics/view.mjs +1 -1
  27. package/plugins/tui-tools/doctor.mjs +3 -1
  28. package/plugins/tui-tools/index.mjs +1 -1
  29. package/plugins/ultra/policy.mjs +0 -16
  30. package/presets/dscode/agent.cordis.yml +1 -1
  31. package/vendor/compaction-basic/index.js +983 -0
  32. package/vendor/compaction-basic/types/config.d.ts +37 -0
  33. package/vendor/compaction-basic/types/index.d.ts +84 -0
  34. package/vendor/compaction-basic/types/region.d.ts +65 -0
  35. package/vendor/compaction-basic/types/summarizer.d.ts +64 -0
  36. package/vendor/compaction-basic/types/types.d.ts +73 -0
  37. package/vendor/deepseek/index.js +1 -1
  38. package/vendor/subagent/index.js +3 -3
  39. package/vendor/tui/dscode-providers/catalog.mjs +28 -26
  40. package/vendor/tui/dscode-providers/effort.mjs +35 -0
  41. package/vendor/tui/dscode-providers/openrouter-account.mjs +171 -0
  42. package/vendor/tui/index.mjs +395 -133
  43. package/vendor/pi-ai/index.js +0 -2702
  44. package/vendor/pi-ai/types/adapter.d.ts +0 -105
  45. package/vendor/pi-ai/types/auth.d.ts +0 -60
  46. package/vendor/pi-ai/types/catalog.d.ts +0 -355
  47. package/vendor/pi-ai/types/config.d.ts +0 -208
  48. package/vendor/pi-ai/types/context.d.ts +0 -42
  49. package/vendor/pi-ai/types/discovery.d.ts +0 -43
  50. package/vendor/pi-ai/types/index.d.ts +0 -69
  51. package/vendor/pi-ai/types/login.d.ts +0 -21
  52. package/vendor/pi-ai/types/provider.d.ts +0 -59
  53. package/vendor/pi-ai/types/replay.d.ts +0 -63
  54. package/vendor/pi-ai/types/stream.d.ts +0 -43
  55. /package/vendor/{pi-ai → compaction-basic}/LICENSE +0 -0
@@ -0,0 +1,171 @@
1
+ // OpenRouter account facts for /openrouter and the footer balance. The inference key
2
+ // reads the account credits and its own limit and usage; an optional management key,
3
+ // which cannot call models, adds every key's usage and the last 30 days of spend.
4
+ // This directory also ships beside the TUI, so the module imports nothing.
5
+ export const OPENROUTER_API = 'https://openrouter.ai/api/v1';
6
+ export const MANAGEMENT_REF = 'OPENROUTER_MANAGEMENT_KEY';
7
+
8
+ const finite = value => {
9
+ const number = Number(value);
10
+ return value !== null && value !== undefined && value !== '' && Number.isFinite(number) ? number : undefined;
11
+ };
12
+
13
+ export class OpenRouterAccountError extends Error {
14
+ constructor(message, status) {
15
+ super(message);
16
+ this.name = 'OpenRouterAccountError';
17
+ this.status = status;
18
+ }
19
+ }
20
+
21
+ async function get(path, key, { fetch: fetchImpl = globalThis.fetch, signal } = {}) {
22
+ let response;
23
+ try {
24
+ response = await fetchImpl(`${OPENROUTER_API}${path}`, { headers: { authorization: `Bearer ${key}`, accept: 'application/json' }, signal });
25
+ } catch (error) {
26
+ throw new OpenRouterAccountError(`OpenRouter is unreachable: ${error instanceof Error ? error.message : String(error)}`);
27
+ }
28
+ let body;
29
+ try { body = await response.json(); } catch { body = undefined; }
30
+ if (!response.ok) throw new OpenRouterAccountError(typeof body?.error?.message === 'string' ? body.error.message : `HTTP ${response.status}`, response.status);
31
+ return body;
32
+ }
33
+
34
+ /** Account credits from a `/credits` body, or undefined when it carries none. */
35
+ export function creditsOf(body) {
36
+ const total = finite(body?.data?.total_credits), used = finite(body?.data?.total_usage);
37
+ if (total === undefined || used === undefined || total < 0 || used < 0) return undefined;
38
+ return { total, used, remaining: Math.max(0, total - used) };
39
+ }
40
+
41
+ /** Remaining USD account credits from a `/credits` body: purchased minus used. */
42
+ export function parseOpenRouterCredits(body) {
43
+ return creditsOf(body)?.remaining ?? null;
44
+ }
45
+
46
+ /** Remaining USD credit limit from a `/key` body; null when the key has no limit. */
47
+ export function parseOpenRouterKeyRemaining(body) {
48
+ const remaining = body?.data?.limit_remaining;
49
+ if (remaining == null) return null;
50
+ const value = Number(remaining);
51
+ return Number.isFinite(value) ? Math.max(0, value) : null;
52
+ }
53
+
54
+ function keyOf(raw) {
55
+ return {
56
+ label: typeof raw?.label === 'string' ? raw.label : undefined,
57
+ name: typeof raw?.name === 'string' && raw.name.length > 0 ? raw.name : undefined,
58
+ disabled: raw?.disabled === true,
59
+ limit: finite(raw?.limit),
60
+ limitRemaining: finite(raw?.limit_remaining),
61
+ usage: finite(raw?.usage),
62
+ usageDaily: finite(raw?.usage_daily),
63
+ usageWeekly: finite(raw?.usage_weekly),
64
+ usageMonthly: finite(raw?.usage_monthly),
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Verify a management key before it is stored. Account activity is the reading OpenRouter
70
+ * refuses an inference key; `/credits`, though documented as management-only, serves both.
71
+ * @throws OpenRouterAccountError with a message fit for the key prompt.
72
+ */
73
+ export async function verifyManagementKey(key, options) {
74
+ try {
75
+ await get('/activity', key, options);
76
+ } catch (error) {
77
+ if (error instanceof OpenRouterAccountError && (error.status === 401 || error.status === 403)) {
78
+ throw new OpenRouterAccountError('This is not a management key: OpenRouter refused it for account data. Create one under Settings → Management keys.', error.status);
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /** The `/activity` rows (last 30 completed UTC days) as totals and the top models with the providers that served them. */
85
+ export function summarizeActivity(rows, top = 5) {
86
+ const models = new Map(), days = new Set();
87
+ let usage = 0, requests = 0;
88
+ for (const row of Array.isArray(rows) ? rows : []) {
89
+ if (typeof row?.model !== 'string') continue;
90
+ const cost = finite(row.usage) ?? 0, count = finite(row.requests) ?? 0;
91
+ usage += cost;
92
+ requests += count;
93
+ if (typeof row.date === 'string') days.add(row.date);
94
+ const entry = models.get(row.model) ?? { model: row.model, usage: 0, requests: 0, providers: new Map() };
95
+ entry.usage += cost;
96
+ entry.requests += count;
97
+ const name = typeof row.provider_name === 'string' && row.provider_name.length > 0 ? row.provider_name : 'unknown';
98
+ const served = entry.providers.get(name) ?? { name, usage: 0, requests: 0 };
99
+ served.usage += cost;
100
+ served.requests += count;
101
+ entry.providers.set(name, served);
102
+ models.set(row.model, entry);
103
+ }
104
+ const ranked = [...models.values()].sort((left, right) => right.usage - left.usage || right.requests - left.requests).slice(0, top)
105
+ .map(entry => ({ ...entry, providers: [...entry.providers.values()].sort((left, right) => right.usage - left.usage) }));
106
+ return { usage, requests, days: days.size, modelCount: models.size, models: ranked };
107
+ }
108
+
109
+ /**
110
+ * Everything /openrouter shows. Each section settles on its own: `{ value }`, `{ error }`,
111
+ * or undefined when the key it needs is missing.
112
+ */
113
+ export async function loadOpenRouterAccount({ apiKey, managementKey, fetch, signal } = {}) {
114
+ const options = { fetch, signal };
115
+ const section = (key, run) => key ? run().then(value => ({ value }), error => ({ error: error instanceof Error ? error.message : String(error) })) : Promise.resolve(undefined);
116
+ const [key, credits, keys, activity] = await Promise.all([
117
+ section(apiKey, async () => keyOf((await get('/key', apiKey, options))?.data)),
118
+ section(managementKey ?? apiKey, async () => {
119
+ const credits = creditsOf(await get('/credits', managementKey ?? apiKey, options));
120
+ if (!credits) throw new Error('OpenRouter returned no account credits');
121
+ return credits;
122
+ }),
123
+ section(managementKey, async () => {
124
+ const body = await get('/keys', managementKey, options);
125
+ return (Array.isArray(body?.data) ? body.data : []).map(keyOf);
126
+ }),
127
+ section(managementKey, async () => summarizeActivity((await get('/activity', managementKey, options))?.data)),
128
+ ]);
129
+ return { hasApiKey: Boolean(apiKey), hasManagementKey: Boolean(managementKey), key, credits, keys, activity };
130
+ }
131
+
132
+ const money = value => Number.isFinite(value) ? `$${value.toFixed(2)}` : '$--';
133
+ const limitText = key => key.limit === undefined ? 'no limit' : `limit ${money(key.limit)}, ${money(key.limitRemaining)} left`;
134
+
135
+ /**
136
+ * The panel's lines for a loaded account.
137
+ * @returns `{ text, tone }` rows; tone is `title`, `value`, `dim` or `error`.
138
+ */
139
+ export function openRouterAccountLines(account, { maxKeys = 5 } = {}) {
140
+ const lines = [];
141
+ const push = (text, tone = 'value') => lines.push({ text, tone });
142
+ if (!account.hasApiKey) push('No OpenRouter API key: run /login openrouter.', 'error');
143
+ if (account.credits?.value) {
144
+ const { remaining, total, used } = account.credits.value;
145
+ push(`Account balance ${money(remaining)} · credits ${money(total)} · used ${money(used)}`, 'title');
146
+ } else if (account.credits?.error) push(`Account balance unavailable: ${account.credits.error}`, 'error');
147
+ else push('Account balance $--', 'dim');
148
+ if (account.key?.value) {
149
+ const key = account.key.value;
150
+ push(`This key ${key.label ?? 'unnamed'} · ${limitText(key)}`, 'title');
151
+ push(` today ${money(key.usageDaily)} · week ${money(key.usageWeekly)} · month ${money(key.usageMonthly)}`, 'dim');
152
+ } else if (account.key?.error) push(`This key unavailable: ${account.key.error}`, 'error');
153
+ if (!account.hasManagementKey) push('API keys and 30-day spend press m to add a management key', 'dim');
154
+ if (account.keys?.value) {
155
+ const keys = [...account.keys.value].sort((left, right) => (right.usageMonthly ?? 0) - (left.usageMonthly ?? 0));
156
+ push(`API keys (${keys.length})`, 'title');
157
+ for (const key of keys.slice(0, maxKeys)) {
158
+ const current = account.key?.value?.label !== undefined && key.label === account.key.value.label;
159
+ push(` ${key.name ?? key.label ?? 'unnamed'}${current ? ' (this key)' : ''}${key.disabled ? ' · disabled' : ''} · today ${money(key.usageDaily)} · month ${money(key.usageMonthly)} · ${limitText(key)}`, key.disabled ? 'dim' : 'value');
160
+ }
161
+ if (keys.length > maxKeys) push(` +${keys.length - maxKeys} more`, 'dim');
162
+ } else if (account.keys?.error) push(`API keys unavailable: ${account.keys.error}`, 'error');
163
+ if (account.activity?.value) {
164
+ const activity = account.activity.value;
165
+ push(`Last 30 days ${money(activity.usage)} · ${activity.requests} requests · ${activity.modelCount} models`, 'title');
166
+ for (const model of activity.models) {
167
+ push(` ${model.model} · ${money(model.usage)} · ${model.requests} req · ${model.providers.map(provider => `${provider.name} ${money(provider.usage)}`).join(', ')}`);
168
+ }
169
+ } else if (account.activity?.error) push(`Activity unavailable: ${account.activity.error}`, 'error');
170
+ return lines;
171
+ }
@@ -4,14 +4,18 @@ import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
4
4
  import { SessionCards } from './manager.mjs';
5
5
  import { TOPIC_PROMPT } from './content.mjs';
6
6
  import { chargeTo } from '../session-metrics/attribution.mjs';
7
+ import { effortFor } from '../providers/effort.mjs';
7
8
  export const name = 'dscode-session-cards';
8
9
  export const inject = ['sessions', 'llm'];
9
10
  export function apply(ctx, config = {}) {
10
11
  const home = process.env.DSH_HOME ?? process.env.DSCODE_HOME ?? join(homedir(), '.local/share/dscode-hub');
11
12
  const cards = new SessionCards({ root: join(home, 'session-cards'), config, generate: async (input, route, signal, sessionId) => {
12
13
  const assembler = new BlockAssembler(); let finished = false, usage;
14
+ // Cards need little reasoning whatever the session runs at: low, or the nearest level the model offers.
15
+ const { reasoningEffort: _sessionEffort, ...base } = route ?? {};
16
+ const reasoningEffort = await effortFor(ctx.llm, base, 'low', signal);
13
17
  await chargeTo(sessionId, 'session-card', async () => {
14
- for await (const chunk of ctx.llm.stream({ ...route, reasoningEffort: 'low', system: TOPIC_PROMPT,
18
+ for await (const chunk of ctx.llm.stream({ ...base, ...(reasoningEffort ? { reasoningEffort } : {}), system: TOPIC_PROMPT,
15
19
  messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
16
20
  maxTokens: 2000, signal })) {
17
21
  signal.throwIfAborted(); assembler.push(chunk);
@@ -1,10 +1,18 @@
1
+ import { OPENROUTER_API, parseOpenRouterCredits, parseOpenRouterKeyRemaining } from '../providers/openrouter-account.mjs';
2
+
3
+ export { parseOpenRouterCredits, parseOpenRouterKeyRemaining };
4
+
1
5
  // Remaining provider balance, refreshed on a long cache. DeepSeek's response
2
6
  // also carries the trusted clock: its `Date` header anchors peak/off-peak pricing
3
- // without a second network call. OpenRouter has no peak window, so only its
4
- // remaining credits are read.
7
+ // without a second network call. OpenRouter has no peak window. Its account
8
+ // credits (`/credits`) are documented as management-only but are served to
9
+ // inference keys too; a key refused them falls back to its own remaining limit (`/key`).
5
10
  const SOURCES = {
6
- 'deepseek-official': { url: 'https://api.deepseek.com/user/balance', env: 'DEEPSEEK_API_KEY', parse: body => parseBalance(body), clock: true },
7
- openrouter: { url: 'https://openrouter.ai/api/v1/credits', env: 'OPENROUTER_API_KEY', parse: body => parseOpenRouterCredits(body), clock: false },
11
+ 'deepseek-official': { env: 'DEEPSEEK_API_KEY', clock: true, requests: ({ key }) => [{ url: 'https://api.deepseek.com/user/balance', key, parse: body => parseBalance(body) }] },
12
+ openrouter: { env: 'OPENROUTER_API_KEY', managementEnv: 'OPENROUTER_MANAGEMENT_KEY', clock: false, requests: ({ key, managementKey }) => [
13
+ { url: `${OPENROUTER_API}/credits`, key: managementKey ?? key, parse: parseOpenRouterCredits },
14
+ { url: `${OPENROUTER_API}/key`, key, parse: parseOpenRouterKeyRemaining },
15
+ ] },
8
16
  };
9
17
  export const BALANCE_PROVIDERS = Object.freeze(Object.keys(SOURCES));
10
18
  const CACHE_MS = 5 * 60 * 1000;
@@ -22,14 +30,6 @@ export function parseBalance(body) {
22
30
  return body?.is_available === false ? null : total;
23
31
  }
24
32
 
25
- /** Remaining USD credits from an OpenRouter `/credits` body: purchased minus used. */
26
- export function parseOpenRouterCredits(body) {
27
- const credits = Number(body?.data?.total_credits), used = Number(body?.data?.total_usage);
28
- if (body?.data?.total_credits == null || body?.data?.total_usage == null) return null;
29
- if (!Number.isFinite(credits) || !Number.isFinite(used) || credits < 0 || used < 0) return null;
30
- return Math.max(0, credits - used);
31
- }
32
-
33
33
  export function balanceNow(provider = 'deepseek-official') { return snapshotOf(provider).balance; }
34
34
  /** Clock anchored to the last DeepSeek balance response's `Date` header, else the local one. */
35
35
  export function trustedNow() {
@@ -43,29 +43,39 @@ export async function refreshBalance(options = {}) {
43
43
  const source = SOURCES[provider];
44
44
  if (!source) return null;
45
45
  const snapshot = snapshotOf(provider);
46
+ const key = options.key ?? process.env[source.env];
47
+ const managementKey = options.managementKey ?? (source.managementEnv ? process.env[source.managementEnv] : undefined);
48
+ const requests = source.requests({ key, managementKey }).filter(request => request.key);
49
+ const mode = managementKey ? 'management' : 'key';
46
50
  const now = Date.now();
47
- if (snapshot.pending || now - snapshot.fetchedAt < CACHE_MS) return snapshot.balance;
51
+ // A newly added (or removed) management key changes the source; it is read at once.
52
+ if (snapshot.pending || snapshot.mode === mode && now - snapshot.fetchedAt < CACHE_MS) return snapshot.balance;
48
53
  const retrySoon = () => Date.now() - (CACHE_MS - RETRY_MS);
49
- const key = options.key ?? process.env[source.env];
50
- if (!key) { snapshots.set(provider, { ...snapshot, fetchedAt: retrySoon(), pending: false }); return snapshot.balance; }
54
+ if (requests.length === 0) { snapshots.set(provider, { ...snapshot, mode, fetchedAt: retrySoon(), pending: false }); return snapshot.balance; }
51
55
  const fetchImpl = options.fetch ?? globalThis.fetch;
52
56
  if (typeof fetchImpl !== 'function') return snapshot.balance;
53
57
  snapshots.set(provider, { ...snapshot, pending: true });
54
58
  try {
55
- const response = await fetchImpl(source.url, { headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' } });
59
+ let response, body, request;
60
+ for (request of requests) {
61
+ response = await fetchImpl(request.url, { headers: { Authorization: `Bearer ${request.key}`, Accept: 'application/json' } });
62
+ body = await response.json();
63
+ // A key refused this reading falls through to the next source.
64
+ if (response.ok || (response.status !== 401 && response.status !== 403)) break;
65
+ }
56
66
  const header = source.clock ? response.headers?.get?.('date') : null;
57
67
  const anchor = header ? Date.parse(header) : NaN;
58
- const body = await response.json();
59
- const parsed = response.ok ? source.parse(body) : null;
68
+ const parsed = response.ok ? request.parse(body) : null;
60
69
  if (Number.isFinite(anchor)) clock = { anchor, skewMs: anchor - Date.now() };
61
70
  snapshots.set(provider, {
71
+ mode,
62
72
  balance: parsed === null && response.ok ? null : parsed ?? snapshot.balance,
63
73
  fetchedAt: parsed === null ? retrySoon() : Date.now(),
64
74
  pending: false,
65
75
  });
66
76
  } catch {
67
77
  // A transient failure keeps the last known balance and retries sooner.
68
- snapshots.set(provider, { ...snapshot, fetchedAt: retrySoon(), pending: false });
78
+ snapshots.set(provider, { ...snapshot, mode, fetchedAt: retrySoon(), pending: false });
69
79
  }
70
80
  return snapshotOf(provider).balance;
71
81
  }
@@ -3,6 +3,8 @@ import { appendMetric } from './store.mjs';
3
3
  import { estimateCost, priceVersionFor } from './pricing.mjs';
4
4
  import { setMetricSource } from './view.mjs';
5
5
  import { BALANCE_PROVIDERS, refreshBalance } from './balance.mjs';
6
+ import { refreshOpenRouterModels } from '../openrouter/models.mjs';
7
+ import { REPLAY_KIND } from '../openrouter/wire.mjs';
6
8
  import { providerSpec } from '../providers/catalog.mjs';
7
9
  import { createWindowRate } from './rate.mjs';
8
10
  import { currentCharge } from './attribution.mjs';
@@ -33,10 +35,17 @@ export function apply(ctx) {
33
35
  const credentials = ctx.get?.('credentials');
34
36
  const refresh = () => Promise.all(BALANCE_PROVIDERS.map(async provider => {
35
37
  try {
36
- const ref = providerSpec(provider).credentialRef;
37
- const resolved = await credentials?.resolve?.(ref);
38
- const key = typeof resolved === 'string' ? resolved : resolved?.value;
39
- await refreshBalance({ provider, key: key ?? process.env[ref] });
38
+ const spec = providerSpec(provider);
39
+ const secret = async ref => {
40
+ if (!ref) return undefined;
41
+ const resolved = await credentials?.resolve?.(ref);
42
+ return (typeof resolved === 'string' ? resolved : resolved?.value) || process.env[ref] || undefined;
43
+ };
44
+ // An OpenRouter management key, when stored, turns the balance into the account's credits.
45
+ const [key, managementKey] = await Promise.all([secret(spec.credentialRef), secret(spec.managementRef)]);
46
+ await refreshBalance({ provider, key, managementKey });
47
+ // The listing needs no key, but only a user with an OpenRouter key uses it; the adapter waits for it before its first call.
48
+ if (provider === 'openrouter' && key) await refreshOpenRouterModels({ home });
40
49
  } catch {
41
50
  /* balance stays unknown */
42
51
  }
@@ -61,19 +70,23 @@ export function apply(ctx) {
61
70
  }
62
71
  const save = entry => { for (const recipient of recipients) record(recipient, { ...entry, sessionId }); };
63
72
  save({ kind: 'start', id, time, provider: options.provider, model: options.model, purpose });
64
- let usage, firstTokenTime;
73
+ let usage, firstTokenTime, billed;
65
74
  const liveSession = purpose === 'agent' ? ctx.agents.get(sessionId)?.session : undefined;
66
75
  try {
67
76
  for await (const chunk of next()) {
68
77
  if (chunk.type === 'usage') usage = chunk.usage;
69
78
  else if (firstTokenTime === undefined && OUTPUT_CHUNKS.has(chunk.type)) firstTokenTime = Date.now();
79
+ // OpenRouter reports what it charged; the finish of its response carries it.
80
+ if (chunk.type === 'finish' && chunk.replayState?.response?.kind === REPLAY_KIND && Number.isFinite(chunk.replayState.response.cost)) billed = chunk.replayState.response.cost;
70
81
  if (liveSession) liveRate.add(liveSession, chunk);
71
82
  yield chunk;
72
83
  }
73
84
  } finally {
74
85
  if (liveSession) liveRate.calibrate(liveSession, usage?.outputTokens);
75
86
  // `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) });
87
+ save({ kind: 'end', id, time, endTime: Date.now(), ...(firstTokenTime === undefined ? {} : { firstTokenTime }), usage: usage ?? null, ...(billed === undefined
88
+ ? { cost: estimateCost(options.provider, options.model, usage, time), priceVersion: priceVersionFor(options.provider, options.model) }
89
+ : { cost: billed, priceVersion: 'openrouter-billed' }) });
77
90
  }
78
91
  });
79
92
  }
@@ -1,10 +1,12 @@
1
+ import { openRouterPriceVersion, openRouterRates } from '../openrouter/models.mjs';
2
+
1
3
  // USD per million tokens. Snapshot of the official page opened 2026-09-11.
2
4
  // https://api-docs.deepseek.com/quick_start/pricing/
3
5
  export const PRICE_SOURCE = 'https://api-docs.deepseek.com/quick_start/pricing/';
4
6
  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].
7
+ // OpenRouter calls carry their billed cost; an unbilled one is estimated from the live model listing (plugins/openrouter/models.mjs).
8
+ // Until that table loads, the DeepSeek models keep these list prices from the pinned
9
+ // pi-ai 0.85.1 catalog. OpenRouter bills no peak window. [cache read, input, output].
8
10
  export const OPENROUTER_PRICE_VERSION = 'openrouter-pi-ai-0.85.1';
9
11
  const OPENROUTER_PRICES = {
10
12
  'deepseek/deepseek-v4-flash': [0.017052, 0.08526, 0.17052],
@@ -12,28 +14,57 @@ const OPENROUTER_PRICES = {
12
14
  'deepseek/deepseek-v4-flash-vision-exp': [0.007, 0.22, 0.66],
13
15
  };
14
16
 
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;
17
+ /** The price table a ledger entry for this route is estimated with. */
18
+ export function priceVersionFor(provider, model) {
19
+ if (provider !== 'openrouter') return PRICE_VERSION;
20
+ return model !== undefined && openRouterRates(model) !== undefined ? openRouterPriceVersion() : OPENROUTER_PRICE_VERSION;
18
21
  }
19
22
 
23
+ const promptTokens = usage => (usage.inputTokens ?? 0) + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
24
+
20
25
  export function estimateCost(provider, model, usage, time) {
21
26
  if (!usage || !Number.isFinite(time)) return null;
22
- if (provider === 'openrouter') return OPENROUTER_PRICES[model] ? charge(usage, OPENROUTER_PRICES[model]) : null;
27
+ if (provider === 'openrouter') {
28
+ // A model that lists no cache-read or cache-write price bills that input at the input rate.
29
+ const live = openRouterRates(model, promptTokens(usage));
30
+ if (live) return charge(usage, [live.cacheRead ?? live.input, live.input, live.output], live.cacheWrite ?? live.input);
31
+ return OPENROUTER_PRICES[model] ? charge(usage, OPENROUTER_PRICES[model]) : null;
32
+ }
23
33
  if (provider !== 'deepseek-official') return null;
34
+ const rates = deepSeekRates(model, time);
35
+ if (!rates) return null;
36
+ const cost = charge(usage, rates);
37
+ return cost === null ? null : cost * (isPeak(time) ? 2 : 1);
38
+ }
39
+
40
+ /** DeepSeek list prices [cache read, input, output] at `time`, before the peak multiplier. */
41
+ function deepSeekRates(model, time) {
24
42
  // Earlier requests require an older price table; never back-price them at today's rate.
25
- if (time < Date.UTC(2026, 8, 11)) return null;
43
+ if (time < Date.UTC(2026, 8, 11)) return undefined;
26
44
  const flash = ['deepseek-flash', 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp'].includes(model)
27
45
  || model === 'deepseek-v4-pro' && time >= Date.UTC(2026, 8, 14, 4);
28
- if (!flash && model !== 'deepseek-v4-pro') return null;
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);
46
+ if (!flash && model !== 'deepseek-v4-pro') return undefined;
47
+ return flash ? [0.003, 0.15, 0.6] : [0.022, 0.66, 1.98];
48
+ }
49
+
50
+ /**
51
+ * Cache-read price over input price for a route, or undefined when the route is unpriced.
52
+ * A model that lists no cache-read price bills cached input at the input rate (1).
53
+ */
54
+ export function cacheReadRatio(provider, model, time = Date.now()) {
55
+ let rates;
56
+ if (provider === 'openrouter') {
57
+ const live = openRouterRates(model);
58
+ rates = live ? [live.cacheRead ?? live.input, live.input] : OPENROUTER_PRICES[model];
59
+ } else if (provider === 'deepseek-official') rates = deepSeekRates(model, time);
60
+ return rates && rates[1] > 0 ? rates[0] / rates[1] : undefined;
31
61
  }
32
62
 
33
- function charge(usage, [read, input, output]) {
63
+ /** Cost in USD; cache writes need a write price, or the call stays unpriced. */
64
+ function charge(usage, [read, input, output], write) {
34
65
  const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens ?? 0, usage.cacheWriteTokens ?? 0];
35
- if (!values.every(n => Number.isFinite(n) && n >= 0) || values[3] !== 0) return null;
36
- return (values[0] * input + values[1] * output + values[2] * read) / 1e6;
66
+ if (!values.every(n => Number.isFinite(n) && n >= 0) || values[3] !== 0 && !Number.isFinite(write)) return null;
67
+ return (values[0] * input + values[1] * output + values[2] * read + values[3] * (write ?? 0)) / 1e6;
37
68
  }
38
69
 
39
70
  /**
@@ -34,8 +34,8 @@ export function summarize(rows, events = [], corrupt = false) {
34
34
  const u = row.usage;
35
35
  if (!u || !Number.isFinite(u.inputTokens) || !Number.isFinite(u.outputTokens)) { cacheUnknown = true; continue; }
36
36
  const total = u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0);
37
+ // OpenRouter reports cache reads only when there are some: a missing count is zero, not unknown.
37
38
  input += total; hit += u.cacheReadTokens ?? 0;
38
- if (total > 0 && u.cacheReadTokens === undefined) cacheUnknown = true;
39
39
  }
40
40
  return { cost, unknown, calls, pending, cache: input > 0 && !cacheUnknown ? Math.min(100, hit / input * 100) : null };
41
41
  }
@@ -5,6 +5,7 @@ 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
7
  import { chargeTo } from '../session-metrics/attribution.mjs';
8
+ import { effortFor } from '../providers/effort.mjs';
8
9
  const L = (key, params) => t(readLanguage(), key, params);
9
10
 
10
11
  const MAX_LOG_BYTES = 1024 * 1024;
@@ -127,8 +128,9 @@ export async function analyzeDoctorEvidence(ctx, evidence, route, signal, { mode
127
128
  const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(45000)]);
128
129
  try {
129
130
  let finished = false;
131
+ const reasoningEffort = await effortFor(ctx.llm, route, 'low', deadline);
130
132
  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,
133
+ for await (const chunk of ctx.llm.stream({ provider: route.provider, model: route.model, ...(reasoningEffort ? { reasoningEffort } : {}), maxTokens: 4096, system: SYSTEM,
132
134
  messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(evidence) }], source: { kind: 'plugin', plugin: 'dscode-doctor' } })], signal: deadline })) {
133
135
  deadline.throwIfAborted(); assembler.push(chunk);
134
136
  if (chunk.type === 'finish') finished = true;
@@ -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' ? ` (${route.provider === 'openrouter' ? 'OpenRouter wire: xhigh' : 'DeepSeek wire: max'}; collaboration enabled)` : ''}`,
95
+ `Effort: ${route?.reasoningEffort ?? 'model default'}${route?.reasoningEffort === 'ultra' ? ` (${route.provider === 'deepseek-official' ? 'DeepSeek wire: max' : route.model?.startsWith('deepseek/') ? 'OpenRouter wire: xhigh' : "sent as the model's max level"}; 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`,
@@ -16,22 +16,6 @@ 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
-
35
19
  export function ultraRequest(options, messages) {
36
20
  if (options.reasoningEffort !== 'ultra' || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return messages;
37
21
  const copy = messages.map(m => ({ ...m }));
@@ -180,7 +180,7 @@
180
180
  toolResultPruner: true
181
181
  config:
182
182
  - id: compaction-basic
183
- name: '@deepseek-ai/dsh-compaction-basic'
183
+ name: '@toddzheng024/dscode-bundle/compaction-basic'
184
184
 
185
185
  - id: command-compact
186
186
  name: '@deepseek-ai/dsh-command-compact'