@love-moon/ai-sdk 0.7.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/context-files.d.ts +16 -0
  5. package/dist/context-files.js +86 -0
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.js +4 -1
  8. package/dist/manager/index.d.ts +1 -0
  9. package/dist/manager/index.js +1 -0
  10. package/dist/manager/install.js +29 -2
  11. package/dist/manager/manager.d.ts +5 -1
  12. package/dist/manager/manager.js +8 -0
  13. package/dist/manager/network.js +4 -2
  14. package/dist/manager/quota/dsh.d.ts +55 -0
  15. package/dist/manager/quota/dsh.js +146 -0
  16. package/dist/manager/types.d.ts +26 -1
  17. package/dist/media-adapters.d.ts +34 -0
  18. package/dist/media-adapters.js +57 -0
  19. package/dist/media-input.d.ts +21 -0
  20. package/dist/media-input.js +197 -0
  21. package/dist/providers/chat-web-session.d.ts +7 -1
  22. package/dist/providers/chat-web-session.js +12 -1
  23. package/dist/providers/claude-agent-sdk-session.d.ts +10 -1
  24. package/dist/providers/claude-agent-sdk-session.js +24 -4
  25. package/dist/providers/codex-app-server-session.d.ts +12 -1
  26. package/dist/providers/codex-app-server-session.js +49 -20
  27. package/dist/providers/codex-exec-session.d.ts +8 -3
  28. package/dist/providers/codex-exec-session.js +13 -13
  29. package/dist/providers/copilot-sdk-session.d.ts +6 -1
  30. package/dist/providers/copilot-sdk-session.js +10 -3
  31. package/dist/providers/dsh-sdk-session.d.ts +194 -0
  32. package/dist/providers/dsh-sdk-session.js +951 -0
  33. package/dist/providers/kimi-cli-session.d.ts +11 -1
  34. package/dist/providers/kimi-cli-session.js +58 -4
  35. package/dist/providers/kimi-print-session.d.ts +10 -3
  36. package/dist/providers/kimi-print-session.js +23 -53
  37. package/dist/providers/opencode-sdk-session.d.ts +6 -1
  38. package/dist/providers/opencode-sdk-session.js +11 -9
  39. package/dist/resume/dsh.d.ts +23 -0
  40. package/dist/resume/dsh.js +122 -0
  41. package/dist/resume/index.js +2 -0
  42. package/dist/session-factory.d.ts +3 -1
  43. package/dist/session-factory.js +5 -1
  44. package/dsh/cordis.yml +100 -0
  45. package/package.json +24 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a15b55d: Add per-turn multi-image and local context-file inputs, plus authenticated attachment materialization from Conductor Web to the executing daemon.
8
+
9
+ ## 0.8.0
10
+
3
11
  ## 0.7.7
4
12
 
5
13
  ## 0.7.6
@@ -23,6 +23,7 @@ export const KIMI_CLI_WIRE_VARIANT: "kimi-cli-wire";
23
23
  export const KIMI_CLI_PRINT_VARIANT: "kimi-cli-print";
24
24
  export const OPENCODE_SDK_VARIANT: "opencode-sdk";
25
25
  export const CHAT_WEB_SESSION_VARIANT: "chat-web-session";
26
+ export const DSH_SDK_VARIANT: "dsh-sdk";
26
27
  /**
27
28
  * @typedef {Object} BuiltInBackendEntry
28
29
  * @property {string} backend Canonical backend name.
@@ -19,6 +19,7 @@ export const KIMI_CLI_WIRE_VARIANT = "kimi-cli-wire";
19
19
  export const KIMI_CLI_PRINT_VARIANT = "kimi-cli-print";
20
20
  export const OPENCODE_SDK_VARIANT = "opencode-sdk";
21
21
  export const CHAT_WEB_SESSION_VARIANT = "chat-web-session";
22
+ export const DSH_SDK_VARIANT = "dsh-sdk";
22
23
  /**
23
24
  * @typedef {Object} BuiltInBackendEntry
24
25
  * @property {string} backend Canonical backend name.
@@ -60,6 +61,11 @@ export const BUILT_IN_BACKENDS = [
60
61
  aliases: ["chat-web", "chatweb", "chat_web", "web-chat"],
61
62
  defaultVariant: CHAT_WEB_SESSION_VARIANT,
62
63
  },
64
+ {
65
+ backend: "dsh",
66
+ aliases: ["dsh", "deepseek-harness"],
67
+ defaultVariant: DSH_SDK_VARIANT,
68
+ },
63
69
  ];
64
70
  const ALIAS_TO_BACKEND = new Map();
65
71
  for (const entry of BUILT_IN_BACKENDS) {
@@ -0,0 +1,16 @@
1
+ export function normalizeContextFiles(value: any): {
2
+ path: any;
3
+ name: string;
4
+ mimeType: any;
5
+ sizeBytes: number;
6
+ }[];
7
+ export function appendContextFilesToPrompt(promptText: any, value: any): {
8
+ prompt: string;
9
+ contextFiles: {
10
+ path: any;
11
+ name: string;
12
+ mimeType: any;
13
+ sizeBytes: number;
14
+ }[];
15
+ };
16
+ export function createContextFileError(message: any, extras?: {}): Error;
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const MAX_CONTEXT_FILES = 20;
4
+ const MAX_CONTEXT_FILE_BYTES = 100 * 1024 * 1024;
5
+ const MAX_TOTAL_CONTEXT_BYTES = 200 * 1024 * 1024;
6
+ export function normalizeContextFiles(value) {
7
+ if (value === undefined || value === null)
8
+ return [];
9
+ if (!Array.isArray(value))
10
+ throw createContextFileError("contextFiles must be an array");
11
+ if (value.length > MAX_CONTEXT_FILES) {
12
+ throw createContextFileError(`contextFiles cannot contain more than ${MAX_CONTEXT_FILES} files`, {
13
+ reason: "context_file_limit_exceeded",
14
+ });
15
+ }
16
+ let totalBytes = 0;
17
+ return value.map((item, index) => {
18
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
19
+ throw createContextFileError(`contextFiles[${index}] must be an object`, { contextFileIndex: index });
20
+ }
21
+ const filePath = typeof item.path === "string" ? item.path.trim() : "";
22
+ if (!filePath || !path.isAbsolute(filePath)) {
23
+ throw createContextFileError(`contextFiles[${index}] path must be absolute`, { contextFileIndex: index });
24
+ }
25
+ let stat;
26
+ try {
27
+ const linkStat = fs.lstatSync(filePath);
28
+ if (linkStat.isSymbolicLink())
29
+ throw new Error("symbolic links are not allowed");
30
+ stat = fs.statSync(filePath);
31
+ }
32
+ catch {
33
+ throw createContextFileError(`contextFiles[${index}] file does not exist`, { contextFileIndex: index });
34
+ }
35
+ if (!stat.isFile()) {
36
+ throw createContextFileError(`contextFiles[${index}] path must reference a file`, { contextFileIndex: index });
37
+ }
38
+ if (stat.size > MAX_CONTEXT_FILE_BYTES) {
39
+ throw createContextFileError(`contextFiles[${index}] exceeds the file size limit`, {
40
+ reason: "context_file_limit_exceeded",
41
+ contextFileIndex: index,
42
+ maxBytes: MAX_CONTEXT_FILE_BYTES,
43
+ });
44
+ }
45
+ totalBytes += stat.size;
46
+ if (totalBytes > MAX_TOTAL_CONTEXT_BYTES) {
47
+ throw createContextFileError("contextFiles exceeds the total size limit", {
48
+ reason: "context_file_limit_exceeded",
49
+ maxBytes: MAX_TOTAL_CONTEXT_BYTES,
50
+ });
51
+ }
52
+ return {
53
+ path: filePath,
54
+ name: typeof item.name === "string" && item.name.trim() ? path.basename(item.name.trim()) : path.basename(filePath),
55
+ mimeType: typeof item.mimeType === "string" && item.mimeType.trim()
56
+ ? item.mimeType.trim().toLowerCase()
57
+ : "application/octet-stream",
58
+ sizeBytes: stat.size,
59
+ };
60
+ });
61
+ }
62
+ export function appendContextFilesToPrompt(promptText, value) {
63
+ const files = normalizeContextFiles(value);
64
+ const prompt = String(promptText || "").trim();
65
+ if (files.length === 0)
66
+ return { prompt, contextFiles: files };
67
+ const manifest = files.map((file, index) => [
68
+ `${index + 1}. ${file.name}`,
69
+ ` Path: ${file.path}`,
70
+ ` MIME: ${file.mimeType}`,
71
+ ].join("\n")).join("\n\n");
72
+ return {
73
+ contextFiles: files,
74
+ prompt: [
75
+ prompt,
76
+ "The following user-provided context files are available locally. Read them when relevant to the request:",
77
+ manifest,
78
+ ].filter(Boolean).join("\n\n"),
79
+ };
80
+ }
81
+ export function createContextFileError(message, extras = {}) {
82
+ const error = new Error(message);
83
+ error.reason = extras.reason || "invalid_context_file";
84
+ Object.assign(error, extras);
85
+ return error;
86
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
2
+ export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
2
3
  export { createAiSession, RemoteAiSession } from "./client.js";
4
+ export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
5
+ export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia } from "./media-input.js";
3
6
  export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
4
- export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount } from "./manager/index.js";
7
+ export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getDshQuota, readCachedDshQuota, resolveDeepSeekCredential, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount } from "./manager/index.js";
5
8
  export { getExternalProviderDescriptor, getExternalProviderRegistry, resetExternalProviderRegistryForTests, resolveExternalBackend } from "./external-provider-registry.js";
6
9
  export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget } from "./resume/index.js";
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  export { createAiSession, RemoteAiSession } from "./client.js";
2
2
  export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
3
+ export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
4
+ export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
5
+ export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia, } from "./media-input.js";
3
6
  export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus, } from "./shared.js";
4
- export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount, } from "./manager/index.js";
7
+ export { AiManager, accountNameFromPath, checkInstall, checkInstallAll, checkNetwork, checkNetworkAll, getClaudeQuota, getCodexQuota, getCopilotQuota, getCurrentCodexAccount, getDshQuota, readCachedDshQuota, resolveDeepSeekCredential, getExternalQuota, getExternalQuotaList, getKimiQuota, listCodexAccounts, loadAiManagerConfig, normalizeExternalQuota, normalizeExternalQuotaList, parseAuthFile, parseAuthFileContents, parseCopilotQuotaSnapshots, readCachedCodexQuota, resolveClaudeCredential, switchCodexAccount, } from "./manager/index.js";
5
8
  export { getExternalProviderDescriptor, getExternalProviderRegistry, resetExternalProviderRegistryForTests, resolveExternalBackend, } from "./external-provider-registry.js";
6
9
  export { resolveResumeContext, buildResumeArgsForBackend, resumeProviderForBackend, findSessionPath, resolveSessionRunDirectory, inspectResumeTarget, } from "./resume/index.js";
@@ -8,5 +8,6 @@ export { getCodexQuota, readCachedCodexQuota, type GetCodexQuotaOptions, } from
8
8
  export { getClaudeQuota, resolveClaudeCredential, type GetClaudeQuotaOptions, type ClaudeCredential, } from "./quota/claude.js";
9
9
  export { getKimiQuota, type GetKimiQuotaOptions } from "./quota/kimi.js";
10
10
  export { getCopilotQuota, parseCopilotQuotaSnapshots, type GetCopilotQuotaOptions, } from "./quota/copilot.js";
11
+ export { getDshQuota, readCachedDshQuota, resolveDeepSeekCredential, parseDshBalance, normalizeDshBaseUrl, type DshQuotaOptions, } from "./quota/dsh.js";
11
12
  export { getExternalQuota, getExternalQuotaList, normalizeExternalQuota, normalizeExternalQuotaList, type GetExternalQuotaListOptions, type GetExternalQuotaOptions, } from "./quota/external.js";
12
13
  export type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, ExternalQuota, ExternalQuotaList, KimiQuota, CopilotQuota, CopilotQuotaSnapshot, InstallStatus, NetworkStatus, QuotaSource, QuotaWindow, SwitchResult, Tool, } from "./types.js";
@@ -8,4 +8,5 @@ export { getCodexQuota, readCachedCodexQuota, } from "./quota/codex.js";
8
8
  export { getClaudeQuota, resolveClaudeCredential, } from "./quota/claude.js";
9
9
  export { getKimiQuota } from "./quota/kimi.js";
10
10
  export { getCopilotQuota, parseCopilotQuotaSnapshots, } from "./quota/copilot.js";
11
+ export { getDshQuota, readCachedDshQuota, resolveDeepSeekCredential, parseDshBalance, normalizeDshBaseUrl, } from "./quota/dsh.js";
11
12
  export { getExternalQuota, getExternalQuotaList, normalizeExternalQuota, normalizeExternalQuotaList, } from "./quota/external.js";
@@ -46,6 +46,9 @@ export async function checkInstall(tool, opts = {}) {
46
46
  if (tool === "copilot") {
47
47
  return checkCopilotSdkInstall();
48
48
  }
49
+ if (tool === "dsh") {
50
+ return checkDshSdkInstall();
51
+ }
49
52
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
50
53
  const bin = tool; // codex / claude
51
54
  const path = await which(bin, timeoutMs);
@@ -69,13 +72,14 @@ function extractSemver(line) {
69
72
  return m ? m[0] : undefined;
70
73
  }
71
74
  export async function checkInstallAll(opts) {
72
- const [codex, claude, kimi, copilot] = await Promise.all([
75
+ const [codex, claude, kimi, copilot, dsh] = await Promise.all([
73
76
  checkInstall("codex", opts),
74
77
  checkInstall("claude", opts),
75
78
  checkInstall("kimi", opts),
76
79
  checkInstall("copilot", opts),
80
+ checkInstall("dsh", opts),
77
81
  ]);
78
- return { codex, claude, kimi, copilot };
82
+ return { codex, claude, kimi, copilot, dsh };
79
83
  }
80
84
  async function checkCopilotSdkInstall() {
81
85
  try {
@@ -94,6 +98,29 @@ async function checkCopilotSdkInstall() {
94
98
  };
95
99
  }
96
100
  }
101
+ /**
102
+ * DeepSeek Harness ships as pinned dependencies of this package rather than a
103
+ * CLI on PATH, so the reported version is the runtime package's own npm
104
+ * version — the same shape as the Copilot SDK probe above.
105
+ */
106
+ async function checkDshSdkInstall() {
107
+ const RUNTIME_PACKAGE = "@deepseek-ai/dsh-sdk-jsonrpc-demo";
108
+ try {
109
+ const entryPath = require.resolve(`${RUNTIME_PACKAGE}/bin`);
110
+ const version = await findPackageVersionForEntry(entryPath, RUNTIME_PACKAGE);
111
+ return {
112
+ installed: true,
113
+ path: RUNTIME_PACKAGE,
114
+ version: version ?? "installed",
115
+ };
116
+ }
117
+ catch (err) {
118
+ return {
119
+ installed: false,
120
+ error: `${RUNTIME_PACKAGE} not available: ${err?.message ?? err}`,
121
+ };
122
+ }
123
+ }
97
124
  /** Exported for tests. */
98
125
  export async function findPackageVersionForEntry(entryPath, packageName) {
99
126
  let dir = dirname(entryPath);
@@ -2,8 +2,9 @@ import { type GetCodexQuotaOptions } from "./quota/codex.js";
2
2
  import { type GetClaudeQuotaOptions } from "./quota/claude.js";
3
3
  import { type GetKimiQuotaOptions } from "./quota/kimi.js";
4
4
  import { type GetCopilotQuotaOptions } from "./quota/copilot.js";
5
+ import { type DshQuotaOptions } from "./quota/dsh.js";
5
6
  import { type GetExternalQuotaListOptions, type GetExternalQuotaOptions } from "./quota/external.js";
6
- import type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, ExternalQuota, ExternalQuotaList, KimiQuota, CopilotQuota, InstallStatus, NetworkStatus, SwitchResult, Tool } from "./types.js";
7
+ import type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, DshQuota, ExternalQuota, ExternalQuotaList, KimiQuota, CopilotQuota, InstallStatus, NetworkStatus, SwitchResult, Tool } from "./types.js";
7
8
  export interface AiManagerOptions {
8
9
  /** Path to conductor config.yaml. Default: $CONDUCTOR_HOME/config.yaml. */
9
10
  configPath?: string;
@@ -43,6 +44,9 @@ export declare class AiManager {
43
44
  getClaudeQuota(opts?: GetClaudeQuotaOptions): Promise<ClaudeQuota>;
44
45
  getKimiQuota(opts?: GetKimiQuotaOptions): Promise<KimiQuota>;
45
46
  getCopilotQuota(opts?: GetCopilotQuotaOptions): Promise<CopilotQuota>;
47
+ getDshQuota(opts?: DshQuotaOptions): Promise<DshQuota>;
48
+ /** Last cached DeepSeek balance, no network call. */
49
+ readCachedDshQuota(opts?: DshQuotaOptions): Promise<DshQuota | null>;
46
50
  getExternalQuota(opts: GetExternalQuotaOptions): Promise<ExternalQuota>;
47
51
  getExternalQuotaList(opts: GetExternalQuotaListOptions): Promise<ExternalQuotaList>;
48
52
  listCodexAccounts(): Promise<CodexAccount[]>;
@@ -7,6 +7,7 @@ import { getCodexQuota, readCachedCodexQuota } from "./quota/codex.js";
7
7
  import { getClaudeQuota } from "./quota/claude.js";
8
8
  import { getKimiQuota } from "./quota/kimi.js";
9
9
  import { getCopilotQuota } from "./quota/copilot.js";
10
+ import { getDshQuota, readCachedDshQuota } from "./quota/dsh.js";
10
11
  import { getExternalQuota, getExternalQuotaList, } from "./quota/external.js";
11
12
  import { DEFAULT_CODEX_AUTH, resolveDefaultConductorConfig } from "./paths.js";
12
13
  export class AiManager {
@@ -84,6 +85,13 @@ export class AiManager {
84
85
  getCopilotQuota(opts) {
85
86
  return getCopilotQuota(opts);
86
87
  }
88
+ getDshQuota(opts) {
89
+ return getDshQuota({ configPath: this.configPath, ...opts });
90
+ }
91
+ /** Last cached DeepSeek balance, no network call. */
92
+ readCachedDshQuota(opts) {
93
+ return readCachedDshQuota({ configPath: this.configPath, ...opts });
94
+ }
87
95
  getExternalQuota(opts) {
88
96
  return getExternalQuota({ configPath: this.configPath, ...opts });
89
97
  }
@@ -3,6 +3,7 @@ const ENDPOINTS = {
3
3
  claude: "https://api.anthropic.com/v1/messages",
4
4
  kimi: "https://api.kimi.com/coding/v1/usages",
5
5
  copilot: "https://api.githubcopilot.com/",
6
+ dsh: "https://api.deepseek.com/",
6
7
  };
7
8
  const DEFAULT_TIMEOUT_MS = 5000;
8
9
  export async function checkNetwork(tool, opts = {}) {
@@ -36,11 +37,12 @@ export async function checkNetwork(tool, opts = {}) {
36
37
  }
37
38
  }
38
39
  export async function checkNetworkAll(opts) {
39
- const [codex, claude, kimi, copilot] = await Promise.all([
40
+ const [codex, claude, kimi, copilot, dsh] = await Promise.all([
40
41
  checkNetwork("codex", opts),
41
42
  checkNetwork("claude", opts),
42
43
  checkNetwork("kimi", opts),
43
44
  checkNetwork("copilot", opts),
45
+ checkNetwork("dsh", opts),
44
46
  ]);
45
- return { codex, claude, kimi, copilot };
47
+ return { codex, claude, kimi, copilot, dsh };
46
48
  }
@@ -0,0 +1,55 @@
1
+ import type { DshBalanceInfo, DshQuota } from "../types.js";
2
+ interface DeepSeekBalanceResponse {
3
+ is_available?: unknown;
4
+ balance_infos?: unknown;
5
+ }
6
+ export interface DshQuotaOptions {
7
+ /**
8
+ * Explicit credential; skips the config/env resolution ladder. An empty
9
+ * string is an explicit "no credential" and does NOT fall back, so a caller
10
+ * can probe the unconfigured path deterministically.
11
+ */
12
+ apiKey?: string;
13
+ /** Endpoint base; a `/v1` suffix is tolerated and stripped. */
14
+ baseURL?: string;
15
+ /** Conductor config file to read `envs.DEEPSEEK_API_KEY` from. */
16
+ configPath?: string;
17
+ /** Cache TTL in seconds (default 60). */
18
+ ttlSeconds?: number;
19
+ /** Bypass the cache and always hit the network. */
20
+ forceRefresh?: boolean;
21
+ timeoutMs?: number;
22
+ cacheDir?: string;
23
+ /** Injected for tests. */
24
+ fetchImpl?: typeof fetch;
25
+ }
26
+ /**
27
+ * Normalize the DeepSeek base URL for the balance endpoint. The inference
28
+ * base commonly carries a `/v1` suffix (it is an OpenAI-compatible route),
29
+ * but `/user/balance` lives at the host root, so the suffix is stripped.
30
+ */
31
+ export declare function normalizeDshBaseUrl(baseURL: string | undefined): string;
32
+ export declare function parseDshBalance(payload: DeepSeekBalanceResponse): {
33
+ isAvailable: boolean;
34
+ balances: DshBalanceInfo[];
35
+ };
36
+ /**
37
+ * Resolve the DeepSeek credential the way {@link DshSdkSession} does, so the
38
+ * balance shown here belongs to the account that actually serves turns:
39
+ * explicit option, then the conductor config's `envs` section, then the
40
+ * launching environment.
41
+ */
42
+ export declare function resolveDeepSeekCredential(options?: Pick<DshQuotaOptions, "apiKey" | "baseURL" | "configPath">): Promise<{
43
+ apiKey: string;
44
+ baseURL: string;
45
+ }>;
46
+ /**
47
+ * Read the DeepSeek account balance. DeepSeek bills per token with no rolling
48
+ * usage window, so this reports prepaid balance instead of a percentage-based
49
+ * {@link QuotaWindow}. A stale cache entry is served when the network call
50
+ * fails, matching the other quota providers.
51
+ */
52
+ export declare function getDshQuota(options?: DshQuotaOptions): Promise<DshQuota>;
53
+ /** Read the last cached balance without touching the network. */
54
+ export declare function readCachedDshQuota(options?: Pick<DshQuotaOptions, "apiKey" | "baseURL" | "configPath" | "cacheDir">): Promise<DshQuota | null>;
55
+ export {};
@@ -0,0 +1,146 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { resolveDefaultConductorConfig } from "../paths.js";
4
+ import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
5
+ const DEFAULT_TTL = 60;
6
+ const DEFAULT_TIMEOUT_MS = 10_000;
7
+ const DEFAULT_BASE_URL = "https://api.deepseek.com";
8
+ const BALANCE_PATH = "/user/balance";
9
+ function toNumber(value) {
10
+ const parsed = typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
11
+ return Number.isFinite(parsed) ? parsed : 0;
12
+ }
13
+ /**
14
+ * Normalize the DeepSeek base URL for the balance endpoint. The inference
15
+ * base commonly carries a `/v1` suffix (it is an OpenAI-compatible route),
16
+ * but `/user/balance` lives at the host root, so the suffix is stripped.
17
+ */
18
+ export function normalizeDshBaseUrl(baseURL) {
19
+ const raw = typeof baseURL === "string" ? baseURL.trim() : "";
20
+ const base = raw || DEFAULT_BASE_URL;
21
+ return base.replace(/\/+$/, "").replace(/\/v\d+$/, "");
22
+ }
23
+ export function parseDshBalance(payload) {
24
+ const rawList = Array.isArray(payload?.balance_infos) ? payload.balance_infos : [];
25
+ const balances = rawList
26
+ .filter((entry) => Boolean(entry) && typeof entry === "object")
27
+ .map((entry) => ({
28
+ currency: typeof entry.currency === "string" ? entry.currency : "",
29
+ totalBalance: toNumber(entry.total_balance),
30
+ grantedBalance: toNumber(entry.granted_balance),
31
+ toppedUpBalance: toNumber(entry.topped_up_balance),
32
+ }));
33
+ return { isAvailable: payload?.is_available === true, balances };
34
+ }
35
+ /**
36
+ * Resolve the DeepSeek credential the way {@link DshSdkSession} does, so the
37
+ * balance shown here belongs to the account that actually serves turns:
38
+ * explicit option, then the conductor config's `envs` section, then the
39
+ * launching environment.
40
+ */
41
+ export async function resolveDeepSeekCredential(options = {}) {
42
+ // `apiKey: ""` is an explicit refusal, distinct from an absent option.
43
+ const keyProvided = typeof options.apiKey === "string";
44
+ let apiKey = keyProvided ? options.apiKey.trim() : "";
45
+ let baseURL = typeof options.baseURL === "string" ? options.baseURL.trim() : "";
46
+ if (!keyProvided || !baseURL) {
47
+ const configPath = options.configPath ?? resolveDefaultConductorConfig();
48
+ try {
49
+ const doc = parseYaml(await readFile(configPath, "utf8"));
50
+ const envs = doc?.envs;
51
+ if (envs && typeof envs === "object") {
52
+ if (!keyProvided && typeof envs.DEEPSEEK_API_KEY === "string") {
53
+ apiKey = envs.DEEPSEEK_API_KEY.trim();
54
+ }
55
+ if (!baseURL && typeof envs.DEEPSEEK_BASE_URL === "string") {
56
+ baseURL = envs.DEEPSEEK_BASE_URL.trim();
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Missing or unreadable config: fall through to the environment.
62
+ }
63
+ }
64
+ if (!keyProvided && !apiKey && typeof process.env.DEEPSEEK_API_KEY === "string") {
65
+ apiKey = process.env.DEEPSEEK_API_KEY.trim();
66
+ }
67
+ if (!baseURL && typeof process.env.DEEPSEEK_BASE_URL === "string") {
68
+ baseURL = process.env.DEEPSEEK_BASE_URL.trim();
69
+ }
70
+ return { apiKey, baseURL: normalizeDshBaseUrl(baseURL) };
71
+ }
72
+ function emptyQuota(error, fetchedAt = Math.floor(Date.now() / 1000)) {
73
+ return {
74
+ tool: "dsh",
75
+ isAvailable: false,
76
+ balances: [],
77
+ fetchedAt,
78
+ source: "unknown",
79
+ error,
80
+ };
81
+ }
82
+ /**
83
+ * Read the DeepSeek account balance. DeepSeek bills per token with no rolling
84
+ * usage window, so this reports prepaid balance instead of a percentage-based
85
+ * {@link QuotaWindow}. A stale cache entry is served when the network call
86
+ * fails, matching the other quota providers.
87
+ */
88
+ export async function getDshQuota(options = {}) {
89
+ const { apiKey, baseURL } = await resolveDeepSeekCredential(options);
90
+ if (!apiKey) {
91
+ return emptyQuota("DEEPSEEK_API_KEY is not configured; set envs.DEEPSEEK_API_KEY in the conductor config");
92
+ }
93
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL;
94
+ const file = cacheFile("dsh", fingerprintKey([baseURL, apiKey]), options.cacheDir);
95
+ const cached = await readCache(file);
96
+ if (!options.forceRefresh && isFresh(cached, ttlSeconds)) {
97
+ return { ...cached.value, source: "cached" };
98
+ }
99
+ const fetchImpl = options.fetchImpl ?? fetch;
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
102
+ try {
103
+ const res = await fetchImpl(`${baseURL}${BALANCE_PATH}`, {
104
+ method: "GET",
105
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
106
+ signal: controller.signal,
107
+ });
108
+ if (!res.ok) {
109
+ throw new Error(`DeepSeek balance request failed with HTTP ${res.status}`);
110
+ }
111
+ const payload = (await res.json());
112
+ const { isAvailable, balances } = parseDshBalance(payload);
113
+ const quota = {
114
+ tool: "dsh",
115
+ isAvailable,
116
+ ...(balances[0] ? { primaryBalance: balances[0] } : {}),
117
+ balances,
118
+ fetchedAt: Math.floor(Date.now() / 1000),
119
+ source: "fresh",
120
+ raw: payload,
121
+ };
122
+ const entry = await writeCache(file, quota);
123
+ return { ...quota, fetchedAt: entry.fetchedAt };
124
+ }
125
+ catch (err) {
126
+ const message = err?.name === "AbortError" ? "timeout" : err?.message ?? String(err);
127
+ if (cached) {
128
+ return { ...cached.value, source: "stale", error: message };
129
+ }
130
+ return emptyQuota(message);
131
+ }
132
+ finally {
133
+ clearTimeout(timer);
134
+ }
135
+ }
136
+ /** Read the last cached balance without touching the network. */
137
+ export async function readCachedDshQuota(options = {}) {
138
+ const { apiKey, baseURL } = await resolveDeepSeekCredential(options);
139
+ if (!apiKey)
140
+ return null;
141
+ const file = cacheFile("dsh", fingerprintKey([baseURL, apiKey]), options.cacheDir);
142
+ const cached = await readCache(file);
143
+ if (!cached)
144
+ return null;
145
+ return { ...cached.value, fetchedAt: cached.fetchedAt, source: "cached" };
146
+ }
@@ -1,4 +1,4 @@
1
- export type Tool = "codex" | "claude" | "kimi" | "copilot";
1
+ export type Tool = "codex" | "claude" | "kimi" | "copilot" | "dsh";
2
2
  export type QuotaSource = "fresh" | "cached" | "stale" | "unknown";
3
3
  export interface InstallStatus {
4
4
  installed: boolean;
@@ -106,6 +106,31 @@ export interface CopilotQuota {
106
106
  raw?: Record<string, unknown>;
107
107
  error?: string;
108
108
  }
109
+ /** One currency bucket of a DeepSeek account balance. */
110
+ export interface DshBalanceInfo {
111
+ currency: string;
112
+ totalBalance: number;
113
+ grantedBalance: number;
114
+ toppedUpBalance: number;
115
+ }
116
+ /**
117
+ * DeepSeek Harness account state. DeepSeek bills per token with no rolling
118
+ * usage window, so this carries a prepaid balance rather than a
119
+ * {@link QuotaWindow}: there is no percentage to render.
120
+ */
121
+ export interface DshQuota {
122
+ tool: "dsh";
123
+ /** Whether DeepSeek considers the account usable for inference. */
124
+ isAvailable: boolean;
125
+ /** The account's primary balance bucket (first entry, usually CNY). */
126
+ primaryBalance?: DshBalanceInfo;
127
+ /** Every currency bucket the API reported. */
128
+ balances: DshBalanceInfo[];
129
+ fetchedAt: number;
130
+ source: QuotaSource;
131
+ raw?: Record<string, unknown>;
132
+ error?: string;
133
+ }
109
134
  export interface ExternalQuota {
110
135
  backend?: string;
111
136
  model: string;
@@ -0,0 +1,34 @@
1
+ export function buildCodexAppServerInput(promptText: any, media: any): any[];
2
+ export function buildClaudeContent(promptText: any, media: any): any[];
3
+ export function buildCopilotAttachments(media: any): any;
4
+ export function buildKimiContent(promptText: any, media: any): any[];
5
+ export function buildOpencodeParts(promptText: any, media: any): any[];
6
+ export const UNSUPPORTED_MEDIA_CAPABILITIES: Readonly<{
7
+ image: "unsupported";
8
+ }>;
9
+ export const PROVIDER_MEDIA_CAPABILITIES: Readonly<{
10
+ "chat-web-session": Readonly<{
11
+ image: "unsupported";
12
+ }>;
13
+ "claude-agent-sdk": Readonly<{
14
+ image: "native";
15
+ }>;
16
+ "codex-app-server": Readonly<{
17
+ image: "native";
18
+ }>;
19
+ "codex-exec": Readonly<{
20
+ image: "native";
21
+ }>;
22
+ "copilot-sdk": Readonly<{
23
+ image: "native";
24
+ }>;
25
+ "kimi-cli-print": Readonly<{
26
+ image: "native";
27
+ }>;
28
+ "kimi-cli-wire": Readonly<{
29
+ image: "native";
30
+ }>;
31
+ "opencode-sdk": Readonly<{
32
+ image: "native";
33
+ }>;
34
+ }>;
@@ -0,0 +1,57 @@
1
+ import { fileMediaToBase64, fileMediaToDataUri } from "./media-input.js";
2
+ export const UNSUPPORTED_MEDIA_CAPABILITIES = Object.freeze({
3
+ image: "unsupported",
4
+ });
5
+ export const PROVIDER_MEDIA_CAPABILITIES = Object.freeze({
6
+ "chat-web-session": UNSUPPORTED_MEDIA_CAPABILITIES,
7
+ "claude-agent-sdk": Object.freeze({ image: "native" }),
8
+ "codex-app-server": Object.freeze({ image: "native" }),
9
+ "codex-exec": Object.freeze({ image: "native" }),
10
+ "copilot-sdk": Object.freeze({ image: "native" }),
11
+ "kimi-cli-print": Object.freeze({ image: "native" }),
12
+ "kimi-cli-wire": Object.freeze({ image: "native" }),
13
+ "opencode-sdk": Object.freeze({ image: "native" }),
14
+ });
15
+ export function buildCodexAppServerInput(promptText, media) {
16
+ return [
17
+ { type: "text", text: promptText },
18
+ ...media.map((item) => ({ type: "localImage", path: item.path })),
19
+ ];
20
+ }
21
+ export function buildClaudeContent(promptText, media) {
22
+ return [
23
+ { type: "text", text: promptText },
24
+ ...media.map((item) => ({
25
+ type: "image",
26
+ source: {
27
+ type: "base64",
28
+ media_type: item.mimeType,
29
+ data: fileMediaToBase64(item),
30
+ },
31
+ })),
32
+ ];
33
+ }
34
+ export function buildCopilotAttachments(media) {
35
+ return media.map((item) => ({
36
+ type: "file",
37
+ path: item.path,
38
+ displayName: item.name,
39
+ }));
40
+ }
41
+ export function buildKimiContent(promptText, media) {
42
+ return [
43
+ { type: "text", text: promptText },
44
+ ...media.map((item) => ({ type: "image_url", image_url: { url: fileMediaToDataUri(item) } })),
45
+ ];
46
+ }
47
+ export function buildOpencodeParts(promptText, media) {
48
+ return [
49
+ { type: "text", text: promptText },
50
+ ...media.map((item) => ({
51
+ type: "file",
52
+ mime: item.mimeType,
53
+ filename: item.name,
54
+ url: fileMediaToDataUri(item),
55
+ })),
56
+ ];
57
+ }