@toddzheng024/dscode-bundle 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +26 -5
- package/package.json +4 -4
- package/plugins/auto-review/index.mjs +6 -1
- package/plugins/compaction/tetris.mjs +65 -0
- package/plugins/compaction/threshold.mjs +46 -0
- package/plugins/credentials/index.mjs +2 -2
- package/plugins/i18n/messages.mjs +18 -0
- package/plugins/openrouter/adapter.mjs +157 -0
- package/plugins/openrouter/index.mjs +112 -0
- package/plugins/openrouter/models.mjs +151 -0
- package/plugins/openrouter/search.mjs +109 -0
- package/plugins/openrouter/wire.mjs +413 -0
- package/plugins/providers/catalog.mjs +18 -68
- package/plugins/providers/openrouter-account.mjs +171 -0
- package/plugins/session-metrics/balance.mjs +29 -19
- package/plugins/session-metrics/index.mjs +19 -9
- package/plugins/session-metrics/pricing.mjs +26 -6
- package/plugins/session-metrics/view.mjs +1 -1
- package/plugins/ultra/policy.mjs +0 -16
- package/presets/dscode/agent.cordis.yml +1 -1
- package/vendor/compaction-basic/index.js +983 -0
- package/vendor/compaction-basic/types/config.d.ts +37 -0
- package/vendor/compaction-basic/types/index.d.ts +84 -0
- package/vendor/compaction-basic/types/region.d.ts +65 -0
- package/vendor/compaction-basic/types/summarizer.d.ts +64 -0
- package/vendor/compaction-basic/types/types.d.ts +73 -0
- package/vendor/tui/dscode-providers/catalog.mjs +18 -68
- package/vendor/tui/dscode-providers/openrouter-account.mjs +171 -0
- package/vendor/tui/index.mjs +390 -165
- package/plugins/session-metrics/openrouter-prices.mjs +0 -96
- package/vendor/pi-ai/index.js +0 -2701
- package/vendor/pi-ai/types/adapter.d.ts +0 -105
- package/vendor/pi-ai/types/auth.d.ts +0 -60
- package/vendor/pi-ai/types/catalog.d.ts +0 -355
- package/vendor/pi-ai/types/config.d.ts +0 -208
- package/vendor/pi-ai/types/context.d.ts +0 -42
- package/vendor/pi-ai/types/discovery.d.ts +0 -43
- package/vendor/pi-ai/types/index.d.ts +0 -69
- package/vendor/pi-ai/types/login.d.ts +0 -21
- package/vendor/pi-ai/types/provider.d.ts +0 -59
- package/vendor/pi-ai/types/replay.d.ts +0 -63
- package/vendor/pi-ai/types/stream.d.ts +0 -43
- /package/vendor/{pi-ai → compaction-basic}/LICENSE +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// OpenRouter's public model listing: context sizes, input modalities, reasoning
|
|
5
|
+
// controls and list prices for every model it serves. The OpenRouter adapter
|
|
6
|
+
// resolves models from it and the session metrics price calls from it, so one
|
|
7
|
+
// table, kept for a day in DSH_HOME, backs both. The listing quotes USD per
|
|
8
|
+
// token; the table keeps USD per million.
|
|
9
|
+
export const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
10
|
+
/** How long a first model lookup waits for the listing before answering without it. */
|
|
11
|
+
export const FORCED_LOAD_TIMEOUT_MS = 10_000;
|
|
12
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const RETRY_MS = 10 * 60 * 1000;
|
|
14
|
+
const FILE = 'openrouter-models.json';
|
|
15
|
+
const VERSION = 2;
|
|
16
|
+
const EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
17
|
+
let table = { fetchedAt: 0, models: {} };
|
|
18
|
+
let attemptedAt = 0, pending;
|
|
19
|
+
|
|
20
|
+
const perMillion = value => {
|
|
21
|
+
const number = Number(value);
|
|
22
|
+
return value != null && value !== '' && Number.isFinite(number) && number >= 0 ? number * 1e6 : undefined;
|
|
23
|
+
};
|
|
24
|
+
const positive = value => Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
25
|
+
const ratesOf = pricing => {
|
|
26
|
+
const input = perMillion(pricing?.prompt), output = perMillion(pricing?.completion);
|
|
27
|
+
if (input === undefined || output === undefined) return undefined;
|
|
28
|
+
return { input, output, cacheRead: perMillion(pricing.input_cache_read), cacheWrite: perMillion(pricing.input_cache_write) };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function reasoningOf(reasoning) {
|
|
32
|
+
if (reasoning === null || typeof reasoning !== 'object') return undefined;
|
|
33
|
+
const offered = Array.isArray(reasoning.supported_efforts) ? EFFORTS.filter(level => reasoning.supported_efforts.includes(level)) : undefined;
|
|
34
|
+
return {
|
|
35
|
+
mandatory: reasoning.mandatory === true,
|
|
36
|
+
...(offered?.length ? { efforts: offered } : {}),
|
|
37
|
+
...(EFFORTS.includes(reasoning.default_effort) ? { defaultEffort: reasoning.default_effort } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Models from an OpenRouter `/models` body.
|
|
43
|
+
* @returns `{ [id]: { input, output, cacheRead?, cacheWrite?, tiers?, name?, contextWindow?, maxOutput?,
|
|
44
|
+
* inputModalities?, tools?, textOutput?, reasoning? } }` with prices in USD per million tokens;
|
|
45
|
+
* a tier applies once a request's prompt reaches its `minPromptTokens`.
|
|
46
|
+
*/
|
|
47
|
+
export function parseOpenRouterModels(body) {
|
|
48
|
+
const models = {};
|
|
49
|
+
for (const model of Array.isArray(body?.data) ? body.data : []) {
|
|
50
|
+
const base = typeof model?.id === 'string' ? ratesOf(model.pricing) : undefined;
|
|
51
|
+
if (!base) continue;
|
|
52
|
+
const tiers = (Array.isArray(model.pricing.overrides) ? model.pricing.overrides : [])
|
|
53
|
+
.map(tier => ({ minPromptTokens: Number(tier?.min_prompt_tokens), ...ratesOf({ ...model.pricing, ...tier }) }))
|
|
54
|
+
.filter(tier => Number.isFinite(tier.minPromptTokens) && tier.input !== undefined)
|
|
55
|
+
.sort((left, right) => left.minPromptTokens - right.minPromptTokens);
|
|
56
|
+
// The routed endpoint can serve less than the model's nominal context.
|
|
57
|
+
const windows = [model.context_length, model.top_provider?.context_length].map(positive).filter(Boolean);
|
|
58
|
+
const input = model.architecture?.input_modalities, output = model.architecture?.output_modalities;
|
|
59
|
+
const reasoning = reasoningOf(model.reasoning);
|
|
60
|
+
models[model.id] = {
|
|
61
|
+
...base, ...(tiers.length ? { tiers } : {}),
|
|
62
|
+
...(typeof model.name === 'string' && model.name.length > 0 ? { name: model.name } : {}),
|
|
63
|
+
...(windows.length ? { contextWindow: Math.min(...windows) } : {}),
|
|
64
|
+
...(positive(model.top_provider?.max_completion_tokens) ? { maxOutput: model.top_provider.max_completion_tokens } : {}),
|
|
65
|
+
...(Array.isArray(input) ? { inputModalities: ['text', 'image'].filter(modality => input.includes(modality)) } : {}),
|
|
66
|
+
...(Array.isArray(model.supported_parameters) ? { tools: model.supported_parameters.includes('tools') } : {}),
|
|
67
|
+
...(Array.isArray(output) ? { textOutput: output.includes('text') } : {}),
|
|
68
|
+
...(reasoning ? { reasoning } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return models;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One model's entry, or undefined when the table does not list it. */
|
|
75
|
+
export function openRouterModel(id) {
|
|
76
|
+
return Object.hasOwn(table.models, id) ? table.models[id] : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Every listed model as `[id, entry]` pairs, in listing order. */
|
|
80
|
+
export function listOpenRouterModels() {
|
|
81
|
+
return Object.entries(table.models);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Prices for one model at a request's prompt size, or undefined when the table lists none. */
|
|
85
|
+
export function openRouterRates(model, promptTokens = 0) {
|
|
86
|
+
const entry = openRouterModel(model);
|
|
87
|
+
if (!entry) return undefined;
|
|
88
|
+
return entry.tiers?.findLast(tier => promptTokens >= tier.minPromptTokens) ?? entry;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Version stamp for rows priced from the live table: the day it was fetched. */
|
|
92
|
+
export function openRouterPriceVersion() {
|
|
93
|
+
return table.fetchedAt > 0 ? `openrouter-models-${new Date(table.fetchedAt).toISOString().slice(0, 10)}` : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Replace the table (and forget the last attempt); for tests and cache loads. */
|
|
97
|
+
export function setOpenRouterModels(models, fetchedAt = Date.now()) {
|
|
98
|
+
table = { fetchedAt, models };
|
|
99
|
+
attemptedAt = 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load the cached table, then refetch it once it is a day old. A failure keeps the
|
|
104
|
+
* last table and waits ten minutes before trying again; never throws.
|
|
105
|
+
*/
|
|
106
|
+
export async function refreshOpenRouterModels({ home, fetch: fetchImpl = globalThis.fetch, now = Date.now() } = {}) {
|
|
107
|
+
const path = home ? join(home, FILE) : undefined;
|
|
108
|
+
if (table.fetchedAt === 0 && path) {
|
|
109
|
+
try {
|
|
110
|
+
const cached = JSON.parse(readFileSync(path, 'utf8'));
|
|
111
|
+
if (cached?.version === VERSION && Number.isFinite(cached.fetchedAt) && cached.models && typeof cached.models === 'object') table = { fetchedAt: cached.fetchedAt, models: cached.models };
|
|
112
|
+
} catch { /* no usable cache */ }
|
|
113
|
+
}
|
|
114
|
+
if (now - table.fetchedAt < MAX_AGE_MS || now - attemptedAt < RETRY_MS || typeof fetchImpl !== 'function') return table;
|
|
115
|
+
if (pending) return pending;
|
|
116
|
+
attemptedAt = now;
|
|
117
|
+
pending = (async () => {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetchImpl(OPENROUTER_MODELS_URL, { headers: { Accept: 'application/json' } });
|
|
120
|
+
if (!response.ok) return table;
|
|
121
|
+
const models = parseOpenRouterModels(await response.json());
|
|
122
|
+
if (Object.keys(models).length === 0) return table;
|
|
123
|
+
table = { fetchedAt: now, models };
|
|
124
|
+
if (path) {
|
|
125
|
+
mkdirSync(home, { recursive: true });
|
|
126
|
+
writeFileSync(`${path}.tmp`, JSON.stringify({ version: VERSION, ...table }));
|
|
127
|
+
renameSync(`${path}.tmp`, path);
|
|
128
|
+
}
|
|
129
|
+
return table;
|
|
130
|
+
} catch {
|
|
131
|
+
return table;
|
|
132
|
+
} finally {
|
|
133
|
+
pending = undefined;
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
return pending;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The table before a model lookup or a priced call: an empty table waits up to
|
|
141
|
+
* `timeoutMs` for the listing, a stale one refreshes in the background. Waits at
|
|
142
|
+
* most once per retry window, so an unreachable listing never stalls every call.
|
|
143
|
+
*/
|
|
144
|
+
export async function ensureOpenRouterModels({ timeoutMs = FORCED_LOAD_TIMEOUT_MS, ...options } = {}) {
|
|
145
|
+
const refresh = refreshOpenRouterModels(options);
|
|
146
|
+
if (Object.keys(table.models).length > 0) return table;
|
|
147
|
+
let timer;
|
|
148
|
+
await Promise.race([refresh, new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); timer.unref?.(); })]);
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
return table;
|
|
151
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { WebError } from '@deepseek-ai/dsh-web';
|
|
2
|
+
|
|
3
|
+
// Web search through OpenRouter's web plugin, and the provider `ctx.web` is pinned to:
|
|
4
|
+
// it searches with the session's own route, so an OpenRouter session never needs a
|
|
5
|
+
// DeepSeek key and a DeepSeek session never bills OpenRouter.
|
|
6
|
+
export const OPENROUTER_SEARCH_ID = 'openrouter';
|
|
7
|
+
export const ROUTED_SEARCH_ID = 'dscode-web';
|
|
8
|
+
const DEFAULT_MAX_RESULTS = 8;
|
|
9
|
+
|
|
10
|
+
/** Sources from the `url_citation` annotations of an OpenRouter message, de-duplicated by URL. */
|
|
11
|
+
export function citationSources(message) {
|
|
12
|
+
const seen = new Set(), sources = [];
|
|
13
|
+
for (const annotation of Array.isArray(message?.annotations) ? message.annotations : []) {
|
|
14
|
+
const citation = annotation?.type === 'url_citation' ? annotation.url_citation : undefined;
|
|
15
|
+
if (typeof citation?.url !== 'string' || citation.url.length === 0 || seen.has(citation.url)) continue;
|
|
16
|
+
seen.add(citation.url);
|
|
17
|
+
sources.push({
|
|
18
|
+
url: citation.url,
|
|
19
|
+
...(typeof citation.title === 'string' && citation.title.length > 0 ? { title: citation.title } : {}),
|
|
20
|
+
...(typeof citation.content === 'string' && citation.content.length > 0 ? { snippet: citation.content } : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return sources;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const aborted = (signal, error) => new WebError('OpenRouter search was cancelled', 'WEB_ABORTED', { cause: error ?? signal?.reason });
|
|
27
|
+
|
|
28
|
+
/** Exa search through OpenRouter's `web` plugin on a small model whose answer is discarded. */
|
|
29
|
+
export class OpenRouterSearchProvider {
|
|
30
|
+
id = OPENROUTER_SEARCH_ID;
|
|
31
|
+
|
|
32
|
+
/** @param resolveOptions - `{ baseURL, model, resolveApiKey(), fetch? }` for the next search. */
|
|
33
|
+
constructor(resolveOptions) {
|
|
34
|
+
this.resolveOptions = resolveOptions;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
available() {
|
|
38
|
+
return URL.canParse(this.resolveOptions().baseURL);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async search(request, signal) {
|
|
42
|
+
const options = this.resolveOptions();
|
|
43
|
+
const apiKey = await options.resolveApiKey();
|
|
44
|
+
if (signal?.aborted) throw aborted(signal);
|
|
45
|
+
if (!apiKey) throw new WebError('OpenRouter search has no API key: run /login openrouter or set OPENROUTER_API_KEY.', 'WEB_PROVIDER_CREDENTIAL_MISSING');
|
|
46
|
+
const body = {
|
|
47
|
+
model: options.model,
|
|
48
|
+
stream: false,
|
|
49
|
+
messages: [{ role: 'user', content: `Search the web for: ${request.query}` }],
|
|
50
|
+
plugins: [{ id: 'web', engine: 'exa', max_results: request.maxResults ?? DEFAULT_MAX_RESULTS }],
|
|
51
|
+
reasoning: { effort: 'none' },
|
|
52
|
+
max_tokens: 256,
|
|
53
|
+
};
|
|
54
|
+
let response, text;
|
|
55
|
+
try {
|
|
56
|
+
response = await (options.fetch ?? globalThis.fetch)(`${options.baseURL}/chat/completions`, {
|
|
57
|
+
method: 'POST', redirect: 'error', signal,
|
|
58
|
+
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json', accept: 'application/json', 'HTTP-Referer': 'https://github.com/qiz029/dscode', 'X-OpenRouter-Title': 'DSCODE' },
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
});
|
|
61
|
+
text = await response.text();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (signal?.aborted) throw aborted(signal, error);
|
|
64
|
+
throw new WebError(`OpenRouter search request failed: ${error instanceof Error ? error.message : String(error)}`, 'WEB_PROVIDER_ERROR', { cause: error });
|
|
65
|
+
}
|
|
66
|
+
let json;
|
|
67
|
+
try { json = JSON.parse(text); } catch { /* reported below */ }
|
|
68
|
+
if (!response.ok || json?.error || !Array.isArray(json?.choices)) {
|
|
69
|
+
const detail = typeof json?.error?.message === 'string' ? json.error.message : text.slice(0, 200);
|
|
70
|
+
throw new WebError(`OpenRouter search failed (HTTP ${response.status}): ${detail}`, 'WEB_PROVIDER_ERROR');
|
|
71
|
+
}
|
|
72
|
+
return { sources: citationSources(json.choices[0]?.message), truncated: false };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The pinned search provider: OpenRouter for an OpenRouter session, DeepSeek for a
|
|
78
|
+
* DeepSeek one, and otherwise whichever provider has a key (DeepSeek first).
|
|
79
|
+
*/
|
|
80
|
+
export class RoutedSearchProvider {
|
|
81
|
+
id = ROUTED_SEARCH_ID;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param routes - `openrouter` provider, `deepseek()` provider lookup, `currentProvider()` of the
|
|
85
|
+
* calling session, and `hasKey(ref)` for credential presence.
|
|
86
|
+
*/
|
|
87
|
+
constructor(routes) {
|
|
88
|
+
this.routes = routes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
available() {
|
|
92
|
+
return this.routes.openrouter.available() || this.routes.deepseek()?.available() === true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async pick() {
|
|
96
|
+
const { openrouter, deepseek: lookup, currentProvider, hasKey } = this.routes;
|
|
97
|
+
const deepseek = lookup();
|
|
98
|
+
const route = currentProvider();
|
|
99
|
+
if (route === 'openrouter') return openrouter;
|
|
100
|
+
if (route === 'deepseek-official' && deepseek) return deepseek;
|
|
101
|
+
if (deepseek && await hasKey('DEEPSEEK_API_KEY')) return deepseek;
|
|
102
|
+
if (await hasKey('OPENROUTER_API_KEY')) return openrouter;
|
|
103
|
+
return deepseek ?? openrouter;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async search(request, signal) {
|
|
107
|
+
return (await this.pick()).search(request, signal);
|
|
108
|
+
}
|
|
109
|
+
}
|