@goodandready/dsh-subscriptions 0.6.37 → 0.6.38
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 -3
- package/lib/adapter-manager.js +6 -1
- package/lib/adapter.js +17 -13
- package/lib/blob.js +17 -15
- package/lib/config-schema.js +52 -0
- package/lib/routes/status.js +7 -3
- package/lib/vendors/claude.js +42 -6
- package/lib/vendors/copilot.js +46 -3
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -13,7 +13,6 @@ export function normalizeSlots(slots) {
|
|
|
13
13
|
const out = []
|
|
14
14
|
const seen = new Set()
|
|
15
15
|
for (const slot of Array.isArray(slots) ? slots : []) {
|
|
16
|
-
// User-editable config: a null or non-object entry must be skipped, not crash the plugin.
|
|
17
16
|
if (!slot || typeof slot !== 'object') continue
|
|
18
17
|
if (!isProvider(slot.provider)) continue
|
|
19
18
|
const index = Number(slot.index)
|
|
@@ -22,10 +21,13 @@ export function normalizeSlots(slots) {
|
|
|
22
21
|
if (seen.has(ref)) continue
|
|
23
22
|
seen.add(ref)
|
|
24
23
|
out.push({
|
|
24
|
+
...slot,
|
|
25
25
|
provider: slot.provider,
|
|
26
26
|
index,
|
|
27
27
|
label: String(slot.label || ''),
|
|
28
28
|
ref,
|
|
29
|
+
...(slot.proxyUrl ? { proxyUrl: String(slot.proxyUrl) } : {}),
|
|
30
|
+
...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
|
|
29
31
|
})
|
|
30
32
|
}
|
|
31
33
|
return out
|
|
@@ -94,16 +96,27 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
async function saveBlob(ref, blob) {
|
|
99
|
+
if (!ref || typeof ref !== "string") throw new Error("invalid ref for saveBlob")
|
|
97
100
|
const prev = writeQueues.get(ref) || Promise.resolve()
|
|
98
101
|
const next = prev.catch(() => {}).then(async () => {
|
|
99
|
-
|
|
102
|
+
try {
|
|
103
|
+
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const wrapErr = new Error(`failed to persist credentials for ${ref}: ${err && err.message || err}`)
|
|
106
|
+
wrapErr.code = "CREDENTIAL_SAVE_FAILED"
|
|
107
|
+
wrapErr.cause = err
|
|
108
|
+
throw wrapErr
|
|
109
|
+
}
|
|
100
110
|
})
|
|
101
111
|
writeQueues.set(ref, next)
|
|
102
112
|
await next
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
async function clearRef(ref) {
|
|
106
|
-
|
|
116
|
+
if (!ref || typeof ref !== "string") return
|
|
117
|
+
try {
|
|
118
|
+
await credentials.unset(credentialRef(ref))
|
|
119
|
+
} catch { /* best-effort unset */ }
|
|
107
120
|
cooldowns.delete(ref)
|
|
108
121
|
quotas.delete(ref)
|
|
109
122
|
quarantines.delete(ref)
|
|
@@ -202,6 +215,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
async function ensureFresh(provider, blob, ref) {
|
|
218
|
+
if (!blob || typeof blob !== 'object') return blob
|
|
219
|
+
if (blob.apiKeyOnly || (blob.apiKey && (!blob.refreshToken || blob.refreshToken === blob.apiKey))) return blob
|
|
205
220
|
if (!blob.refreshToken) return blob
|
|
206
221
|
if (blob.expiresAt && blob.expiresAt - SKEW_MS > Date.now()) return blob
|
|
207
222
|
const lockKey = ref || (provider + ":" + (blob.accountId || blob.email || (blob.refreshToken && blob.refreshToken.slice(-16)) || "anon"))
|
package/lib/adapter-manager.js
CHANGED
|
@@ -202,6 +202,8 @@ export function createAdapterManager({
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
let lastModelRefresh = 0
|
|
205
|
+
const providerModelCache = new Map()
|
|
206
|
+
|
|
205
207
|
async function refreshModels() {
|
|
206
208
|
if (Date.now() - lastModelRefresh < 60 * 1000) return
|
|
207
209
|
lastModelRefresh = Date.now()
|
|
@@ -214,7 +216,10 @@ export function createAdapterManager({
|
|
|
214
216
|
if (!blob) continue
|
|
215
217
|
const fresh = await store.ensureFresh(provider, blob, slot.ref)
|
|
216
218
|
const cfg = vendorConfig(provider, live())
|
|
217
|
-
await getVendor(provider).listModels(fresh, cfg, fetch).catch(() =>
|
|
219
|
+
const liveModels = await getVendor(provider).listModels(fresh, cfg, fetch).catch(() => null)
|
|
220
|
+
if (Array.isArray(liveModels) && liveModels.length) {
|
|
221
|
+
providerModelCache.set(provider, liveModels)
|
|
222
|
+
}
|
|
218
223
|
} catch { /* best-effort */ }
|
|
219
224
|
}
|
|
220
225
|
}
|
package/lib/adapter.js
CHANGED
|
@@ -45,20 +45,24 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
45
45
|
async listModels(provider) {
|
|
46
46
|
const accounts = await this.deps.listAccounts(provider)
|
|
47
47
|
const targetAccount = accounts.find((a) => a.hasToken)
|
|
48
|
-
if (!targetAccount) return []
|
|
49
|
-
const cfg = this.deps.vendorConfig(provider)
|
|
50
|
-
const fetchImpl = pickFetch(this.deps, targetAccount.ref)
|
|
51
48
|
let models = null
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
if (targetAccount) {
|
|
50
|
+
const cfg = this.deps.vendorConfig(provider)
|
|
51
|
+
const fetchImpl = pickFetch(this.deps, targetAccount.ref)
|
|
52
|
+
try {
|
|
53
|
+
const blob = await this.deps.ensureFresh(
|
|
54
|
+
provider,
|
|
55
|
+
await this.deps.loadBlob(targetAccount.ref),
|
|
56
|
+
targetAccount.ref,
|
|
57
|
+
)
|
|
58
|
+
const live = await getVendor(provider).listModels(blob, cfg, fetchImpl)
|
|
59
|
+
if (Array.isArray(live) && live.length) models = live
|
|
60
|
+
} catch { /* use built-in catalog */ }
|
|
61
|
+
}
|
|
62
|
+
if (!models) {
|
|
63
|
+
const cfg = this.deps.vendorConfig(provider)
|
|
64
|
+
models = modelCatalog(provider, cfg ? cfg.models : undefined)
|
|
65
|
+
}
|
|
62
66
|
// #94: optionally hide test/preview/beta/legacy ids from the picker.
|
|
63
67
|
if (typeof this.deps.hideDeprecatedModels === 'function' && this.deps.hideDeprecatedModels()) {
|
|
64
68
|
models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
|
package/lib/blob.js
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
function asString(value) {
|
|
2
|
-
return value == null ?
|
|
2
|
+
return value == null ? "" : String(value)
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function serializeBlob(obj) {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
if (!obj || typeof obj !== "object") throw new Error("invalid oauth blob")
|
|
7
|
+
const accessToken = asString(obj.accessToken)
|
|
8
|
+
const refreshToken = asString(obj.refreshToken)
|
|
9
|
+
if (!accessToken && !refreshToken && !obj.apiKey) {
|
|
10
|
+
throw new Error("oauth blob needs accessToken or refreshToken")
|
|
10
11
|
}
|
|
11
12
|
return JSON.stringify({
|
|
13
|
+
...obj,
|
|
12
14
|
accessToken,
|
|
13
15
|
refreshToken,
|
|
14
|
-
expiresAt: Number(obj
|
|
15
|
-
label: asString(obj
|
|
16
|
-
email: asString(obj
|
|
17
|
-
accountId: asString(obj
|
|
18
|
-
projectId: asString(obj
|
|
19
|
-
...(Array.isArray(obj
|
|
20
|
-
...(
|
|
16
|
+
expiresAt: Number(obj.expiresAt) || 0,
|
|
17
|
+
label: asString(obj.label),
|
|
18
|
+
email: asString(obj.email),
|
|
19
|
+
accountId: asString(obj.accountId),
|
|
20
|
+
projectId: asString(obj.projectId),
|
|
21
|
+
...(Array.isArray(obj.usage) ? { usage: obj.usage } : {}),
|
|
22
|
+
...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
|
|
21
23
|
})
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function parseBlob(text) {
|
|
25
|
-
const obj = typeof text ===
|
|
26
|
-
if (!obj || typeof obj !==
|
|
27
|
+
const obj = typeof text === "string" ? JSON.parse(text) : text
|
|
28
|
+
if (!obj || typeof obj !== "object") throw new Error("invalid oauth blob")
|
|
27
29
|
return {
|
|
30
|
+
...obj,
|
|
28
31
|
accessToken: asString(obj.accessToken),
|
|
29
32
|
refreshToken: asString(obj.refreshToken),
|
|
30
33
|
expiresAt: Number(obj.expiresAt) || 0,
|
|
@@ -36,4 +39,3 @@ export function parseBlob(text) {
|
|
|
36
39
|
...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
|
package/lib/config-schema.js
CHANGED
|
@@ -149,3 +149,55 @@ export function publicConfig(cfg) {
|
|
|
149
149
|
|
|
150
150
|
return clone
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
export function restoreMaskedSecrets(incoming, existing) {
|
|
154
|
+
if (!incoming || typeof incoming !== "object") return incoming
|
|
155
|
+
if (!existing || typeof existing !== "object") return incoming
|
|
156
|
+
|
|
157
|
+
const out = { ...incoming }
|
|
158
|
+
|
|
159
|
+
for (const key of Object.keys(out)) {
|
|
160
|
+
if ((key.endsWith("ClientSecret") || key.endsWith("Secret")) && typeof out[key] === "string") {
|
|
161
|
+
if (out[key].includes("••••")) {
|
|
162
|
+
out[key] = existing[key] || ""
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(out.slots) && Array.isArray(existing.slots)) {
|
|
168
|
+
out.slots = out.slots.map((s) => {
|
|
169
|
+
if (!s || typeof s !== "object" || typeof s.proxyUrl !== "string" || !s.proxyUrl.includes("••••")) return s
|
|
170
|
+
const match = existing.slots.find((e) => e && e.provider === s.provider && Number(e.index) === Number(s.index))
|
|
171
|
+
if (match && match.proxyUrl) {
|
|
172
|
+
return { ...s, proxyUrl: match.proxyUrl }
|
|
173
|
+
}
|
|
174
|
+
return s
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(out.customVendors) && Array.isArray(existing.customVendors)) {
|
|
179
|
+
out.customVendors = out.customVendors.map((cv) => {
|
|
180
|
+
if (!cv || typeof cv !== "object") return cv
|
|
181
|
+
const match = existing.customVendors.find((e) => e && e.id === cv.id)
|
|
182
|
+
if (!match) return cv
|
|
183
|
+
const copy = { ...cv }
|
|
184
|
+
for (const f of ["apiKey", "token", "secret", "clientSecret"]) {
|
|
185
|
+
if (typeof copy[f] === "string" && copy[f].includes("••••")) {
|
|
186
|
+
copy[f] = match[f] || ""
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (copy.headers && typeof copy.headers === "object" && match.headers && typeof match.headers === "object") {
|
|
190
|
+
const h = { ...copy.headers }
|
|
191
|
+
for (const hk of Object.keys(h)) {
|
|
192
|
+
if (typeof h[hk] === "string" && h[hk].includes("••••")) {
|
|
193
|
+
h[hk] = match.headers[hk] || ""
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
copy.headers = h
|
|
197
|
+
}
|
|
198
|
+
return copy
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return out
|
|
203
|
+
}
|
package/lib/routes/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// #374: Sensitive GET routes protected by same-origin check
|
|
2
2
|
import { getRecentAlerts, clearAlerts } from '../alerts.js'
|
|
3
3
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
4
|
-
import { Config, plainConfig, publicConfig } from '../config-schema.js'
|
|
4
|
+
import { Config, plainConfig, publicConfig, restoreMaskedSecrets } from '../config-schema.js'
|
|
5
5
|
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
6
6
|
import { quotaSnapshot } from '../ratelimit.js'
|
|
7
7
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
@@ -65,7 +65,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
65
65
|
return
|
|
66
66
|
}
|
|
67
67
|
// #379: revision conflict detection (CAS)
|
|
68
|
-
const
|
|
68
|
+
const rawRev = payload?.revision ?? payload?.config?.revision ?? req.headers?.['if-match']
|
|
69
|
+
const baseRev = rawRev != null ? Number(rawRev) : null
|
|
69
70
|
if (baseRev != null && !Number.isNaN(baseRev) && baseRev !== currentRevision) {
|
|
70
71
|
writeJson(res, 409, {
|
|
71
72
|
ok: false,
|
|
@@ -80,13 +81,16 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
80
81
|
|
|
81
82
|
if (payload && typeof payload.config === 'object') payload = payload.config
|
|
82
83
|
try {
|
|
84
|
+
payload = restoreMaskedSecrets(payload, live())
|
|
83
85
|
if (Array.isArray(payload.slots)) payload.slots = stripLegacySlots(payload.slots)
|
|
84
86
|
const parsed = plainConfig(Config(payload))
|
|
85
87
|
const dropped = droppedCredentialRefs(live().slots, parsed.slots)
|
|
86
88
|
await getSettingsApi().replace(parsed)
|
|
87
89
|
currentRevision++
|
|
88
90
|
syncCustomVendors()
|
|
89
|
-
for (const ref of dropped)
|
|
91
|
+
for (const ref of dropped) {
|
|
92
|
+
try { await store.clearRef(ref) } catch { /* best-effort cleanup */ }
|
|
93
|
+
}
|
|
90
94
|
await syncAdapter()
|
|
91
95
|
writeJson(res, 200, await configResponse())
|
|
92
96
|
} catch (e) {
|
package/lib/vendors/claude.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchWithTimeout } from '../http.js'
|
|
1
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
2
3
|
import { anthropicPayload, modelCatalog } from '../messages.js'
|
|
3
4
|
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
@@ -119,15 +120,50 @@ export async function refresh(cfg, blob, fetchImpl) {
|
|
|
119
120
|
return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
export async function listModels(blob, cfg) {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
123
|
+
export async function listModels(blob, cfg, fetchImpl) {
|
|
124
|
+
const impl = fetchImpl || fetch
|
|
125
|
+
const rawDefault = (cfg && cfg.models && cfg.models.length) ? cfg.models : defaults().models
|
|
126
|
+
const defaultCatalog = modelCatalog(id, rawDefault.map((entry) => {
|
|
127
|
+
const model = typeof entry === "string" ? { id: entry, name: entry } : { ...entry }
|
|
126
128
|
const levels = effortLevelsForModel(model.id || model.name)
|
|
127
129
|
if (!levels) return model
|
|
128
130
|
return { ...model, reasoning: { efforts: levels.map((level) => ({ id: level, name: level })) } }
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
+
}))
|
|
132
|
+
|
|
133
|
+
if (!blob || !blob.accessToken) return defaultCatalog
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const headers = {
|
|
137
|
+
"Accept": "application/json",
|
|
138
|
+
"anthropic-version": "2023-06-01",
|
|
139
|
+
}
|
|
140
|
+
if (blob.accessToken.startsWith("sk-ant-")) {
|
|
141
|
+
headers["x-api-key"] = blob.accessToken
|
|
142
|
+
} else {
|
|
143
|
+
headers["Authorization"] = `Bearer ${blob.accessToken}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const res = await fetchWithTimeout(impl, "https://api.anthropic.com/v1/models", { headers }, { timeoutMs: 15000 })
|
|
147
|
+
if (!res.ok) return defaultCatalog
|
|
148
|
+
const json = await readJson(res)
|
|
149
|
+
const items = json && json.data
|
|
150
|
+
if (!Array.isArray(items) || !items.length) return defaultCatalog
|
|
151
|
+
const rows = []
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
const modelId = item.id || item.display_name
|
|
154
|
+
if (!modelId) continue
|
|
155
|
+
const levels = effortLevelsForModel(modelId)
|
|
156
|
+
rows.push({
|
|
157
|
+
id: modelId,
|
|
158
|
+
name: item.display_name || modelId,
|
|
159
|
+
...(levels ? { reasoning: { efforts: levels.map((level) => ({ id: level, name: level })) } } : {}),
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
if (!rows.length) return defaultCatalog
|
|
163
|
+
return modelCatalog(id, rows)
|
|
164
|
+
} catch {
|
|
165
|
+
return defaultCatalog
|
|
166
|
+
}
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
// Advertise adaptive thinking + effort only for models whose capability table
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LlmError } from "@deepseek-ai/dsh-llm"
|
|
2
|
-
import { openaiMessages, openaiTools } from "../messages.js"
|
|
2
|
+
import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
|
|
3
|
+
import { fetchWithTimeout } from "../http.js"
|
|
3
4
|
import { openaiChatStream, readJson, httpError } from "../wire.js"
|
|
4
5
|
import { asUsageSnapshot } from "../usage.js"
|
|
5
6
|
|
|
@@ -68,8 +69,50 @@ export function authorizeUrl() {
|
|
|
68
69
|
return "https://github.com/login/device"
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
export async function listModels() {
|
|
72
|
-
|
|
72
|
+
export async function listModels(blob, cfg, fetchImpl) {
|
|
73
|
+
const impl = fetchImpl || fetch
|
|
74
|
+
const fallback = modelCatalog(id, (cfg && cfg.models) || COPILOT_MODELS)
|
|
75
|
+
if (!blob) return fallback
|
|
76
|
+
const githubToken = blob.refreshToken || blob.githubToken
|
|
77
|
+
let copilotToken = blob.accessToken
|
|
78
|
+
try {
|
|
79
|
+
if ((!copilotToken || !blob.expiresAt || blob.expiresAt < Date.now() + 60000) && githubToken) {
|
|
80
|
+
const exchange = await exchangeCopilotToken(githubToken, impl)
|
|
81
|
+
if (exchange && exchange.token) {
|
|
82
|
+
copilotToken = exchange.token
|
|
83
|
+
blob.accessToken = exchange.token
|
|
84
|
+
if (exchange.expires_at) blob.expiresAt = exchange.expires_at * 1000
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!copilotToken) return fallback
|
|
88
|
+
const res = await fetchWithTimeout(impl, "https://api.githubcopilot.com/models", {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
91
|
+
Accept: "application/json",
|
|
92
|
+
"User-Agent": "GitHubCopilotChat/0.24.0",
|
|
93
|
+
},
|
|
94
|
+
}, { timeoutMs: 15000 })
|
|
95
|
+
if (!res.ok) return fallback
|
|
96
|
+
const json = await readJson(res)
|
|
97
|
+
const items = json && (json.data || json.models || json)
|
|
98
|
+
if (!Array.isArray(items) || !items.length) return fallback
|
|
99
|
+
const rows = []
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const modelId = item.id || item.name
|
|
102
|
+
if (!modelId) continue
|
|
103
|
+
if (item.capabilities && item.capabilities.type && item.capabilities.type !== "chat") continue
|
|
104
|
+
rows.push({
|
|
105
|
+
id: modelId,
|
|
106
|
+
name: item.name || modelId,
|
|
107
|
+
...(item.capabilities && item.capabilities.limits && item.capabilities.limits.max_prompt_tokens ? { contextWindow: item.capabilities.limits.max_prompt_tokens } : {}),
|
|
108
|
+
...(item.capabilities && item.capabilities.supports && item.capabilities.supports.vision ? { inputModalities: ["text", "image"] } : {}),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
if (!rows.length) return fallback
|
|
112
|
+
return modelCatalog(id, rows)
|
|
113
|
+
} catch {
|
|
114
|
+
return fallback
|
|
115
|
+
}
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
export function getTelemetryHeaders(sessionId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.38",
|
|
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",
|