@love-moon/ai-sdk 0.3.2 → 0.4.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.
- package/CHANGELOG.md +11 -0
- package/dist/built-in-backends.d.ts +1 -0
- package/dist/built-in-backends.js +6 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +103 -1
- package/dist/external-provider-registry.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/manager/account.d.ts +6 -0
- package/dist/manager/account.js +121 -0
- package/dist/manager/auth-parser.d.ts +27 -0
- package/dist/manager/auth-parser.js +54 -0
- package/dist/manager/config.d.ts +6 -0
- package/dist/manager/config.js +32 -0
- package/dist/manager/index.d.ts +12 -0
- package/dist/manager/index.js +11 -0
- package/dist/manager/install.d.ts +9 -0
- package/dist/manager/install.js +117 -0
- package/dist/manager/manager.d.ts +51 -0
- package/dist/manager/manager.js +105 -0
- package/dist/manager/network.d.ts +8 -0
- package/dist/manager/network.js +46 -0
- package/dist/manager/paths.d.ts +6 -0
- package/dist/manager/paths.js +16 -0
- package/dist/manager/quota/cache.d.ts +9 -0
- package/dist/manager/quota/cache.js +33 -0
- package/dist/manager/quota/claude.d.ts +19 -0
- package/dist/manager/quota/claude.js +193 -0
- package/dist/manager/quota/codex.d.ts +27 -0
- package/dist/manager/quota/codex.js +182 -0
- package/dist/manager/quota/copilot.d.ts +64 -0
- package/dist/manager/quota/copilot.js +718 -0
- package/dist/manager/quota/external.d.ts +29 -0
- package/dist/manager/quota/external.js +176 -0
- package/dist/manager/quota/headers.d.ts +5 -0
- package/dist/manager/quota/headers.js +29 -0
- package/dist/manager/quota/kimi.d.ts +24 -0
- package/dist/manager/quota/kimi.js +230 -0
- package/dist/manager/types.d.ts +166 -0
- package/dist/manager/types.js +1 -0
- package/dist/providers/chat-web-session.d.ts +218 -0
- package/dist/providers/chat-web-session.js +584 -0
- package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
- package/dist/providers/claude-agent-sdk-session.js +109 -1
- package/dist/providers/codex-app-server-session.d.ts +107 -0
- package/dist/providers/codex-app-server-session.js +479 -9
- package/dist/providers/copilot-sdk-session.d.ts +1 -1
- package/dist/resume/chat-web.d.ts +20 -0
- package/dist/resume/chat-web.js +44 -0
- package/dist/resume/index.js +2 -0
- package/dist/session-factory.d.ts +3 -1
- package/dist/session-factory.js +17 -4
- package/dist/shared.d.ts +159 -0
- package/dist/shared.js +111 -0
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +45 -1
- package/dist/worker.js +19 -5
- package/package.json +10 -3
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { parseAuthFile } from "../auth-parser.js";
|
|
3
|
+
import { DEFAULT_CODEX_AUTH, DEFAULT_CODEX_CONFIG } from "../paths.js";
|
|
4
|
+
import { bool, headersToMap, num, str } from "./headers.js";
|
|
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
|
+
const DEFAULT_TTL = 60; // seconds
|
|
34
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
35
|
+
function windowFromHeaders(map, prefix) {
|
|
36
|
+
const usedPercent = num(map, `${prefix}-used-percent`) ?? 0;
|
|
37
|
+
return {
|
|
38
|
+
usedPercent,
|
|
39
|
+
remainingPercent: Math.max(0, 100 - usedPercent),
|
|
40
|
+
resetAfterSeconds: num(map, `${prefix}-reset-after-seconds`),
|
|
41
|
+
resetAt: num(map, `${prefix}-reset-at`),
|
|
42
|
+
windowMinutes: num(map, `${prefix}-window-minutes`),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function parseCodexHeaders(map, extras) {
|
|
46
|
+
return {
|
|
47
|
+
plan: str(map, "x-codex-plan-type"),
|
|
48
|
+
activeLimit: str(map, "x-codex-active-limit"),
|
|
49
|
+
fiveHour: windowFromHeaders(map, "x-codex-primary"),
|
|
50
|
+
weekly: windowFromHeaders(map, "x-codex-secondary"),
|
|
51
|
+
credits: {
|
|
52
|
+
hasCredits: bool(map, "x-codex-credits-has-credits") ?? false,
|
|
53
|
+
balance: str(map, "x-codex-credits-balance"),
|
|
54
|
+
unlimited: bool(map, "x-codex-credits-unlimited") ?? false,
|
|
55
|
+
},
|
|
56
|
+
email: extras.email,
|
|
57
|
+
accountId: extras.accountId,
|
|
58
|
+
raw: Object.fromEntries(Object.entries(map).filter(([k]) => k.startsWith("x-codex-"))),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function getCodexQuota(opts = {}) {
|
|
62
|
+
const codexAuthPath = opts.codexAuthPath ?? DEFAULT_CODEX_AUTH;
|
|
63
|
+
const ttl = opts.ttlSeconds ?? DEFAULT_TTL;
|
|
64
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
65
|
+
const authInfo = await parseAuthFile(codexAuthPath);
|
|
66
|
+
if (!authInfo.accessToken) {
|
|
67
|
+
return emptyQuota("unknown", "codex auth.json missing access_token");
|
|
68
|
+
}
|
|
69
|
+
const fp = fingerprintKey(["codex", authInfo.identityFingerprint ?? "unknown"]);
|
|
70
|
+
const file = cacheFile("codex", fp, opts.cacheDir);
|
|
71
|
+
if (!opts.forceRefresh && ttl > 0) {
|
|
72
|
+
const cached = await readCache(file);
|
|
73
|
+
if (isFresh(cached, ttl) && cached) {
|
|
74
|
+
return { ...cached.value, source: "cached" };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const model = opts.model ??
|
|
78
|
+
(await readConfiguredModel(opts.codexConfigPath ?? DEFAULT_CODEX_CONFIG)) ??
|
|
79
|
+
"gpt-5";
|
|
80
|
+
const body = JSON.stringify({
|
|
81
|
+
model,
|
|
82
|
+
instructions: "Reply with exactly one character.",
|
|
83
|
+
input: [
|
|
84
|
+
{ type: "message", role: "user", content: [{ type: "input_text", text: "." }] },
|
|
85
|
+
],
|
|
86
|
+
stream: true,
|
|
87
|
+
store: false,
|
|
88
|
+
});
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
91
|
+
let res;
|
|
92
|
+
try {
|
|
93
|
+
res = await fetch(RESPONSES_URL, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${authInfo.accessToken}`,
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"OpenAI-Beta": "responses=experimental",
|
|
99
|
+
...(authInfo.accountId ? { "chatgpt-account-id": authInfo.accountId } : {}),
|
|
100
|
+
},
|
|
101
|
+
body,
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
return await fallbackFromCache(file, ttl, err?.message ?? String(err));
|
|
108
|
+
}
|
|
109
|
+
// Abort the body stream immediately once headers are in; they're all we need.
|
|
110
|
+
try {
|
|
111
|
+
await res.body?.cancel();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
const map = headersToMap(res.headers);
|
|
118
|
+
if (res.status === 401 || res.status === 403) {
|
|
119
|
+
return await fallbackFromCache(file, ttl, `auth failed: HTTP ${res.status}`);
|
|
120
|
+
}
|
|
121
|
+
const parsed = parseCodexHeaders(map, {
|
|
122
|
+
email: authInfo.email,
|
|
123
|
+
accountId: authInfo.accountId,
|
|
124
|
+
});
|
|
125
|
+
const now = Math.floor(Date.now() / 1000);
|
|
126
|
+
const fresh = {
|
|
127
|
+
tool: "codex",
|
|
128
|
+
fetchedAt: now,
|
|
129
|
+
source: "fresh",
|
|
130
|
+
...parsed,
|
|
131
|
+
};
|
|
132
|
+
// Only persist if we actually got rate-limit headers — otherwise the upstream was unhealthy.
|
|
133
|
+
if (Object.keys(fresh.raw ?? {}).length > 0) {
|
|
134
|
+
await writeCache(file, fresh);
|
|
135
|
+
return fresh;
|
|
136
|
+
}
|
|
137
|
+
return await fallbackFromCache(file, ttl, `no x-codex-* headers on HTTP ${res.status}`);
|
|
138
|
+
}
|
|
139
|
+
async function fallbackFromCache(file, ttl, error) {
|
|
140
|
+
const cached = await readCache(file);
|
|
141
|
+
if (cached) {
|
|
142
|
+
return {
|
|
143
|
+
...cached.value,
|
|
144
|
+
source: isFresh(cached, ttl) ? "cached" : "stale",
|
|
145
|
+
error,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return emptyQuota("unknown", error);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read the on-disk quota cache for a given codex auth.json without triggering
|
|
152
|
+
* any network call. Returns whatever snapshot was last written (regardless of
|
|
153
|
+
* age) or `null` if nothing has been cached for this identity yet. Used by the
|
|
154
|
+
* daemon's `list_accounts` handler to surface "last known" quota for inactive
|
|
155
|
+
* accounts so the web UI can restore them across page refreshes.
|
|
156
|
+
*/
|
|
157
|
+
export async function readCachedCodexQuota(codexAuthPath, opts = {}) {
|
|
158
|
+
try {
|
|
159
|
+
const authInfo = await parseAuthFile(codexAuthPath);
|
|
160
|
+
if (!authInfo.identityFingerprint)
|
|
161
|
+
return null;
|
|
162
|
+
const fp = fingerprintKey(["codex", authInfo.identityFingerprint]);
|
|
163
|
+
const file = cacheFile("codex", fp, opts.cacheDir);
|
|
164
|
+
const entry = await readCache(file);
|
|
165
|
+
if (!entry)
|
|
166
|
+
return null;
|
|
167
|
+
return { ...entry.value, source: "cached", fetchedAt: entry.fetchedAt };
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function emptyQuota(source, error) {
|
|
174
|
+
return {
|
|
175
|
+
tool: "codex",
|
|
176
|
+
source,
|
|
177
|
+
error,
|
|
178
|
+
fetchedAt: Math.floor(Date.now() / 1000),
|
|
179
|
+
fiveHour: { usedPercent: 0, remainingPercent: 0 },
|
|
180
|
+
weekly: { usedPercent: 0, remainingPercent: 0 },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CopilotClientOptions } from "@github/copilot-sdk";
|
|
2
|
+
import type { CopilotQuota } from "../types.js";
|
|
3
|
+
interface AccountGetQuotaResult {
|
|
4
|
+
quotaSnapshots?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface CopilotAuthStatus {
|
|
7
|
+
isAuthenticated: boolean;
|
|
8
|
+
authType?: string;
|
|
9
|
+
host?: string;
|
|
10
|
+
login?: string;
|
|
11
|
+
statusMessage?: string;
|
|
12
|
+
}
|
|
13
|
+
interface CopilotAuthInfo {
|
|
14
|
+
authType?: string;
|
|
15
|
+
host?: string;
|
|
16
|
+
login?: string;
|
|
17
|
+
loginSource?: "sdk" | "github_token";
|
|
18
|
+
}
|
|
19
|
+
interface CopilotClientLike {
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
stop(): Promise<Error[]>;
|
|
22
|
+
forceStop?: () => Promise<void>;
|
|
23
|
+
getAuthStatus?: () => Promise<CopilotAuthStatus>;
|
|
24
|
+
rpc: {
|
|
25
|
+
account: {
|
|
26
|
+
getQuota(): Promise<AccountGetQuotaResult>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface CopilotSdkModule {
|
|
31
|
+
CopilotClient: new (options?: CopilotClientOptions) => CopilotClientLike;
|
|
32
|
+
}
|
|
33
|
+
interface ResolveBundledCopilotCliPathOptions {
|
|
34
|
+
platform?: NodeJS.Platform | string;
|
|
35
|
+
arch?: string;
|
|
36
|
+
resolvePackage?: (packageName: string) => string;
|
|
37
|
+
resolvePackagePaths?: (packageName: string) => string[] | null;
|
|
38
|
+
existsSyncFn?: (path: string) => boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface GetCopilotQuotaOptions {
|
|
41
|
+
/** Cache TTL in seconds. Default 60. */
|
|
42
|
+
ttlSeconds?: number;
|
|
43
|
+
forceRefresh?: boolean;
|
|
44
|
+
/** Overall timeout for SDK start/auth/quota in ms. Cleanup has a separate small cap. */
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
cacheDir?: string;
|
|
47
|
+
/** Explicit token to pass to the Copilot SDK; otherwise SDK auth discovery is used. */
|
|
48
|
+
githubToken?: string;
|
|
49
|
+
/** SDK client options. Useful for enterprise hosts or tests. */
|
|
50
|
+
clientOptions?: CopilotClientOptions;
|
|
51
|
+
/** Override global fetch for GitHub login lookup tests. */
|
|
52
|
+
fetcher?: typeof fetch;
|
|
53
|
+
/** Override SDK module for tests. */
|
|
54
|
+
sdkModule?: CopilotSdkModule;
|
|
55
|
+
/** Override stored-token lookup for Copilot logged-in users. */
|
|
56
|
+
storedTokenResolver?: (authInfo: CopilotAuthInfo | undefined) => Promise<string | undefined>;
|
|
57
|
+
}
|
|
58
|
+
export declare function getCopilotQuota(opts?: GetCopilotQuotaOptions): Promise<CopilotQuota>;
|
|
59
|
+
export declare function resolveBundledCopilotCliPath({ platform, arch, resolvePackage, resolvePackagePaths, existsSyncFn, }?: ResolveBundledCopilotCliPathOptions): string | undefined;
|
|
60
|
+
/** Exported for tests. */
|
|
61
|
+
export declare function parseCopilotQuotaSnapshots(rawSnapshots: unknown): Omit<CopilotQuota, "tool" | "fetchedAt" | "source" | "error" | "login" | "authType" | "host" | "loginSource">;
|
|
62
|
+
/** Exported for tests. */
|
|
63
|
+
export declare function parseCopilotUserQuota(rawUser: unknown): Omit<CopilotQuota, "tool" | "fetchedAt" | "source" | "error" | "login" | "authType" | "host" | "loginSource">;
|
|
64
|
+
export {};
|