@toddzheng024/dscode-bundle 0.7.12 → 0.7.14
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +6 -2
- package/package.json +7 -5
- package/plugins/code-review/index.mjs +5 -2
- package/plugins/compaction/threshold.mjs +12 -0
- package/plugins/credentials/index.mjs +13 -0
- package/plugins/grok/adapter.mjs +114 -0
- package/plugins/grok/auth.mjs +76 -0
- package/plugins/grok/billing.mjs +146 -0
- package/plugins/grok/index.mjs +118 -0
- package/plugins/grok/models.mjs +129 -0
- package/plugins/grok/status.mjs +28 -0
- package/plugins/grok/wire.mjs +268 -0
- package/plugins/i18n/messages.mjs +6 -6
- package/plugins/providers/catalog.mjs +2 -0
- package/plugins/session-metrics/view.mjs +34 -3
- package/vendor/compaction-basic/index.js +91 -1
- package/vendor/persistent/index.js +10 -9
- package/vendor/terminal/index.js +5 -0
- package/vendor/tui/lib/app.mjs +6711 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +84 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2200 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1864 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44441
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { emailAddress as address } from './contacts.mjs';
|
|
2
|
-
import nodemailer from 'nodemailer';
|
|
3
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { createEmailInbox } from './inbox.mjs';
|
|
6
|
-
import { emailStore } from './store.mjs';
|
|
7
|
-
|
|
8
|
-
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
9
|
-
|
|
10
|
-
export function outgoingEmail({ to, subject, body, idempotency_key }) {
|
|
11
|
-
if (!address(to)) throw Error('Specify one plain recipient email address.');
|
|
12
|
-
if (typeof subject !== 'string' || !subject.trim() || subject.length > 500 || /[\x00-\x1f\x7f]/.test(subject)) throw Error('Invalid email subject.');
|
|
13
|
-
if (typeof body !== 'string' || !body.trim() || body.includes('\0') || Buffer.byteLength(body) > 262144) throw Error('Email requires plain text of at most 256 KiB.');
|
|
14
|
-
if (typeof idempotency_key !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(idempotency_key)) throw Error('Use a stable idempotency_key of 1..128 letters, digits, underscores or hyphens.');
|
|
15
|
-
subject = subject.trim();
|
|
16
|
-
if (!subject.startsWith('[ToAgent]')) subject = '[ToAgent] ' + subject;
|
|
17
|
-
return { to, subject, body, idempotency_key };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function createEmailSender({ directory = createEmailInbox().directory, transportFactory = options => nodemailer.createTransport(options) } = {}) {
|
|
21
|
-
const credentials = emailStore(join(directory, 'imap'));
|
|
22
|
-
const outbox = emailStore(join(directory, 'outbox'));
|
|
23
|
-
const filename = key => hash(key) + '.json';
|
|
24
|
-
return {
|
|
25
|
-
status(key) {
|
|
26
|
-
if (typeof key !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(key)) throw Error('Invalid idempotency_key.');
|
|
27
|
-
const entry = outbox.read(filename(key));
|
|
28
|
-
return entry ? entry.receipt : { status: 'not_found' };
|
|
29
|
-
},
|
|
30
|
-
async send(value, { signal } = {}) {
|
|
31
|
-
const mail = outgoingEmail(value);
|
|
32
|
-
return outbox.locked(async () => {
|
|
33
|
-
const file = filename(mail.idempotency_key), digest = hash(JSON.stringify(mail));
|
|
34
|
-
const existing = outbox.read(file);
|
|
35
|
-
if (existing) {
|
|
36
|
-
if (existing.digest !== digest) throw Error('This idempotency_key belongs to a different email.');
|
|
37
|
-
return existing.receipt;
|
|
38
|
-
}
|
|
39
|
-
const config = credentials.read('connection.json')?.config;
|
|
40
|
-
if (!config || config.host !== 'imap.gmail.com' || !address(config.account) || !config.password) throw Error('Configure Gmail with an application password in /email first.');
|
|
41
|
-
if (signal?.aborted) return { status: 'cancelled' };
|
|
42
|
-
const messageId = `<${randomUUID()}@${config.account.split('@')[1]}>`;
|
|
43
|
-
const receipt = { status: 'uncertain', messageId, from: config.account, to: mail.to, subject: mail.subject, attemptedAt: new Date().toISOString() };
|
|
44
|
-
// Persist before SMTP: a crash or lost final response must never cause a blind retry.
|
|
45
|
-
outbox.write(file, { digest, receipt });
|
|
46
|
-
let transport;
|
|
47
|
-
try {
|
|
48
|
-
transport = transportFactory({ host: 'smtp.gmail.com', port: 465, secure: true,
|
|
49
|
-
auth: { user: config.account, pass: config.password },
|
|
50
|
-
tls: { rejectUnauthorized: true, minVersion: 'TLSv1.2' },
|
|
51
|
-
logger: false, debug: false, disableFileAccess: true, disableUrlAccess: true,
|
|
52
|
-
connectionTimeout: 15000, greetingTimeout: 15000, socketTimeout: 30000 });
|
|
53
|
-
// Once SMTP starts, cancellation cannot recall a submitted message.
|
|
54
|
-
// Finish recording its outcome even if the agent turn is cancelled.
|
|
55
|
-
const info = await transport.sendMail({ from: config.account, to: mail.to,
|
|
56
|
-
envelope: { from: config.account, to: [mail.to] }, subject: mail.subject, text: mail.body,
|
|
57
|
-
messageId, disableFileAccess: true, disableUrlAccess: true });
|
|
58
|
-
receipt.status = info.accepted?.includes(mail.to) ? 'accepted' : 'rejected';
|
|
59
|
-
outbox.write(file, { digest, receipt });
|
|
60
|
-
} catch {
|
|
61
|
-
// SMTP errors may contain secrets or message content. Only expose the durable receipt.
|
|
62
|
-
} finally {
|
|
63
|
-
transport?.close();
|
|
64
|
-
}
|
|
65
|
-
return receipt;
|
|
66
|
-
});
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock';
|
|
5
|
-
|
|
6
|
-
export function emailStore(directory, label = 'email') {
|
|
7
|
-
const read = name => {
|
|
8
|
-
try { return JSON.parse(readFileSync(join(directory, name), 'utf8')); }
|
|
9
|
-
catch (error) { if (error.code === 'ENOENT') return null; throw Error('Cannot read local ' + label + ' configuration.', { cause: error }); }
|
|
10
|
-
};
|
|
11
|
-
return {
|
|
12
|
-
directory, read,
|
|
13
|
-
exists: name => existsSync(join(directory, name)),
|
|
14
|
-
write(name, value) {
|
|
15
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
16
|
-
const temp = join(directory, '.' + randomUUID());
|
|
17
|
-
try {
|
|
18
|
-
writeFileSync(temp, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
|
|
19
|
-
renameSync(temp, join(directory, name));
|
|
20
|
-
} finally { try { unlinkSync(temp); } catch { /* best-effort cleanup: never mask the write's own outcome */ } }
|
|
21
|
-
},
|
|
22
|
-
async locked(action) {
|
|
23
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
24
|
-
const fd = openSync(join(directory, '.sync.lock'), 'a', 0o600);
|
|
25
|
-
try {
|
|
26
|
-
try { await tryLockExclusive(fd); }
|
|
27
|
-
catch (error) { if (['EAGAIN', 'EWOULDBLOCK'].includes(error.code)) return { busy: true }; throw error; }
|
|
28
|
-
return await action();
|
|
29
|
-
} finally { closeSync(fd); }
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// Model providers `/provider` switches between. DeepSeek's official API is the
|
|
2
|
-
// native `llm-deepseek` route; OpenRouter is DSCODE's own adapter
|
|
3
|
-
// (plugins/openrouter), which serves OpenRouter's live model listing and is always
|
|
4
|
-
// registered. This module ships beside the TUI too, so it imports nothing.
|
|
5
|
-
|
|
6
|
-
export const PROVIDERS = Object.freeze([
|
|
7
|
-
{ id: 'deepseek-official', name: 'DeepSeek', aliases: ['deepseek', 'deepseek-official', 'official'], credentialRef: 'DEEPSEEK_API_KEY', defaultModel: 'deepseek-flash' },
|
|
8
|
-
// The optional management key reads account data only; it cannot call models.
|
|
9
|
-
{ id: 'openrouter', name: 'OpenRouter', aliases: ['openrouter', 'open-router'], credentialRef: 'OPENROUTER_API_KEY', managementRef: 'OPENROUTER_MANAGEMENT_KEY', defaultModel: 'deepseek/deepseek-v4-flash' },
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
// The pi-ai adapter served OpenRouter until 0.7.6, from this settings section.
|
|
13
|
-
const PI_AI_NS = 'llm-pi-ai';
|
|
14
|
-
|
|
15
|
-
// OpenRouter serves DeepSeek V4 thinking as none/high/xhigh. DeepSeek itself
|
|
16
|
-
// answers `low` as high and `max` as xhigh, so the route offers the official
|
|
17
|
-
// low/high/max detents (and Ultra on top of max) with the wire spelling OpenRouter
|
|
18
|
-
// accepts; session cards and delegated children that ask for `low` keep working.
|
|
19
|
-
export const OPENROUTER_EFFORTS = Object.freeze({ off: 'none', low: 'high', high: 'high', max: 'xhigh' });
|
|
20
|
-
|
|
21
|
-
/** The DeepSeek models the OpenRouter route serves, with their official-route counterparts. */
|
|
22
|
-
export const OPENROUTER_MODELS = Object.freeze([
|
|
23
|
-
{ id: 'deepseek/deepseek-v4-flash', name: 'DeepSeek V4 Flash', official: ['deepseek-flash', 'deepseek-v4-flash'] },
|
|
24
|
-
{ id: 'deepseek/deepseek-v4-pro', name: 'DeepSeek V4 Pro', official: ['deepseek-v4-pro'] },
|
|
25
|
-
{ id: 'deepseek/deepseek-v4-flash-vision-exp', name: 'DeepSeek V4 Flash Vision Exp', official: ['deepseek-v4-flash-vision-exp'] },
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Remove the `openrouter` profile earlier builds wrote into the pi-ai section. The
|
|
30
|
-
* pi-ai adapter is no longer mounted, so the profile is inert, and it would claim the
|
|
31
|
-
* route a second time if that adapter were ever mounted again.
|
|
32
|
-
* Never throws: /model and /provider must open regardless.
|
|
33
|
-
* @returns whether the settings changed.
|
|
34
|
-
*/
|
|
35
|
-
export async function migrateOpenRouterProfile(settings) {
|
|
36
|
-
try {
|
|
37
|
-
const descriptor = settings?.describe?.({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
38
|
-
if (!descriptor || settings.writable !== true || descriptor.value?.providers?.openrouter === undefined) return false;
|
|
39
|
-
await settings.mutate(PI_AI_NS, [{ op: 'unset', path: ['providers', 'openrouter'] }], descriptor.revision);
|
|
40
|
-
return true;
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function providerSpec(id) {
|
|
47
|
-
return PROVIDERS.find(provider => provider.id === id);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Resolve a `/provider` or `/login` argument.
|
|
52
|
-
* @param raw - text after the command name.
|
|
53
|
-
* @returns a provider id, `undefined` for no argument, or `null` when unrecognized. Callers must not echo the text: it may be a pasted key.
|
|
54
|
-
*/
|
|
55
|
-
export function providerArgument(raw) {
|
|
56
|
-
const value = String(raw ?? '').trim().toLowerCase();
|
|
57
|
-
if (value === '') return undefined;
|
|
58
|
-
return PROVIDERS.find(provider => provider.aliases.includes(value))?.id ?? null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Split a `provider/model` label at the first slash: OpenRouter model ids carry their own `vendor/` segment. */
|
|
62
|
-
export function splitModelLabel(label) {
|
|
63
|
-
const text = typeof label === 'string' ? label : '';
|
|
64
|
-
const cut = text.indexOf('/');
|
|
65
|
-
return cut > 0 ? { provider: text.slice(0, cut), model: text.slice(cut + 1) } : { provider: '', model: text };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** The switchable provider a label names, defaulting to DeepSeek for unknown or bare labels. */
|
|
69
|
-
export function providerOfLabel(label) {
|
|
70
|
-
return providerSpec(splitModelLabel(label).provider)?.id ?? PROVIDERS[0].id;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Provider id leading a footer header (`provider: model @ effort`), if any. */
|
|
74
|
-
export function providerOfHeader(header) {
|
|
75
|
-
return typeof header === 'string' ? header.match(/^([^\s:/]+): /)?.[1] : undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function counterpart(from, model, to) {
|
|
79
|
-
if (from === to) return model;
|
|
80
|
-
if (from === 'deepseek-official' && to === 'openrouter') return OPENROUTER_MODELS.find(entry => entry.official.includes(model))?.id;
|
|
81
|
-
if (from === 'openrouter' && to === 'deepseek-official') return OPENROUTER_MODELS.find(entry => entry.id === model)?.official[0];
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The model a provider switch lands on: the current model's counterpart, else the
|
|
87
|
-
* provider default, else its first model. The effort carries over only when the
|
|
88
|
-
* target offers it; otherwise the model's own default applies.
|
|
89
|
-
* @param rows - model directory rows (`provider`, `model`, `reasoning`).
|
|
90
|
-
* @returns `{ row, effort }`, or `undefined` when the provider serves no model yet.
|
|
91
|
-
*/
|
|
92
|
-
export function pickModel(rows, provider, currentLabel, effort) {
|
|
93
|
-
const candidates = rows.filter(row => row.provider === provider);
|
|
94
|
-
if (candidates.length === 0) return undefined;
|
|
95
|
-
const current = splitModelLabel(currentLabel);
|
|
96
|
-
const wanted = counterpart(current.provider, current.model, provider);
|
|
97
|
-
const row = candidates.find(candidate => candidate.model === wanted)
|
|
98
|
-
?? candidates.find(candidate => candidate.model === providerSpec(provider)?.defaultModel)
|
|
99
|
-
?? candidates[0];
|
|
100
|
-
const offered = row.reasoning?.efforts.map(level => level.id) ?? [];
|
|
101
|
-
return { row, effort: effort && offered.includes(effort) ? effort : undefined };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Credential status of a provider-settings row.
|
|
106
|
-
* @returns `saved`, `env`, `missing`, `readonly` (an empty read-only source), `error`, or `unavailable` (no row).
|
|
107
|
-
*/
|
|
108
|
-
export function credentialState(row) {
|
|
109
|
-
if (!row) return 'unavailable';
|
|
110
|
-
const credential = row.credential;
|
|
111
|
-
if (credential?.kind === 'error') return 'error';
|
|
112
|
-
if (credential?.kind !== 'facts') return 'missing';
|
|
113
|
-
if (credential.configured) return credential.source === 'env' ? 'env' : 'saved';
|
|
114
|
-
return credential.writable ? 'missing' : 'readonly';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Prepare a provider before a switch. Both routes are always registered; switching
|
|
119
|
-
* to OpenRouter only clears the inert pi-ai profile earlier builds wrote.
|
|
120
|
-
* @param settings - the host settings service.
|
|
121
|
-
* @returns whether the settings changed.
|
|
122
|
-
*/
|
|
123
|
-
export async function ensureProviderRoute(settings, provider) {
|
|
124
|
-
return provider === 'openrouter' ? migrateOpenRouterProfile(settings) : false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Wait for a route's models to reach the model directory. */
|
|
128
|
-
export async function waitForModels(loadModels, provider, { attempts = 30, delayMs = 100 } = {}) {
|
|
129
|
-
let directory;
|
|
130
|
-
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
131
|
-
directory = await loadModels();
|
|
132
|
-
if (directory.rows.some(row => row.provider === provider)) return directory;
|
|
133
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
134
|
-
}
|
|
135
|
-
return directory;
|
|
136
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
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
|
-
}
|