@goodandready/dsh-subscriptions 0.6.41 → 0.6.43
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/lib/accounts.js +18 -8
- package/lib/adapter.js +8 -4
- package/lib/backoff.js +13 -1
- package/lib/code-assist.js +1 -0
- package/lib/google-validation.js +1 -1
- package/lib/http.js +90 -6
- package/lib/import-auth.js +91 -44
- package/lib/oauth.js +12 -0
- package/lib/routes/accounts.js +5 -4
- package/lib/routes/status.js +6 -8
- package/lib/runaway-guard.js +5 -0
- package/lib/session-pin.js +9 -4
- package/lib/sse.js +6 -1
- package/lib/stream-rotate.js +1 -1
- package/lib/vendor-factory.js +6 -4
- package/lib/vendors/claude-cli.js +2 -1
- package/lib/vendors/codex.js +4 -2
- package/lib/vendors/copilot.js +1 -0
- package/lib/vendors/cursor.js +1 -0
- package/lib/vendors/glm.js +1 -0
- package/lib/vendors/kimi.js +1 -0
- package/lib/vendors/kiro.js +1 -0
- package/lib/vendors/qwen.js +1 -0
- package/lib/wire.js +12 -11
- package/package.json +13 -8
package/lib/accounts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { needsWarmupProbe, enterProbing, resolveWarmupSuccess, resolveWarmupFailure, STATUS_QUARANTINE, STATUS_PROBING } from "./quarantine.js"
|
|
2
|
-
import { fetchWithTimeout } from "./http.js"
|
|
2
|
+
import { fetchWithTimeout, isTimeoutError } from "./http.js"
|
|
3
3
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
4
4
|
import { oauthRef, isProvider } from './refs.js'
|
|
5
5
|
import { parseBlob, serializeBlob } from './blob.js'
|
|
@@ -75,7 +75,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
75
75
|
const doFetch = fetchImpl || fetch
|
|
76
76
|
// #88: per-account proxy fetch for token refresh / usage (falls back to doFetch).
|
|
77
77
|
const rawFetchFor = (ref) => (typeof fetchForRef === 'function' && fetchForRef(ref)) || doFetch
|
|
78
|
-
const fetchFor = (ref) => (url, init, opts) => fetchWithTimeout(rawFetchFor(ref), url, init, { timeoutMs:
|
|
78
|
+
const fetchFor = (ref, defaultTimeoutMs = 25000) => (url, init, opts) => fetchWithTimeout(rawFetchFor(ref), url, init, { timeoutMs: defaultTimeoutMs, ...(opts || {}) })
|
|
79
79
|
|
|
80
80
|
async function resolveRaw(ref) {
|
|
81
81
|
try {
|
|
@@ -89,7 +89,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
89
89
|
async function loadBlob(ref) {
|
|
90
90
|
const raw = await resolveRaw(ref)
|
|
91
91
|
if (!raw) {
|
|
92
|
-
const err = new Error(`no token for ${ref}`)
|
|
92
|
+
const err = /** @type {Error & { code?: string }} */ (new Error(`no token for ${ref}`))
|
|
93
93
|
err.code = 'AUTH'
|
|
94
94
|
throw err
|
|
95
95
|
}
|
|
@@ -103,7 +103,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
103
103
|
try {
|
|
104
104
|
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
105
105
|
} catch (err) {
|
|
106
|
-
const wrapErr = new Error(`failed to persist credentials for ${ref}: ${err && err.message || err}`)
|
|
106
|
+
const wrapErr = /** @type {Error & { code?: string, cause?: any }} */ (new Error(`failed to persist credentials for ${ref}: ${err && err.message || err}`))
|
|
107
107
|
wrapErr.code = "CREDENTIAL_SAVE_FAILED"
|
|
108
108
|
wrapErr.cause = err
|
|
109
109
|
throw wrapErr
|
|
@@ -171,6 +171,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
171
171
|
healthBadge: healthBadge(computeHealthScore(health.get(ref) || null)),
|
|
172
172
|
usagePercent: usage.has(ref) ? usage.get(ref) : null,
|
|
173
173
|
refreshError: refreshFailures.get(ref)?.error || '',
|
|
174
|
+
isTimeout: !!refreshFailures.get(ref)?.isTimeout,
|
|
174
175
|
validationUrl: base.validationUrl || '',
|
|
175
176
|
validationMessage: base.validationMessage || '',
|
|
176
177
|
accountNotice: base.accountNotice || '',
|
|
@@ -227,7 +228,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
227
228
|
const curConfig = getConfig()
|
|
228
229
|
const cfg = vendorConfig(provider, curConfig)
|
|
229
230
|
const next = await executeWithRetry(
|
|
230
|
-
() => getVendor(provider).refresh(cfg, blob, fetchFor(ref)),
|
|
231
|
+
() => getVendor(provider).refresh(cfg, blob, fetchFor(ref, 30000)),
|
|
231
232
|
{
|
|
232
233
|
maxRetries: 2,
|
|
233
234
|
initialDelayMs: 300,
|
|
@@ -250,7 +251,11 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
250
251
|
if (ref) refreshFailures.delete(ref)
|
|
251
252
|
return merged
|
|
252
253
|
} catch (e) {
|
|
253
|
-
|
|
254
|
+
const isTimeout = isTimeoutError(e)
|
|
255
|
+
const errMsg = isTimeout
|
|
256
|
+
? `Network timeout refreshing token: ${e.message || 'timed out'}`
|
|
257
|
+
: String(e && e.message || e)
|
|
258
|
+
if (ref) refreshFailures.set(ref, { at: Date.now(), error: errMsg, isTimeout })
|
|
254
259
|
throw e
|
|
255
260
|
} finally {
|
|
256
261
|
refreshLocks.delete(lockKey)
|
|
@@ -287,7 +292,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
287
292
|
try {
|
|
288
293
|
usageFetched.set(slot.ref, Date.now())
|
|
289
294
|
const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
|
|
290
|
-
const
|
|
295
|
+
const usageTimeout = Number(curConfig?.usageTimeoutMs) || 15000
|
|
296
|
+
const snap = await vendor.usage(blob, cfg, fetchFor(slot.ref, usageTimeout))
|
|
291
297
|
if (!snap) return
|
|
292
298
|
if (Number.isFinite(Number(snap.usedPercent))) {
|
|
293
299
|
usage.set(slot.ref, Number(snap.usedPercent))
|
|
@@ -322,8 +328,12 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
322
328
|
}
|
|
323
329
|
} catch { /* persistence best-effort */ }
|
|
324
330
|
}
|
|
325
|
-
} catch {
|
|
331
|
+
} catch (err) {
|
|
326
332
|
usageFetched.set(slot.ref, Date.now())
|
|
333
|
+
const isTimeout = isTimeoutError(err)
|
|
334
|
+
if (isTimeout && !refreshFailures.has(slot.ref)) {
|
|
335
|
+
refreshFailures.set(slot.ref, { at: Date.now(), error: `Network timeout fetching usage: ${err.message || 'timed out'}`, isTimeout: true })
|
|
336
|
+
}
|
|
327
337
|
} finally {
|
|
328
338
|
slotUsageLocks.delete(slot.ref)
|
|
329
339
|
}
|
package/lib/adapter.js
CHANGED
|
@@ -53,14 +53,16 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
53
53
|
let models = null
|
|
54
54
|
if (targetAccount) {
|
|
55
55
|
const cfg = this.deps.vendorConfig(provider)
|
|
56
|
-
const
|
|
56
|
+
const baseFetch = pickFetch(this.deps, targetAccount.ref)
|
|
57
|
+
const listModelsFetch = (url, init, opts) =>
|
|
58
|
+
fetchWithTimeout(baseFetch, url, init, { timeoutMs: 15000, ...(opts || {}) })
|
|
57
59
|
try {
|
|
58
60
|
const blob = await this.deps.ensureFresh(
|
|
59
61
|
provider,
|
|
60
62
|
await this.deps.loadBlob(targetAccount.ref),
|
|
61
63
|
targetAccount.ref,
|
|
62
64
|
)
|
|
63
|
-
const live = await getVendor(provider).listModels(blob, cfg,
|
|
65
|
+
const live = await getVendor(provider).listModels(blob, cfg, listModelsFetch)
|
|
64
66
|
if (Array.isArray(live) && live.length) models = live
|
|
65
67
|
} catch { /* use built-in catalog */ }
|
|
66
68
|
}
|
|
@@ -200,7 +202,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
200
202
|
// ponytail: capture x-ratelimit headers without touching vendors
|
|
201
203
|
const baseFetch = pickFetch(deps, account.ref)
|
|
202
204
|
const quotaFetch = async (url, init) => {
|
|
203
|
-
const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs:
|
|
205
|
+
const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs: 60000, stream: true })
|
|
204
206
|
try {
|
|
205
207
|
const snap = quotaSnapshot(provider, res.headers, null, Date.now())
|
|
206
208
|
if (snap && typeof deps.rememberQuota === "function") {
|
|
@@ -221,7 +223,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
221
223
|
} catch { /* ignore quota capture failure */ }
|
|
222
224
|
if (!res.ok && res.clone) {
|
|
223
225
|
try {
|
|
224
|
-
const
|
|
226
|
+
const textPromise = res.clone().text()
|
|
227
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))
|
|
228
|
+
const txt = await Promise.race([textPromise, timeoutPromise])
|
|
225
229
|
let j=null; try { j = JSON.parse(txt) } catch { j = null }
|
|
226
230
|
if (j) {
|
|
227
231
|
const b = parseBody(provider, j, Date.now())
|
package/lib/backoff.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @param {() => Promise<T>} fn
|
|
4
|
+
* @param {{
|
|
5
|
+
* maxRetries?: number,
|
|
6
|
+
* initialDelayMs?: number,
|
|
7
|
+
* maxDelayMs?: number,
|
|
8
|
+
* factor?: number,
|
|
9
|
+
* isRetryable?: (err: any) => boolean,
|
|
10
|
+
* }} [options]
|
|
11
|
+
* @returns {Promise<T>}
|
|
12
|
+
*/
|
|
1
13
|
export async function executeWithRetry(fn, {
|
|
2
14
|
maxRetries = 3,
|
|
3
15
|
initialDelayMs = 200,
|
|
4
16
|
maxDelayMs = 3000,
|
|
5
17
|
factor = 2,
|
|
6
|
-
isRetryable = () => true
|
|
18
|
+
isRetryable = (_err) => true
|
|
7
19
|
} = {}) {
|
|
8
20
|
let attempt = 0
|
|
9
21
|
let delay = initialDelayMs
|
package/lib/code-assist.js
CHANGED
|
@@ -204,6 +204,7 @@ export function streamEnvelope({ projectId, model, request, userAgent, sessionId
|
|
|
204
204
|
const session = sessionId || randomUUID()
|
|
205
205
|
const body = { ...(request || {}) }
|
|
206
206
|
if (!body.session_id) body.session_id = session
|
|
207
|
+
/** @type {Record<string, any>} */
|
|
207
208
|
const envelope = {
|
|
208
209
|
...(projectId ? { project: projectId } : {}),
|
|
209
210
|
model,
|
package/lib/google-validation.js
CHANGED
|
@@ -98,7 +98,7 @@ export function validationFromHttpError(status, bodyText) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export function validationRequiredError(details) {
|
|
101
|
-
const err = new Error(`${details.message} Verify at ${details.validationUrl}`)
|
|
101
|
+
const err = /** @type {Error & { code?: string, validationUrl?: string, learnMoreUrl?: string }} */ (new Error(`${details.message} Verify at ${details.validationUrl}`))
|
|
102
102
|
err.code = 'VALIDATION_REQUIRED'
|
|
103
103
|
err.validationUrl = details.validationUrl
|
|
104
104
|
if (details.learnMoreUrl) err.learnMoreUrl = details.learnMoreUrl
|
package/lib/http.js
CHANGED
|
@@ -81,7 +81,7 @@ export function isTrustedSettingsRequest(request) {
|
|
|
81
81
|
if (secFetchSite === 'cross-site') return false
|
|
82
82
|
|
|
83
83
|
// 2. If origin header is present, it must match host
|
|
84
|
-
const host = headers.host || headers['x-forwarded-host']
|
|
84
|
+
const host = String(Array.isArray(headers.host) ? headers.host[0] : (headers.host || headers['x-forwarded-host'] || ''))
|
|
85
85
|
const origin = headers.origin
|
|
86
86
|
if (origin) {
|
|
87
87
|
try {
|
|
@@ -167,17 +167,36 @@ export function sanitizeRequestHeaders(url, init = {}, options = {}) {
|
|
|
167
167
|
delete headers.Cookie
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
} catch {
|
|
170
|
+
} catch (err) {
|
|
171
|
+
void err
|
|
172
|
+
return headers
|
|
173
|
+
}
|
|
171
174
|
return headers
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
export
|
|
177
|
+
export function isTimeoutError(err) {
|
|
178
|
+
if (!err) return false
|
|
179
|
+
return err.name === 'TimeoutError'
|
|
180
|
+
|| err.code === 'ETIMEDOUT'
|
|
181
|
+
|| (err.name === 'AbortError' && String(err.message || '').toLowerCase().includes('timeout'))
|
|
182
|
+
|| String(err.message || '').toLowerCase().includes('aborted due to timeout')
|
|
183
|
+
|| String(err.message || '').toLowerCase().includes('timed out')
|
|
184
|
+
|| String(err.message || '').toLowerCase().includes('operation was aborted')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {any} fetchImpl
|
|
189
|
+
* @param {string} url
|
|
190
|
+
* @param {any} [init]
|
|
191
|
+
* @param {{ timeoutMs?: number, allowedOrigins?: string[], stream?: boolean }} [options]
|
|
192
|
+
*/
|
|
193
|
+
export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs = 30000, allowedOrigins, stream = false } = {}) {
|
|
175
194
|
const safeHeaders = sanitizeRequestHeaders(url, init, { allowedOrigins })
|
|
176
195
|
init = { ...init, headers: safeHeaders }
|
|
177
196
|
const impl = fetchImpl || fetch
|
|
178
197
|
if (!timeoutMs || timeoutMs <= 0) return impl(url, init)
|
|
179
198
|
const controller = new AbortController()
|
|
180
|
-
|
|
199
|
+
let timer = setTimeout(() => {
|
|
181
200
|
controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
182
201
|
}, timeoutMs)
|
|
183
202
|
|
|
@@ -186,9 +205,74 @@ export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs =
|
|
|
186
205
|
: controller.signal
|
|
187
206
|
|
|
188
207
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
const res = await impl(url, { ...init, signal })
|
|
209
|
+
if (stream) {
|
|
210
|
+
clearTimeout(timer)
|
|
211
|
+
return res
|
|
212
|
+
}
|
|
213
|
+
const origJson = typeof res.json === 'function' ? res.json.bind(res) : null
|
|
214
|
+
const origText = typeof res.text === 'function' ? res.text.bind(res) : null
|
|
215
|
+
if (origJson) {
|
|
216
|
+
res.json = () => {
|
|
217
|
+
if (controller.signal.aborted) {
|
|
218
|
+
return Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
219
|
+
}
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
const onAbort = () => reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
222
|
+
controller.signal.addEventListener('abort', onAbort, { once: true })
|
|
223
|
+
origJson().then(
|
|
224
|
+
(val) => {
|
|
225
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
226
|
+
clearTimeout(timer)
|
|
227
|
+
resolve(val)
|
|
228
|
+
},
|
|
229
|
+
(err) => {
|
|
230
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
231
|
+
clearTimeout(timer)
|
|
232
|
+
if (controller.signal.aborted) {
|
|
233
|
+
reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
234
|
+
} else {
|
|
235
|
+
reject(err)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (origText) {
|
|
243
|
+
res.text = () => {
|
|
244
|
+
if (controller.signal.aborted) {
|
|
245
|
+
return Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
246
|
+
}
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const onAbort = () => reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
249
|
+
controller.signal.addEventListener('abort', onAbort, { once: true })
|
|
250
|
+
origText().then(
|
|
251
|
+
(val) => {
|
|
252
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
253
|
+
clearTimeout(timer)
|
|
254
|
+
resolve(val)
|
|
255
|
+
},
|
|
256
|
+
(err) => {
|
|
257
|
+
controller.signal.removeEventListener('abort', onAbort)
|
|
258
|
+
clearTimeout(timer)
|
|
259
|
+
if (controller.signal.aborted) {
|
|
260
|
+
reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
261
|
+
} else {
|
|
262
|
+
reject(err)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return res
|
|
270
|
+
} catch (err) {
|
|
191
271
|
clearTimeout(timer)
|
|
272
|
+
if (controller.signal.aborted && !isTimeoutError(err)) {
|
|
273
|
+
throw new DOMException('The operation was aborted due to timeout', 'TimeoutError')
|
|
274
|
+
}
|
|
275
|
+
throw err
|
|
192
276
|
}
|
|
193
277
|
}
|
|
194
278
|
|
package/lib/import-auth.js
CHANGED
|
@@ -39,6 +39,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
39
39
|
const tok = codex.tokens || codex
|
|
40
40
|
detected.codex = {
|
|
41
41
|
provider: 'codex',
|
|
42
|
+
source: 'file',
|
|
42
43
|
path: homeFile('.codex', 'auth.json'),
|
|
43
44
|
email: codex.email || codex.account || '',
|
|
44
45
|
hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
|
|
@@ -54,6 +55,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
54
55
|
if (grok && (grok.access_token || grok.token || (grok.tokens && grok.tokens.access_token))) {
|
|
55
56
|
detected.grok = {
|
|
56
57
|
provider: 'grok',
|
|
58
|
+
source: 'file',
|
|
57
59
|
path: p,
|
|
58
60
|
email: grok.email || grok.account || '',
|
|
59
61
|
hasRefreshToken: !!(grok.refresh_token || (grok.tokens && grok.tokens.refresh_token)),
|
|
@@ -73,6 +75,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
73
75
|
if (tok && (tok.access_token || tok.accessToken)) {
|
|
74
76
|
detected.antigravity = {
|
|
75
77
|
provider: 'antigravity',
|
|
78
|
+
source: 'file',
|
|
76
79
|
path: p,
|
|
77
80
|
email: agy.email || tok.account || '',
|
|
78
81
|
hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
|
|
@@ -86,6 +89,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
86
89
|
if (kimi && (kimi.access_token || kimi.token)) {
|
|
87
90
|
detected.kimi = {
|
|
88
91
|
provider: 'kimi',
|
|
92
|
+
source: 'file',
|
|
89
93
|
path: homeFile('.kimi-code', 'credentials', 'kimi-code.json'),
|
|
90
94
|
email: kimi.email || kimi.account || '',
|
|
91
95
|
hasRefreshToken: !!(kimi.refresh_token || kimi.refreshToken),
|
|
@@ -103,6 +107,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
103
107
|
if (glm && (glm.apiKey || glm.api_key || (glm.provider && glm.provider.apiKey))) {
|
|
104
108
|
detected.glm = {
|
|
105
109
|
provider: 'glm',
|
|
110
|
+
source: 'file',
|
|
106
111
|
path: p,
|
|
107
112
|
email: 'ZCode CLI Account',
|
|
108
113
|
hasRefreshToken: false,
|
|
@@ -111,20 +116,31 @@ async function discoverLocalCliSessionsInner() {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
// 6. Cursor Token (
|
|
119
|
+
// 6. Cursor Token (CLI config, auth.json, or env)
|
|
115
120
|
const cursorCliConfig = await readJson(homeFile('.cursor', 'cli-config.json'))
|
|
116
|
-
|
|
121
|
+
const cursorAuthConfig = await readJson(homeFile('.cursor', 'auth.json'))
|
|
122
|
+
if (cursorCliConfig && (cursorCliConfig.authInfo || cursorCliConfig.serverConfigCache)) {
|
|
117
123
|
detected.cursor = {
|
|
118
124
|
provider: 'cursor',
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
source: 'file',
|
|
126
|
+
path: homeFile('.cursor', 'cli-config.json'),
|
|
127
|
+
email: (cursorCliConfig.authInfo && cursorCliConfig.authInfo.email) || 'Cursor CLI User',
|
|
121
128
|
hasRefreshToken: false,
|
|
122
129
|
}
|
|
123
|
-
} else if (
|
|
130
|
+
} else if (cursorAuthConfig && (cursorAuthConfig.accessToken || cursorAuthConfig.access_token || (cursorAuthConfig.authInfo && cursorAuthConfig.authInfo.authId))) {
|
|
124
131
|
detected.cursor = {
|
|
125
132
|
provider: 'cursor',
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
source: 'file',
|
|
134
|
+
path: homeFile('.cursor', 'auth.json'),
|
|
135
|
+
email: (cursorAuthConfig.authInfo && cursorAuthConfig.authInfo.email) || cursorAuthConfig.email || 'Cursor User',
|
|
136
|
+
hasRefreshToken: false,
|
|
137
|
+
}
|
|
138
|
+
} else if (process.env.CURSOR_ACCESS_TOKEN) {
|
|
139
|
+
detected.cursor = {
|
|
140
|
+
provider: 'cursor',
|
|
141
|
+
source: 'env',
|
|
142
|
+
path: 'CURSOR_ACCESS_TOKEN',
|
|
143
|
+
email: 'Cursor IDE User',
|
|
128
144
|
hasRefreshToken: false,
|
|
129
145
|
}
|
|
130
146
|
}
|
|
@@ -139,6 +155,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
139
155
|
if (k && (k.accessToken || k.access_token || k.token)) {
|
|
140
156
|
detected.kiro = {
|
|
141
157
|
provider: 'kiro',
|
|
158
|
+
source: 'file',
|
|
142
159
|
path: p,
|
|
143
160
|
email: k.email || k.account || 'AWS Kiro User',
|
|
144
161
|
hasRefreshToken: !!(k.refreshToken || k.refresh_token),
|
|
@@ -149,7 +166,8 @@ async function discoverLocalCliSessionsInner() {
|
|
|
149
166
|
if (!detected.kiro && process.env.KIRO_API_KEY) {
|
|
150
167
|
detected.kiro = {
|
|
151
168
|
provider: 'kiro',
|
|
152
|
-
|
|
169
|
+
source: 'env',
|
|
170
|
+
path: 'KIRO_API_KEY',
|
|
153
171
|
email: 'AWS Kiro API Key',
|
|
154
172
|
hasRefreshToken: false,
|
|
155
173
|
}
|
|
@@ -158,12 +176,15 @@ async function discoverLocalCliSessionsInner() {
|
|
|
158
176
|
// 8. Claude CLI Direct Import
|
|
159
177
|
const claudeCreds = await readClaudeCredentials(activeHome)
|
|
160
178
|
if (claudeCreds) {
|
|
161
|
-
|
|
179
|
+
const claudeEntry = {
|
|
162
180
|
provider: 'claude',
|
|
181
|
+
source: 'file',
|
|
163
182
|
path: homeFile('.claude', 'credentials.json'),
|
|
164
|
-
email: 'Claude Code CLI User',
|
|
183
|
+
email: claudeCreds.email || 'Claude Code CLI User',
|
|
165
184
|
hasRefreshToken: !!claudeCreds.refreshToken,
|
|
166
185
|
}
|
|
186
|
+
detected.claude = claudeEntry
|
|
187
|
+
detected['claude-cli'] = claudeEntry
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
// 9. GitHub Copilot token
|
|
@@ -179,6 +200,7 @@ async function discoverLocalCliSessionsInner() {
|
|
|
179
200
|
if (tok) {
|
|
180
201
|
detected.copilot = {
|
|
181
202
|
provider: 'copilot',
|
|
203
|
+
source: 'file',
|
|
182
204
|
path: p,
|
|
183
205
|
email: gh.user || 'GitHub Copilot User',
|
|
184
206
|
hasRefreshToken: false,
|
|
@@ -190,37 +212,37 @@ async function discoverLocalCliSessionsInner() {
|
|
|
190
212
|
if (!detected.copilot && (process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN)) {
|
|
191
213
|
detected.copilot = {
|
|
192
214
|
provider: 'copilot',
|
|
193
|
-
|
|
215
|
+
source: 'env',
|
|
216
|
+
path: process.env.GITHUB_COPILOT_TOKEN ? 'GITHUB_COPILOT_TOKEN' : 'GH_TOKEN',
|
|
194
217
|
email: 'GitHub Copilot User',
|
|
195
218
|
hasRefreshToken: false,
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
222
|
return detected
|
|
202
223
|
}
|
|
203
224
|
|
|
204
225
|
export async function loadLocalCliBlob(provider, options = {}) {
|
|
226
|
+
const canonical = (provider === 'claude-cli') ? 'claude' : provider
|
|
205
227
|
const discovered = await discoverLocalCliSessions(options)
|
|
206
|
-
const info = discovered[provider]
|
|
228
|
+
const info = discovered[canonical] || discovered[provider]
|
|
207
229
|
if (!info) throw new Error(`no local CLI session found for ${provider}`)
|
|
208
230
|
|
|
209
|
-
if (
|
|
231
|
+
if (canonical === 'claude') {
|
|
210
232
|
const creds = await readClaudeCredentials(options && options.home)
|
|
211
233
|
if (!creds) throw new Error('Claude CLI credentials not found')
|
|
212
234
|
return {
|
|
213
235
|
accessToken: creds.accessToken,
|
|
214
|
-
refreshToken: creds.refreshToken,
|
|
236
|
+
refreshToken: creds.refreshToken || '',
|
|
215
237
|
expiresAt: creds.expiresAt,
|
|
216
|
-
email: 'Claude Code CLI User'
|
|
238
|
+
email: creds.email || 'Claude Code CLI User',
|
|
217
239
|
}
|
|
218
240
|
}
|
|
219
241
|
|
|
220
|
-
if (
|
|
242
|
+
if (canonical === 'copilot') {
|
|
221
243
|
let tok = process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN || ''
|
|
222
244
|
let email = 'GitHub Copilot User'
|
|
223
|
-
if (info.
|
|
245
|
+
if (info.source === 'file' && info.path) {
|
|
224
246
|
const cp = await readJson(info.path)
|
|
225
247
|
if (cp) {
|
|
226
248
|
const gh = cp['github.com'] || cp
|
|
@@ -228,6 +250,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
228
250
|
email = gh.user || email
|
|
229
251
|
}
|
|
230
252
|
}
|
|
253
|
+
if (!tok) throw new Error('no Copilot token found')
|
|
231
254
|
return {
|
|
232
255
|
accessToken: tok,
|
|
233
256
|
refreshToken: tok,
|
|
@@ -238,10 +261,54 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
238
261
|
}
|
|
239
262
|
}
|
|
240
263
|
|
|
264
|
+
if (canonical === 'cursor') {
|
|
265
|
+
if (info.source === 'env' || !info.path || info.path.includes('env') || info.path === 'CURSOR_ACCESS_TOKEN') {
|
|
266
|
+
const token = process.env.CURSOR_ACCESS_TOKEN
|
|
267
|
+
if (!token) throw new Error('CURSOR_ACCESS_TOKEN environment variable not set')
|
|
268
|
+
return {
|
|
269
|
+
accessToken: token,
|
|
270
|
+
refreshToken: '',
|
|
271
|
+
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
272
|
+
email: 'Cursor IDE User',
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const raw = await readJson(info.path)
|
|
276
|
+
if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
|
|
277
|
+
const token = (raw && (raw.accessToken || raw.access_token || (raw.authInfo && raw.authInfo.authId))) || process.env.CURSOR_ACCESS_TOKEN || ''
|
|
278
|
+
return {
|
|
279
|
+
accessToken: token,
|
|
280
|
+
refreshToken: '',
|
|
281
|
+
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
282
|
+
email: (raw && raw.authInfo && raw.authInfo.email) || raw.email || 'Cursor User',
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (canonical === 'kiro') {
|
|
287
|
+
if (info.source === 'env' || !info.path || info.path.includes('env') || info.path === 'KIRO_API_KEY') {
|
|
288
|
+
const token = process.env.KIRO_API_KEY
|
|
289
|
+
if (!token) throw new Error('KIRO_API_KEY environment variable not set')
|
|
290
|
+
return {
|
|
291
|
+
accessToken: token,
|
|
292
|
+
refreshToken: '',
|
|
293
|
+
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
294
|
+
email: 'AWS Kiro API Key',
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const raw = await readJson(info.path)
|
|
298
|
+
if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
|
|
299
|
+
const token = (raw && (raw.accessToken || raw.access_token || raw.token)) || process.env.KIRO_API_KEY || ''
|
|
300
|
+
return {
|
|
301
|
+
accessToken: token,
|
|
302
|
+
refreshToken: (raw && (raw.refreshToken || raw.refresh_token)) || '',
|
|
303
|
+
expiresAt: (raw && raw.expiresAt) || (Date.now() + 30 * 86400 * 1000),
|
|
304
|
+
email: (raw && raw.email) || 'AWS Kiro User',
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
241
308
|
const raw = await readJson(info.path)
|
|
242
309
|
if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
|
|
243
310
|
|
|
244
|
-
if (
|
|
311
|
+
if (canonical === 'codex') {
|
|
245
312
|
const tok = raw.tokens || raw
|
|
246
313
|
return {
|
|
247
314
|
accessToken: tok.access_token || tok.accessToken || '',
|
|
@@ -252,7 +319,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
252
319
|
}
|
|
253
320
|
}
|
|
254
321
|
|
|
255
|
-
if (
|
|
322
|
+
if (canonical === 'grok') {
|
|
256
323
|
const tok = raw.tokens || raw
|
|
257
324
|
return {
|
|
258
325
|
accessToken: tok.access_token || tok.token || '',
|
|
@@ -262,7 +329,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
262
329
|
}
|
|
263
330
|
}
|
|
264
331
|
|
|
265
|
-
if (
|
|
332
|
+
if (canonical === 'antigravity') {
|
|
266
333
|
const tok = raw.token || raw
|
|
267
334
|
return {
|
|
268
335
|
accessToken: tok.access_token || tok.accessToken || '',
|
|
@@ -273,7 +340,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
273
340
|
}
|
|
274
341
|
}
|
|
275
342
|
|
|
276
|
-
if (
|
|
343
|
+
if (canonical === 'kimi') {
|
|
277
344
|
return {
|
|
278
345
|
accessToken: raw.access_token || raw.token || '',
|
|
279
346
|
refreshToken: raw.refresh_token || '',
|
|
@@ -282,7 +349,7 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
282
349
|
}
|
|
283
350
|
}
|
|
284
351
|
|
|
285
|
-
if (
|
|
352
|
+
if (canonical === 'glm') {
|
|
286
353
|
const apiKey = raw.apiKey || raw.api_key || (raw.provider && raw.provider.apiKey) || ''
|
|
287
354
|
return {
|
|
288
355
|
accessToken: apiKey,
|
|
@@ -292,25 +359,5 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
292
359
|
}
|
|
293
360
|
}
|
|
294
361
|
|
|
295
|
-
if (provider === 'cursor') {
|
|
296
|
-
const token = process.env.CURSOR_ACCESS_TOKEN || (raw && (raw.accessToken || raw.access_token || (raw.authInfo && raw.authInfo.authId))) || ''
|
|
297
|
-
return {
|
|
298
|
-
accessToken: token,
|
|
299
|
-
refreshToken: '',
|
|
300
|
-
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
301
|
-
email: (raw && raw.authInfo && raw.authInfo.email) || 'Cursor User',
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (provider === 'kiro') {
|
|
306
|
-
const token = process.env.KIRO_API_KEY || (raw && (raw.accessToken || raw.access_token || raw.token)) || ''
|
|
307
|
-
return {
|
|
308
|
-
accessToken: token,
|
|
309
|
-
refreshToken: (raw && (raw.refreshToken || raw.refresh_token)) || '',
|
|
310
|
-
expiresAt: (raw && raw.expiresAt) || (Date.now() + 30 * 86400 * 1000),
|
|
311
|
-
email: (raw && raw.email) || 'AWS Kiro User',
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
362
|
throw new Error(`unsupported local CLI import for provider ${provider}`)
|
|
316
363
|
}
|
package/lib/oauth.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{
|
|
3
|
+
* authUrl: string,
|
|
4
|
+
* clientId: string,
|
|
5
|
+
* redirectUri: string,
|
|
6
|
+
* challenge: string,
|
|
7
|
+
* state: string,
|
|
8
|
+
* extra?: Record<string, any>,
|
|
9
|
+
* scope?: string,
|
|
10
|
+
* }} params
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
1
13
|
export function buildAuthorizeUrl({
|
|
2
14
|
authUrl,
|
|
3
15
|
clientId,
|
package/lib/routes/accounts.js
CHANGED
|
@@ -143,7 +143,9 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
143
143
|
} catch {
|
|
144
144
|
body = null
|
|
145
145
|
}
|
|
146
|
-
|
|
146
|
+
const rawProv = body && body.provider
|
|
147
|
+
const prov = (rawProv === 'claude-cli') ? 'claude' : rawProv
|
|
148
|
+
if (!prov || !isProvider(prov)) {
|
|
147
149
|
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'unknown or missing provider' } })
|
|
148
150
|
return
|
|
149
151
|
}
|
|
@@ -153,9 +155,8 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
153
155
|
return
|
|
154
156
|
}
|
|
155
157
|
try {
|
|
156
|
-
const blob = await loadLocalCliBlob(
|
|
157
|
-
const
|
|
158
|
-
const idx = Number(body.index) || 1
|
|
158
|
+
const blob = await loadLocalCliBlob(prov)
|
|
159
|
+
const idx = idxNum
|
|
159
160
|
const curSlots = Array.isArray(live().slots) ? live().slots.slice() : []
|
|
160
161
|
const exists = curSlots.some((s) => s && s.provider === prov && Number(s.index) === idx)
|
|
161
162
|
if (!exists && getSettingsApi()) {
|
package/lib/routes/status.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getRecentAlerts, clearAlerts } from '../alerts.js'
|
|
3
3
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
4
4
|
import { Config, plainConfig, publicConfig, restoreMaskedSecrets } from '../config-schema.js'
|
|
5
|
-
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
5
|
+
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson, fetchWithTimeout, isTimeoutError } from '../http.js'
|
|
6
6
|
import { quotaSnapshot } from '../ratelimit.js'
|
|
7
7
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
8
8
|
import { getVendor } from '../vendors/index.js'
|
|
@@ -257,10 +257,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
257
257
|
}
|
|
258
258
|
let capturedQuota = null
|
|
259
259
|
const probeFetch = async (url, init = {}) => {
|
|
260
|
-
const timeoutSignal = AbortSignal.timeout(15_000)
|
|
261
|
-
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
262
260
|
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
263
|
-
const res2 = await doFetch
|
|
261
|
+
const res2 = await fetchWithTimeout(doFetch, url, init, { timeoutMs: 15_000 })
|
|
264
262
|
try {
|
|
265
263
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
266
264
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
|
@@ -272,12 +270,14 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
272
270
|
const latencyMs = Date.now() - t0;
|
|
273
271
|
writeJson(res, 200, { ok: true, provider, index: index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
|
|
274
272
|
} catch (e) {
|
|
273
|
+
const isTimeout = isTimeoutError(e)
|
|
275
274
|
writeJson(res, 200, {
|
|
276
275
|
ok: false, provider, index: index, ref,
|
|
277
276
|
latencyMs: Date.now() - t0,
|
|
278
277
|
email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
|
|
279
278
|
quota: capturedQuota || info.quota || null,
|
|
280
|
-
|
|
279
|
+
isTimeout,
|
|
280
|
+
error: { code: isTimeout ? 'TIMEOUT' : (e && e.code ? e.code : 'VENDOR'), message: String(e && e.message || e).slice(0, 300) },
|
|
281
281
|
})
|
|
282
282
|
}
|
|
283
283
|
}),
|
|
@@ -384,10 +384,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
384
384
|
if (typeof vendor.check === 'function') {
|
|
385
385
|
let capturedQuota = null
|
|
386
386
|
const probeFetch = async (url, init = {}) => {
|
|
387
|
-
const timeoutSignal = AbortSignal.timeout(15_000)
|
|
388
|
-
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
389
387
|
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
390
|
-
const res2 = await doFetch
|
|
388
|
+
const res2 = await fetchWithTimeout(doFetch, url, init, { timeoutMs: 15_000 })
|
|
391
389
|
try {
|
|
392
390
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
393
391
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
package/lib/runaway-guard.js
CHANGED
|
@@ -68,6 +68,11 @@ export class RunawayDetector {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @param {AsyncIterable<any>} stream
|
|
73
|
+
* @param {Record<string, any>} [options]
|
|
74
|
+
* @returns {AsyncGenerator<any, void, unknown>}
|
|
75
|
+
*/
|
|
71
76
|
export async function* withRunawayGuard(stream, options = {}) {
|
|
72
77
|
if (options && options.enabled === false) {
|
|
73
78
|
yield* stream
|
package/lib/session-pin.js
CHANGED
|
@@ -2,10 +2,8 @@ const sessionPins = new Map()
|
|
|
2
2
|
const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {string|undefined} accountRef
|
|
8
|
-
* @param {number} [ttlMs]
|
|
5
|
+
* Prune expired session pin entries.
|
|
6
|
+
* @param {number} [now]
|
|
9
7
|
* @returns {void}
|
|
10
8
|
*/
|
|
11
9
|
export function pruneSessionPins(now = Date.now()) {
|
|
@@ -16,6 +14,13 @@ export function pruneSessionPins(now = Date.now()) {
|
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Bind a session id to an account ref for ttlMs (default 30 minutes).
|
|
19
|
+
* @param {string|undefined} sessionId
|
|
20
|
+
* @param {string|undefined} accountRef
|
|
21
|
+
* @param {number} [ttlMs]
|
|
22
|
+
* @returns {void}
|
|
23
|
+
*/
|
|
19
24
|
export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
|
|
20
25
|
if (!sessionId || !accountRef) return
|
|
21
26
|
if (sessionPins.size >= 50) {
|
package/lib/sse.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {any} body
|
|
3
|
+
* @param {{ idleTimeoutMs?: number, signal?: AbortSignal }} [options]
|
|
4
|
+
* @returns {AsyncGenerator<string, void, unknown>}
|
|
5
|
+
*/
|
|
1
6
|
export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {}) {
|
|
2
7
|
const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
|
|
3
8
|
const decoder = new TextDecoder()
|
|
@@ -38,7 +43,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
38
43
|
const timeoutPromise = new Promise((_, reject) => {
|
|
39
44
|
if (!idleTimeoutMs || idleTimeoutMs <= 0) return
|
|
40
45
|
timer = setTimeout(() => {
|
|
41
|
-
const err = new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`)
|
|
46
|
+
const err = /** @type {Error & { code?: string }} */ (new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`))
|
|
42
47
|
err.code = 'TIMEOUT'
|
|
43
48
|
reject(err)
|
|
44
49
|
}, idleTimeoutMs)
|
package/lib/stream-rotate.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function* streamWithRotation({
|
|
|
41
41
|
return
|
|
42
42
|
}
|
|
43
43
|
if (lastError) throw lastError
|
|
44
|
-
const err = new Error('no usable subscription account for this provider')
|
|
44
|
+
const err = /** @type {Error & { code?: string }} */ (new Error('no usable subscription account for this provider'))
|
|
45
45
|
err.code = 'RATE_LIMIT'
|
|
46
46
|
throw err
|
|
47
47
|
}
|
package/lib/vendor-factory.js
CHANGED
|
@@ -69,7 +69,8 @@ export function createVendorFromProfile(profile) {
|
|
|
69
69
|
code_verifier: pkce.verifier,
|
|
70
70
|
}
|
|
71
71
|
const req = tokenStyle === "json" ? jsonTokenRequest : formTokenRequest
|
|
72
|
-
const
|
|
72
|
+
const tokenFetch = (u, i, o) => fetchWithTimeout(fetchImpl || fetch, u, i, { timeoutMs: 30000, ...(o || {}) })
|
|
73
|
+
const json = await req(profile.tokenUrl, params, tokenFetch)
|
|
73
74
|
const blob = tokenBlobFromOAuth(json)
|
|
74
75
|
return { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || name }
|
|
75
76
|
},
|
|
@@ -80,7 +81,8 @@ export function createVendorFromProfile(profile) {
|
|
|
80
81
|
refresh_token: blob.refreshToken,
|
|
81
82
|
}
|
|
82
83
|
const req = tokenStyle === "json" ? jsonTokenRequest : formTokenRequest
|
|
83
|
-
const
|
|
84
|
+
const tokenFetch = (u, i, o) => fetchWithTimeout(fetchImpl || fetch, u, i, { timeoutMs: 30000, ...(o || {}) })
|
|
85
|
+
const json = await req(profile.tokenUrl, params, tokenFetch)
|
|
84
86
|
return tokenBlobFromOAuth(json, { label: blob.label, email: blob.email })
|
|
85
87
|
},
|
|
86
88
|
async listModels(blob, cfg, fetchImpl) {
|
|
@@ -112,7 +114,7 @@ export function createVendorFromProfile(profile) {
|
|
|
112
114
|
async* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
113
115
|
const body = codexResponsesBody(options, "")
|
|
114
116
|
if (!body.instructions) delete body.instructions
|
|
115
|
-
const res = await (fetchImpl || fetch
|
|
117
|
+
const res = await fetchWithTimeout(fetchImpl || fetch, `${base}/responses`, {
|
|
116
118
|
method: "POST",
|
|
117
119
|
headers: {
|
|
118
120
|
...headers,
|
|
@@ -122,7 +124,7 @@ export function createVendorFromProfile(profile) {
|
|
|
122
124
|
},
|
|
123
125
|
body: JSON.stringify(body),
|
|
124
126
|
signal,
|
|
125
|
-
})
|
|
127
|
+
}, { timeoutMs: 60000, stream: true })
|
|
126
128
|
if (!res.ok) throw httpError(res.status, await res.text())
|
|
127
129
|
yield* codexResponsesStream(res.body)
|
|
128
130
|
},
|
|
@@ -18,7 +18,8 @@ export async function readClaudeCredentials(home) {
|
|
|
18
18
|
accessToken: oauth.accessToken || oauth.access_token,
|
|
19
19
|
refreshToken: oauth.refreshToken || oauth.refresh_token || '',
|
|
20
20
|
expiresAt: oauth.expiresAt || (oauth.expires_in ? Date.now() + oauth.expires_in * 1000 : Date.now() + 3600000),
|
|
21
|
-
subscriptionType: oauth.subscriptionType || 'Claude Code'
|
|
21
|
+
subscriptionType: oauth.subscriptionType || 'Claude Code',
|
|
22
|
+
email: oauth.email || json.email || 'Claude Code CLI User',
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
return null
|
package/lib/vendors/codex.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTimeoutError } from "../http.js"
|
|
1
2
|
import { randomUUID, createHash } from 'node:crypto'
|
|
2
3
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
4
|
import { codexResponsesBody, modelCatalog } from '../messages.js'
|
|
@@ -203,14 +204,15 @@ export async function usage(blob, cfg, fetchImpl) {
|
|
|
203
204
|
const snap = asUsageSnapshot(deepestUsedPercent(json))
|
|
204
205
|
if (snap) snap.windows = usageWindows(json)
|
|
205
206
|
return snap
|
|
206
|
-
} catch {
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (isTimeoutError(err)) throw err
|
|
207
209
|
return null
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
212
214
|
const base = (config.baseUrl || defaults().baseUrl).replace(/\/$/, '')
|
|
213
|
-
const body = codexResponsesBody(options, INSTRUCTIONS, config)
|
|
215
|
+
const body = /** @type {Record<string, any>} */ (codexResponsesBody(options, INSTRUCTIONS, config))
|
|
214
216
|
const sessionId = deriveSessionId(options)
|
|
215
217
|
|
|
216
218
|
// Prompt Cache Affinity
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -296,6 +296,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
296
296
|
|
|
297
297
|
if (!copilotToken) throw new LlmError("GitHub Copilot not authenticated", "AUTH")
|
|
298
298
|
|
|
299
|
+
/** @type {Record<string, any>} */
|
|
299
300
|
const body = {
|
|
300
301
|
model: options.model || "claude-3.7-sonnet",
|
|
301
302
|
messages: openaiMessages(options),
|
package/lib/vendors/cursor.js
CHANGED
|
@@ -175,6 +175,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
175
175
|
const token = blob && (blob.accessToken || blob.access_token)
|
|
176
176
|
if (!token) throw new LlmError('Cursor not authenticated', 'AUTH')
|
|
177
177
|
|
|
178
|
+
/** @type {Record<string, any>} */
|
|
178
179
|
const body = {
|
|
179
180
|
model: options.model || 'composer-2',
|
|
180
181
|
messages: openaiMessages(options),
|
package/lib/vendors/glm.js
CHANGED
|
@@ -84,6 +84,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
84
84
|
const token = blob && (blob.accessToken || blob.access_token)
|
|
85
85
|
if (!token) throw new LlmError('GLM not authenticated', 'AUTH')
|
|
86
86
|
|
|
87
|
+
/** @type {Record<string, any>} */
|
|
87
88
|
const body = {
|
|
88
89
|
model: options.model || 'glm-4-plus',
|
|
89
90
|
messages: openaiMessages(options),
|
package/lib/vendors/kimi.js
CHANGED
|
@@ -88,6 +88,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
88
88
|
const token = blob && (blob.accessToken || blob.access_token)
|
|
89
89
|
if (!token) throw new LlmError('Kimi not authenticated', 'AUTH')
|
|
90
90
|
|
|
91
|
+
/** @type {Record<string, any>} */
|
|
91
92
|
const body = {
|
|
92
93
|
model: options.model || 'kimi-for-coding',
|
|
93
94
|
messages: openaiMessages(options),
|
package/lib/vendors/kiro.js
CHANGED
|
@@ -87,6 +87,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
87
87
|
const token = blob && (blob.accessToken || blob.access_token)
|
|
88
88
|
if (!token) throw new LlmError('AWS Kiro not authenticated', 'AUTH')
|
|
89
89
|
|
|
90
|
+
/** @type {Record<string, any>} */
|
|
90
91
|
const body = {
|
|
91
92
|
model: options.model || 'claude-sonnet-5',
|
|
92
93
|
messages: openaiMessages(options),
|
package/lib/vendors/qwen.js
CHANGED
|
@@ -57,6 +57,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
57
57
|
const key = blob && (blob.accessToken || blob.apiKey)
|
|
58
58
|
if (!key) throw new LlmError('Qwen API Key or Token is required', 'AUTH')
|
|
59
59
|
|
|
60
|
+
/** @type {Record<string, any>} */
|
|
60
61
|
const body = {
|
|
61
62
|
model: options.model || 'qwen-2.5-coder-32b-instruct',
|
|
62
63
|
messages: openaiMessages(options),
|
package/lib/wire.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "./http.js"
|
|
1
2
|
import { iterateSse, jsonSse } from './sse.js'
|
|
2
3
|
import { validationFromHttpError, validationRequiredError, googleRateLimitMessage, googleLicenseMessage } from './google-validation.js'
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @param {number} status
|
|
7
|
+
* @param {string} [bodyText]
|
|
8
|
+
* @param {string} [code]
|
|
9
|
+
* @returns {Error & { status?: number, code?: string }}
|
|
10
|
+
*/
|
|
4
11
|
export function httpError(status, bodyText, code) {
|
|
5
12
|
const snippet = String(bodyText || '').slice(0, 400)
|
|
6
|
-
const err = new Error(snippet ? `vendor http ${status}: ${snippet}` : `vendor http ${status}`)
|
|
13
|
+
const err = /** @type {Error & { status?: number, code?: string }} */ (new Error(snippet ? `vendor http ${status}: ${snippet}` : `vendor http ${status}`))
|
|
7
14
|
err.status = status
|
|
8
15
|
if (code) {
|
|
9
16
|
err.code = code
|
|
@@ -401,25 +408,19 @@ export async function* googleStream(body) {
|
|
|
401
408
|
}
|
|
402
409
|
|
|
403
410
|
export async function formTokenRequest(url, params, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
|
|
404
|
-
const
|
|
405
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
406
|
-
const res = await impl(url, {
|
|
411
|
+
const res = await fetchWithTimeout(fetchImpl || fetch, url, {
|
|
407
412
|
method: 'POST',
|
|
408
413
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', ...(headers || {}) },
|
|
409
414
|
body: new URLSearchParams(params),
|
|
410
|
-
|
|
411
|
-
})
|
|
415
|
+
}, { timeoutMs })
|
|
412
416
|
return readJson(res)
|
|
413
417
|
}
|
|
414
418
|
|
|
415
419
|
export async function jsonTokenRequest(url, body, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
|
|
416
|
-
const
|
|
417
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
418
|
-
const res = await impl(url, {
|
|
420
|
+
const res = await fetchWithTimeout(fetchImpl || fetch, url, {
|
|
419
421
|
method: 'POST',
|
|
420
422
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(headers || {}) },
|
|
421
423
|
body: JSON.stringify(body),
|
|
422
|
-
|
|
423
|
-
})
|
|
424
|
+
}, { timeoutMs })
|
|
424
425
|
return readJson(res)
|
|
425
426
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.43",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
-
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
58
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
59
|
-
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
60
|
-
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.0-rc.2",
|
|
61
61
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
@@ -68,9 +68,14 @@
|
|
|
68
68
|
"undici": "^8.10.1"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@deepseek-ai/
|
|
71
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
72
|
+
"@deepseek-ai/dsh-credentials": "^0.2.0-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
76
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
72
77
|
"@types/node": "^22.20.2",
|
|
73
|
-
"
|
|
74
|
-
"
|
|
78
|
+
"eslint": "^10.4.0",
|
|
79
|
+
"typescript": "^7.0.2"
|
|
75
80
|
}
|
|
76
81
|
}
|