@toddzheng024/dscode-bundle 0.7.5 → 0.7.6
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/package.json +1 -1
- package/plugins/code-review/index.mjs +9 -4
- package/plugins/dscode/index.mjs +4 -10
- package/plugins/exec/cli.mjs +3 -2
- package/plugins/exec/index.mjs +6 -1
- package/plugins/memory/index.mjs +7 -3
- package/plugins/providers/catalog.mjs +59 -7
- package/plugins/providers/effort.mjs +35 -0
- package/plugins/session-cards/index.mjs +5 -1
- package/plugins/session-metrics/index.mjs +4 -1
- package/plugins/session-metrics/openrouter-prices.mjs +96 -0
- package/plugins/session-metrics/pricing.mjs +21 -10
- package/plugins/session-metrics/view.mjs +1 -1
- package/plugins/tui-tools/doctor.mjs +3 -1
- package/plugins/tui-tools/index.mjs +1 -1
- package/plugins/ultra/policy.mjs +4 -4
- package/vendor/deepseek/index.js +1 -1
- package/vendor/pi-ai/index.js +2 -3
- package/vendor/subagent/index.js +3 -3
- package/vendor/tui/dscode-providers/catalog.mjs +59 -7
- package/vendor/tui/dscode-providers/effort.mjs +35 -0
- package/vendor/tui/index.mjs +44 -7
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { collectReviewDiff, parseReviewCommand, isGitAvailableSync, isGitWorkspa
|
|
|
5
5
|
import { baselineStore } from './baseline.mjs';
|
|
6
6
|
import { redact } from '../auto-review/policy.mjs';
|
|
7
7
|
import { chargeTo } from '../session-metrics/attribution.mjs';
|
|
8
|
+
import { effortFor } from '../providers/effort.mjs';
|
|
8
9
|
|
|
9
10
|
export const name = 'dscode-code-review';
|
|
10
11
|
export const inject = ['tools', 'commands', 'llm', 'systemPrompt'];
|
|
@@ -47,8 +48,11 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
|
|
|
47
48
|
const diffHash = createHash('sha256').update(JSON.stringify({ diff, task, label, model: route.model })).digest('hex').slice(0, 16);
|
|
48
49
|
const prior = results.get(agent);
|
|
49
50
|
if (prior?.diffHash === diffHash) return { ...prior.result, cached: true };
|
|
50
|
-
|
|
51
|
+
// A reasoning reviewer writing a long report needs minutes, not seconds; the caller's signal still cancels at once.
|
|
52
|
+
const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(10 * 60 * 1000)]);
|
|
51
53
|
const request = { task, scope: label, diff: redact(diff) };
|
|
54
|
+
// Ultra is the session's collaboration mode, not a reviewer level: review at high, or the nearest level the model offers.
|
|
55
|
+
const reasoningEffort = route.reasoningEffort === 'ultra' ? await effortFor(ctx.llm, route, 'high', deadline) : route.reasoningEffort;
|
|
52
56
|
// One model attempt: returns the assembler plus whether the stream delivered a finish chunk.
|
|
53
57
|
// Charged to this session's ledger; the request itself carries no sessionId.
|
|
54
58
|
const attempt = () => chargeTo(agent.session.id, 'review', async () => {
|
|
@@ -56,8 +60,9 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
|
|
|
56
60
|
const operation = (async () => {
|
|
57
61
|
let finished = false;
|
|
58
62
|
for await (const chunk of ctx.llm.stream({
|
|
59
|
-
provider: route.provider, model: route.model, reasoningEffort
|
|
60
|
-
|
|
63
|
+
provider: route.provider, model: route.model, reasoningEffort, purpose: 'review',
|
|
64
|
+
// No output cap of our own: the model's route default applies (256k on DeepSeek, the catalog limit on OpenRouter).
|
|
65
|
+
system: POLICY, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(request) }], source: { kind: 'plugin', plugin: name } })], signal: deadline,
|
|
61
66
|
})) {
|
|
62
67
|
deadline.throwIfAborted();
|
|
63
68
|
assembler.push(chunk);
|
|
@@ -92,7 +97,7 @@ export async function independentReview(ctx, agent, options = {}, signal, collec
|
|
|
92
97
|
if (blocks.some(block => !['text', 'reasoning'].includes(block.type))) throw Error('Code reviewer returned unexpected output.');
|
|
93
98
|
const report = blocks.filter(block => block.type === 'text').map(block => block.text).join('').trim();
|
|
94
99
|
if (!report) throw Error(truncated ? 'Code reviewer ran out of output tokens before writing the report; narrow the diff with path and retry.' : 'Code reviewer returned an empty report.');
|
|
95
|
-
const result = { status: omitted.length || truncated ? 'partial' : 'reviewed', scope: label, report: `${redact(report).slice(0,
|
|
100
|
+
const result = { status: omitted.length || truncated ? 'partial' : 'reviewed', scope: label, report: `${redact(report).slice(0, 64000)}${omitted.length ? `\n\nReview incomplete: ${omitted.length} file(s) were omitted or binary and could not be inspected from the diff.` : ''}${truncated ? '\n\nReview incomplete: the reviewer hit its output limit; later findings may be missing. Narrow the diff with path for a complete pass.' : ''}`, diffHash, usage: assembler.usage ?? null };
|
|
96
101
|
results.set(agent, { diffHash, result });
|
|
97
102
|
return result;
|
|
98
103
|
}
|
package/plugins/dscode/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ export const inject = ['systemPrompt', 'tools', 'agents', 'commands', 'terminals
|
|
|
3
3
|
export const SHELL_POLICY = `Use bash as the persistent shell for reading, searching and modifying files. Prefer rg/rg --files, sed and standard CLI tools. Use apply_patch with a standard unified diff on stdin (git apply format, a/ and b/ paths); apply_patch --check validates before writing. It is not the *** Begin Patch format. Quote heredoc delimiters to avoid shell interpolation.
|
|
4
4
|
Each agent has its own persistent shell, initially in the session workspace. cd, exported variables, functions and background jobs persist only while this shell lives. Timeout, cancellation, exit, /shell reset and process restart discard shell state; resume restores conversation, not an OS process. Never assume an environment from a past session still exists. Inspect pwd when paths matter. Keep long-running processes controlled and clean them up when done.
|
|
5
5
|
Normal bash remains confined by the active sandbox. After a genuine sandbox denial, shell_retry provides a fresh, one-shot shell with the existing approval/escalation mechanism. Set an explicit absolute workdir and reconstruct needed non-secret setup; it does not inherit the persistent shell's cd, exports, functions or jobs. Use existing credential-aware CLIs; never paste secrets into arguments. Approval rejection is final for that action; do not work around it.
|
|
6
|
-
Delegation to child agents (subagent, subagent_fork)
|
|
6
|
+
Delegation to child agents (subagent, subagent_fork) is available at every effort. Below ultra, delegation is the exception: do the work in this agent by default. Delegate only a substantial, independent part of the task whose parallel work clearly shortens completion, or a broad read-only investigation that would otherwise crowd this context; never delegate a bounded edit, a single-file change, a quick lookup, one test run or a routine review. Below ultra run at most one child at a time, give it a bounded objective, choose the lowest reasoning_effort the model offers that fits, and verify and integrate its result yourself. Ultra adds its own delegation guidance to the request; the preset allows one delegation level and the runtime caps a parent at three concurrently running children.`;
|
|
7
7
|
|
|
8
8
|
/** Child names: 1-10 characters, letters/digits/underscores, starting and ending with a letter. */
|
|
9
9
|
export const CHILD_NAME = /^[A-Za-z](?:[A-Za-z0-9_]{0,8}[A-Za-z])?$/;
|
|
@@ -46,19 +46,13 @@ export function apply(ctx) {
|
|
|
46
46
|
if (exec.name === 'send_message' || exec.name === 'interrupt_agent') exec.arguments.agent_id = resolveAgentPath(owner, exec.arguments.agent_id);
|
|
47
47
|
if (exec.name === 'interrupt_agent') return next();
|
|
48
48
|
if (owner.session.header.origin === 'subagent' && DELEGATION_TOOLS.includes(exec.name)) throw new Error('Child agents cannot delegate again. Complete the assigned work and report to the parent.');
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
if (DELEGATION_TOOLS.includes(exec.name)) throw new Error('Child-agent work requires Ultra. Select /effort ultra before delegating.');
|
|
52
|
-
const target = ctx.agents.get(exec.arguments.agent_id);
|
|
53
|
-
if (target?.status !== 'running' && target?.session.header.parentSession === owner.session.id) throw new Error('Waking a child agent requires Ultra. Select /effort ultra first.');
|
|
54
|
-
return next();
|
|
55
|
-
}
|
|
56
|
-
if (['workflow', 'ralph'].includes(exec.name)) throw new Error('Use capped subagent/subagent_fork delegation in Ultra; workflow and ralph are unavailable in dscode.');
|
|
49
|
+
// Delegation is open at every effort: the shell policy keeps it rare below Ultra, and the cap below applies throughout.
|
|
50
|
+
if (['workflow', 'ralph'].includes(exec.name)) throw new Error('Use capped subagent/subagent_fork delegation; workflow and ralph are unavailable in dscode.');
|
|
57
51
|
const target = exec.name === 'send_message' ? ctx.agents.get(exec.arguments.agent_id) : undefined;
|
|
58
52
|
if (exec.name === 'send_message' && (exec.arguments.agent_id === owner.session.header.parentSession || target?.status === 'running')) return next();
|
|
59
53
|
const id = owner.session.id;
|
|
60
54
|
const running = ctx.agents.list().filter(a => a.session.header.origin === 'subagent' && a.session.header.parentSession === id && a.status === 'running').length;
|
|
61
|
-
if (running + (reservations.get(id) ?? 0) >= 3) throw new Error('
|
|
55
|
+
if (running + (reservations.get(id) ?? 0) >= 3) throw new Error('Concurrent child limit reached (3). Wait for a child to settle, then delegate or send more work.');
|
|
62
56
|
const childName = exec.name === 'send_message' ? undefined : exec.arguments.name;
|
|
63
57
|
if (childName !== undefined) {
|
|
64
58
|
if (typeof childName !== 'string' || !CHILD_NAME.test(childName)) throw new Error(CHILD_NAME_RULE);
|
package/plugins/exec/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ The prompt is read from stdin when omitted or given as "-".
|
|
|
8
8
|
Options:
|
|
9
9
|
--cwd DIR workspace for the agent (default: current directory)
|
|
10
10
|
--model PROVIDER/ID model route (default: the saved default model)
|
|
11
|
-
--effort LEVEL reasoning effort: low, high, max or ultra
|
|
11
|
+
--effort LEVEL reasoning effort the model offers: off, minimal, low, medium, high, xhigh, max or ultra
|
|
12
12
|
--permission PRESET permission preset: auto, ask, workspace-write, read-only, danger-full-access
|
|
13
13
|
--approve-all answer every approval request with allow (no human is present)
|
|
14
14
|
--resume SESSION_ID continue an existing session instead of starting a new one
|
|
@@ -44,7 +44,8 @@ export function parseExecArgs(argv) {
|
|
|
44
44
|
words.push(arg);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// Only the level name is checked here; the Host checks it against the model. This file ships alone in the launcher, so the names are inlined.
|
|
48
|
+
if (options.effort !== undefined && !['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(options.effort)) throw new Error('--effort expects off, minimal, low, medium, high, xhigh, max or ultra');
|
|
48
49
|
if (options.model !== undefined && !/^[^/]+\/.+$/.test(options.model)) throw new Error('--model expects provider/model');
|
|
49
50
|
options.prompt = words.join(' ');
|
|
50
51
|
return options;
|
package/plugins/exec/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
6
6
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
7
7
|
|
|
8
8
|
export const name = 'dscode-exec';
|
|
9
|
-
export const inject = ['agents', 'agentPresets', 'agentDefaultModel', 'permissionPresets'];
|
|
9
|
+
export const inject = ['agents', 'agentPresets', 'agentDefaultModel', 'permissionPresets', 'llm'];
|
|
10
10
|
|
|
11
11
|
export function apply(ctx) {
|
|
12
12
|
void run(ctx).catch(error => { process.stderr.write(`dscode exec: ${error.message}\n`); ctx.get('appExit')(1); });
|
|
@@ -39,6 +39,11 @@ async function run(ctx) {
|
|
|
39
39
|
const selection = ctx.agentDefaultModel.currentSelection();
|
|
40
40
|
const [provider, model] = options.model ? splitRoute(options.model) : [selection.provider, selection.model];
|
|
41
41
|
const effort = options.effort ?? selection.reasoningEffort;
|
|
42
|
+
// Levels belong to the model: refuse one it does not offer before the turn, not at its first request.
|
|
43
|
+
if (options.effort) {
|
|
44
|
+
const offered = (await ctx.llm.resolveModelInfo(provider, model)).reasoning?.efforts.map(level => level.id) ?? [];
|
|
45
|
+
if (!offered.includes(options.effort)) throw new Error(`--effort ${options.effort} is not offered by ${provider}/${model}${offered.length ? `; it offers ${offered.join(', ')}` : ', which has no reasoning levels'}`);
|
|
46
|
+
}
|
|
42
47
|
const agentOptions = { provider, model, ...(effort ? { reasoningEffort: effort } : {}) };
|
|
43
48
|
const setup = async agentCtx => { await ctx.agentPresets.mount(agentCtx, 'dscode'); };
|
|
44
49
|
const handle = options.resume
|
package/plugins/memory/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
6
6
|
import { MemoryStore } from './store.mjs';
|
|
7
7
|
import { defaults, runPipeline } from './pipeline.mjs';
|
|
8
8
|
import { chargeTo } from '../session-metrics/attribution.mjs';
|
|
9
|
+
import { EFFORT_LEVELS, effortFor } from '../providers/effort.mjs';
|
|
9
10
|
|
|
10
11
|
export const name = 'dscode-memory';
|
|
11
12
|
export const inject = ['llm', 'sessions', 'sessionPersistence', 'systemPrompt', 'tools', 'commands'];
|
|
@@ -20,7 +21,7 @@ export function resolveConfig(options = {}) {
|
|
|
20
21
|
for (const key of ['maxPerRun', 'maxCandidates', 'maxInputChars', 'maxConsolidationChars', 'timeoutMs']) {
|
|
21
22
|
if (!Number.isSafeInteger(config[key])) throw Error(`Invalid memory ${key}: expected a safe integer`);
|
|
22
23
|
}
|
|
23
|
-
for (const key of ['extractEffort', 'consolidationEffort']) if (!
|
|
24
|
+
for (const key of ['extractEffort', 'consolidationEffort']) if (!EFFORT_LEVELS.includes(config[key])) throw Error(`Invalid memory ${key}`);
|
|
24
25
|
if (!!config.provider !== !!config.model) throw Error('Memory provider and model must be configured together');
|
|
25
26
|
if (typeof config.generate !== 'boolean' || typeof config.use !== 'boolean') throw Error('Invalid memory switches');
|
|
26
27
|
return config;
|
|
@@ -39,11 +40,14 @@ export function apply(ctx, options = {}) {
|
|
|
39
40
|
const deadline = AbortSignal.any([signal, AbortSignal.timeout(config.timeoutMs)]);
|
|
40
41
|
const assembler = new BlockAssembler();
|
|
41
42
|
let terminal = false, usage;
|
|
43
|
+
const target = { provider: config.provider ?? route.provider, model: config.model ?? route.model };
|
|
44
|
+
// The configured level, or the nearest one the memory model offers.
|
|
45
|
+
const reasoningEffort = await effortFor(ctx.llm, target, effort, deadline);
|
|
42
46
|
try {
|
|
43
47
|
// Background work is charged to the live session that scheduled it.
|
|
44
48
|
await chargeTo(live.has(lastSession) ? lastSession : undefined, 'memory', async () => {
|
|
45
49
|
for await (const chunk of ctx.llm.stream({
|
|
46
|
-
|
|
50
|
+
...target, ...(reasoningEffort ? { reasoningEffort } : {}),
|
|
47
51
|
system, messages: [createUserMessage({ content: [{ type: 'text', text: JSON.stringify(input) }], source: { kind: 'plugin', plugin: name } })],
|
|
48
52
|
maxTokens: 12000, signal: deadline,
|
|
49
53
|
})) {
|
|
@@ -58,7 +62,7 @@ export function apply(ctx, options = {}) {
|
|
|
58
62
|
const text = blocks.filter(b => b.type === 'text').map(b => b.text).join('').trim();
|
|
59
63
|
return JSON.parse(text.replace(/^```(?:json)?\s*/, '').replace(/\s*```$/, ''));
|
|
60
64
|
} finally {
|
|
61
|
-
if (!controller.signal.aborted) store.recordCall({ time: Date.now(),
|
|
65
|
+
if (!controller.signal.aborted) store.recordCall({ time: Date.now(), ...target, effort: reasoningEffort, usage: usage ?? null });
|
|
62
66
|
}
|
|
63
67
|
};
|
|
64
68
|
const schedule = (route, sessionId) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Model providers `/provider` switches between. DeepSeek's official API is the
|
|
2
|
-
// native `llm-deepseek` route; OpenRouter
|
|
3
|
-
// through pi-ai's catalog route, which the base
|
|
4
|
-
// a `llm-pi-ai:` settings section declares it.
|
|
2
|
+
// native `llm-deepseek` route; OpenRouter serves the DeepSeek models and the rest
|
|
3
|
+
// of pi-ai's OpenRouter catalog through pi-ai's catalog route, which the base
|
|
4
|
+
// composition mounts dormant until a `llm-pi-ai:` settings section declares it.
|
|
5
5
|
|
|
6
6
|
export const PROVIDERS = Object.freeze([
|
|
7
7
|
{ id: 'deepseek-official', name: 'DeepSeek', aliases: ['deepseek', 'deepseek-official', 'official'], credentialRef: 'DEEPSEEK_API_KEY', defaultModel: 'deepseek-flash' },
|
|
@@ -23,17 +23,67 @@ export const OPENROUTER_MODELS = Object.freeze([
|
|
|
23
23
|
{ id: 'deepseek/deepseek-v4-flash-vision-exp', name: 'DeepSeek V4 Flash Vision Exp', official: ['deepseek-v4-flash-vision-exp'] },
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
-
/** The `llm-pi-ai` profile `/provider openrouter` writes:
|
|
26
|
+
/** The `llm-pi-ai` profile `/provider openrouter` writes: pi-ai's whole OpenRouter catalog. */
|
|
27
27
|
export function openRouterProfile() {
|
|
28
28
|
return {
|
|
29
29
|
displayName: 'OpenRouter',
|
|
30
30
|
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
31
|
-
// Like the official route, requests default to high;
|
|
31
|
+
// Like the official route, requests default to high; a model without high uses its own default.
|
|
32
|
+
reasoning: 'high',
|
|
33
|
+
// No models list, so the route serves every catalog model; the overrides give the
|
|
34
|
+
// DeepSeek models the official detents (and /effort its detent bar).
|
|
35
|
+
modelOverrides: Object.fromEntries(OPENROUTER_MODELS.map(({ id, name }) => [id, { name, reasoningEfforts: { ...OPENROUTER_EFFORTS } }])),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The profile 0.7.3 to 0.7.5 wrote: the catalog narrowed to the three DeepSeek models. */
|
|
40
|
+
export function narrowOpenRouterProfile() {
|
|
41
|
+
return {
|
|
42
|
+
displayName: 'OpenRouter',
|
|
43
|
+
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
32
44
|
reasoning: 'high',
|
|
33
45
|
models: OPENROUTER_MODELS.map(({ id, name }) => ({ id, name, reasoningEfforts: { ...OPENROUTER_EFFORTS } })),
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
// Fields a user sets to point the route elsewhere or shape its requests. The settings
|
|
50
|
+
// service describes a profile with its resolved defaults, which fill these with empty
|
|
51
|
+
// values (`input: []`, `compat: { chatTemplateKwargs: {}, ... }`), so empty counts as unset.
|
|
52
|
+
const USER_FIELDS = ['api', 'baseURL', 'modelOverrides', 'headers', 'compat', 'thinkingBudgets', 'cacheRetention', 'transport'];
|
|
53
|
+
const empty = value => value === undefined || (Array.isArray(value) ? value.length === 0
|
|
54
|
+
: value !== null && typeof value === 'object' && Object.values(value).every(empty));
|
|
55
|
+
const sameEfforts = (left, right) => left !== null && typeof left === 'object'
|
|
56
|
+
&& Object.keys(left).length === Object.keys(right).length && Object.entries(right).every(([level, wire]) => left[level] === wire);
|
|
57
|
+
|
|
58
|
+
/** Whether a stored profile is exactly the narrow one DSCODE wrote, so replacing it discards nothing the user chose. */
|
|
59
|
+
export function isNarrowOpenRouterProfile(profile) {
|
|
60
|
+
if (profile === null || typeof profile !== 'object' || !Array.isArray(profile.models)) return false;
|
|
61
|
+
const narrow = narrowOpenRouterProfile();
|
|
62
|
+
return profile.displayName === narrow.displayName && profile.apiKeyEnv === narrow.apiKeyEnv && profile.reasoning === narrow.reasoning
|
|
63
|
+
&& USER_FIELDS.every(field => empty(profile[field]))
|
|
64
|
+
&& profile.models.length === narrow.models.length
|
|
65
|
+
&& narrow.models.every((expected, index) => {
|
|
66
|
+
const { id, name, reasoningEfforts, ...rest } = profile.models[index] ?? {};
|
|
67
|
+
return id === expected.id && name === expected.name && sameEfforts(reasoningEfforts, expected.reasoningEfforts) && Object.values(rest).every(empty);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the narrow profile earlier builds wrote with the whole-catalog one.
|
|
73
|
+
* Never declares a route and never throws: /model must open regardless.
|
|
74
|
+
* @returns whether the settings changed.
|
|
75
|
+
*/
|
|
76
|
+
export async function migrateOpenRouterProfile(settings) {
|
|
77
|
+
try {
|
|
78
|
+
const descriptor = settings?.describe?.({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
79
|
+
if (!descriptor || settings.writable !== true || !isNarrowOpenRouterProfile(descriptor.value?.providers?.openrouter)) return false;
|
|
80
|
+
await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', 'openrouter'], value: openRouterProfile() }], descriptor.revision);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
37
87
|
export function providerSpec(id) {
|
|
38
88
|
return PROVIDERS.find(provider => provider.id === id);
|
|
39
89
|
}
|
|
@@ -107,7 +157,8 @@ export function credentialState(row) {
|
|
|
107
157
|
|
|
108
158
|
/**
|
|
109
159
|
* Declare a provider's route before it is used. Only OpenRouter needs one; a
|
|
110
|
-
* profile the user already has (their own models or endpoint) is left alone
|
|
160
|
+
* profile the user already has (their own models or endpoint) is left alone,
|
|
161
|
+
* while the narrow profile earlier builds wrote is replaced.
|
|
111
162
|
* @param settings - the host settings service.
|
|
112
163
|
* @returns whether the settings changed.
|
|
113
164
|
*/
|
|
@@ -116,7 +167,8 @@ export async function ensureProviderRoute(settings, provider) {
|
|
|
116
167
|
if (typeof settings?.describe !== 'function' || typeof settings.mutate !== 'function') throw new Error('settings are unavailable; OpenRouter cannot be configured in this profile');
|
|
117
168
|
const descriptor = settings.describe({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
118
169
|
if (!descriptor) throw new Error('the OpenRouter adapter (llm-pi-ai) is not mounted in this profile');
|
|
119
|
-
|
|
170
|
+
const existing = descriptor.value?.providers?.openrouter;
|
|
171
|
+
if (existing !== undefined) return migrateOpenRouterProfile(settings);
|
|
120
172
|
if (settings.writable !== true) throw new Error('settings are read-only; OpenRouter cannot be configured here');
|
|
121
173
|
await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', 'openrouter'], value: openRouterProfile() }], descriptor.revision);
|
|
122
174
|
return true;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Reasoning levels differ per model: DeepSeek offers low/high/max, GPT models
|
|
2
|
+
// minimal through high, and many OpenRouter models none at all. Auxiliary calls
|
|
3
|
+
// name the level they would like and send the nearest one the model offers, or no
|
|
4
|
+
// effort when the model offers no levels.
|
|
5
|
+
|
|
6
|
+
/** Standard reasoning levels, lowest first. */
|
|
7
|
+
export const EFFORT_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The level to request from a model.
|
|
11
|
+
* @param offered - the model's effort ids, or `undefined` when its capability is unknown.
|
|
12
|
+
* @param wanted - the level the caller would like.
|
|
13
|
+
* @returns `wanted` when offered or when the capability is unknown; otherwise the nearest
|
|
14
|
+
* offered level at or above it, else the highest below; `undefined` when the model offers
|
|
15
|
+
* no standard level.
|
|
16
|
+
*/
|
|
17
|
+
export function chooseEffort(offered, wanted) {
|
|
18
|
+
if (wanted === undefined || offered === undefined || offered.includes(wanted)) return wanted;
|
|
19
|
+
const rank = EFFORT_LEVELS.indexOf(wanted);
|
|
20
|
+
const levels = EFFORT_LEVELS.filter(level => offered.includes(level));
|
|
21
|
+
if (rank < 0 || levels.length === 0) return undefined;
|
|
22
|
+
return levels.find(level => EFFORT_LEVELS.indexOf(level) >= rank) ?? levels.at(-1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* {@link chooseEffort} for a route, reading the model's levels from the LLM service.
|
|
27
|
+
* A service without model metadata, or a failed lookup, keeps `wanted`.
|
|
28
|
+
*/
|
|
29
|
+
export async function effortFor(llm, route, wanted, signal) {
|
|
30
|
+
if (wanted === undefined || typeof llm?.resolveModelInfo !== 'function' || !route?.provider || !route?.model) return wanted;
|
|
31
|
+
let info;
|
|
32
|
+
try { info = await llm.resolveModelInfo(route.provider, route.model, signal); }
|
|
33
|
+
catch { return wanted; }
|
|
34
|
+
return chooseEffort(info?.reasoning?.efforts?.map(effort => effort.id) ?? [], wanted);
|
|
35
|
+
}
|
|
@@ -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({ ...
|
|
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);
|
|
@@ -3,6 +3,7 @@ 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 { refreshOpenRouterPrices } from './openrouter-prices.mjs';
|
|
6
7
|
import { providerSpec } from '../providers/catalog.mjs';
|
|
7
8
|
import { createWindowRate } from './rate.mjs';
|
|
8
9
|
import { currentCharge } from './attribution.mjs';
|
|
@@ -37,6 +38,8 @@ export function apply(ctx) {
|
|
|
37
38
|
const resolved = await credentials?.resolve?.(ref);
|
|
38
39
|
const key = typeof resolved === 'string' ? resolved : resolved?.value;
|
|
39
40
|
await refreshBalance({ provider, key: key ?? process.env[ref] });
|
|
41
|
+
// OpenRouter list prices need no key, but only a session with an OpenRouter key uses them.
|
|
42
|
+
if (provider === 'openrouter' && (key ?? process.env[ref])) await refreshOpenRouterPrices({ home });
|
|
40
43
|
} catch {
|
|
41
44
|
/* balance stays unknown */
|
|
42
45
|
}
|
|
@@ -73,7 +76,7 @@ export function apply(ctx) {
|
|
|
73
76
|
} finally {
|
|
74
77
|
if (liveSession) liveRate.calibrate(liveSession, usage?.outputTokens);
|
|
75
78
|
// `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) });
|
|
79
|
+
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, options.model) });
|
|
77
80
|
}
|
|
78
81
|
});
|
|
79
82
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// OpenRouter list prices for every model it serves, read from its public model
|
|
5
|
+
// listing (no key needed) and kept for a day in DSH_HOME, so any OpenRouter model
|
|
6
|
+
// can be priced. The listing quotes USD per token; the table keeps USD per million.
|
|
7
|
+
export const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
8
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const RETRY_MS = 10 * 60 * 1000;
|
|
10
|
+
const FILE = 'openrouter-prices.json';
|
|
11
|
+
let table = { fetchedAt: 0, models: {} };
|
|
12
|
+
let attemptedAt = 0, pending;
|
|
13
|
+
|
|
14
|
+
const perMillion = value => {
|
|
15
|
+
const number = Number(value);
|
|
16
|
+
return value != null && value !== '' && Number.isFinite(number) && number >= 0 ? number * 1e6 : undefined;
|
|
17
|
+
};
|
|
18
|
+
const ratesOf = pricing => {
|
|
19
|
+
const input = perMillion(pricing?.prompt), output = perMillion(pricing?.completion);
|
|
20
|
+
if (input === undefined || output === undefined) return undefined;
|
|
21
|
+
return { input, output, cacheRead: perMillion(pricing.input_cache_read), cacheWrite: perMillion(pricing.input_cache_write) };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-model prices from an OpenRouter `/models` body.
|
|
26
|
+
* @returns `{ [id]: { input, output, cacheRead?, cacheWrite?, tiers? } }` in USD per million tokens;
|
|
27
|
+
* a tier applies once a request's prompt reaches its `minPromptTokens`.
|
|
28
|
+
*/
|
|
29
|
+
export function parseOpenRouterModels(body) {
|
|
30
|
+
const models = {};
|
|
31
|
+
for (const model of Array.isArray(body?.data) ? body.data : []) {
|
|
32
|
+
const base = typeof model?.id === 'string' ? ratesOf(model.pricing) : undefined;
|
|
33
|
+
if (!base) continue;
|
|
34
|
+
const tiers = (Array.isArray(model.pricing.overrides) ? model.pricing.overrides : [])
|
|
35
|
+
.map(tier => ({ minPromptTokens: Number(tier?.min_prompt_tokens), ...ratesOf({ ...model.pricing, ...tier }) }))
|
|
36
|
+
.filter(tier => Number.isFinite(tier.minPromptTokens) && tier.input !== undefined)
|
|
37
|
+
.sort((left, right) => left.minPromptTokens - right.minPromptTokens);
|
|
38
|
+
models[model.id] = { ...base, ...(tiers.length ? { tiers } : {}) };
|
|
39
|
+
}
|
|
40
|
+
return models;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Prices for one model at a request's prompt size, or undefined when the table lists none. */
|
|
44
|
+
export function openRouterRates(model, promptTokens = 0) {
|
|
45
|
+
const entry = table.models[model];
|
|
46
|
+
if (!entry) return undefined;
|
|
47
|
+
return entry.tiers?.findLast(tier => promptTokens >= tier.minPromptTokens) ?? entry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Version stamp for rows priced from the live table: the day it was fetched. */
|
|
51
|
+
export function openRouterPriceVersion() {
|
|
52
|
+
return table.fetchedAt > 0 ? `openrouter-models-${new Date(table.fetchedAt).toISOString().slice(0, 10)}` : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Replace the table (and forget the last attempt); for tests and cache loads. */
|
|
56
|
+
export function setOpenRouterPrices(models, fetchedAt = Date.now()) {
|
|
57
|
+
table = { fetchedAt, models };
|
|
58
|
+
attemptedAt = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load the cached table, then refetch it once it is a day old. A failure keeps the
|
|
63
|
+
* last table and waits ten minutes before trying again; never throws.
|
|
64
|
+
*/
|
|
65
|
+
export async function refreshOpenRouterPrices({ home, fetch: fetchImpl = globalThis.fetch, now = Date.now() } = {}) {
|
|
66
|
+
const path = home ? join(home, FILE) : undefined;
|
|
67
|
+
if (table.fetchedAt === 0 && path) {
|
|
68
|
+
try {
|
|
69
|
+
const cached = JSON.parse(readFileSync(path, 'utf8'));
|
|
70
|
+
if (Number.isFinite(cached?.fetchedAt) && cached.models && typeof cached.models === 'object') table = { fetchedAt: cached.fetchedAt, models: cached.models };
|
|
71
|
+
} catch { /* no usable cache */ }
|
|
72
|
+
}
|
|
73
|
+
if (now - table.fetchedAt < MAX_AGE_MS || now - attemptedAt < RETRY_MS || typeof fetchImpl !== 'function') return table;
|
|
74
|
+
if (pending) return pending;
|
|
75
|
+
attemptedAt = now;
|
|
76
|
+
pending = (async () => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetchImpl(OPENROUTER_MODELS_URL, { headers: { Accept: 'application/json' } });
|
|
79
|
+
if (!response.ok) return table;
|
|
80
|
+
const models = parseOpenRouterModels(await response.json());
|
|
81
|
+
if (Object.keys(models).length === 0) return table;
|
|
82
|
+
table = { fetchedAt: now, models };
|
|
83
|
+
if (path) {
|
|
84
|
+
mkdirSync(home, { recursive: true });
|
|
85
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(table));
|
|
86
|
+
renameSync(`${path}.tmp`, path);
|
|
87
|
+
}
|
|
88
|
+
return table;
|
|
89
|
+
} catch {
|
|
90
|
+
return table;
|
|
91
|
+
} finally {
|
|
92
|
+
pending = undefined;
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
return pending;
|
|
96
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { openRouterPriceVersion, openRouterRates } from './openrouter-prices.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
|
|
6
|
-
//
|
|
7
|
-
// OpenRouter bills no peak window. [cache read, input, output].
|
|
7
|
+
// OpenRouter calls are priced from its live model listing (openrouter-prices.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,14 +14,22 @@ 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
|
|
16
|
-
export function priceVersionFor(provider) {
|
|
17
|
-
|
|
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')
|
|
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;
|
|
24
34
|
// Earlier requests require an older price table; never back-price them at today's rate.
|
|
25
35
|
if (time < Date.UTC(2026, 8, 11)) return null;
|
|
@@ -30,10 +40,11 @@ export function estimateCost(provider, model, usage, time) {
|
|
|
30
40
|
return cost === null ? null : cost * (isPeak(time) ? 2 : 1);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
/** Cost in USD; cache writes need a write price, or the call stays unpriced. */
|
|
44
|
+
function charge(usage, [read, input, output], write) {
|
|
34
45
|
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;
|
|
46
|
+
if (!values.every(n => Number.isFinite(n) && n >= 0) || values[3] !== 0 && !Number.isFinite(write)) return null;
|
|
47
|
+
return (values[0] * input + values[1] * output + values[2] * read + values[3] * (write ?? 0)) / 1e6;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
@@ -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
|
+
// pi-ai (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:
|
|
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 === '
|
|
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`,
|
package/plugins/ultra/policy.mjs
CHANGED
|
@@ -18,13 +18,13 @@ export function flashRequest(options, messages) {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The same shaping for pi-ai routes (OpenRouter), on harness-format options:
|
|
21
|
-
* Ultra sends max and adds the collaboration policy
|
|
22
|
-
*
|
|
21
|
+
* Ultra sends max and adds the collaboration policy. Delegation tools are offered
|
|
22
|
+
* at every effort (the shell policy keeps delegation rare below Ultra); workflow
|
|
23
|
+
* and ralph never are. Returns a copy; the logged input is never mutated.
|
|
23
24
|
*/
|
|
24
25
|
export function piAiRequest(options) {
|
|
25
26
|
const ultra = options.reasoningEffort === 'ultra';
|
|
26
|
-
const
|
|
27
|
-
const next = { ...options, ...(options.tools ? { tools: options.tools.filter(tool => !hidden.includes(tool.name)) } : {}), ...(ultra ? { reasoningEffort: 'max' } : {}) };
|
|
27
|
+
const next = { ...options, ...(options.tools ? { tools: options.tools.filter(tool => tool.name !== 'workflow' && tool.name !== 'ralph') } : {}), ...(ultra ? { reasoningEffort: 'max' } : {}) };
|
|
28
28
|
if (!ultra || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return next;
|
|
29
29
|
if (typeof next.system === 'string') return { ...next, system: next.system + '\n\n' + ULTRA_POLICY };
|
|
30
30
|
const [first, ...rest] = next.messages ?? [];
|
package/vendor/deepseek/index.js
CHANGED
|
@@ -247,7 +247,7 @@ async function serializeMessagesWithImages(messages, images) {
|
|
|
247
247
|
function requestWithMessages(options, messages, defaults) {
|
|
248
248
|
messages = flashRequest(options, messages);
|
|
249
249
|
messages = ultraRequest(options, messages);
|
|
250
|
-
const tools = options.tools?.filter((tool) =>
|
|
250
|
+
const tools = options.tools?.filter((tool) => tool.name !== "workflow" && tool.name !== "ralph").map((tool) => ({
|
|
251
251
|
type: "function",
|
|
252
252
|
function: {
|
|
253
253
|
name: tool.name,
|
package/vendor/pi-ai/index.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
const ULTRA_POLICY = "DSCODE ULTRA — max reasoning with task-proportional execution.\nUse the depth needed to resolve actual uncertainty. Ultra is capability available on demand, not a requirement to maximize investigation, planning, delegation or verification. Briefly choose the smallest sufficient approach, then act. Do not repeatedly reassess a decision without new evidence.\nFor a bounded task such as adding a unit test, a small bug fix or a local edit: work directly in the parent. Read the target implementation, applicable instructions and a nearby relevant example; make the requested change; run the focused test and required project checks; fix observed failures; then report the result and stop. Do not scan the whole repository, add a formal plan, launch reviewers, broaden coverage or refactor unrelated code unless concrete evidence makes it necessary. Once acceptance criteria and required checks pass, do not invent additional work or rerun passing checks without a relevant change. If the task turns out to involve an unclear contract, a broad regression or a shared interface, expand only to resolve that specific uncertainty.\nWhen delegating, explicitly choose reasoning_effort for each child instead of automatically propagating ultra. Prefer low for bounded implementation, unit tests and factual lookup; high for nontrivial debugging or review; max for exceptional uncertainty or complex design. These are guidelines, not a substitute for judging the task. Use only efforts supported by the child model. Omission inherits the parent; choosing a child effort never changes the parent effort. Both subagent and subagent_fork support effort-only selection.\nFor substantial tasks, delegate only independent work that is likely to shorten completion or resolve meaningful uncertainty. Before delegating, identify the independent boundary, concrete wall-clock benefit, and useful work you will do while the child runs. Give each child a bounded objective, relevant context, file ownership and acceptance criteria. Give each child a unique name (1-10 characters, letters, digits and underscores, starting and ending with a letter, such as read_code) and address it as /name in send_message and interrupt_agent; a child addresses you as /. Prefer subagent_fork when established conversation history is relevant; use fresh subagent for self-contained work that does not benefit from that history. Fork excludes the current unfinished turn, so always give a self-contained assignment. Keep useful work for yourself while children run. For read-only work or tasks needing the parent's uncommitted files, omit worktree and assign disjoint files if writing. For independent parallel edits on a clean repository, set worktree: true; the child starts at HEAD in an isolated checkout. Never have multiple agents edit the same files in a shared workspace. Inspect and integrate worktree changes before removing the checkout.\nIn ultra use subagent/subagent_fork and send_message for delegation, not workflow or ralph. Use at most three child agents concurrently across this root session. Children complete their assigned work themselves and cannot delegate again; do not duplicate investigations across agents. The parent owns integration, verifies child claims, resolves conflicts and runs appropriate checks. Seek independent review of substantial changes when useful; do not add a review round merely because ultra is enabled. Parent/child messages are available; sibling direct messaging is not. Preserve the user's permission policy: ultra grants no extra authority. Reuse findings and stop delegating when coordination costs outweigh value. If progress stalls, name the concrete blocker and take the next diagnostic step rather than silently extending deliberation.";
|
|
3
3
|
function piAiRequest(options) {
|
|
4
4
|
const ultra = options.reasoningEffort === 'ultra';
|
|
5
|
-
const
|
|
6
|
-
const next = { ...options, ...(options.tools ? { tools: options.tools.filter(tool => !hidden.includes(tool.name)) } : {}), ...(ultra ? { reasoningEffort: 'max' } : {}) };
|
|
5
|
+
const next = { ...options, ...(options.tools ? { tools: options.tools.filter(tool => tool.name !== 'workflow' && tool.name !== 'ralph') } : {}), ...(ultra ? { reasoningEffort: 'max' } : {}) };
|
|
7
6
|
if (!ultra || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return next;
|
|
8
7
|
if (typeof next.system === 'string') return { ...next, system: next.system + '\n\n' + ULTRA_POLICY };
|
|
9
8
|
const [first, ...rest] = next.messages ?? [];
|
|
@@ -1848,7 +1847,7 @@ var PiAiAdapter = class extends LlmAdapter {
|
|
|
1848
1847
|
if (options.stop !== void 0) throw new LlmError("llm-pi-ai does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
|
|
1849
1848
|
const profile = this.profileOf(snapshot, options.provider);
|
|
1850
1849
|
const model = this.modelOf(snapshot, options.provider, options.model);
|
|
1851
|
-
const reasoning = resolveReasoningLevel(model, options.reasoningEffort ?? profile.reasoning);
|
|
1850
|
+
const reasoning = resolveReasoningLevel(model, options.reasoningEffort ?? describableReasoningLevel(model, profile.reasoning));
|
|
1852
1851
|
const apiKey = await this.config.resolveApiKey(options.provider, profile);
|
|
1853
1852
|
const consumer = new AbortController();
|
|
1854
1853
|
const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
|
package/vendor/subagent/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dscode-child-name-v1
|
|
2
2
|
// dscode-child-worktree-v3
|
|
3
|
-
// dscode-child-effort-
|
|
3
|
+
// dscode-child-effort-v2
|
|
4
4
|
import { createChildWorktree, discardCleanChildWorktree } from "../../plugins/worktree-subagent/worktree.mjs";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
6
|
import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
|
|
@@ -396,7 +396,7 @@ function apply(ctx, config, session) {
|
|
|
396
396
|
assertSubagentProviderConfiguration(subagentProvider);
|
|
397
397
|
const wording = providerWording(subagentProvider.inheritsParentContext);
|
|
398
398
|
const providerRouteDefaults = subagentProvider.agentRouteDefaults;
|
|
399
|
-
const choiceDescription = !modelSelectionEnabled ? (subagentProvider.capabilities.agentOptions ? " Optionally set reasoning_effort for this child without changing its provider/model. Omit to inherit.
|
|
399
|
+
const choiceDescription = !modelSelectionEnabled ? (subagentProvider.capabilities.agentOptions ? " Optionally set reasoning_effort for this child without changing its provider/model. Omit to inherit. Use a level the current model offers: the lowest that fits the task, raised only for difficult work or real uncertainty." : "") : (providerRouteDefaults !== void 0 ? " Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort." : " Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.") + (subagentProvider.inheritsParentContext ? " Changing the route can prevent provider-side reuse of the inherited conversation prefix." : "");
|
|
400
400
|
mounted = {
|
|
401
401
|
subagentProvider,
|
|
402
402
|
disposeTool: runtimeCtx.tools.register(defineTool({
|
|
@@ -434,7 +434,7 @@ function apply(ctx, config, session) {
|
|
|
434
434
|
} : {},
|
|
435
435
|
...!modelSelectionEnabled && subagentProvider.capabilities.agentOptions ? { reasoning_effort: {
|
|
436
436
|
type: "string",
|
|
437
|
-
description: "Reasoning effort for this child only, validated against its model. Omit to inherit. Prefer
|
|
437
|
+
description: "Reasoning effort for this child only, validated against its model; use a level that model offers. Omit to inherit. Prefer the lowest level that fits the task and raise it only for difficult work or real uncertainty. Provider/model remain unchanged."
|
|
438
438
|
} } : {},
|
|
439
439
|
...continuable && (config.provider === "spawn" || config.provider === "fork") ? { worktree: {
|
|
440
440
|
type: "boolean",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Model providers `/provider` switches between. DeepSeek's official API is the
|
|
2
|
-
// native `llm-deepseek` route; OpenRouter
|
|
3
|
-
// through pi-ai's catalog route, which the base
|
|
4
|
-
// a `llm-pi-ai:` settings section declares it.
|
|
2
|
+
// native `llm-deepseek` route; OpenRouter serves the DeepSeek models and the rest
|
|
3
|
+
// of pi-ai's OpenRouter catalog through pi-ai's catalog route, which the base
|
|
4
|
+
// composition mounts dormant until a `llm-pi-ai:` settings section declares it.
|
|
5
5
|
|
|
6
6
|
export const PROVIDERS = Object.freeze([
|
|
7
7
|
{ id: 'deepseek-official', name: 'DeepSeek', aliases: ['deepseek', 'deepseek-official', 'official'], credentialRef: 'DEEPSEEK_API_KEY', defaultModel: 'deepseek-flash' },
|
|
@@ -23,17 +23,67 @@ export const OPENROUTER_MODELS = Object.freeze([
|
|
|
23
23
|
{ id: 'deepseek/deepseek-v4-flash-vision-exp', name: 'DeepSeek V4 Flash Vision Exp', official: ['deepseek-v4-flash-vision-exp'] },
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
-
/** The `llm-pi-ai` profile `/provider openrouter` writes:
|
|
26
|
+
/** The `llm-pi-ai` profile `/provider openrouter` writes: pi-ai's whole OpenRouter catalog. */
|
|
27
27
|
export function openRouterProfile() {
|
|
28
28
|
return {
|
|
29
29
|
displayName: 'OpenRouter',
|
|
30
30
|
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
31
|
-
// Like the official route, requests default to high;
|
|
31
|
+
// Like the official route, requests default to high; a model without high uses its own default.
|
|
32
|
+
reasoning: 'high',
|
|
33
|
+
// No models list, so the route serves every catalog model; the overrides give the
|
|
34
|
+
// DeepSeek models the official detents (and /effort its detent bar).
|
|
35
|
+
modelOverrides: Object.fromEntries(OPENROUTER_MODELS.map(({ id, name }) => [id, { name, reasoningEfforts: { ...OPENROUTER_EFFORTS } }])),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The profile 0.7.3 to 0.7.5 wrote: the catalog narrowed to the three DeepSeek models. */
|
|
40
|
+
export function narrowOpenRouterProfile() {
|
|
41
|
+
return {
|
|
42
|
+
displayName: 'OpenRouter',
|
|
43
|
+
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
32
44
|
reasoning: 'high',
|
|
33
45
|
models: OPENROUTER_MODELS.map(({ id, name }) => ({ id, name, reasoningEfforts: { ...OPENROUTER_EFFORTS } })),
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
// Fields a user sets to point the route elsewhere or shape its requests. The settings
|
|
50
|
+
// service describes a profile with its resolved defaults, which fill these with empty
|
|
51
|
+
// values (`input: []`, `compat: { chatTemplateKwargs: {}, ... }`), so empty counts as unset.
|
|
52
|
+
const USER_FIELDS = ['api', 'baseURL', 'modelOverrides', 'headers', 'compat', 'thinkingBudgets', 'cacheRetention', 'transport'];
|
|
53
|
+
const empty = value => value === undefined || (Array.isArray(value) ? value.length === 0
|
|
54
|
+
: value !== null && typeof value === 'object' && Object.values(value).every(empty));
|
|
55
|
+
const sameEfforts = (left, right) => left !== null && typeof left === 'object'
|
|
56
|
+
&& Object.keys(left).length === Object.keys(right).length && Object.entries(right).every(([level, wire]) => left[level] === wire);
|
|
57
|
+
|
|
58
|
+
/** Whether a stored profile is exactly the narrow one DSCODE wrote, so replacing it discards nothing the user chose. */
|
|
59
|
+
export function isNarrowOpenRouterProfile(profile) {
|
|
60
|
+
if (profile === null || typeof profile !== 'object' || !Array.isArray(profile.models)) return false;
|
|
61
|
+
const narrow = narrowOpenRouterProfile();
|
|
62
|
+
return profile.displayName === narrow.displayName && profile.apiKeyEnv === narrow.apiKeyEnv && profile.reasoning === narrow.reasoning
|
|
63
|
+
&& USER_FIELDS.every(field => empty(profile[field]))
|
|
64
|
+
&& profile.models.length === narrow.models.length
|
|
65
|
+
&& narrow.models.every((expected, index) => {
|
|
66
|
+
const { id, name, reasoningEfforts, ...rest } = profile.models[index] ?? {};
|
|
67
|
+
return id === expected.id && name === expected.name && sameEfforts(reasoningEfforts, expected.reasoningEfforts) && Object.values(rest).every(empty);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace the narrow profile earlier builds wrote with the whole-catalog one.
|
|
73
|
+
* Never declares a route and never throws: /model must open regardless.
|
|
74
|
+
* @returns whether the settings changed.
|
|
75
|
+
*/
|
|
76
|
+
export async function migrateOpenRouterProfile(settings) {
|
|
77
|
+
try {
|
|
78
|
+
const descriptor = settings?.describe?.({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
79
|
+
if (!descriptor || settings.writable !== true || !isNarrowOpenRouterProfile(descriptor.value?.providers?.openrouter)) return false;
|
|
80
|
+
await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', 'openrouter'], value: openRouterProfile() }], descriptor.revision);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
37
87
|
export function providerSpec(id) {
|
|
38
88
|
return PROVIDERS.find(provider => provider.id === id);
|
|
39
89
|
}
|
|
@@ -107,7 +157,8 @@ export function credentialState(row) {
|
|
|
107
157
|
|
|
108
158
|
/**
|
|
109
159
|
* Declare a provider's route before it is used. Only OpenRouter needs one; a
|
|
110
|
-
* profile the user already has (their own models or endpoint) is left alone
|
|
160
|
+
* profile the user already has (their own models or endpoint) is left alone,
|
|
161
|
+
* while the narrow profile earlier builds wrote is replaced.
|
|
111
162
|
* @param settings - the host settings service.
|
|
112
163
|
* @returns whether the settings changed.
|
|
113
164
|
*/
|
|
@@ -116,7 +167,8 @@ export async function ensureProviderRoute(settings, provider) {
|
|
|
116
167
|
if (typeof settings?.describe !== 'function' || typeof settings.mutate !== 'function') throw new Error('settings are unavailable; OpenRouter cannot be configured in this profile');
|
|
117
168
|
const descriptor = settings.describe({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
118
169
|
if (!descriptor) throw new Error('the OpenRouter adapter (llm-pi-ai) is not mounted in this profile');
|
|
119
|
-
|
|
170
|
+
const existing = descriptor.value?.providers?.openrouter;
|
|
171
|
+
if (existing !== undefined) return migrateOpenRouterProfile(settings);
|
|
120
172
|
if (settings.writable !== true) throw new Error('settings are read-only; OpenRouter cannot be configured here');
|
|
121
173
|
await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', 'openrouter'], value: openRouterProfile() }], descriptor.revision);
|
|
122
174
|
return true;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Reasoning levels differ per model: DeepSeek offers low/high/max, GPT models
|
|
2
|
+
// minimal through high, and many OpenRouter models none at all. Auxiliary calls
|
|
3
|
+
// name the level they would like and send the nearest one the model offers, or no
|
|
4
|
+
// effort when the model offers no levels.
|
|
5
|
+
|
|
6
|
+
/** Standard reasoning levels, lowest first. */
|
|
7
|
+
export const EFFORT_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The level to request from a model.
|
|
11
|
+
* @param offered - the model's effort ids, or `undefined` when its capability is unknown.
|
|
12
|
+
* @param wanted - the level the caller would like.
|
|
13
|
+
* @returns `wanted` when offered or when the capability is unknown; otherwise the nearest
|
|
14
|
+
* offered level at or above it, else the highest below; `undefined` when the model offers
|
|
15
|
+
* no standard level.
|
|
16
|
+
*/
|
|
17
|
+
export function chooseEffort(offered, wanted) {
|
|
18
|
+
if (wanted === undefined || offered === undefined || offered.includes(wanted)) return wanted;
|
|
19
|
+
const rank = EFFORT_LEVELS.indexOf(wanted);
|
|
20
|
+
const levels = EFFORT_LEVELS.filter(level => offered.includes(level));
|
|
21
|
+
if (rank < 0 || levels.length === 0) return undefined;
|
|
22
|
+
return levels.find(level => EFFORT_LEVELS.indexOf(level) >= rank) ?? levels.at(-1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* {@link chooseEffort} for a route, reading the model's levels from the LLM service.
|
|
27
|
+
* A service without model metadata, or a failed lookup, keeps `wanted`.
|
|
28
|
+
*/
|
|
29
|
+
export async function effortFor(llm, route, wanted, signal) {
|
|
30
|
+
if (wanted === undefined || typeof llm?.resolveModelInfo !== 'function' || !route?.provider || !route?.model) return wanted;
|
|
31
|
+
let info;
|
|
32
|
+
try { info = await llm.resolveModelInfo(route.provider, route.model, signal); }
|
|
33
|
+
catch { return wanted; }
|
|
34
|
+
return chooseEffort(info?.reasoning?.efforts?.map(effort => effort.id) ?? [], wanted);
|
|
35
|
+
}
|
package/vendor/tui/index.mjs
CHANGED
|
@@ -39,6 +39,8 @@ function dscodeLoadFlag(name, fallback = false) {
|
|
|
39
39
|
function dscodeSaveFlag(name, value) {
|
|
40
40
|
try { fs.mkdirSync(join(homedir(), ".dsh", "dsh-code"), { recursive: true }); fs.writeFileSync(dscodeFlagFile(name), JSON.stringify({ [name]: value }, null, 2) + "\n"); } catch {}
|
|
41
41
|
}
|
|
42
|
+
// dscode-model-search-v1
|
|
43
|
+
import { migrateOpenRouterProfile as dscodeMigrateOpenRouter } from "./dscode-providers/catalog.mjs";
|
|
42
44
|
// dscode-provider-v1
|
|
43
45
|
import { PROVIDERS as DSCODE_PROVIDERS, providerSpec as dscodeProviderSpec, providerArgument as dscodeProviderArgument, providerOfLabel as dscodeProviderOfLabel, splitModelLabel as dscodeSplitModelLabel, pickModel as dscodePickModel, credentialState as dscodeCredentialState, ensureProviderRoute as dscodeEnsureProviderRoute, waitForModels as dscodeWaitForModels } from "./dscode-providers/catalog.mjs";
|
|
44
46
|
import { readClipboardImage as dscodeReadClipboardImage } from "./dscode-clipboard-image/index.mjs";
|
|
@@ -32374,7 +32376,7 @@ function Header({ cwd = "", model = "", effort = "", animated = false }) {
|
|
|
32374
32376
|
if (!full) return (0, import_react.createElement)(Box, { flexDirection: "column", paddingX: 2, marginBottom: 1 },
|
|
32375
32377
|
(0, import_react.createElement)(Text, { wrap: "truncate-end" },
|
|
32376
32378
|
(0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright), bold: true }, "❄ DSCODE"),
|
|
32377
|
-
(0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, " v0.7.
|
|
32379
|
+
(0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, " v0.7.6")),
|
|
32378
32380
|
(0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(modelName + " · " + effortName, width)),
|
|
32379
32381
|
(0, import_react.createElement)(Text, { color: inkColor(getPalette().dim), wrap: "truncate-end" }, welcomePath(cwd, width)));
|
|
32380
32382
|
return (0, import_react.createElement)(Box, { flexDirection: "column", width, borderStyle: "round", borderColor: inkColor(getPalette().brand), paddingX: 1 },
|
|
@@ -32385,7 +32387,7 @@ function Header({ cwd = "", model = "", effort = "", animated = false }) {
|
|
|
32385
32387
|
(0, import_react.createElement)(Box, { flexDirection: "column", width: detailsWidth, marginTop: 2 },
|
|
32386
32388
|
(0, import_react.createElement)(Text, { color: inkColor(getPalette().text), bold: true }, "DSCODE"),
|
|
32387
32389
|
(0, import_react.createElement)(Text, { color: inkColor(getPalette().brandDeep) }, "────────────"),
|
|
32388
|
-
(0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, "v0.7.
|
|
32390
|
+
(0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, "v0.7.6"),
|
|
32389
32391
|
(0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(dscodePadEnd(dscodeT("welcome.model"), 9) + modelName, detailsWidth)),
|
|
32390
32392
|
(0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(dscodePadEnd(dscodeT("welcome.effort"), 9) + effortName, detailsWidth)),
|
|
32391
32393
|
(0, import_react.createElement)(Text, null, " "),
|
|
@@ -33162,11 +33164,22 @@ function QuestionBar({ store, snapshot, locked }) {
|
|
|
33162
33164
|
}, dim(truncateColumns(footer, viewport.contentColumns))));
|
|
33163
33165
|
}
|
|
33164
33166
|
/** The /model panel: a scrolling list over the advisory model directory. */
|
|
33167
|
+
function dscodeFilterModels(rows, query) {
|
|
33168
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
33169
|
+
if (terms.length === 0) return rows;
|
|
33170
|
+
return rows.filter((row) => {
|
|
33171
|
+
const haystack = [row.providerName, row.modelName, row.provider + '/' + row.model].filter(Boolean).join(' ').toLowerCase();
|
|
33172
|
+
return terms.every((term) => haystack.includes(term));
|
|
33173
|
+
});
|
|
33174
|
+
}
|
|
33165
33175
|
function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry, onClose }) {
|
|
33166
33176
|
const [cursor, setCursor] = (0, import_react.useState)(0);
|
|
33177
|
+
const [dscodeQuery, setDscodeQuery] = (0, import_react.useState)("");
|
|
33178
|
+
const [dscodeSearching, setDscodeSearching] = (0, import_react.useState)(false);
|
|
33167
33179
|
const stdout = useStdout().stdout;
|
|
33168
33180
|
const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
|
|
33169
|
-
const
|
|
33181
|
+
const dscodeRows = directory?.rows ?? [];
|
|
33182
|
+
const rows = (0, import_react.useMemo)(() => dscodeFilterModels(dscodeRows, dscodeQuery), [dscodeRows, dscodeQuery]);
|
|
33170
33183
|
const positioned = (0, import_react.useRef)(false);
|
|
33171
33184
|
(0, import_react.useEffect)(() => {
|
|
33172
33185
|
if (positioned.current || rows.length === 0 || current === void 0) {
|
|
@@ -33188,6 +33201,30 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
33188
33201
|
current
|
|
33189
33202
|
]);
|
|
33190
33203
|
useInput((input, key) => {
|
|
33204
|
+
if (dscodeSearching) {
|
|
33205
|
+
if (key.escape) {
|
|
33206
|
+
setDscodeSearching(false);
|
|
33207
|
+
setDscodeQuery("");
|
|
33208
|
+
setCursor(0);
|
|
33209
|
+
return;
|
|
33210
|
+
}
|
|
33211
|
+
if (key.return) {
|
|
33212
|
+
setDscodeSearching(false);
|
|
33213
|
+
if (rows[cursor] !== void 0) onSelect(rows[cursor]);
|
|
33214
|
+
return;
|
|
33215
|
+
}
|
|
33216
|
+
if (!(key.upArrow || key.downArrow || key.pageUp || key.pageDown || key.ctrl && input === "c")) {
|
|
33217
|
+
const next = editQuery(dscodeQuery, input, key);
|
|
33218
|
+
if (next !== void 0) {
|
|
33219
|
+
setDscodeQuery(next.slice(0, 120));
|
|
33220
|
+
setCursor(0);
|
|
33221
|
+
}
|
|
33222
|
+
return;
|
|
33223
|
+
}
|
|
33224
|
+
} else if (input === "/" || isSearchToggle(input, key)) {
|
|
33225
|
+
setDscodeSearching(true);
|
|
33226
|
+
return;
|
|
33227
|
+
}
|
|
33191
33228
|
if (key.escape || input === "q") {
|
|
33192
33229
|
onClose();
|
|
33193
33230
|
return;
|
|
@@ -33252,7 +33289,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
33252
33289
|
key: "empty",
|
|
33253
33290
|
dimColor: true,
|
|
33254
33291
|
wrap: "truncate-end"
|
|
33255
|
-
}, " no models available")] : []]).slice(0, viewport.bodyRows);
|
|
33292
|
+
}, dscodeQuery === "" ? " no models available" : " no models match the search")] : []]).slice(0, viewport.bodyRows);
|
|
33256
33293
|
const rowBudget = Math.max(0, viewport.bodyRows - visibleStateRows.length);
|
|
33257
33294
|
const first = selectionWindow(cursor, rows.length, rowBudget);
|
|
33258
33295
|
const visible = rowBudget === 0 ? [] : rows.slice(first, first + rowBudget);
|
|
@@ -33266,7 +33303,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
33266
33303
|
color: inkColor(getPalette().brand),
|
|
33267
33304
|
bold: true,
|
|
33268
33305
|
wrap: "truncate-end"
|
|
33269
|
-
}, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
|
|
33306
|
+
}, truncateColumns(`/model — select model${rows.length === 0 ? "" : ` · ${cursor + 1}/${rows.length}`}${dscodeSearching || dscodeQuery !== "" ? ` · ${searchLine(dscodeSearching, dscodeQuery)}` : ""}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...visibleStateRows, ...visible.map((row) => {
|
|
33270
33307
|
const index = rows.indexOf(row);
|
|
33271
33308
|
const capability = row.inputModalities?.includes("image") === true ? " · image" : "";
|
|
33272
33309
|
const label = displayText(`${row.providerName} · ${row.modelName}${capability}`);
|
|
@@ -33278,7 +33315,7 @@ function ModelPanel({ directory, error, current, onSelect, onProviders, onRetry,
|
|
|
33278
33315
|
}), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
|
|
33279
33316
|
dimColor: true,
|
|
33280
33317
|
wrap: "truncate-end"
|
|
33281
|
-
}, dim(truncateColumns(`↑↓ move · pgup/pgdn page · enter select${onProviders === void 0 ? "" : " · tab providers"} · r retry · esc/q close`, viewport.contentColumns))));
|
|
33318
|
+
}, dim(truncateColumns(`↑↓ move · pgup/pgdn page · ${dscodeSearching ? "esc clears search" : "/ search"} · enter select${onProviders === void 0 ? "" : " · tab providers"} · r retry · esc/q close`, viewport.contentColumns))));
|
|
33282
33319
|
}
|
|
33283
33320
|
/** Compact provider-state copy; only value-free credential facts cross this boundary. */
|
|
33284
33321
|
function providerStateLabel(row) {
|
|
@@ -39877,7 +39914,7 @@ async function run(ctx, startup, io) {
|
|
|
39877
39914
|
steer,
|
|
39878
39915
|
interrupt,
|
|
39879
39916
|
quit,
|
|
39880
|
-
loadModels: () => loadModelDirectory(ctx),
|
|
39917
|
+
loadModels: () => dscodeMigrateOpenRouter(ctx.get("settings")).then(() => loadModelDirectory(ctx)),
|
|
39881
39918
|
dscodeEnsureProviderRoute: (provider) => dscodeEnsureProviderRoute(ctx.get("settings"), provider),
|
|
39882
39919
|
loadModelProviders: () => loadProviderSettings(ctx),
|
|
39883
39920
|
subscribeModelProviders: (listener) => subscribeProviderSettings(ctx, listener),
|