@goodandready/dsh-subscriptions 0.6.72 → 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.
Files changed (47) hide show
  1. package/README.md +7 -0
  2. package/README.ru.md +7 -0
  3. package/README.zh.md +7 -0
  4. package/lib/accounts-view.js +97 -5
  5. package/lib/accounts.js +137 -11
  6. package/lib/adapter.js +3 -0
  7. package/lib/alerts.js +35 -0
  8. package/lib/analytics.js +31 -2
  9. package/lib/billing.js +99 -0
  10. package/lib/blob.js +18 -0
  11. package/lib/capabilities.js +208 -0
  12. package/lib/client.js +344 -17
  13. package/lib/config-schema.js +46 -4
  14. package/lib/diagnostic-wizard.js +2 -5
  15. package/lib/diagnostics.js +2 -2
  16. package/lib/error-buffer.js +2 -2
  17. package/lib/forecast.js +56 -0
  18. package/lib/headless-auth.js +379 -0
  19. package/lib/history.js +36 -0
  20. package/lib/http.js +26 -4
  21. package/lib/index.js +98 -11
  22. package/lib/jwt.js +84 -17
  23. package/lib/loopback.js +3 -2
  24. package/lib/mask.js +16 -0
  25. package/lib/oauth-pending.js +12 -0
  26. package/lib/pricing.js +89 -0
  27. package/lib/refs.js +2 -0
  28. package/lib/reset-clock.js +55 -0
  29. package/lib/rotate.js +18 -4
  30. package/lib/routes/accounts.js +131 -0
  31. package/lib/routes/oauth.js +44 -2
  32. package/lib/routes/proxy.js +14 -0
  33. package/lib/scope-inspector.js +51 -0
  34. package/lib/stream-rotate.js +31 -10
  35. package/lib/updater.js +21 -0
  36. package/lib/vault-sync.js +412 -0
  37. package/lib/vault.js +1 -1
  38. package/lib/vendor-factory.js +6 -2
  39. package/lib/vendors/antigravity.js +11 -2
  40. package/lib/vendors/claude.js +56 -8
  41. package/lib/vendors/codex.js +34 -6
  42. package/lib/vendors/copilot-cli.js +8 -1
  43. package/lib/vendors/ernie.js +9 -2
  44. package/lib/vendors/grok.js +66 -6
  45. package/lib/vendors/jetbrains.js +133 -13
  46. package/lib/vendors/perplexity.js +148 -17
  47. package/package.json +1 -1
@@ -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 url = `${ERNIE_OAUTH_URL}?grant_type=client_credentials&client_id=${encodeURIComponent(apiKey)}&client_secret=${encodeURIComponent(secretKey)}`
48
- const res = await impl(url, { method: 'POST' })
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
  }
@@ -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 = String(blob.accessToken || '').slice(-24)
42
+ const key = catalogKey(blob.accessToken)
43
+ const now = Date.now()
20
44
  const hit = catalogCache.get(key)
21
- if (hit && Date.now() - hit.at < CATALOG_TTL_MS) return hit.map
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) catalogCache.set(key, { at: Date.now(), map })
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 (/grok\.com/i.test(base)) Object.assign(headers, identityHeaders(blob, config))
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
- return { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || 'Grok' }
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) {
@@ -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 { openaiMessages } from '../messages.js'
3
- import { openaiChatStream, httpError } from '../wire.js'
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
- return 'https://account.jetbrains.com/'
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
- export async function listModels() {
41
- return JETBRAINS_MODELS
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())
@@ -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 { openaiMessages } from '../messages.js'
3
- import { openaiChatStream, httpError } from '../wire.js'
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
- inputModalities: ['text']
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,116 @@ 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
- return 'https://www.perplexity.ai/settings/api'
77
+ export function authorizeUrl(cfg = {}, pkce = {}) {
78
+ const c = cfg || {}
79
+ const p = pkce || {}
80
+ return buildAuthorizeUrl({
81
+ authUrl: AUTH,
82
+ clientId: c.clientId || defaults().clientId,
83
+ redirectUri: c.redirectUri || defaults().redirectUri,
84
+ challenge: p.challenge || 'challenge',
85
+ state: p.state || 'state',
86
+ scope: SCOPE,
87
+ })
38
88
  }
39
89
 
40
- export async function listModels() {
41
- return PERPLEXITY_MODELS
90
+ async function decorate(blob, fetchImpl) {
91
+ let email = blob.email || emailFromToken(blob.accessToken) || emailFromToken(blob.idToken)
92
+ let plan = blob.plan || planFromToken(blob.accessToken)
93
+ let avatarUrl = blob.avatarUrl || avatarFromToken(blob.accessToken)
94
+ let scopes = blob.scopes || scopesFromToken(blob.accessToken)
95
+
96
+ if (fetchImpl && blob.accessToken && (!email || !plan)) {
97
+ try {
98
+ const res = await fetchImpl(PROFILE, {
99
+ headers: {
100
+ Authorization: `Bearer ${blob.accessToken}`,
101
+ Accept: 'application/json',
102
+ },
103
+ })
104
+ if (res.ok) {
105
+ const json = await readJson(res)
106
+ email = email || json.email || ''
107
+ plan = plan || json.plan || (json.has_pro ? 'Perplexity Pro' : 'Free')
108
+ avatarUrl = avatarUrl || json.picture || json.avatar_url || ''
109
+ if (json.scope && !scopes.length) scopes = String(json.scope).split(/[\s,]+/).filter(Boolean)
110
+ }
111
+ } catch { /* profile is optional */ }
112
+ }
113
+
114
+ return {
115
+ ...blob,
116
+ email: email || '',
117
+ plan: plan || 'Perplexity Pro',
118
+ avatarUrl: avatarUrl || '',
119
+ scopes,
120
+ label: blob.label || email || 'Perplexity Pro',
121
+ }
42
122
  }
43
123
 
44
- export async function usage(blob) {
45
- if (blob && (blob.accessToken || blob.apiKey)) {
46
- return { usedPercent: null, plan: 'Perplexity Pro', timestamp: Date.now() }
124
+ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
125
+ const json = await jsonTokenRequest(TOKEN, {
126
+ grant_type: 'authorization_code',
127
+ client_id: cfg.clientId || defaults().clientId,
128
+ code,
129
+ redirect_uri: cfg.redirectUri || defaults().redirectUri,
130
+ code_verifier: pkce.verifier,
131
+ state: pkce.state,
132
+ }, fetchImpl)
133
+ return decorate(tokenBlobFromOAuth(json), fetchImpl)
134
+ }
135
+
136
+ export async function refresh(cfg, blob, fetchImpl) {
137
+ const json = await jsonTokenRequest(TOKEN, {
138
+ grant_type: 'refresh_token',
139
+ client_id: cfg.clientId || defaults().clientId,
140
+ refresh_token: blob.refreshToken,
141
+ }, fetchImpl)
142
+ return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
143
+ }
144
+
145
+ export async function listModels(_blob, cfg) {
146
+ const customModels = (cfg && Array.isArray(cfg.models) && cfg.models.length) ? cfg.models : null
147
+ if (customModels) {
148
+ const enriched = customModels.map((m) => {
149
+ const mid = typeof m === 'string' ? m : m.id
150
+ const found = PERPLEXITY_MODELS.find((p) => p.id === mid)
151
+ return found ? { ...found, ...(typeof m === 'object' ? m : {}) } : (typeof m === 'string' ? { id: m, name: m } : m)
152
+ })
153
+ return modelCatalog(id, enriched)
47
154
  }
48
- return null
155
+ return modelCatalog(id, PERPLEXITY_MODELS)
156
+ }
157
+
158
+ export async function usage(blob, _cfg, fetchImpl) {
159
+ if (!blob || (!blob.accessToken && !blob.apiKey)) return null
160
+ const key = blob.accessToken || blob.apiKey
161
+ const impl = fetchImpl || fetch
162
+ try {
163
+ const res = await impl(USAGE, {
164
+ headers: {
165
+ Authorization: `Bearer ${key}`,
166
+ Accept: 'application/json',
167
+ },
168
+ })
169
+ if (res.ok) {
170
+ const d = await readJson(res)
171
+ return {
172
+ usedPercent: typeof d.used_percent === 'number' ? d.used_percent : null,
173
+ plan: d.plan || blob.plan || 'Perplexity Pro',
174
+ timestamp: Date.now(),
175
+ quota: d.quota ? { used: d.quota.used, limit: d.quota.limit, resetAt: d.quota.reset_at } : undefined,
176
+ }
177
+ }
178
+ } catch { /* usage endpoint is optional */ }
179
+ return { usedPercent: null, plan: blob.plan || 'Perplexity Pro', timestamp: Date.now() }
49
180
  }
50
181
 
51
182
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
@@ -57,7 +188,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
57
188
  const body = {
58
189
  model: options.model || 'sonar-pro',
59
190
  messages: openaiMessages(options),
60
- stream: true
191
+ stream: true,
61
192
  }
62
193
 
63
194
  const res = await impl(url, {
@@ -65,10 +196,10 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
65
196
  headers: {
66
197
  ...headers,
67
198
  'Authorization': `Bearer ${key}`,
68
- 'Content-Type': 'application/json'
199
+ 'Content-Type': 'application/json',
69
200
  },
70
201
  body: JSON.stringify(body),
71
- signal
202
+ signal,
72
203
  })
73
204
 
74
205
  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.72",
3
+ "version": "0.6.82",
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",