@goodandready/dsh-subscriptions 0.6.71 → 0.6.82
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/README.md +13 -0
- package/README.ru.md +13 -0
- package/README.zh.md +13 -0
- package/lib/accounts-view.js +101 -5
- package/lib/accounts.js +150 -11
- package/lib/adapter.js +3 -0
- package/lib/alerts.js +35 -0
- package/lib/analytics.js +31 -2
- package/lib/billing.js +99 -0
- package/lib/blob.js +18 -0
- package/lib/capabilities.js +208 -0
- package/lib/client.js +944 -828
- package/lib/config-schema.js +46 -4
- package/lib/diagnostic-wizard.js +2 -5
- package/lib/diagnostics.js +2 -2
- package/lib/doctor.js +163 -0
- package/lib/error-buffer.js +2 -2
- package/lib/forecast.js +56 -0
- package/lib/headless-auth.js +379 -0
- package/lib/history.js +36 -0
- package/lib/http.js +26 -4
- package/lib/index.js +98 -11
- package/lib/instructions.js +138 -0
- package/lib/jwt.js +84 -17
- package/lib/loopback.js +3 -2
- package/lib/mask.js +16 -0
- package/lib/oauth-pending.js +12 -0
- package/lib/pricing.js +89 -0
- package/lib/refs.js +2 -0
- package/lib/reset-clock.js +55 -0
- package/lib/rotate.js +18 -4
- package/lib/routes/accounts.js +203 -0
- package/lib/routes/oauth.js +44 -2
- package/lib/routes/proxy.js +14 -0
- package/lib/routes/status.js +99 -5
- package/lib/scope-inspector.js +51 -0
- package/lib/stream-rotate.js +31 -10
- package/lib/updater.js +21 -0
- package/lib/vault-sync.js +412 -0
- package/lib/vault.js +1 -1
- package/lib/vendor-factory.js +6 -2
- package/lib/vendors/antigravity.js +11 -2
- package/lib/vendors/claude.js +56 -8
- package/lib/vendors/codex.js +34 -6
- package/lib/vendors/copilot-cli.js +8 -1
- package/lib/vendors/ernie.js +9 -2
- package/lib/vendors/grok.js +66 -6
- package/lib/vendors/jetbrains.js +133 -13
- package/lib/vendors/perplexity.js +148 -17
- package/package.json +1 -1
package/lib/vendors/claude.js
CHANGED
|
@@ -2,7 +2,7 @@ import { fetchWithTimeout } from '../http.js'
|
|
|
2
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
3
|
import { anthropicPayload, modelCatalog } from '../messages.js'
|
|
4
4
|
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
5
|
-
import { emailFromToken } from '../jwt.js'
|
|
5
|
+
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
6
6
|
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
|
|
7
7
|
|
|
8
8
|
export const id = 'claude'
|
|
@@ -81,13 +81,19 @@ export function defaults() {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function oauthHeaders(token) {
|
|
85
|
-
|
|
84
|
+
function oauthHeaders(token, blob) {
|
|
85
|
+
const h = {
|
|
86
86
|
Authorization: `Bearer ${token}`,
|
|
87
87
|
Accept: 'application/json',
|
|
88
88
|
'anthropic-version': '2023-06-01',
|
|
89
89
|
'anthropic-beta': BETA,
|
|
90
90
|
}
|
|
91
|
+
const org = blob && (blob.selectedOrgId || blob.organizationId)
|
|
92
|
+
if (org) {
|
|
93
|
+
h['anthropic-organization-id'] = String(org)
|
|
94
|
+
h['x-anthropic-organization'] = String(org)
|
|
95
|
+
}
|
|
96
|
+
return h
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export function authorizeUrl(cfg, pkce) {
|
|
@@ -104,14 +110,56 @@ export function authorizeUrl(cfg, pkce) {
|
|
|
104
110
|
|
|
105
111
|
async function decorate(blob, fetchImpl) {
|
|
106
112
|
let email = blob.email || emailFromToken(blob.accessToken)
|
|
107
|
-
|
|
113
|
+
let plan = blob.plan || planFromToken(blob.accessToken)
|
|
114
|
+
let avatarUrl = blob.avatarUrl || avatarFromToken(blob.accessToken)
|
|
115
|
+
let scopes = blob.scopes || scopesFromToken(blob.accessToken)
|
|
116
|
+
let orgs = Array.isArray(blob.organizations) ? [...blob.organizations] : []
|
|
117
|
+
if (fetchImpl && blob.accessToken && (!email || !plan || !orgs.length)) {
|
|
108
118
|
try {
|
|
109
|
-
const res = await fetchImpl(PROFILE, { headers: oauthHeaders(blob.accessToken) })
|
|
119
|
+
const res = await fetchImpl(PROFILE, { headers: oauthHeaders(blob.accessToken, blob) })
|
|
110
120
|
const json = await readJson(res)
|
|
111
|
-
email = json.email || (json.account && json.account.email) ||
|
|
121
|
+
email = email || json.email || (json.account && json.account.email) || ""
|
|
122
|
+
plan = plan || json.plan || (json.account && (json.account.plan_type || (json.account.has_team ? "Team" : (json.account.has_pro ? "Pro" : "Free")))) || ""
|
|
123
|
+
avatarUrl = avatarUrl || json.picture || (json.account && json.account.picture) || ""
|
|
124
|
+
if (json.scope && !scopes.length) scopes = String(json.scope).split(/[\s,]+/).filter(Boolean)
|
|
125
|
+
if (Array.isArray(json.organizations)) {
|
|
126
|
+
for (const o of json.organizations) {
|
|
127
|
+
if (o && (o.id || o.uuid)) orgs.push({ id: String(o.id || o.uuid), name: String(o.name || o.title || o.id) })
|
|
128
|
+
}
|
|
129
|
+
} else if (Array.isArray(json.workspaces)) {
|
|
130
|
+
for (const w of json.workspaces) {
|
|
131
|
+
if (w && (w.id || w.uuid)) orgs.push({ id: String(w.id || w.uuid), name: String(w.name || w.title || w.id) })
|
|
132
|
+
}
|
|
133
|
+
} else if (json.account && Array.isArray(json.account.memberships)) {
|
|
134
|
+
for (const m of json.account.memberships) {
|
|
135
|
+
const o = m.organization || m
|
|
136
|
+
if (o && (o.id || o.uuid)) orgs.push({ id: String(o.id || o.uuid), name: String(o.name || o.title || o.id), role: m.role ? String(m.role) : undefined })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
112
139
|
} catch { /* profile is optional */ }
|
|
113
140
|
}
|
|
114
|
-
|
|
141
|
+
const dedupedOrgs = []
|
|
142
|
+
const seen = new Set()
|
|
143
|
+
for (const o of orgs) {
|
|
144
|
+
if (!seen.has(o.id)) {
|
|
145
|
+
seen.add(o.id)
|
|
146
|
+
dedupedOrgs.push(o)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const selectedOrgId = blob.selectedOrgId || blob.organizationId || (dedupedOrgs[0] ? dedupedOrgs[0].id : '')
|
|
150
|
+
const selectedOrgName = blob.selectedOrgName || (dedupedOrgs.find(o => o.id === selectedOrgId)?.name || selectedOrgId)
|
|
151
|
+
return {
|
|
152
|
+
...blob,
|
|
153
|
+
email: email || "",
|
|
154
|
+
plan: plan || "Claude Pro",
|
|
155
|
+
avatarUrl: avatarUrl || "",
|
|
156
|
+
scopes,
|
|
157
|
+
selectedOrgId,
|
|
158
|
+
selectedOrgName,
|
|
159
|
+
organizationId: selectedOrgId,
|
|
160
|
+
organizations: dedupedOrgs,
|
|
161
|
+
label: blob.label || email || "Claude",
|
|
162
|
+
}
|
|
115
163
|
}
|
|
116
164
|
|
|
117
165
|
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
@@ -302,7 +350,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
302
350
|
method: 'POST',
|
|
303
351
|
headers: {
|
|
304
352
|
...headers,
|
|
305
|
-
...oauthHeaders(blob.accessToken),
|
|
353
|
+
...oauthHeaders(blob.accessToken, blob),
|
|
306
354
|
'Content-Type': 'application/json',
|
|
307
355
|
Accept: 'text/event-stream',
|
|
308
356
|
},
|
package/lib/vendors/codex.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isTimeoutError, fetchWithTimeout } from "../http.js"
|
|
|
2
2
|
import { randomUUID, createHash } from 'node:crypto'
|
|
3
3
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
4
4
|
import { codexResponsesBody, modelCatalog } from '../messages.js'
|
|
5
|
-
import { chatgptAccountId, emailFromToken } from '../jwt.js'
|
|
5
|
+
import { chatgptAccountId, emailFromToken, planFromToken, avatarFromToken, scopesFromToken, organizationsFromToken } from '../jwt.js'
|
|
6
6
|
import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from '../wire.js'
|
|
7
7
|
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
|
|
8
8
|
|
|
@@ -62,11 +62,13 @@ export function authorizeUrl(cfg, pkce) {
|
|
|
62
62
|
|
|
63
63
|
function identityHeaders(blob, config, extra) {
|
|
64
64
|
const originator = config.originator || 'codex_cli_rs'
|
|
65
|
+
const activeOrgId = blob.selectedOrgId || blob.accountId || ''
|
|
65
66
|
return {
|
|
66
67
|
Authorization: `Bearer ${blob.accessToken}`,
|
|
67
68
|
originator,
|
|
68
|
-
'chatgpt-account-id':
|
|
69
|
-
'ChatGPT-Account-ID':
|
|
69
|
+
'chatgpt-account-id': activeOrgId,
|
|
70
|
+
'ChatGPT-Account-ID': activeOrgId,
|
|
71
|
+
...(activeOrgId ? { 'OpenAI-Organization': activeOrgId } : {}),
|
|
70
72
|
'User-Agent': `${originator}/0.0.1`,
|
|
71
73
|
'OpenAI-Beta': 'responses=experimental',
|
|
72
74
|
...(extra || {}),
|
|
@@ -75,11 +77,37 @@ function identityHeaders(blob, config, extra) {
|
|
|
75
77
|
|
|
76
78
|
function decorate(blob, json) {
|
|
77
79
|
const idToken = (json && json.id_token) || blob.idToken || ''
|
|
80
|
+
const email = blob.email || emailFromToken(idToken) || emailFromToken(blob.accessToken)
|
|
81
|
+
const plan = blob.plan || planFromToken(idToken) || planFromToken(blob.accessToken)
|
|
82
|
+
const avatarUrl = blob.avatarUrl || avatarFromToken(idToken) || avatarFromToken(blob.accessToken)
|
|
83
|
+
const scopes = blob.scopes?.length ? blob.scopes : (json?.scope ? String(json.scope).split(/[\s,]+/).filter(Boolean) : scopesFromToken(idToken || blob.accessToken))
|
|
84
|
+
const parsedOrgs = [
|
|
85
|
+
...organizationsFromToken(idToken),
|
|
86
|
+
...organizationsFromToken(blob.accessToken),
|
|
87
|
+
]
|
|
88
|
+
const dedupedOrgs = Array.isArray(blob.organizations) && blob.organizations.length ? [...blob.organizations] : []
|
|
89
|
+
const seen = new Set(dedupedOrgs.map(o => o.id))
|
|
90
|
+
for (const o of parsedOrgs) {
|
|
91
|
+
if (!seen.has(o.id)) {
|
|
92
|
+
seen.add(o.id)
|
|
93
|
+
dedupedOrgs.push(o)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const accountId = blob.accountId || chatgptAccountId(idToken) || chatgptAccountId(blob.accessToken)
|
|
97
|
+
const selectedOrgId = blob.selectedOrgId || accountId || (dedupedOrgs[0] ? dedupedOrgs[0].id : '')
|
|
98
|
+
const selectedOrgName = blob.selectedOrgName || (dedupedOrgs.find(o => o.id === selectedOrgId)?.name || selectedOrgId)
|
|
99
|
+
|
|
78
100
|
return {
|
|
79
101
|
...blob,
|
|
80
|
-
accountId
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
accountId,
|
|
103
|
+
selectedOrgId,
|
|
104
|
+
selectedOrgName,
|
|
105
|
+
organizations: dedupedOrgs,
|
|
106
|
+
email,
|
|
107
|
+
plan: plan || 'ChatGPT',
|
|
108
|
+
avatarUrl,
|
|
109
|
+
scopes,
|
|
110
|
+
label: blob.label || email || 'ChatGPT',
|
|
83
111
|
}
|
|
84
112
|
}
|
|
85
113
|
|
|
@@ -187,8 +187,13 @@ export function isCopilotCliAvailable(options = {}) {
|
|
|
187
187
|
* @param {string} [_binaryPath]
|
|
188
188
|
* @returns {{ args: string[], env: NodeJS.ProcessEnv, inputPayload: string }}
|
|
189
189
|
*/
|
|
190
|
+
export const COPILOT_MODEL_PATTERN = /^[A-Za-z0-9._:/][A-Za-z0-9._:/-]{0,119}$/
|
|
191
|
+
|
|
190
192
|
export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'copilot') {
|
|
191
|
-
const model = options.model || 'claude-3.7-sonnet'
|
|
193
|
+
const model = String(options.model || 'claude-3.7-sonnet')
|
|
194
|
+
if (!COPILOT_MODEL_PATTERN.test(model)) {
|
|
195
|
+
throw new LlmError(`Invalid Copilot model id: ${model.slice(0, 80)}`, 'INVALID_MODEL')
|
|
196
|
+
}
|
|
192
197
|
const token = blob.accessToken || blob.githubToken || blob.refreshToken || ''
|
|
193
198
|
|
|
194
199
|
const args = [
|
|
@@ -265,6 +270,8 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
if (child.stdin) {
|
|
273
|
+
// EPIPE is expected when the CLI exits before reading stdin; never let it crash the host.
|
|
274
|
+
child.stdin.on('error', () => {})
|
|
268
275
|
child.stdin.write(inputPayload)
|
|
269
276
|
child.stdin.end()
|
|
270
277
|
}
|
package/lib/vendors/ernie.js
CHANGED
|
@@ -44,8 +44,15 @@ export async function listModels() {
|
|
|
44
44
|
|
|
45
45
|
async function refreshErnieToken(apiKey, secretKey, fetchImpl) {
|
|
46
46
|
const impl = fetchImpl || fetch
|
|
47
|
-
const
|
|
48
|
-
|
|
47
|
+
const res = await impl(ERNIE_OAUTH_URL, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
50
|
+
body: new URLSearchParams({
|
|
51
|
+
grant_type: 'client_credentials',
|
|
52
|
+
client_id: apiKey,
|
|
53
|
+
client_secret: secretKey,
|
|
54
|
+
}),
|
|
55
|
+
})
|
|
49
56
|
if (!res.ok) throw httpError(res.status, await res.text())
|
|
50
57
|
return readJson(res)
|
|
51
58
|
}
|
package/lib/vendors/grok.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
2
3
|
import { modelCatalog, codexResponsesBody } from '../messages.js'
|
|
3
4
|
import { formTokenRequest, codexResponsesStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
4
|
-
import { emailFromToken } from '../jwt.js'
|
|
5
|
+
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
5
6
|
import { asUsageSnapshot, usageWindows, grokBillingPercent } from '../usage.js'
|
|
6
7
|
|
|
7
8
|
export const id = 'grok'
|
|
@@ -13,14 +14,41 @@ const BILLING = 'https://cli-chat-proxy.grok.com/v1/billing?format=credits'
|
|
|
13
14
|
const MODELS = 'https://api.x.ai/v1/models'
|
|
14
15
|
const CLI_MODELS = 'https://cli-chat-proxy.grok.com/v1/models'
|
|
15
16
|
const CATALOG_TTL_MS = 5 * 60 * 1000
|
|
17
|
+
const CATALOG_CACHE_MAX = 32
|
|
16
18
|
const catalogCache = new Map()
|
|
17
19
|
|
|
20
|
+
function catalogKey(token) {
|
|
21
|
+
return createHash('sha256').update(String(token || '')).digest('hex').slice(0, 16)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pruneCatalogCache(now = Date.now()) {
|
|
25
|
+
for (const [key, entry] of catalogCache) {
|
|
26
|
+
if (now - entry.at >= CATALOG_TTL_MS) catalogCache.delete(key)
|
|
27
|
+
}
|
|
28
|
+
while (catalogCache.size > CATALOG_CACHE_MAX) {
|
|
29
|
+
catalogCache.delete(catalogCache.keys().next().value)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function grokCatalogCacheSize() {
|
|
34
|
+
return catalogCache.size
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearGrokCatalogCache() {
|
|
38
|
+
catalogCache.clear()
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
async function cliCatalogCached(blob, cfg, fetchImpl) {
|
|
19
|
-
const key =
|
|
42
|
+
const key = catalogKey(blob.accessToken)
|
|
43
|
+
const now = Date.now()
|
|
20
44
|
const hit = catalogCache.get(key)
|
|
21
|
-
if (hit &&
|
|
45
|
+
if (hit && now - hit.at < CATALOG_TTL_MS) return hit.map
|
|
46
|
+
if (hit) catalogCache.delete(key)
|
|
22
47
|
const map = await cliCatalog(blob, cfg, fetchImpl)
|
|
23
|
-
if (map.size > 0)
|
|
48
|
+
if (map.size > 0) {
|
|
49
|
+
catalogCache.set(key, { at: now, map })
|
|
50
|
+
pruneCatalogCache(now)
|
|
51
|
+
}
|
|
24
52
|
return map
|
|
25
53
|
}
|
|
26
54
|
|
|
@@ -78,13 +106,41 @@ function identityHeaders(blob, config) {
|
|
|
78
106
|
}
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
const TRUSTED_AUTH_HOSTS = new Set(['api.x.ai', 'cli-chat-proxy.grok.com'])
|
|
110
|
+
|
|
111
|
+
function isTrustedAuthHost(hostname) {
|
|
112
|
+
const host = String(hostname || '').toLowerCase()
|
|
113
|
+
return TRUSTED_AUTH_HOSTS.has(host)
|
|
114
|
+
|| host === 'grok.com'
|
|
115
|
+
|| host.endsWith('.grok.com')
|
|
116
|
+
|| host === 'localhost'
|
|
117
|
+
|| host === '127.0.0.1'
|
|
118
|
+
|| host === '::1'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isGrokHost(hostname) {
|
|
122
|
+
const host = String(hostname || '').toLowerCase()
|
|
123
|
+
return host === 'grok.com' || host.endsWith('.grok.com')
|
|
124
|
+
}
|
|
125
|
+
|
|
81
126
|
function streamHeaders(blob, config, extra) {
|
|
82
127
|
const base = String(config.baseUrl || defaults().baseUrl)
|
|
128
|
+
let hostname = ''
|
|
129
|
+
try {
|
|
130
|
+
hostname = new URL(base).hostname
|
|
131
|
+
} catch {
|
|
132
|
+
hostname = ''
|
|
133
|
+
}
|
|
134
|
+
if (!isTrustedAuthHost(hostname)) {
|
|
135
|
+
const err = new Error(`Untrusted Grok baseUrl host: ${hostname || base}`)
|
|
136
|
+
err.code = 'GROK_UNTRUSTED_HOST'
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
83
139
|
const headers = {
|
|
84
140
|
Authorization: `Bearer ${blob.accessToken}`,
|
|
85
141
|
...(extra || {}),
|
|
86
142
|
}
|
|
87
|
-
if (
|
|
143
|
+
if (isGrokHost(hostname)) Object.assign(headers, identityHeaders(blob, config))
|
|
88
144
|
return headers
|
|
89
145
|
}
|
|
90
146
|
|
|
@@ -108,7 +164,11 @@ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
|
108
164
|
code_verifier: pkce.verifier,
|
|
109
165
|
}, fetchImpl)
|
|
110
166
|
const blob = tokenBlobFromOAuth(json)
|
|
111
|
-
|
|
167
|
+
const email = blob.email || emailFromToken(blob.idToken || blob.accessToken)
|
|
168
|
+
const plan = blob.plan || planFromToken(blob.idToken || blob.accessToken)
|
|
169
|
+
const avatarUrl = blob.avatarUrl || avatarFromToken(blob.idToken || blob.accessToken)
|
|
170
|
+
const scopes = blob.scopes?.length ? blob.scopes : (json?.scope ? String(json.scope).split(/[\s,]+/).filter(Boolean) : scopesFromToken(blob.idToken || blob.accessToken))
|
|
171
|
+
return { ...blob, email, plan, avatarUrl, scopes, label: blob.label || email || "Grok" }
|
|
112
172
|
}
|
|
113
173
|
|
|
114
174
|
export async function refresh(cfg, blob, fetchImpl) {
|
package/lib/vendors/jetbrains.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
// #563: JetBrains AI Assistant Subscription OAuth Vendor
|
|
2
|
+
// Supports OAuth PKCE authentication, token refresh, and JetBrains AI models.
|
|
3
|
+
|
|
1
4
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
5
|
+
import { buildAuthorizeUrl } from '../oauth.js'
|
|
6
|
+
import { openaiMessages, modelCatalog } from '../messages.js'
|
|
7
|
+
import { jsonTokenRequest, tokenBlobFromOAuth, openaiChatStream, httpError, readJson } from '../wire.js'
|
|
8
|
+
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
4
9
|
|
|
5
10
|
export const id = 'jetbrains'
|
|
11
|
+
|
|
12
|
+
const AUTH = 'https://account.jetbrains.com/oauth2/auth'
|
|
13
|
+
const TOKEN = 'https://oauth.jetbrains.com/oauth2/token'
|
|
6
14
|
const JETBRAINS_API_BASE = 'https://api.jetbrains.ai'
|
|
15
|
+
const USER_INFO = 'https://account.jetbrains.com/api/user'
|
|
16
|
+
const SCOPE = 'openid profile email ai.assistant offline_access'
|
|
7
17
|
|
|
8
18
|
const JETBRAINS_MODELS = [
|
|
9
19
|
{
|
|
@@ -11,15 +21,50 @@ const JETBRAINS_MODELS = [
|
|
|
11
21
|
name: 'Claude 3.5 Sonnet (JetBrains AI)',
|
|
12
22
|
contextWindow: 200000,
|
|
13
23
|
maxTokens: 8192,
|
|
14
|
-
inputModalities: ['text']
|
|
24
|
+
inputModalities: ['text'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'jb-claude-3.7-sonnet',
|
|
28
|
+
name: 'Claude 3.7 Sonnet (JetBrains AI)',
|
|
29
|
+
contextWindow: 200000,
|
|
30
|
+
maxTokens: 8192,
|
|
31
|
+
reasoning: {
|
|
32
|
+
efforts: [
|
|
33
|
+
{ id: 'low', name: 'low' },
|
|
34
|
+
{ id: 'medium', name: 'medium' },
|
|
35
|
+
{ id: 'high', name: 'high' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
inputModalities: ['text'],
|
|
15
39
|
},
|
|
16
40
|
{
|
|
17
41
|
id: 'jb-gpt-4o',
|
|
18
42
|
name: 'GPT-4o (JetBrains AI)',
|
|
19
43
|
contextWindow: 128000,
|
|
20
44
|
maxTokens: 8192,
|
|
21
|
-
inputModalities: ['text']
|
|
22
|
-
}
|
|
45
|
+
inputModalities: ['text'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'jb-o3-mini',
|
|
49
|
+
name: 'o3-mini (JetBrains AI)',
|
|
50
|
+
contextWindow: 128000,
|
|
51
|
+
maxTokens: 8192,
|
|
52
|
+
reasoning: {
|
|
53
|
+
efforts: [
|
|
54
|
+
{ id: 'low', name: 'low' },
|
|
55
|
+
{ id: 'medium', name: 'medium' },
|
|
56
|
+
{ id: 'high', name: 'high' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
inputModalities: ['text'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'jb-deepseek-r1',
|
|
63
|
+
name: 'DeepSeek R1 (JetBrains AI)',
|
|
64
|
+
contextWindow: 128000,
|
|
65
|
+
maxTokens: 8192,
|
|
66
|
+
inputModalities: ['text'],
|
|
67
|
+
},
|
|
23
68
|
]
|
|
24
69
|
|
|
25
70
|
export function providerInfo() {
|
|
@@ -28,17 +73,92 @@ export function providerInfo() {
|
|
|
28
73
|
|
|
29
74
|
export function defaults() {
|
|
30
75
|
return {
|
|
76
|
+
clientId: 'jetbrains-ai-assistant',
|
|
77
|
+
redirectUri: 'http://localhost:1455/auth/callback',
|
|
31
78
|
apiBase: JETBRAINS_API_BASE,
|
|
32
|
-
models: JETBRAINS_MODELS.map((m) => m.id)
|
|
79
|
+
models: JETBRAINS_MODELS.map((m) => m.id),
|
|
33
80
|
}
|
|
34
81
|
}
|
|
35
82
|
|
|
36
|
-
export function authorizeUrl() {
|
|
37
|
-
|
|
83
|
+
export function authorizeUrl(cfg = {}, pkce = {}) {
|
|
84
|
+
const c = cfg || {}
|
|
85
|
+
const p = pkce || {}
|
|
86
|
+
return buildAuthorizeUrl({
|
|
87
|
+
authUrl: AUTH,
|
|
88
|
+
clientId: c.clientId || defaults().clientId,
|
|
89
|
+
redirectUri: c.redirectUri || defaults().redirectUri,
|
|
90
|
+
challenge: p.challenge || 'challenge',
|
|
91
|
+
state: p.state || 'state',
|
|
92
|
+
scope: SCOPE,
|
|
93
|
+
})
|
|
38
94
|
}
|
|
39
95
|
|
|
40
|
-
|
|
41
|
-
|
|
96
|
+
async function decorate(blob, fetchImpl) {
|
|
97
|
+
let email = blob.email || emailFromToken(blob.accessToken) || emailFromToken(blob.idToken)
|
|
98
|
+
let plan = blob.plan || planFromToken(blob.accessToken)
|
|
99
|
+
let avatarUrl = blob.avatarUrl || avatarFromToken(blob.accessToken)
|
|
100
|
+
let scopes = blob.scopes || scopesFromToken(blob.accessToken)
|
|
101
|
+
|
|
102
|
+
if (fetchImpl && blob.accessToken && (!email || !plan)) {
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetchImpl(USER_INFO, {
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${blob.accessToken}`,
|
|
107
|
+
Accept: 'application/json',
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
const json = await readJson(res)
|
|
112
|
+
email = email || json.email || json.username || ''
|
|
113
|
+
plan = plan || json.plan || (json.has_ai_assistant ? 'JetBrains AI Pro' : 'JetBrains Account')
|
|
114
|
+
avatarUrl = avatarUrl || json.picture || json.avatar_url || ''
|
|
115
|
+
if (json.scope && !scopes.length) scopes = String(json.scope).split(/[\s,]+/).filter(Boolean)
|
|
116
|
+
}
|
|
117
|
+
} catch { /* user info is optional */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...blob,
|
|
122
|
+
email: email || '',
|
|
123
|
+
plan: plan || 'JetBrains AI Pro',
|
|
124
|
+
avatarUrl: avatarUrl || '',
|
|
125
|
+
scopes,
|
|
126
|
+
label: blob.label || email || 'JetBrains AI',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
131
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
132
|
+
grant_type: 'authorization_code',
|
|
133
|
+
client_id: cfg.clientId || defaults().clientId,
|
|
134
|
+
code,
|
|
135
|
+
redirect_uri: cfg.redirectUri || defaults().redirectUri,
|
|
136
|
+
code_verifier: pkce.verifier,
|
|
137
|
+
state: pkce.state,
|
|
138
|
+
}, fetchImpl)
|
|
139
|
+
return decorate(tokenBlobFromOAuth(json), fetchImpl)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function refresh(cfg, blob, fetchImpl) {
|
|
143
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
144
|
+
grant_type: 'refresh_token',
|
|
145
|
+
client_id: cfg.clientId || defaults().clientId,
|
|
146
|
+
refresh_token: blob.refreshToken,
|
|
147
|
+
}, fetchImpl)
|
|
148
|
+
return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function listModels(_blob, cfg) {
|
|
152
|
+
const customModels = (cfg && Array.isArray(cfg.models) && cfg.models.length) ? cfg.models : null
|
|
153
|
+
if (customModels) {
|
|
154
|
+
const enriched = customModels.map((m) => {
|
|
155
|
+
const mid = typeof m === 'string' ? m : m.id
|
|
156
|
+
const found = JETBRAINS_MODELS.find((p) => p.id === mid)
|
|
157
|
+
return found ? { ...found, ...(typeof m === 'object' ? m : {}) } : (typeof m === 'string' ? { id: m, name: m } : m)
|
|
158
|
+
})
|
|
159
|
+
return modelCatalog(id, enriched)
|
|
160
|
+
}
|
|
161
|
+
return modelCatalog(id, JETBRAINS_MODELS)
|
|
42
162
|
}
|
|
43
163
|
|
|
44
164
|
export async function usage() {
|
|
@@ -55,7 +175,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
55
175
|
const body = {
|
|
56
176
|
model,
|
|
57
177
|
messages: openaiMessages(options),
|
|
58
|
-
stream: true
|
|
178
|
+
stream: true,
|
|
59
179
|
}
|
|
60
180
|
|
|
61
181
|
const res = await impl(url, {
|
|
@@ -64,10 +184,10 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
64
184
|
...headers,
|
|
65
185
|
'Authorization': `Bearer ${token}`,
|
|
66
186
|
'Content-Type': 'application/json',
|
|
67
|
-
'User-Agent': 'IntelliJ-IDEA/2026.1'
|
|
187
|
+
'User-Agent': 'IntelliJ-IDEA/2026.1',
|
|
68
188
|
},
|
|
69
189
|
body: JSON.stringify(body),
|
|
70
|
-
signal
|
|
190
|
+
signal,
|
|
71
191
|
})
|
|
72
192
|
|
|
73
193
|
if (!res.ok) throw httpError(res.status, await res.text())
|