@goodandready/dsh-subscriptions 0.6.72 → 0.6.83
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 +8 -0
- package/README.ru.md +8 -0
- package/README.zh.md +8 -0
- package/lib/accounts-view.js +97 -5
- package/lib/accounts.js +137 -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 +22 -0
- package/lib/capabilities.js +208 -0
- package/lib/client.js +344 -17
- package/lib/config-schema.js +50 -4
- package/lib/diagnostic-wizard.js +2 -5
- package/lib/diagnostics.js +2 -2
- package/lib/error-buffer.js +2 -2
- package/lib/forecast.js +56 -0
- package/lib/headless-auth.js +392 -0
- package/lib/history.js +36 -0
- package/lib/http.js +67 -4
- package/lib/import-auth.js +3 -6
- package/lib/index.js +99 -11
- 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 +142 -7
- package/lib/routes/oauth.js +51 -2
- package/lib/routes/proxy.js +14 -0
- package/lib/scope-inspector.js +51 -0
- package/lib/stream-rotate.js +31 -10
- package/lib/subscriptions.js +24 -1
- package/lib/updater.js +21 -0
- package/lib/vault-sync.js +414 -0
- package/lib/vault.js +1 -1
- package/lib/vendor-factory.js +6 -2
- package/lib/vendors/antigravity.js +13 -4
- package/lib/vendors/claude.js +56 -8
- package/lib/vendors/codex.js +34 -6
- package/lib/vendors/copilot-cli.js +36 -5
- package/lib/vendors/ernie.js +9 -2
- package/lib/vendors/grok.js +66 -6
- package/lib/vendors/jetbrains.js +136 -13
- package/lib/vendors/perplexity.js +151 -17
- package/package.json +1 -1
|
@@ -8,6 +8,13 @@ import { join, delimiter } from 'node:path'
|
|
|
8
8
|
import { spawn } from 'node:child_process'
|
|
9
9
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
10
10
|
|
|
11
|
+
const MAX_STDERR_CHARS = 2048
|
|
12
|
+
const GITHUB_TOKEN_PATTERN = /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g
|
|
13
|
+
|
|
14
|
+
export function redactCopilotStderr(text) {
|
|
15
|
+
return String(text || '').replace(GITHUB_TOKEN_PATTERN, '[REDACTED]')
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Standard candidate binary names for GitHub Copilot CLI.
|
|
13
20
|
*/
|
|
@@ -144,8 +151,11 @@ export function findCopilotCli(customPath, env = process.env, platform = process
|
|
|
144
151
|
)
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
const candidates = platform === 'win32'
|
|
155
|
+
? COPILOT_CLI_BINARIES.filter((bin) => /\.(cmd|exe|bat)$/i.test(bin))
|
|
156
|
+
: COPILOT_CLI_BINARIES.filter((bin) => !/\.(cmd|exe|bat)$/i.test(bin))
|
|
147
157
|
for (const dir of pathDirs) {
|
|
148
|
-
for (const bin of
|
|
158
|
+
for (const bin of candidates) {
|
|
149
159
|
const fullPath = join(dir, bin)
|
|
150
160
|
try {
|
|
151
161
|
if (existsSync(fullPath)) {
|
|
@@ -187,8 +197,13 @@ export function isCopilotCliAvailable(options = {}) {
|
|
|
187
197
|
* @param {string} [_binaryPath]
|
|
188
198
|
* @returns {{ args: string[], env: NodeJS.ProcessEnv, inputPayload: string }}
|
|
189
199
|
*/
|
|
200
|
+
export const COPILOT_MODEL_PATTERN = /^[A-Za-z0-9._:/][A-Za-z0-9._:/-]{0,119}$/
|
|
201
|
+
|
|
190
202
|
export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'copilot') {
|
|
191
|
-
const model = options.model || 'claude-3.7-sonnet'
|
|
203
|
+
const model = String(options.model || 'claude-3.7-sonnet')
|
|
204
|
+
if (!COPILOT_MODEL_PATTERN.test(model)) {
|
|
205
|
+
throw new LlmError(`Invalid Copilot model id: ${model.slice(0, 80)}`, 'INVALID_MODEL')
|
|
206
|
+
}
|
|
192
207
|
const token = blob.accessToken || blob.githubToken || blob.refreshToken || ''
|
|
193
208
|
|
|
194
209
|
const args = [
|
|
@@ -227,6 +242,16 @@ export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'c
|
|
|
227
242
|
return { args, env, inputPayload }
|
|
228
243
|
}
|
|
229
244
|
|
|
245
|
+
export function resolveSpawnCommand(binaryPath, args, platform = process.platform) {
|
|
246
|
+
if (platform === 'win32' && /\.(cmd|bat)$/i.test(String(binaryPath))) {
|
|
247
|
+
return {
|
|
248
|
+
command: process.env.ComSpec || 'cmd.exe',
|
|
249
|
+
args: ['/d', '/s', '/c', binaryPath, ...args],
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { command: binaryPath, args }
|
|
253
|
+
}
|
|
254
|
+
|
|
230
255
|
/**
|
|
231
256
|
* Stream execution from local Copilot CLI process.
|
|
232
257
|
* Yields canonical DSH events: block-start, text-delta, tool-call-delta, finish, usage.
|
|
@@ -246,10 +271,11 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
246
271
|
|
|
247
272
|
const { args, env, inputPayload } = prepareCopilotCliInvocation(options, blob, binaryPath)
|
|
248
273
|
const spawner = spawnImpl || spawn
|
|
274
|
+
const invocation = resolveSpawnCommand(binaryPath, args)
|
|
249
275
|
|
|
250
276
|
let child
|
|
251
277
|
try {
|
|
252
|
-
child = spawner(
|
|
278
|
+
child = spawner(invocation.command, invocation.args, {
|
|
253
279
|
env,
|
|
254
280
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
255
281
|
})
|
|
@@ -265,6 +291,8 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
265
291
|
}
|
|
266
292
|
|
|
267
293
|
if (child.stdin) {
|
|
294
|
+
// EPIPE is expected when the CLI exits before reading stdin; never let it crash the host.
|
|
295
|
+
child.stdin.on('error', () => {})
|
|
268
296
|
child.stdin.write(inputPayload)
|
|
269
297
|
child.stdin.end()
|
|
270
298
|
}
|
|
@@ -278,7 +306,10 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
278
306
|
let stderrBuffer = ''
|
|
279
307
|
|
|
280
308
|
if (child.stderr) {
|
|
281
|
-
child.stderr.on('data', (d) => {
|
|
309
|
+
child.stderr.on('data', (d) => {
|
|
310
|
+
if (stderrBuffer.length >= MAX_STDERR_CHARS) return
|
|
311
|
+
stderrBuffer += String(d).slice(0, MAX_STDERR_CHARS - stderrBuffer.length)
|
|
312
|
+
})
|
|
282
313
|
}
|
|
283
314
|
|
|
284
315
|
const chunkQueue = []
|
|
@@ -319,7 +350,7 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
319
350
|
stdoutBuffer = ''
|
|
320
351
|
}
|
|
321
352
|
if (code !== 0 && !chunkQueue.length) {
|
|
322
|
-
processError = new LlmError(`Copilot CLI exited with code ${code}: ${stderrBuffer.trim() || 'unknown error'}`, 'VENDOR')
|
|
353
|
+
processError = new LlmError(`Copilot CLI exited with code ${code}: ${redactCopilotStderr(stderrBuffer.trim()) || 'unknown error'}`, 'VENDOR')
|
|
323
354
|
}
|
|
324
355
|
if (resolveNext) {
|
|
325
356
|
const r = resolveNext
|
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,95 @@ 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),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function authorizeUrl(cfg = {}, pkce = {}) {
|
|
84
|
+
const c = cfg || {}
|
|
85
|
+
const p = pkce || {}
|
|
86
|
+
if (!p.challenge || !p.state) {
|
|
87
|
+
throw new Error('JetBrains OAuth requires PKCE challenge and state')
|
|
88
|
+
}
|
|
89
|
+
return buildAuthorizeUrl({
|
|
90
|
+
authUrl: AUTH,
|
|
91
|
+
clientId: c.clientId || defaults().clientId,
|
|
92
|
+
redirectUri: c.redirectUri || defaults().redirectUri,
|
|
93
|
+
challenge: p.challenge,
|
|
94
|
+
state: p.state,
|
|
95
|
+
scope: SCOPE,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function decorate(blob, fetchImpl) {
|
|
100
|
+
let email = blob.email || emailFromToken(blob.accessToken) || emailFromToken(blob.idToken)
|
|
101
|
+
let plan = blob.plan || planFromToken(blob.accessToken)
|
|
102
|
+
let avatarUrl = blob.avatarUrl || avatarFromToken(blob.accessToken)
|
|
103
|
+
let scopes = blob.scopes || scopesFromToken(blob.accessToken)
|
|
104
|
+
|
|
105
|
+
if (fetchImpl && blob.accessToken && (!email || !plan)) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetchImpl(USER_INFO, {
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${blob.accessToken}`,
|
|
110
|
+
Accept: 'application/json',
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
if (res.ok) {
|
|
114
|
+
const json = await readJson(res)
|
|
115
|
+
email = email || json.email || json.username || ''
|
|
116
|
+
plan = plan || json.plan || (json.has_ai_assistant ? 'JetBrains AI Pro' : 'JetBrains Account')
|
|
117
|
+
avatarUrl = avatarUrl || json.picture || json.avatar_url || ''
|
|
118
|
+
if (json.scope && !scopes.length) scopes = String(json.scope).split(/[\s,]+/).filter(Boolean)
|
|
119
|
+
}
|
|
120
|
+
} catch { /* user info is optional */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...blob,
|
|
125
|
+
email: email || '',
|
|
126
|
+
plan: plan || 'JetBrains AI Pro',
|
|
127
|
+
avatarUrl: avatarUrl || '',
|
|
128
|
+
scopes,
|
|
129
|
+
label: blob.label || email || 'JetBrains AI',
|
|
33
130
|
}
|
|
34
131
|
}
|
|
35
132
|
|
|
36
|
-
export function
|
|
37
|
-
|
|
133
|
+
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
134
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
135
|
+
grant_type: 'authorization_code',
|
|
136
|
+
client_id: cfg.clientId || defaults().clientId,
|
|
137
|
+
code,
|
|
138
|
+
redirect_uri: cfg.redirectUri || defaults().redirectUri,
|
|
139
|
+
code_verifier: pkce.verifier,
|
|
140
|
+
state: pkce.state,
|
|
141
|
+
}, fetchImpl)
|
|
142
|
+
return decorate(tokenBlobFromOAuth(json), fetchImpl)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function refresh(cfg, blob, fetchImpl) {
|
|
146
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
147
|
+
grant_type: 'refresh_token',
|
|
148
|
+
client_id: cfg.clientId || defaults().clientId,
|
|
149
|
+
refresh_token: blob.refreshToken,
|
|
150
|
+
}, fetchImpl)
|
|
151
|
+
return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
|
|
38
152
|
}
|
|
39
153
|
|
|
40
|
-
export async function listModels() {
|
|
41
|
-
|
|
154
|
+
export async function listModels(_blob, cfg) {
|
|
155
|
+
const customModels = (cfg && Array.isArray(cfg.models) && cfg.models.length) ? cfg.models : null
|
|
156
|
+
if (customModels) {
|
|
157
|
+
const enriched = customModels.map((m) => {
|
|
158
|
+
const mid = typeof m === 'string' ? m : m.id
|
|
159
|
+
const found = JETBRAINS_MODELS.find((p) => p.id === mid)
|
|
160
|
+
return found ? { ...found, ...(typeof m === 'object' ? m : {}) } : (typeof m === 'string' ? { id: m, name: m } : m)
|
|
161
|
+
})
|
|
162
|
+
return modelCatalog(id, enriched)
|
|
163
|
+
}
|
|
164
|
+
return modelCatalog(id, JETBRAINS_MODELS)
|
|
42
165
|
}
|
|
43
166
|
|
|
44
167
|
export async function usage() {
|
|
@@ -55,7 +178,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
55
178
|
const body = {
|
|
56
179
|
model,
|
|
57
180
|
messages: openaiMessages(options),
|
|
58
|
-
stream: true
|
|
181
|
+
stream: true,
|
|
59
182
|
}
|
|
60
183
|
|
|
61
184
|
const res = await impl(url, {
|
|
@@ -64,10 +187,10 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
64
187
|
...headers,
|
|
65
188
|
'Authorization': `Bearer ${token}`,
|
|
66
189
|
'Content-Type': 'application/json',
|
|
67
|
-
'User-Agent': 'IntelliJ-IDEA/2026.1'
|
|
190
|
+
'User-Agent': 'IntelliJ-IDEA/2026.1',
|
|
68
191
|
},
|
|
69
192
|
body: JSON.stringify(body),
|
|
70
|
-
signal
|
|
193
|
+
signal,
|
|
71
194
|
})
|
|
72
195
|
|
|
73
196
|
if (!res.ok) throw httpError(res.status, await res.text())
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
// #563: Perplexity Pro Subscription OAuth Vendor
|
|
2
|
+
// Supports OAuth PKCE authentication, token refresh, and Pro subscription 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 = 'perplexity'
|
|
11
|
+
|
|
12
|
+
const AUTH = 'https://www.perplexity.ai/oauth/authorize'
|
|
13
|
+
const TOKEN = 'https://api.perplexity.ai/oauth/token'
|
|
6
14
|
const PERPLEXITY_API_BASE = 'https://api.perplexity.ai'
|
|
15
|
+
const PROFILE = 'https://api.perplexity.ai/api/v1/user/profile'
|
|
16
|
+
const USAGE = 'https://api.perplexity.ai/api/v1/user/usage'
|
|
17
|
+
const SCOPE = 'openid profile email pro:search offline_access'
|
|
7
18
|
|
|
8
19
|
const PERPLEXITY_MODELS = [
|
|
9
20
|
{
|
|
@@ -11,15 +22,43 @@ const PERPLEXITY_MODELS = [
|
|
|
11
22
|
name: 'Sonar Reasoning Pro',
|
|
12
23
|
contextWindow: 128000,
|
|
13
24
|
maxTokens: 8192,
|
|
14
|
-
|
|
25
|
+
reasoning: {
|
|
26
|
+
efforts: [
|
|
27
|
+
{ id: 'low', name: 'low' },
|
|
28
|
+
{ id: 'medium', name: 'medium' },
|
|
29
|
+
{ id: 'high', name: 'high' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
inputModalities: ['text'],
|
|
15
33
|
},
|
|
16
34
|
{
|
|
17
35
|
id: 'sonar-pro',
|
|
18
36
|
name: 'Sonar Pro',
|
|
19
37
|
contextWindow: 200000,
|
|
20
38
|
maxTokens: 8192,
|
|
21
|
-
inputModalities: ['text']
|
|
22
|
-
}
|
|
39
|
+
inputModalities: ['text'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'sonar-reasoning',
|
|
43
|
+
name: 'Sonar Reasoning',
|
|
44
|
+
contextWindow: 128000,
|
|
45
|
+
maxTokens: 8192,
|
|
46
|
+
inputModalities: ['text'],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'sonar',
|
|
50
|
+
name: 'Sonar',
|
|
51
|
+
contextWindow: 128000,
|
|
52
|
+
maxTokens: 8192,
|
|
53
|
+
inputModalities: ['text'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'r1-1776',
|
|
57
|
+
name: 'DeepSeek R1 (Perplexity)',
|
|
58
|
+
contextWindow: 128000,
|
|
59
|
+
maxTokens: 8192,
|
|
60
|
+
inputModalities: ['text'],
|
|
61
|
+
},
|
|
23
62
|
]
|
|
24
63
|
|
|
25
64
|
export function providerInfo() {
|
|
@@ -28,24 +67,119 @@ export function providerInfo() {
|
|
|
28
67
|
|
|
29
68
|
export function defaults() {
|
|
30
69
|
return {
|
|
70
|
+
clientId: 'perplexity-cli-oauth',
|
|
71
|
+
redirectUri: 'http://localhost:1455/auth/callback',
|
|
31
72
|
apiBase: PERPLEXITY_API_BASE,
|
|
32
|
-
models: PERPLEXITY_MODELS.map((m) => m.id)
|
|
73
|
+
models: PERPLEXITY_MODELS.map((m) => m.id),
|
|
33
74
|
}
|
|
34
75
|
}
|
|
35
76
|
|
|
36
|
-
export function authorizeUrl() {
|
|
37
|
-
|
|
77
|
+
export function authorizeUrl(cfg = {}, pkce = {}) {
|
|
78
|
+
const c = cfg || {}
|
|
79
|
+
const p = pkce || {}
|
|
80
|
+
if (!p.challenge || !p.state) {
|
|
81
|
+
throw new Error('Perplexity OAuth requires PKCE challenge and state')
|
|
82
|
+
}
|
|
83
|
+
return buildAuthorizeUrl({
|
|
84
|
+
authUrl: AUTH,
|
|
85
|
+
clientId: c.clientId || defaults().clientId,
|
|
86
|
+
redirectUri: c.redirectUri || defaults().redirectUri,
|
|
87
|
+
challenge: p.challenge,
|
|
88
|
+
state: p.state,
|
|
89
|
+
scope: SCOPE,
|
|
90
|
+
})
|
|
38
91
|
}
|
|
39
92
|
|
|
40
|
-
|
|
41
|
-
|
|
93
|
+
async function decorate(blob, fetchImpl) {
|
|
94
|
+
let email = blob.email || emailFromToken(blob.accessToken) || emailFromToken(blob.idToken)
|
|
95
|
+
let plan = blob.plan || planFromToken(blob.accessToken)
|
|
96
|
+
let avatarUrl = blob.avatarUrl || avatarFromToken(blob.accessToken)
|
|
97
|
+
let scopes = blob.scopes || scopesFromToken(blob.accessToken)
|
|
98
|
+
|
|
99
|
+
if (fetchImpl && blob.accessToken && (!email || !plan)) {
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetchImpl(PROFILE, {
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${blob.accessToken}`,
|
|
104
|
+
Accept: 'application/json',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
if (res.ok) {
|
|
108
|
+
const json = await readJson(res)
|
|
109
|
+
email = email || json.email || ''
|
|
110
|
+
plan = plan || json.plan || (json.has_pro ? 'Perplexity Pro' : 'Free')
|
|
111
|
+
avatarUrl = avatarUrl || json.picture || json.avatar_url || ''
|
|
112
|
+
if (json.scope && !scopes.length) scopes = String(json.scope).split(/[\s,]+/).filter(Boolean)
|
|
113
|
+
}
|
|
114
|
+
} catch { /* profile is optional */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...blob,
|
|
119
|
+
email: email || '',
|
|
120
|
+
plan: plan || 'Perplexity Pro',
|
|
121
|
+
avatarUrl: avatarUrl || '',
|
|
122
|
+
scopes,
|
|
123
|
+
label: blob.label || email || 'Perplexity Pro',
|
|
124
|
+
}
|
|
42
125
|
}
|
|
43
126
|
|
|
44
|
-
export async function
|
|
45
|
-
|
|
46
|
-
|
|
127
|
+
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
128
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
129
|
+
grant_type: 'authorization_code',
|
|
130
|
+
client_id: cfg.clientId || defaults().clientId,
|
|
131
|
+
code,
|
|
132
|
+
redirect_uri: cfg.redirectUri || defaults().redirectUri,
|
|
133
|
+
code_verifier: pkce.verifier,
|
|
134
|
+
state: pkce.state,
|
|
135
|
+
}, fetchImpl)
|
|
136
|
+
return decorate(tokenBlobFromOAuth(json), fetchImpl)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function refresh(cfg, blob, fetchImpl) {
|
|
140
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
141
|
+
grant_type: 'refresh_token',
|
|
142
|
+
client_id: cfg.clientId || defaults().clientId,
|
|
143
|
+
refresh_token: blob.refreshToken,
|
|
144
|
+
}, fetchImpl)
|
|
145
|
+
return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function listModels(_blob, cfg) {
|
|
149
|
+
const customModels = (cfg && Array.isArray(cfg.models) && cfg.models.length) ? cfg.models : null
|
|
150
|
+
if (customModels) {
|
|
151
|
+
const enriched = customModels.map((m) => {
|
|
152
|
+
const mid = typeof m === 'string' ? m : m.id
|
|
153
|
+
const found = PERPLEXITY_MODELS.find((p) => p.id === mid)
|
|
154
|
+
return found ? { ...found, ...(typeof m === 'object' ? m : {}) } : (typeof m === 'string' ? { id: m, name: m } : m)
|
|
155
|
+
})
|
|
156
|
+
return modelCatalog(id, enriched)
|
|
47
157
|
}
|
|
48
|
-
return
|
|
158
|
+
return modelCatalog(id, PERPLEXITY_MODELS)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function usage(blob, _cfg, fetchImpl) {
|
|
162
|
+
if (!blob || (!blob.accessToken && !blob.apiKey)) return null
|
|
163
|
+
const key = blob.accessToken || blob.apiKey
|
|
164
|
+
const impl = fetchImpl || fetch
|
|
165
|
+
try {
|
|
166
|
+
const res = await impl(USAGE, {
|
|
167
|
+
headers: {
|
|
168
|
+
Authorization: `Bearer ${key}`,
|
|
169
|
+
Accept: 'application/json',
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
if (res.ok) {
|
|
173
|
+
const d = await readJson(res)
|
|
174
|
+
return {
|
|
175
|
+
usedPercent: typeof d.used_percent === 'number' ? d.used_percent : null,
|
|
176
|
+
plan: d.plan || blob.plan || 'Perplexity Pro',
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
quota: d.quota ? { used: d.quota.used, limit: d.quota.limit, resetAt: d.quota.reset_at } : undefined,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch { /* usage endpoint is optional */ }
|
|
182
|
+
return { usedPercent: null, plan: blob.plan || 'Perplexity Pro', timestamp: Date.now() }
|
|
49
183
|
}
|
|
50
184
|
|
|
51
185
|
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
@@ -57,7 +191,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
57
191
|
const body = {
|
|
58
192
|
model: options.model || 'sonar-pro',
|
|
59
193
|
messages: openaiMessages(options),
|
|
60
|
-
stream: true
|
|
194
|
+
stream: true,
|
|
61
195
|
}
|
|
62
196
|
|
|
63
197
|
const res = await impl(url, {
|
|
@@ -65,10 +199,10 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
65
199
|
headers: {
|
|
66
200
|
...headers,
|
|
67
201
|
'Authorization': `Bearer ${key}`,
|
|
68
|
-
'Content-Type': 'application/json'
|
|
202
|
+
'Content-Type': 'application/json',
|
|
69
203
|
},
|
|
70
204
|
body: JSON.stringify(body),
|
|
71
|
-
signal
|
|
205
|
+
signal,
|
|
72
206
|
})
|
|
73
207
|
|
|
74
208
|
if (!res.ok) throw httpError(res.status, await res.text())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.83",
|
|
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",
|