@love-moon/ai-sdk 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/manager/config.js +2 -2
- package/dist/manager/manager.d.ts +2 -2
- package/dist/manager/manager.js +3 -3
- package/dist/manager/paths.d.ts +5 -0
- package/dist/manager/paths.js +35 -7
- package/dist/manager/quota/cache.js +2 -2
- package/dist/manager/quota/codex.d.ts +11 -8
- package/dist/manager/quota/codex.js +107 -122
- package/dist/manager/types.d.ts +2 -4
- package/dist/shared.js +23 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.7.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7bbb412: Add `CONDUCTOR_HOME` support for relocating user-level configuration, logs,
|
|
8
|
+
Fire locks, sessions, update metadata, and AI manager caches while leaving
|
|
9
|
+
project-scoped `.conductor` directories and Fire task markers in place.
|
|
10
|
+
|
|
11
|
+
Migrate device authorization to `conductor.conductor-ai.top` while preserving
|
|
12
|
+
compatibility with the legacy official endpoint and self-hosted backends.
|
|
13
|
+
|
|
14
|
+
## 0.7.5
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- f91a5df: Read Codex weekly quota from the official app-server account rate-limit RPC, ignore model-specific buckets, and invalidate legacy response-header caches.
|
|
19
|
+
|
|
3
20
|
## 0.7.4
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/dist/manager/config.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
|
-
import {
|
|
3
|
+
import { expandHome, resolveDefaultConductorConfig } from "./paths.js";
|
|
4
4
|
const EMPTY = { codex: { authJson: [] } };
|
|
5
5
|
/**
|
|
6
6
|
* Load the `ai_manager` section from a conductor config file.
|
|
7
7
|
* Returns an empty config if the file does not exist or the section is missing.
|
|
8
8
|
*/
|
|
9
|
-
export async function loadAiManagerConfig(configPath =
|
|
9
|
+
export async function loadAiManagerConfig(configPath = resolveDefaultConductorConfig()) {
|
|
10
10
|
let raw;
|
|
11
11
|
try {
|
|
12
12
|
raw = await readFile(configPath, "utf8");
|
|
@@ -5,7 +5,7 @@ import { type GetCopilotQuotaOptions } from "./quota/copilot.js";
|
|
|
5
5
|
import { type GetExternalQuotaListOptions, type GetExternalQuotaOptions } from "./quota/external.js";
|
|
6
6
|
import type { AiManagerConfig, CodexAccount, CodexQuota, ClaudeQuota, ExternalQuota, ExternalQuotaList, KimiQuota, CopilotQuota, InstallStatus, NetworkStatus, SwitchResult, Tool } from "./types.js";
|
|
7
7
|
export interface AiManagerOptions {
|
|
8
|
-
/** Path to conductor config.yaml. Default:
|
|
8
|
+
/** Path to conductor config.yaml. Default: $CONDUCTOR_HOME/config.yaml. */
|
|
9
9
|
configPath?: string;
|
|
10
10
|
/** Path to the active codex auth.json. Default: ~/.codex/auth.json */
|
|
11
11
|
codexAuthPath?: string;
|
|
@@ -20,7 +20,7 @@ export declare class AiManager {
|
|
|
20
20
|
constructor(opts?: AiManagerOptions);
|
|
21
21
|
/**
|
|
22
22
|
* Read the latest ai_manager config. Cached against the config file's mtime,
|
|
23
|
-
* so edits to
|
|
23
|
+
* so edits to the resolved Conductor config.yaml are picked up immediately without
|
|
24
24
|
* restarting the daemon, while a 30s polling cycle does not re-parse YAML
|
|
25
25
|
* on every action. Falls back to always-reload if stat() fails.
|
|
26
26
|
* The `config` constructor option short-circuits disk reads (used by tests).
|
package/dist/manager/manager.js
CHANGED
|
@@ -8,20 +8,20 @@ import { getClaudeQuota } from "./quota/claude.js";
|
|
|
8
8
|
import { getKimiQuota } from "./quota/kimi.js";
|
|
9
9
|
import { getCopilotQuota } from "./quota/copilot.js";
|
|
10
10
|
import { getExternalQuota, getExternalQuotaList, } from "./quota/external.js";
|
|
11
|
-
import { DEFAULT_CODEX_AUTH,
|
|
11
|
+
import { DEFAULT_CODEX_AUTH, resolveDefaultConductorConfig } from "./paths.js";
|
|
12
12
|
export class AiManager {
|
|
13
13
|
configPath;
|
|
14
14
|
codexAuthPath;
|
|
15
15
|
configOverride;
|
|
16
16
|
cached;
|
|
17
17
|
constructor(opts = {}) {
|
|
18
|
-
this.configPath = opts.configPath ??
|
|
18
|
+
this.configPath = opts.configPath ?? resolveDefaultConductorConfig();
|
|
19
19
|
this.codexAuthPath = opts.codexAuthPath ?? DEFAULT_CODEX_AUTH;
|
|
20
20
|
this.configOverride = opts.config;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Read the latest ai_manager config. Cached against the config file's mtime,
|
|
24
|
-
* so edits to
|
|
24
|
+
* so edits to the resolved Conductor config.yaml are picked up immediately without
|
|
25
25
|
* restarting the daemon, while a 30s polling cycle does not re-parse YAML
|
|
26
26
|
* on every action. Falls back to always-reload if stat() fails.
|
|
27
27
|
* The `config` constructor option short-circuits disk reads (used by tests).
|
package/dist/manager/paths.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
type PathEnv = Record<string, string | undefined>;
|
|
1
2
|
export declare function expandHome(p: string): string;
|
|
3
|
+
export declare function resolveConductorHome(env?: PathEnv): string;
|
|
4
|
+
export declare function resolveDefaultConductorConfig(env?: PathEnv): string;
|
|
5
|
+
export declare function resolveDefaultQuotaCacheDir(env?: PathEnv): string;
|
|
2
6
|
export declare const DEFAULT_CONDUCTOR_CONFIG: string;
|
|
3
7
|
export declare const DEFAULT_CODEX_AUTH: string;
|
|
4
8
|
export declare const DEFAULT_CODEX_CONFIG: string;
|
|
@@ -6,3 +10,4 @@ export declare const DEFAULT_KIMI_CODE_HOME: string;
|
|
|
6
10
|
export declare const DEFAULT_KIMI_CREDENTIAL: string;
|
|
7
11
|
export declare const LEGACY_KIMI_CREDENTIAL: string;
|
|
8
12
|
export declare const DEFAULT_QUOTA_CACHE_DIR: string;
|
|
13
|
+
export {};
|
package/dist/manager/paths.js
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
function optionalString(value) {
|
|
4
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
5
|
+
}
|
|
6
|
+
function userHome(env = process.env) {
|
|
7
|
+
return optionalString(env.HOME) || optionalString(env.USERPROFILE) || homedir();
|
|
8
|
+
}
|
|
9
|
+
function expandHomeWithEnv(p, env = process.env) {
|
|
4
10
|
if (!p)
|
|
5
11
|
return p;
|
|
12
|
+
const home = userHome(env);
|
|
6
13
|
if (p === "~")
|
|
7
|
-
return
|
|
8
|
-
if (p.startsWith("~/"))
|
|
9
|
-
return join(
|
|
14
|
+
return home;
|
|
15
|
+
if (p.startsWith("~/") || p.startsWith("~\\"))
|
|
16
|
+
return join(home, p.slice(2));
|
|
10
17
|
return p;
|
|
11
18
|
}
|
|
12
|
-
export
|
|
19
|
+
export function expandHome(p) {
|
|
20
|
+
return expandHomeWithEnv(p);
|
|
21
|
+
}
|
|
22
|
+
export function resolveConductorHome(env = process.env) {
|
|
23
|
+
const configured = optionalString(env.CONDUCTOR_HOME);
|
|
24
|
+
if (configured) {
|
|
25
|
+
const expanded = expandHomeWithEnv(configured, env);
|
|
26
|
+
return resolve(expanded);
|
|
27
|
+
}
|
|
28
|
+
return join(resolve(userHome(env)), ".conductor");
|
|
29
|
+
}
|
|
30
|
+
export function resolveDefaultConductorConfig(env = process.env) {
|
|
31
|
+
const configured = optionalString(env.CONDUCTOR_CONFIG);
|
|
32
|
+
if (configured) {
|
|
33
|
+
return resolve(expandHomeWithEnv(configured, env));
|
|
34
|
+
}
|
|
35
|
+
return join(resolveConductorHome(env), "config.yaml");
|
|
36
|
+
}
|
|
37
|
+
export function resolveDefaultQuotaCacheDir(env = process.env) {
|
|
38
|
+
return join(resolveConductorHome(env), "cache", "ai-manager");
|
|
39
|
+
}
|
|
40
|
+
export const DEFAULT_CONDUCTOR_CONFIG = resolveDefaultConductorConfig();
|
|
13
41
|
export const DEFAULT_CODEX_AUTH = join(homedir(), ".codex", "auth.json");
|
|
14
42
|
export const DEFAULT_CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
|
|
15
43
|
export const DEFAULT_KIMI_CODE_HOME = process.env.KIMI_CODE_HOME?.trim() || join(homedir(), ".kimi-code");
|
|
16
44
|
export const DEFAULT_KIMI_CREDENTIAL = join(DEFAULT_KIMI_CODE_HOME, "credentials", "kimi-code.json");
|
|
17
45
|
export const LEGACY_KIMI_CREDENTIAL = join(homedir(), ".kimi", "credentials", "kimi-code.json");
|
|
18
|
-
export const DEFAULT_QUOTA_CACHE_DIR =
|
|
46
|
+
export const DEFAULT_QUOTA_CACHE_DIR = resolveDefaultQuotaCacheDir();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveDefaultQuotaCacheDir } from "../paths.js";
|
|
5
5
|
export function fingerprintKey(parts) {
|
|
6
6
|
const h = createHash("sha1");
|
|
7
7
|
for (const p of parts)
|
|
@@ -23,7 +23,7 @@ export async function writeCache(file, value) {
|
|
|
23
23
|
await writeFile(file, JSON.stringify(entry), "utf8");
|
|
24
24
|
return entry;
|
|
25
25
|
}
|
|
26
|
-
export function cacheFile(tool, fingerprint, dir =
|
|
26
|
+
export function cacheFile(tool, fingerprint, dir = resolveDefaultQuotaCacheDir()) {
|
|
27
27
|
return join(dir, `quota-${tool}-${fingerprint}.json`);
|
|
28
28
|
}
|
|
29
29
|
export function isFresh(entry, ttlSeconds) {
|
|
@@ -7,25 +7,28 @@ export interface GetCodexQuotaOptions {
|
|
|
7
7
|
forceRefresh?: boolean;
|
|
8
8
|
/** Request timeout in ms. */
|
|
9
9
|
timeoutMs?: number;
|
|
10
|
-
/**
|
|
10
|
+
/** @deprecated Quota is account-scoped and no longer fetched through a model response. */
|
|
11
11
|
model?: string;
|
|
12
|
-
/**
|
|
12
|
+
/** @deprecated The Codex app-server reads its own config from CODEX_HOME. */
|
|
13
13
|
codexConfigPath?: string;
|
|
14
14
|
/** Override cache directory (mainly for tests). */
|
|
15
15
|
cacheDir?: string;
|
|
16
|
+
/** Override the app-server reader (tests only). */
|
|
17
|
+
rateLimitsReader?: (options: {
|
|
18
|
+
codexAuthPath: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
}) => Promise<unknown>;
|
|
16
21
|
}
|
|
17
|
-
/**
|
|
18
|
-
export declare function
|
|
22
|
+
/** Normalize Codex app-server `account/rateLimits/read` into the manager shape. */
|
|
23
|
+
export declare function parseCodexRateLimitsResponse(response: unknown, extras?: {
|
|
19
24
|
email?: string;
|
|
20
25
|
accountId?: string;
|
|
21
26
|
}): Omit<CodexQuota, "tool" | "fetchedAt" | "source">;
|
|
22
27
|
export declare function getCodexQuota(opts?: GetCodexQuotaOptions): Promise<CodexQuota>;
|
|
23
28
|
/**
|
|
24
29
|
* Read the on-disk quota cache for a given codex auth.json without triggering
|
|
25
|
-
* any network call.
|
|
26
|
-
*
|
|
27
|
-
* daemon's `list_accounts` handler to surface "last known" quota for inactive
|
|
28
|
-
* accounts so the web UI can restore them across page refreshes.
|
|
30
|
+
* any network call. Only app-server snapshots are considered; legacy caches
|
|
31
|
+
* from response headers are intentionally ignored.
|
|
29
32
|
*/
|
|
30
33
|
export declare function readCachedCodexQuota(codexAuthPath: string, opts?: {
|
|
31
34
|
cacheDir?: string;
|
|
@@ -1,77 +1,72 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { basename, dirname } from "node:path";
|
|
2
2
|
import { parseAuthFile } from "../auth-parser.js";
|
|
3
|
-
import { DEFAULT_CODEX_AUTH
|
|
4
|
-
import {
|
|
3
|
+
import { DEFAULT_CODEX_AUTH } from "../paths.js";
|
|
4
|
+
import { CodexAppServerTransport } from "../../transports/codex-app-server-transport.js";
|
|
5
5
|
import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
|
|
6
|
-
/** Best-effort: read the configured model from ~/.codex/config.toml. */
|
|
7
|
-
async function readConfiguredModel(path) {
|
|
8
|
-
try {
|
|
9
|
-
const raw = await readFile(path, "utf8");
|
|
10
|
-
// Minimal top-level key scan: `model = "..."` (skip lines inside [tables]).
|
|
11
|
-
let inRootTable = true;
|
|
12
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
13
|
-
const trimmed = line.trim();
|
|
14
|
-
if (trimmed.startsWith("#") || trimmed === "")
|
|
15
|
-
continue;
|
|
16
|
-
if (/^\[/.test(trimmed)) {
|
|
17
|
-
inRootTable = false;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (!inRootTable)
|
|
21
|
-
continue;
|
|
22
|
-
const m = /^model\s*=\s*"([^"]+)"/.exec(trimmed);
|
|
23
|
-
if (m)
|
|
24
|
-
return m[1];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// ignore
|
|
29
|
-
}
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
const RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
33
6
|
const DEFAULT_TTL = 60; // seconds
|
|
34
7
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!suffixes.some((suffix) => map[`${prefix}-${suffix}`] !== undefined)) {
|
|
8
|
+
const WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
|
|
9
|
+
// The old cache was populated from /backend-api/codex/responses headers. Keep
|
|
10
|
+
// the new app-server snapshots in a separate namespace so an upgrade cannot
|
|
11
|
+
// surface a model-specific response limit as the account's weekly limit.
|
|
12
|
+
const CACHE_TOOL = "codex-app-server";
|
|
13
|
+
function finiteNumber(value) {
|
|
14
|
+
if (typeof value !== "number" && (typeof value !== "string" || !value.trim())) {
|
|
43
15
|
return undefined;
|
|
44
16
|
}
|
|
45
|
-
const
|
|
17
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
19
|
+
}
|
|
20
|
+
function nonEmptyString(value) {
|
|
21
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
22
|
+
}
|
|
23
|
+
function quotaWindow(value) {
|
|
24
|
+
if (!value || typeof value !== "object")
|
|
25
|
+
return undefined;
|
|
26
|
+
const usedPercent = finiteNumber(value.usedPercent);
|
|
27
|
+
if (usedPercent === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const resetAt = finiteNumber(value.resetsAt);
|
|
30
|
+
const now = Math.floor(Date.now() / 1000);
|
|
46
31
|
return {
|
|
47
32
|
usedPercent,
|
|
48
33
|
remainingPercent: Math.max(0, 100 - usedPercent),
|
|
49
|
-
|
|
50
|
-
resetAt:
|
|
51
|
-
windowMinutes:
|
|
34
|
+
resetAt,
|
|
35
|
+
resetAfterSeconds: resetAt === undefined ? undefined : Math.max(0, resetAt - now),
|
|
36
|
+
windowMinutes: finiteNumber(value.windowDurationMins),
|
|
52
37
|
};
|
|
53
38
|
}
|
|
54
|
-
/**
|
|
55
|
-
export function
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
const
|
|
39
|
+
/** Normalize Codex app-server `account/rateLimits/read` into the manager shape. */
|
|
40
|
+
export function parseCodexRateLimitsResponse(response, extras = {}) {
|
|
41
|
+
const payload = response && typeof response === "object"
|
|
42
|
+
? response
|
|
43
|
+
: {};
|
|
44
|
+
// `rateLimits` is the official backward-compatible account bucket. Do not
|
|
45
|
+
// pick a model-specific entry from rateLimitsByLimitId: e.g. Spark can have
|
|
46
|
+
// a different weekly percentage from the main Codex account limit.
|
|
47
|
+
const snapshot = payload.rateLimits && typeof payload.rateLimits === "object"
|
|
48
|
+
? payload.rateLimits
|
|
49
|
+
: undefined;
|
|
50
|
+
const windows = [quotaWindow(snapshot?.primary), quotaWindow(snapshot?.secondary)];
|
|
51
|
+
const weekly = windows.find((window) => (window?.windowMinutes ?? 0) >= WEEKLY_WINDOW_MINUTES);
|
|
52
|
+
const credits = snapshot?.credits && typeof snapshot.credits === "object"
|
|
53
|
+
? {
|
|
54
|
+
hasCredits: snapshot.credits.hasCredits === true,
|
|
55
|
+
balance: nonEmptyString(snapshot.credits.balance),
|
|
56
|
+
unlimited: snapshot.credits.unlimited === true,
|
|
57
|
+
}
|
|
58
|
+
: undefined;
|
|
59
|
+
const limitId = nonEmptyString(snapshot?.limitId);
|
|
60
|
+
const limitName = nonEmptyString(snapshot?.limitName);
|
|
63
61
|
return {
|
|
64
|
-
plan:
|
|
65
|
-
activeLimit:
|
|
62
|
+
plan: nonEmptyString(snapshot?.planType),
|
|
63
|
+
activeLimit: limitName ?? limitId,
|
|
66
64
|
weekly,
|
|
67
|
-
credits
|
|
68
|
-
hasCredits: bool(map, "x-codex-credits-has-credits") ?? false,
|
|
69
|
-
balance: str(map, "x-codex-credits-balance"),
|
|
70
|
-
unlimited: bool(map, "x-codex-credits-unlimited") ?? false,
|
|
71
|
-
},
|
|
65
|
+
credits,
|
|
72
66
|
email: extras.email,
|
|
73
67
|
accountId: extras.accountId,
|
|
74
|
-
raw:
|
|
68
|
+
raw: payload,
|
|
69
|
+
...(!weekly ? { error: "Codex rate-limit response has no weekly window" } : {}),
|
|
75
70
|
};
|
|
76
71
|
}
|
|
77
72
|
export async function getCodexQuota(opts = {}) {
|
|
@@ -82,81 +77,75 @@ export async function getCodexQuota(opts = {}) {
|
|
|
82
77
|
if (!authInfo.accessToken) {
|
|
83
78
|
return emptyQuota("unknown", "codex auth.json missing access_token");
|
|
84
79
|
}
|
|
85
|
-
const fp = fingerprintKey([
|
|
86
|
-
const file = cacheFile(
|
|
80
|
+
const fp = fingerprintKey([CACHE_TOOL, authInfo.identityFingerprint ?? "unknown"]);
|
|
81
|
+
const file = cacheFile(CACHE_TOOL, fp, opts.cacheDir);
|
|
87
82
|
if (!opts.forceRefresh && ttl > 0) {
|
|
88
83
|
const cached = await readCache(file);
|
|
89
84
|
if (isFresh(cached, ttl) && cached) {
|
|
90
|
-
return { ...
|
|
85
|
+
return { ...cached.value, source: "cached" };
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
|
-
|
|
94
|
-
(await readConfiguredModel(opts.codexConfigPath ?? DEFAULT_CODEX_CONFIG)) ??
|
|
95
|
-
"gpt-5";
|
|
96
|
-
const body = JSON.stringify({
|
|
97
|
-
model,
|
|
98
|
-
instructions: "Reply with exactly one character.",
|
|
99
|
-
input: [
|
|
100
|
-
{ type: "message", role: "user", content: [{ type: "input_text", text: "." }] },
|
|
101
|
-
],
|
|
102
|
-
stream: true,
|
|
103
|
-
store: false,
|
|
104
|
-
});
|
|
105
|
-
const controller = new AbortController();
|
|
106
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
107
|
-
let res;
|
|
88
|
+
let response;
|
|
108
89
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Authorization: `Bearer ${authInfo.accessToken}`,
|
|
113
|
-
"Content-Type": "application/json",
|
|
114
|
-
"OpenAI-Beta": "responses=experimental",
|
|
115
|
-
...(authInfo.accountId ? { "chatgpt-account-id": authInfo.accountId } : {}),
|
|
116
|
-
},
|
|
117
|
-
body,
|
|
118
|
-
signal: controller.signal,
|
|
90
|
+
response = await (opts.rateLimitsReader ?? readCodexRateLimits)({
|
|
91
|
+
codexAuthPath,
|
|
92
|
+
timeoutMs,
|
|
119
93
|
});
|
|
120
94
|
}
|
|
121
|
-
catch (
|
|
122
|
-
|
|
123
|
-
return await fallbackFromCache(file, ttl, err?.message ?? String(err));
|
|
124
|
-
}
|
|
125
|
-
// Abort the body stream immediately once headers are in; they're all we need.
|
|
126
|
-
try {
|
|
127
|
-
await res.body?.cancel();
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
// ignore
|
|
131
|
-
}
|
|
132
|
-
clearTimeout(timer);
|
|
133
|
-
const map = headersToMap(res.headers);
|
|
134
|
-
if (res.status === 401 || res.status === 403) {
|
|
135
|
-
return await fallbackFromCache(file, ttl, `auth failed: HTTP ${res.status}`);
|
|
95
|
+
catch (error) {
|
|
96
|
+
return fallbackFromCache(file, ttl, errorMessage(error));
|
|
136
97
|
}
|
|
137
|
-
const parsed =
|
|
98
|
+
const parsed = parseCodexRateLimitsResponse(response, {
|
|
138
99
|
email: authInfo.email,
|
|
139
100
|
accountId: authInfo.accountId,
|
|
140
101
|
});
|
|
141
|
-
|
|
102
|
+
if (!parsed.weekly) {
|
|
103
|
+
return fallbackFromCache(file, ttl, parsed.error ?? "Codex weekly quota unavailable");
|
|
104
|
+
}
|
|
142
105
|
const fresh = {
|
|
143
106
|
tool: "codex",
|
|
144
|
-
fetchedAt: now,
|
|
107
|
+
fetchedAt: Math.floor(Date.now() / 1000),
|
|
145
108
|
source: "fresh",
|
|
146
109
|
...parsed,
|
|
147
110
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
111
|
+
await writeCache(file, fresh);
|
|
112
|
+
return fresh;
|
|
113
|
+
}
|
|
114
|
+
async function readCodexRateLimits(options) {
|
|
115
|
+
if (basename(options.codexAuthPath) !== "auth.json") {
|
|
116
|
+
throw new Error(`Codex app-server quota requires an active auth.json path; got ${options.codexAuthPath}`);
|
|
117
|
+
}
|
|
118
|
+
const transport = new CodexAppServerTransport({
|
|
119
|
+
env: { CODEX_HOME: dirname(options.codexAuthPath) },
|
|
120
|
+
ignoreCodexApiKey: true,
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
return await withTimeout(transport.request("account/rateLimits/read"), options.timeoutMs, "Codex account/rateLimits/read timed out");
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await transport.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
130
|
+
let timer;
|
|
131
|
+
try {
|
|
132
|
+
return await Promise.race([
|
|
133
|
+
promise,
|
|
134
|
+
new Promise((_resolve, reject) => {
|
|
135
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (timer)
|
|
141
|
+
clearTimeout(timer);
|
|
152
142
|
}
|
|
153
|
-
return await fallbackFromCache(file, ttl, `no x-codex-* headers on HTTP ${res.status}`);
|
|
154
143
|
}
|
|
155
144
|
async function fallbackFromCache(file, ttl, error) {
|
|
156
145
|
const cached = await readCache(file);
|
|
157
146
|
if (cached) {
|
|
158
147
|
return {
|
|
159
|
-
...
|
|
148
|
+
...cached.value,
|
|
160
149
|
source: isFresh(cached, ttl) ? "cached" : "stale",
|
|
161
150
|
error,
|
|
162
151
|
};
|
|
@@ -165,23 +154,21 @@ async function fallbackFromCache(file, ttl, error) {
|
|
|
165
154
|
}
|
|
166
155
|
/**
|
|
167
156
|
* Read the on-disk quota cache for a given codex auth.json without triggering
|
|
168
|
-
* any network call.
|
|
169
|
-
*
|
|
170
|
-
* daemon's `list_accounts` handler to surface "last known" quota for inactive
|
|
171
|
-
* accounts so the web UI can restore them across page refreshes.
|
|
157
|
+
* any network call. Only app-server snapshots are considered; legacy caches
|
|
158
|
+
* from response headers are intentionally ignored.
|
|
172
159
|
*/
|
|
173
160
|
export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
|
|
174
161
|
try {
|
|
175
162
|
const authInfo = await parseAuthFile(codexAuthPath);
|
|
176
163
|
if (!authInfo.identityFingerprint)
|
|
177
164
|
return null;
|
|
178
|
-
const fp = fingerprintKey([
|
|
179
|
-
const file = cacheFile(
|
|
165
|
+
const fp = fingerprintKey([CACHE_TOOL, authInfo.identityFingerprint]);
|
|
166
|
+
const file = cacheFile(CACHE_TOOL, fp, opts.cacheDir);
|
|
180
167
|
const entry = await readCache(file);
|
|
181
168
|
if (!entry)
|
|
182
169
|
return null;
|
|
183
170
|
return {
|
|
184
|
-
...
|
|
171
|
+
...entry.value,
|
|
185
172
|
source: "cached",
|
|
186
173
|
fetchedAt: entry.fetchedAt,
|
|
187
174
|
};
|
|
@@ -190,9 +177,8 @@ export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
|
|
|
190
177
|
return null;
|
|
191
178
|
}
|
|
192
179
|
}
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
return current;
|
|
180
|
+
function errorMessage(error) {
|
|
181
|
+
return error instanceof Error ? error.message : String(error);
|
|
196
182
|
}
|
|
197
183
|
function emptyQuota(source, error) {
|
|
198
184
|
return {
|
|
@@ -200,6 +186,5 @@ function emptyQuota(source, error) {
|
|
|
200
186
|
source,
|
|
201
187
|
error,
|
|
202
188
|
fetchedAt: Math.floor(Date.now() / 1000),
|
|
203
|
-
weekly: { usedPercent: 0, remainingPercent: 0 },
|
|
204
189
|
};
|
|
205
190
|
}
|
package/dist/manager/types.d.ts
CHANGED
|
@@ -37,9 +37,7 @@ export interface CodexQuota {
|
|
|
37
37
|
tool: "codex";
|
|
38
38
|
plan?: string;
|
|
39
39
|
activeLimit?: string;
|
|
40
|
-
|
|
41
|
-
fiveHour?: QuotaWindow;
|
|
42
|
-
weekly: QuotaWindow;
|
|
40
|
+
weekly?: QuotaWindow;
|
|
43
41
|
credits?: {
|
|
44
42
|
hasCredits: boolean;
|
|
45
43
|
balance?: string;
|
|
@@ -49,7 +47,7 @@ export interface CodexQuota {
|
|
|
49
47
|
source: QuotaSource;
|
|
50
48
|
email?: string;
|
|
51
49
|
accountId?: string;
|
|
52
|
-
raw?: Record<string,
|
|
50
|
+
raw?: Record<string, unknown>;
|
|
53
51
|
error?: string;
|
|
54
52
|
}
|
|
55
53
|
export interface ClaudeQuota {
|
package/dist/shared.js
CHANGED
|
@@ -280,8 +280,29 @@ export function extractLongFlagFromCommandLine(commandLine, flag) {
|
|
|
280
280
|
return "";
|
|
281
281
|
}
|
|
282
282
|
export function resolveConductorConfigPath(configFilePath) {
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
if (configFilePath) {
|
|
284
|
+
return configFilePath;
|
|
285
|
+
}
|
|
286
|
+
const userHome = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
287
|
+
if (typeof process.env.CONDUCTOR_CONFIG === "string" && process.env.CONDUCTOR_CONFIG.trim()) {
|
|
288
|
+
const configuredPath = process.env.CONDUCTOR_CONFIG.trim();
|
|
289
|
+
const expandedPath = configuredPath === "~"
|
|
290
|
+
? userHome
|
|
291
|
+
: configuredPath.startsWith("~/") || configuredPath.startsWith("~\\")
|
|
292
|
+
? path.join(userHome, configuredPath.slice(2))
|
|
293
|
+
: configuredPath;
|
|
294
|
+
return path.resolve(expandedPath);
|
|
295
|
+
}
|
|
296
|
+
const configuredHome = typeof process.env.CONDUCTOR_HOME === "string" ? process.env.CONDUCTOR_HOME.trim() : "";
|
|
297
|
+
if (configuredHome) {
|
|
298
|
+
const expandedHome = configuredHome === "~"
|
|
299
|
+
? userHome
|
|
300
|
+
: configuredHome.startsWith("~/") || configuredHome.startsWith("~\\")
|
|
301
|
+
? path.join(userHome, configuredHome.slice(2))
|
|
302
|
+
: configuredHome;
|
|
303
|
+
return path.join(path.resolve(expandedHome), "config.yaml");
|
|
304
|
+
}
|
|
305
|
+
return path.join(userHome, ".conductor", "config.yaml");
|
|
285
306
|
}
|
|
286
307
|
export function loadYamlConfig(configFilePath) {
|
|
287
308
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"zod": "^4.1.5"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@love-moon/chat-web": "^0.7.
|
|
35
|
+
"@love-moon/chat-web": "^0.7.6"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.10.2",
|
|
39
39
|
"tsx": "^4.20.6",
|
|
40
40
|
"typescript": "^5.6.3"
|
|
41
41
|
},
|
|
42
|
-
"gitCommitId": "
|
|
42
|
+
"gitCommitId": "c939df2"
|
|
43
43
|
}
|