@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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load-time validation and routed-model policy resolution for compaction-basic.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-compaction-basic/config
|
|
5
|
+
*/
|
|
6
|
+
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm';
|
|
7
|
+
import type { BasicCompactionConfig, ResolvedCompactSpec, ResolvedConfig, ResolvedTargetPolicy } from './types.ts';
|
|
8
|
+
/** Target-specific pressure configuration failure eligible for warning suppression. */
|
|
9
|
+
export declare class TargetPressureConfigError extends Error {
|
|
10
|
+
readonly targetKey: string;
|
|
11
|
+
/**
|
|
12
|
+
* @param targetKey - exact provider/model route used as the warning key.
|
|
13
|
+
* @param message - actionable configuration failure detail.
|
|
14
|
+
*/
|
|
15
|
+
constructor(targetKey: string, message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve and validate service defaults plus exact-target partial overrides.
|
|
19
|
+
* @param config - untrusted plugin configuration after Loader normalization.
|
|
20
|
+
* @returns detached immutable defaults and validated exact-target overrides.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveConfig(config?: BasicCompactionConfig): ResolvedConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Merge the exact provider/model override over the validated default policy.
|
|
25
|
+
* @param config - validated service defaults and override table.
|
|
26
|
+
* @param target - exact durable provider/model route to match.
|
|
27
|
+
* @returns detached immutable policy before model-capacity scaling.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveTargetPolicy(config: ResolvedConfig, target: Pick<LlmCallConfig, 'provider' | 'model'>): ResolvedTargetPolicy;
|
|
30
|
+
/**
|
|
31
|
+
* Scale one routed policy into concrete token budgets for its model capacity.
|
|
32
|
+
* @param policy - merged policy for the exact routed target.
|
|
33
|
+
* @param contextWindow - positive adapter-owned capacity for that target.
|
|
34
|
+
* @returns detached immutable pressure and retention budgets.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveCompactSpec(policy: ResolvedTargetPolicy, contextWindow: number): ResolvedCompactSpec;
|
|
37
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic replay-aware compaction backend.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-compaction-basic
|
|
5
|
+
*/
|
|
6
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
import { CompactionEngine } from '@deepseek-ai/dsh-compaction';
|
|
9
|
+
import type { CompactionResult, CompactionTrigger } from '@deepseek-ai/dsh-compaction';
|
|
10
|
+
import type { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
11
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
12
|
+
import type { CommandId } from '@deepseek-ai/dsh-commands/brand';
|
|
13
|
+
import type { SummarizationInput, SummaryResult } from './summarizer.ts';
|
|
14
|
+
import type { BasicCompactionConfig, ResolvedConfig } from './types.ts';
|
|
15
|
+
export type { BasicCompactionConfig, CompactionPolicyConfig, ModelCompactPolicyConfig, ResolvedCompactSpec, ResolvedConfig, ResolvedRetention, ResolvedTargetPolicy, } from './types.ts';
|
|
16
|
+
/**
|
|
17
|
+
* Dependency-light compaction backend using `ctx.tokenMeter` for pressure,
|
|
18
|
+
* retention, cited source events, and summary-convergence pricing.
|
|
19
|
+
*
|
|
20
|
+
* `summarize()` is the sole subclass customization hook; the replay and durable
|
|
21
|
+
* mutation strategy stays fixed so every pricing decision uses the singleton
|
|
22
|
+
* token meter.
|
|
23
|
+
*/
|
|
24
|
+
export declare class BasicCompactionEngine extends CompactionEngine {
|
|
25
|
+
static inject: string[];
|
|
26
|
+
static Config: z<BasicCompactionConfig>;
|
|
27
|
+
/** Resolved and validated compaction configuration. */
|
|
28
|
+
readonly config: ResolvedConfig;
|
|
29
|
+
private readonly warnedPressureConfigTargets;
|
|
30
|
+
private readonly overflowRetries;
|
|
31
|
+
private readonly overflowAgents;
|
|
32
|
+
constructor(ctx: Context, config?: BasicCompactionConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Register automatic between-step pressure and model-request overflow
|
|
35
|
+
* recovery. `compactIfNeeded` stays dynamically dispatched so subclass
|
|
36
|
+
* overrides are honored at event time.
|
|
37
|
+
*/
|
|
38
|
+
private _registerAutomaticCompaction;
|
|
39
|
+
/**
|
|
40
|
+
* Summarize the replayed conversation region through a direct one-shot
|
|
41
|
+
* `ctx.llm.stream()` call whose prefix reuses the conversation's own system
|
|
42
|
+
* prompt, tools, and messages so the provider's KV cache is not invalidated.
|
|
43
|
+
* Override this sole hook for a template or remote summarizer.
|
|
44
|
+
* @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
|
|
45
|
+
* @param agent - supplies routed-model history, fallback model, and session id.
|
|
46
|
+
* @param signal - optional cancellation forwarded to the adapter.
|
|
47
|
+
* @returns safe text summary blocks and the exact auxiliary call envelope and output.
|
|
48
|
+
*/
|
|
49
|
+
protected summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Compact for replayed step-boundary pressure or one provider-confirmed context
|
|
52
|
+
* overflow. Both triggers price the latest durable routed request envelope;
|
|
53
|
+
* overflow bypasses the normal threshold and retained-tail policy so it can
|
|
54
|
+
* force one useful balanced reduction.
|
|
55
|
+
* @param agent - agent whose latest durable routed request is measured.
|
|
56
|
+
* @param trigger - normal step-boundary pressure or context-overflow recovery.
|
|
57
|
+
* @param signal - live turn cancellation signal forwarded to summarization.
|
|
58
|
+
* @returns the latest summary compaction result, or `null` when no summary ran.
|
|
59
|
+
*/
|
|
60
|
+
compactIfNeeded(agent: Agent, trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Compact one inclusive positional range from the agent-owned surface using
|
|
63
|
+
* the effective token meter for all retention and shrink pricing.
|
|
64
|
+
* @param start - inclusive first surface-node seq.
|
|
65
|
+
* @param end - inclusive last surface-node seq.
|
|
66
|
+
* @param agent - owner of the target session, used by the summarizer.
|
|
67
|
+
* @param signal - optional summarization cancellation signal.
|
|
68
|
+
* @returns the successful durable compaction result.
|
|
69
|
+
*/
|
|
70
|
+
compactRegion(start: SessionSeq, end: SessionSeq, agent: Agent, signal?: AbortSignal): Promise<CompactionResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Force one useful idle-session compaction below the pressure threshold, and
|
|
73
|
+
* resolve only after its standalone marker pair is durably checkpointed.
|
|
74
|
+
* @param agent - idle agent whose next-turn admission this call reserves.
|
|
75
|
+
* @param signal - cancellation scoped to this compaction request.
|
|
76
|
+
* @param sourceCommandId - initiating command identity for presentation correlation.
|
|
77
|
+
* @returns the committed result, or `null` when no safe useful range exists.
|
|
78
|
+
*/
|
|
79
|
+
compactNow(agent: Agent, signal: AbortSignal, sourceCommandId?: CommandId): Promise<CompactionResult | null>;
|
|
80
|
+
/** Bind the effective token meter and dynamically dispatched summarizer hook. */
|
|
81
|
+
private regionDependencies;
|
|
82
|
+
}
|
|
83
|
+
export default BasicCompactionEngine;
|
|
84
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface retention selection and the shared log-recorded compaction
|
|
3
|
+
* transaction for automatic open-turn and manual idle-session compaction.
|
|
4
|
+
*
|
|
5
|
+
* @module @deepseek-ai/dsh-compaction-basic/region
|
|
6
|
+
*/
|
|
7
|
+
import type { CompactionResult } from '@deepseek-ai/dsh-compaction';
|
|
8
|
+
import type { CommandId } from '@deepseek-ai/dsh-commands/brand';
|
|
9
|
+
import type { TokenMeasurement, TokenMeter } from '@deepseek-ai/dsh-token-meter';
|
|
10
|
+
import { SessionSeq, type Session } from '@deepseek-ai/dsh-session';
|
|
11
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
12
|
+
import type { SummarizationInput, SummaryResult } from './summarizer.ts';
|
|
13
|
+
interface RegionDependencies {
|
|
14
|
+
readonly meter: TokenMeter;
|
|
15
|
+
summarize(input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>;
|
|
16
|
+
}
|
|
17
|
+
interface CompactionTransactionOptions {
|
|
18
|
+
/** `current-turn` derives a numbered owner; `null` writes a standalone bracket. */
|
|
19
|
+
readonly owner: 'current-turn' | null;
|
|
20
|
+
/** Surface relationship that must survive asynchronous summarization. */
|
|
21
|
+
readonly stability: 'whole-surface' | 'selected-span';
|
|
22
|
+
/** Optional durability checkpoint after a successfully closed bracket. */
|
|
23
|
+
readonly flush?: () => Promise<void>;
|
|
24
|
+
/** Manual command that initiated this transaction, when present. */
|
|
25
|
+
readonly sourceCommandId?: CommandId;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the next range starting at the first non-system surface node while
|
|
29
|
+
* retaining a priced recent tail and never splitting an assistant
|
|
30
|
+
* tool-call/result pair. A `system/message` at surface node 0 is never inside
|
|
31
|
+
* the range; without one the range starts at node 0.
|
|
32
|
+
* @param session - session supplying authoritative current surface positions.
|
|
33
|
+
* @param measurement - unified pressure and surface measurement from the conversation meter.
|
|
34
|
+
* @param retainTokens - minimum recent tail budget retained verbatim.
|
|
35
|
+
* @returns the inclusive positional seq range to compact, or `null`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function selectCompactableRange(session: Session, measurement: TokenMeasurement, retainTokens: number): {
|
|
38
|
+
start: SessionSeq;
|
|
39
|
+
end: SessionSeq;
|
|
40
|
+
} | null;
|
|
41
|
+
/**
|
|
42
|
+
* Run the single compaction transaction over one selected positional span.
|
|
43
|
+
* Selection and validation are read-only. Idle/log validation and
|
|
44
|
+
* `compaction/start` are synchronously adjacent, so the durable opening marker is
|
|
45
|
+
* the compaction lock before summarization yields. Every later failure makes
|
|
46
|
+
* exactly one `compaction/end` attempt; a failed close deliberately leaves the
|
|
47
|
+
* unmatched start detectable.
|
|
48
|
+
* @param dependencies - conversation meter and dynamically dispatched summarizer hook.
|
|
49
|
+
* @param session - session whose surface is mutated.
|
|
50
|
+
* @param start - inclusive first surface-node seq.
|
|
51
|
+
* @param end - inclusive last surface-node seq.
|
|
52
|
+
* @param agent - agent used by the summarizer.
|
|
53
|
+
* @param options - bracket owner, stability rule, and optional durability checkpoint.
|
|
54
|
+
* @param signal - optional summarization cancellation signal.
|
|
55
|
+
* @returns the successful durable compaction result.
|
|
56
|
+
*/
|
|
57
|
+
export declare function compactSurfaceRegion(dependencies: RegionDependencies, session: Session, start: SessionSeq, end: SessionSeq, agent: Agent, options: CompactionTransactionOptions, signal?: AbortSignal): Promise<CompactionResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Recheck the durable compaction lock after an asynchronous policy decision.
|
|
60
|
+
* @param session - session whose latest marker state is inspected.
|
|
61
|
+
* @param stage - operation label included in the busy diagnostic.
|
|
62
|
+
*/
|
|
63
|
+
export declare function assertNoActiveCompaction(session: Session, stage: string): void;
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=region.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default one-shot summarization and durable checkpoint framing.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-compaction-basic/summarizer
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
import type { ContentBlock, Message, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
9
|
+
interface SummaryConfig {
|
|
10
|
+
readonly summarizationProvider: string;
|
|
11
|
+
readonly summarizationModel: string;
|
|
12
|
+
readonly maxTokens: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The replayed conversation surface the summarizer condenses. Reproducing the
|
|
16
|
+
* last routed request's system prompt, tools, and leading messages verbatim
|
|
17
|
+
* lets the auxiliary call reuse the provider's warm prefix cache; the trailing
|
|
18
|
+
* compaction instruction is then the only novel input.
|
|
19
|
+
*/
|
|
20
|
+
export interface SummarizationInput {
|
|
21
|
+
/** The conversation's tool schemas, reused for prefix-cache alignment; absent when the request carried none. */
|
|
22
|
+
readonly tools?: readonly ToolSchema[];
|
|
23
|
+
/** The derived system head, when present, followed by the shadowed region in surface order. */
|
|
24
|
+
readonly messages: readonly Message[];
|
|
25
|
+
}
|
|
26
|
+
/** Safe summary content plus the exact auxiliary call envelope recorded with it. */
|
|
27
|
+
export type SummaryResult = {
|
|
28
|
+
summary: ContentBlock[];
|
|
29
|
+
provider: string;
|
|
30
|
+
model: string;
|
|
31
|
+
maxTokens?: number;
|
|
32
|
+
/** Provider-reported usage for this summarization request. */
|
|
33
|
+
usage?: TokenUsage;
|
|
34
|
+
} & ({
|
|
35
|
+
/** Complete provider output before the text-only summary projection. */
|
|
36
|
+
rawOutput: ContentBlock[];
|
|
37
|
+
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
|
|
38
|
+
llmStreamCall: true;
|
|
39
|
+
} | {
|
|
40
|
+
/** Optional complete output from an unmarked template, remote, or other summarizer. */
|
|
41
|
+
rawOutput?: ContentBlock[];
|
|
42
|
+
/** An unmarked result does not identify a call through this context's LLM seam. */
|
|
43
|
+
llmStreamCall?: never;
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Run the default cache-reusing `ctx.llm.stream()` summarization call: replay
|
|
47
|
+
* the conversation prefix, then append the compaction instruction as the final
|
|
48
|
+
* user message so the provider's warm prefix cache is reused.
|
|
49
|
+
* @param ctx - context providing the LLM service.
|
|
50
|
+
* @param config - resolved backend configuration.
|
|
51
|
+
* @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
|
|
52
|
+
* @param agent - supplies routed-model history, fallback model, and session id.
|
|
53
|
+
* @param signal - optional cancellation forwarded to the adapter.
|
|
54
|
+
* @returns safe text-only summary blocks and the exact call envelope and output.
|
|
55
|
+
*/
|
|
56
|
+
export declare function summarizeWithLlm(ctx: Context, config: SummaryConfig, input: SummarizationInput, agent: Agent, signal?: AbortSignal): Promise<SummaryResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Wrap raw summary blocks in the durable checkpoint framing.
|
|
59
|
+
* @param summary - safe text-only model output.
|
|
60
|
+
* @returns content for the synthesized replacement user message.
|
|
61
|
+
*/
|
|
62
|
+
export declare function frameSummary(summary: readonly ContentBlock[]): ContentBlock[];
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=summarizer.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration vocabulary for the replay-aware basic compaction backend.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-compaction-basic/types
|
|
5
|
+
*/
|
|
6
|
+
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm';
|
|
7
|
+
/** Policy fields shared by the default policy and exact model overrides. */
|
|
8
|
+
export interface CompactionPolicyConfig {
|
|
9
|
+
/** Compact at this fraction of the model's context window. Defaults to `0.8`. */
|
|
10
|
+
thresholdRatio?: number;
|
|
11
|
+
/** Recent context retained as a fraction of the model's window. Defaults to `0.16`. */
|
|
12
|
+
retainRatio?: number;
|
|
13
|
+
/** Absolute recent-context budget; mutually exclusive with `retainRatio`. */
|
|
14
|
+
retainTokens?: number;
|
|
15
|
+
/** Summary provider; set together with `summarizationModel`, or inherit the conversation target. */
|
|
16
|
+
summarizationProvider?: string;
|
|
17
|
+
/** Summary model; set together with `summarizationProvider`, or inherit the conversation target. */
|
|
18
|
+
summarizationModel?: string;
|
|
19
|
+
/** Provider generation cap for summarization. Defaults to `8192`. */
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
/** Extra attempts after the first compaction when pressure remains above threshold. Defaults to `1`. */
|
|
22
|
+
compactionRetries?: number;
|
|
23
|
+
/** Maximum retries after canonical context overflow; `0` disables recovery. Defaults to `1`. */
|
|
24
|
+
maxOverflowRetries?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Exact provider/model override merged over the default compaction policy. */
|
|
27
|
+
export interface ModelCompactPolicyConfig extends CompactionPolicyConfig {
|
|
28
|
+
/** Registered provider route to match. */
|
|
29
|
+
provider: string;
|
|
30
|
+
/** Exact routed model id to match within `provider`. */
|
|
31
|
+
model: string;
|
|
32
|
+
}
|
|
33
|
+
/** Basic compaction configuration with an optional exact-target policy table. */
|
|
34
|
+
export interface BasicCompactionConfig extends CompactionPolicyConfig {
|
|
35
|
+
/** Exact provider/model overrides; duplicate targets fail plugin load. */
|
|
36
|
+
modelPolicies?: ModelCompactPolicyConfig[];
|
|
37
|
+
/** Enable automatic step-boundary pressure and overflow-recovery listeners. Defaults to `true`. */
|
|
38
|
+
auto?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Exactly one validated retention form. */
|
|
41
|
+
export type ResolvedRetention = {
|
|
42
|
+
readonly retainRatio: number;
|
|
43
|
+
readonly retainTokens?: never;
|
|
44
|
+
} | {
|
|
45
|
+
readonly retainRatio?: never;
|
|
46
|
+
readonly retainTokens: number;
|
|
47
|
+
};
|
|
48
|
+
/** Validated policy fields shared before and after exact-target matching. */
|
|
49
|
+
interface ResolvedPolicyFields {
|
|
50
|
+
readonly thresholdRatio: number;
|
|
51
|
+
readonly summarizationProvider: string;
|
|
52
|
+
readonly summarizationModel: string;
|
|
53
|
+
readonly maxTokens: number;
|
|
54
|
+
readonly compactionRetries: number;
|
|
55
|
+
readonly maxOverflowRetries: number;
|
|
56
|
+
}
|
|
57
|
+
/** Validated immutable config whose target-specific defaults remain unresolved. */
|
|
58
|
+
export type ResolvedConfig = ResolvedPolicyFields & ResolvedRetention & {
|
|
59
|
+
readonly modelPolicies: readonly Readonly<ModelCompactPolicyConfig>[];
|
|
60
|
+
readonly auto: boolean;
|
|
61
|
+
};
|
|
62
|
+
/** Fully merged policy for one routed conversation target, before capacity scaling. */
|
|
63
|
+
export type ResolvedTargetPolicy = ResolvedPolicyFields & ResolvedRetention & {
|
|
64
|
+
readonly target: Pick<LlmCallConfig, 'provider' | 'model'>;
|
|
65
|
+
};
|
|
66
|
+
/** One routed model's concrete pressure and retention budget. */
|
|
67
|
+
export type ResolvedCompactSpec = Omit<ResolvedTargetPolicy, 'retainRatio' | 'retainTokens'> & {
|
|
68
|
+
readonly contextWindow: number;
|
|
69
|
+
readonly thresholdTokens: number;
|
|
70
|
+
readonly retainTokens: number;
|
|
71
|
+
};
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1,83 +1,42 @@
|
|
|
1
1
|
// Model providers `/provider` switches between. DeepSeek's official API is the
|
|
2
|
-
// native `llm-deepseek` route; OpenRouter
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
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' },
|
|
8
|
-
|
|
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' },
|
|
9
10
|
]);
|
|
10
11
|
|
|
12
|
+
// The pi-ai adapter served OpenRouter until 0.7.6, from this settings section.
|
|
11
13
|
const PI_AI_NS = 'llm-pi-ai';
|
|
12
14
|
|
|
13
15
|
// OpenRouter serves DeepSeek V4 thinking as none/high/xhigh. DeepSeek itself
|
|
14
16
|
// answers `low` as high and `max` as xhigh, so the route offers the official
|
|
15
17
|
// low/high/max detents (and Ultra on top of max) with the wire spelling OpenRouter
|
|
16
18
|
// accepts; session cards and delegated children that ask for `low` keep working.
|
|
17
|
-
const OPENROUTER_EFFORTS = Object.freeze({ off: 'none', low: 'high', high: 'high', max: 'xhigh' });
|
|
19
|
+
export const OPENROUTER_EFFORTS = Object.freeze({ off: 'none', low: 'high', high: 'high', max: 'xhigh' });
|
|
18
20
|
|
|
19
|
-
/** The DeepSeek models the OpenRouter route
|
|
21
|
+
/** The DeepSeek models the OpenRouter route serves, with their official-route counterparts. */
|
|
20
22
|
export const OPENROUTER_MODELS = Object.freeze([
|
|
21
23
|
{ id: 'deepseek/deepseek-v4-flash', name: 'DeepSeek V4 Flash', official: ['deepseek-flash', 'deepseek-v4-flash'] },
|
|
22
24
|
{ id: 'deepseek/deepseek-v4-pro', name: 'DeepSeek V4 Pro', official: ['deepseek-v4-pro'] },
|
|
23
25
|
{ id: 'deepseek/deepseek-v4-flash-vision-exp', name: 'DeepSeek V4 Flash Vision Exp', official: ['deepseek-v4-flash-vision-exp'] },
|
|
24
26
|
]);
|
|
25
27
|
|
|
26
|
-
/** The `llm-pi-ai` profile `/provider openrouter` writes: pi-ai's whole OpenRouter catalog. */
|
|
27
|
-
export function openRouterProfile() {
|
|
28
|
-
return {
|
|
29
|
-
displayName: 'OpenRouter',
|
|
30
|
-
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
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',
|
|
44
|
-
reasoning: 'high',
|
|
45
|
-
models: OPENROUTER_MODELS.map(({ id, name }) => ({ id, name, reasoningEfforts: { ...OPENROUTER_EFFORTS } })),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
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
28
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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.
|
|
74
33
|
* @returns whether the settings changed.
|
|
75
34
|
*/
|
|
76
35
|
export async function migrateOpenRouterProfile(settings) {
|
|
77
36
|
try {
|
|
78
37
|
const descriptor = settings?.describe?.({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
79
|
-
if (!descriptor || settings.writable !== true ||
|
|
80
|
-
await settings.mutate(PI_AI_NS, [{ op: '
|
|
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);
|
|
81
40
|
return true;
|
|
82
41
|
} catch {
|
|
83
42
|
return false;
|
|
@@ -156,25 +115,16 @@ export function credentialState(row) {
|
|
|
156
115
|
}
|
|
157
116
|
|
|
158
117
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* while the narrow profile earlier builds wrote is replaced.
|
|
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.
|
|
162
120
|
* @param settings - the host settings service.
|
|
163
121
|
* @returns whether the settings changed.
|
|
164
122
|
*/
|
|
165
123
|
export async function ensureProviderRoute(settings, provider) {
|
|
166
|
-
|
|
167
|
-
if (typeof settings?.describe !== 'function' || typeof settings.mutate !== 'function') throw new Error('settings are unavailable; OpenRouter cannot be configured in this profile');
|
|
168
|
-
const descriptor = settings.describe({ redactSecrets: true }).find(entry => entry.ns === PI_AI_NS);
|
|
169
|
-
if (!descriptor) throw new Error('the OpenRouter adapter (llm-pi-ai) is not mounted in this profile');
|
|
170
|
-
const existing = descriptor.value?.providers?.openrouter;
|
|
171
|
-
if (existing !== undefined) return migrateOpenRouterProfile(settings);
|
|
172
|
-
if (settings.writable !== true) throw new Error('settings are read-only; OpenRouter cannot be configured here');
|
|
173
|
-
await settings.mutate(PI_AI_NS, [{ op: 'set', path: ['providers', 'openrouter'], value: openRouterProfile() }], descriptor.revision);
|
|
174
|
-
return true;
|
|
124
|
+
return provider === 'openrouter' ? migrateOpenRouterProfile(settings) : false;
|
|
175
125
|
}
|
|
176
126
|
|
|
177
|
-
/** Wait for a
|
|
127
|
+
/** Wait for a route's models to reach the model directory. */
|
|
178
128
|
export async function waitForModels(loadModels, provider, { attempts = 30, delayMs = 100 } = {}) {
|
|
179
129
|
let directory;
|
|
180
130
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// OpenRouter account facts for /openrouter and the footer balance. The inference key
|
|
2
|
+
// reads the account credits and its own limit and usage; an optional management key,
|
|
3
|
+
// which cannot call models, adds every key's usage and the last 30 days of spend.
|
|
4
|
+
// This directory also ships beside the TUI, so the module imports nothing.
|
|
5
|
+
export const OPENROUTER_API = 'https://openrouter.ai/api/v1';
|
|
6
|
+
export const MANAGEMENT_REF = 'OPENROUTER_MANAGEMENT_KEY';
|
|
7
|
+
|
|
8
|
+
const finite = value => {
|
|
9
|
+
const number = Number(value);
|
|
10
|
+
return value !== null && value !== undefined && value !== '' && Number.isFinite(number) ? number : undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class OpenRouterAccountError extends Error {
|
|
14
|
+
constructor(message, status) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'OpenRouterAccountError';
|
|
17
|
+
this.status = status;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function get(path, key, { fetch: fetchImpl = globalThis.fetch, signal } = {}) {
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await fetchImpl(`${OPENROUTER_API}${path}`, { headers: { authorization: `Bearer ${key}`, accept: 'application/json' }, signal });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new OpenRouterAccountError(`OpenRouter is unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
let body;
|
|
29
|
+
try { body = await response.json(); } catch { body = undefined; }
|
|
30
|
+
if (!response.ok) throw new OpenRouterAccountError(typeof body?.error?.message === 'string' ? body.error.message : `HTTP ${response.status}`, response.status);
|
|
31
|
+
return body;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Account credits from a `/credits` body, or undefined when it carries none. */
|
|
35
|
+
export function creditsOf(body) {
|
|
36
|
+
const total = finite(body?.data?.total_credits), used = finite(body?.data?.total_usage);
|
|
37
|
+
if (total === undefined || used === undefined || total < 0 || used < 0) return undefined;
|
|
38
|
+
return { total, used, remaining: Math.max(0, total - used) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Remaining USD account credits from a `/credits` body: purchased minus used. */
|
|
42
|
+
export function parseOpenRouterCredits(body) {
|
|
43
|
+
return creditsOf(body)?.remaining ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Remaining USD credit limit from a `/key` body; null when the key has no limit. */
|
|
47
|
+
export function parseOpenRouterKeyRemaining(body) {
|
|
48
|
+
const remaining = body?.data?.limit_remaining;
|
|
49
|
+
if (remaining == null) return null;
|
|
50
|
+
const value = Number(remaining);
|
|
51
|
+
return Number.isFinite(value) ? Math.max(0, value) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function keyOf(raw) {
|
|
55
|
+
return {
|
|
56
|
+
label: typeof raw?.label === 'string' ? raw.label : undefined,
|
|
57
|
+
name: typeof raw?.name === 'string' && raw.name.length > 0 ? raw.name : undefined,
|
|
58
|
+
disabled: raw?.disabled === true,
|
|
59
|
+
limit: finite(raw?.limit),
|
|
60
|
+
limitRemaining: finite(raw?.limit_remaining),
|
|
61
|
+
usage: finite(raw?.usage),
|
|
62
|
+
usageDaily: finite(raw?.usage_daily),
|
|
63
|
+
usageWeekly: finite(raw?.usage_weekly),
|
|
64
|
+
usageMonthly: finite(raw?.usage_monthly),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Verify a management key before it is stored. Account activity is the reading OpenRouter
|
|
70
|
+
* refuses an inference key; `/credits`, though documented as management-only, serves both.
|
|
71
|
+
* @throws OpenRouterAccountError with a message fit for the key prompt.
|
|
72
|
+
*/
|
|
73
|
+
export async function verifyManagementKey(key, options) {
|
|
74
|
+
try {
|
|
75
|
+
await get('/activity', key, options);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof OpenRouterAccountError && (error.status === 401 || error.status === 403)) {
|
|
78
|
+
throw new OpenRouterAccountError('This is not a management key: OpenRouter refused it for account data. Create one under Settings → Management keys.', error.status);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The `/activity` rows (last 30 completed UTC days) as totals and the top models with the providers that served them. */
|
|
85
|
+
export function summarizeActivity(rows, top = 5) {
|
|
86
|
+
const models = new Map(), days = new Set();
|
|
87
|
+
let usage = 0, requests = 0;
|
|
88
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
89
|
+
if (typeof row?.model !== 'string') continue;
|
|
90
|
+
const cost = finite(row.usage) ?? 0, count = finite(row.requests) ?? 0;
|
|
91
|
+
usage += cost;
|
|
92
|
+
requests += count;
|
|
93
|
+
if (typeof row.date === 'string') days.add(row.date);
|
|
94
|
+
const entry = models.get(row.model) ?? { model: row.model, usage: 0, requests: 0, providers: new Map() };
|
|
95
|
+
entry.usage += cost;
|
|
96
|
+
entry.requests += count;
|
|
97
|
+
const name = typeof row.provider_name === 'string' && row.provider_name.length > 0 ? row.provider_name : 'unknown';
|
|
98
|
+
const served = entry.providers.get(name) ?? { name, usage: 0, requests: 0 };
|
|
99
|
+
served.usage += cost;
|
|
100
|
+
served.requests += count;
|
|
101
|
+
entry.providers.set(name, served);
|
|
102
|
+
models.set(row.model, entry);
|
|
103
|
+
}
|
|
104
|
+
const ranked = [...models.values()].sort((left, right) => right.usage - left.usage || right.requests - left.requests).slice(0, top)
|
|
105
|
+
.map(entry => ({ ...entry, providers: [...entry.providers.values()].sort((left, right) => right.usage - left.usage) }));
|
|
106
|
+
return { usage, requests, days: days.size, modelCount: models.size, models: ranked };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Everything /openrouter shows. Each section settles on its own: `{ value }`, `{ error }`,
|
|
111
|
+
* or undefined when the key it needs is missing.
|
|
112
|
+
*/
|
|
113
|
+
export async function loadOpenRouterAccount({ apiKey, managementKey, fetch, signal } = {}) {
|
|
114
|
+
const options = { fetch, signal };
|
|
115
|
+
const section = (key, run) => key ? run().then(value => ({ value }), error => ({ error: error instanceof Error ? error.message : String(error) })) : Promise.resolve(undefined);
|
|
116
|
+
const [key, credits, keys, activity] = await Promise.all([
|
|
117
|
+
section(apiKey, async () => keyOf((await get('/key', apiKey, options))?.data)),
|
|
118
|
+
section(managementKey ?? apiKey, async () => {
|
|
119
|
+
const credits = creditsOf(await get('/credits', managementKey ?? apiKey, options));
|
|
120
|
+
if (!credits) throw new Error('OpenRouter returned no account credits');
|
|
121
|
+
return credits;
|
|
122
|
+
}),
|
|
123
|
+
section(managementKey, async () => {
|
|
124
|
+
const body = await get('/keys', managementKey, options);
|
|
125
|
+
return (Array.isArray(body?.data) ? body.data : []).map(keyOf);
|
|
126
|
+
}),
|
|
127
|
+
section(managementKey, async () => summarizeActivity((await get('/activity', managementKey, options))?.data)),
|
|
128
|
+
]);
|
|
129
|
+
return { hasApiKey: Boolean(apiKey), hasManagementKey: Boolean(managementKey), key, credits, keys, activity };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const money = value => Number.isFinite(value) ? `$${value.toFixed(2)}` : '$--';
|
|
133
|
+
const limitText = key => key.limit === undefined ? 'no limit' : `limit ${money(key.limit)}, ${money(key.limitRemaining)} left`;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The panel's lines for a loaded account.
|
|
137
|
+
* @returns `{ text, tone }` rows; tone is `title`, `value`, `dim` or `error`.
|
|
138
|
+
*/
|
|
139
|
+
export function openRouterAccountLines(account, { maxKeys = 5 } = {}) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
const push = (text, tone = 'value') => lines.push({ text, tone });
|
|
142
|
+
if (!account.hasApiKey) push('No OpenRouter API key: run /login openrouter.', 'error');
|
|
143
|
+
if (account.credits?.value) {
|
|
144
|
+
const { remaining, total, used } = account.credits.value;
|
|
145
|
+
push(`Account balance ${money(remaining)} · credits ${money(total)} · used ${money(used)}`, 'title');
|
|
146
|
+
} else if (account.credits?.error) push(`Account balance unavailable: ${account.credits.error}`, 'error');
|
|
147
|
+
else push('Account balance $--', 'dim');
|
|
148
|
+
if (account.key?.value) {
|
|
149
|
+
const key = account.key.value;
|
|
150
|
+
push(`This key ${key.label ?? 'unnamed'} · ${limitText(key)}`, 'title');
|
|
151
|
+
push(` today ${money(key.usageDaily)} · week ${money(key.usageWeekly)} · month ${money(key.usageMonthly)}`, 'dim');
|
|
152
|
+
} else if (account.key?.error) push(`This key unavailable: ${account.key.error}`, 'error');
|
|
153
|
+
if (!account.hasManagementKey) push('API keys and 30-day spend press m to add a management key', 'dim');
|
|
154
|
+
if (account.keys?.value) {
|
|
155
|
+
const keys = [...account.keys.value].sort((left, right) => (right.usageMonthly ?? 0) - (left.usageMonthly ?? 0));
|
|
156
|
+
push(`API keys (${keys.length})`, 'title');
|
|
157
|
+
for (const key of keys.slice(0, maxKeys)) {
|
|
158
|
+
const current = account.key?.value?.label !== undefined && key.label === account.key.value.label;
|
|
159
|
+
push(` ${key.name ?? key.label ?? 'unnamed'}${current ? ' (this key)' : ''}${key.disabled ? ' · disabled' : ''} · today ${money(key.usageDaily)} · month ${money(key.usageMonthly)} · ${limitText(key)}`, key.disabled ? 'dim' : 'value');
|
|
160
|
+
}
|
|
161
|
+
if (keys.length > maxKeys) push(` +${keys.length - maxKeys} more`, 'dim');
|
|
162
|
+
} else if (account.keys?.error) push(`API keys unavailable: ${account.keys.error}`, 'error');
|
|
163
|
+
if (account.activity?.value) {
|
|
164
|
+
const activity = account.activity.value;
|
|
165
|
+
push(`Last 30 days ${money(activity.usage)} · ${activity.requests} requests · ${activity.modelCount} models`, 'title');
|
|
166
|
+
for (const model of activity.models) {
|
|
167
|
+
push(` ${model.model} · ${money(model.usage)} · ${model.requests} req · ${model.providers.map(provider => `${provider.name} ${money(provider.usage)}`).join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
} else if (account.activity?.error) push(`Activity unavailable: ${account.activity.error}`, 'error');
|
|
170
|
+
return lines;
|
|
171
|
+
}
|