@goodandready/dsh-subscriptions 0.4.23 → 0.5.1

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/client.js CHANGED
@@ -81,6 +81,8 @@ window.__ModuleLoader__.load({
81
81
  '.dsub-brandGrok{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3)}' +
82
82
  '.dsub-brandAgy{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3)}' +
83
83
  '.dsub-brandOllama{background:rgba(107,114,128,.15);color:#9ca3af;border:1px solid rgba(107,114,128,.3)}' +
84
+ '.dsub-brandKimi{background:rgba(236,72,153,.15);color:#f472b6;border:1px solid rgba(236,72,153,.3)}' +
85
+ '.dsub-brandGlm{background:rgba(20,184,166,.15);color:#2dd4bf;border:1px solid rgba(20,184,166,.3)}' +
84
86
  '.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
85
87
  '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
86
88
  '.dsub-accName{font-size:13px;font-weight:600}' +
@@ -210,6 +212,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
210
212
  'checking': 'Checking…',
211
213
  'importToken': 'Import',
212
214
  'importTokenPlace': 'Paste an existing token',
215
+ 'importLocalCli': 'Import from local CLI',
216
+ 'importLocalSuccess': 'Imported from local CLI!',
213
217
  'reconnectRequired': 'Reconnect required',
214
218
  'manualTitle': 'If the browser did not come back',
215
219
  'windowPrimary': 'Primary window',
@@ -220,6 +224,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
220
224
  'checking': 'Проверяю…',
221
225
  'importToken': 'Внести',
222
226
  'importTokenPlace': 'Вставить уже готовый токен',
227
+ 'importLocalCli': 'Импортировать из локального CLI',
228
+ 'importLocalSuccess': 'Импортировано из локального CLI!',
223
229
  'reconnectRequired': 'Нужно переподключить',
224
230
  'manualTitle': 'Если браузер не вернулся сам',
225
231
  'windowPrimary': 'Основное окно',
@@ -316,6 +322,22 @@ const cssId = 'dsh-subscriptions/settings.module.css'
316
322
  }
317
323
 
318
324
  // #51: live countdown to the quota window reset (account.quota.resetAt).
325
+ function formatRelativeReset(resetAt, lang, now) {
326
+ if (!resetAt || !Number.isFinite(resetAt) || resetAt <= 0) return ''
327
+ var delta = resetAt - (now || Date.now())
328
+ var isRu = lang !== 'en'
329
+ if (delta <= 0) return isRu ? 'только что' : 'just now'
330
+ var totalMinutes = Math.max(1, Math.round(delta / 60000))
331
+ var days = Math.floor(totalMinutes / 1440)
332
+ var hours = Math.floor((totalMinutes % 1440) / 60)
333
+ var minutes = totalMinutes % 60
334
+ var bits = []
335
+ if (days) bits.push(days + (isRu ? ' дн' : 'd'))
336
+ if (hours) bits.push(hours + (isRu ? ' ч' : 'h'))
337
+ if (minutes || !bits.length) bits.push(minutes + (isRu ? ' мин' : 'm'))
338
+ return (isRu ? 'через ' : 'in ') + bits.join(' ')
339
+ }
340
+
319
341
  function ResetCountdown(props) {
320
342
  const [now, setNow] = React.useState(Date.now())
321
343
  React.useEffect(() => {
@@ -325,12 +347,15 @@ const cssId = 'dsh-subscriptions/settings.module.css'
325
347
  const ms = (props.resetAt || 0) - now
326
348
  if (ms <= 0) return null
327
349
  const total = Math.floor(ms / 1000)
328
- const h = Math.floor(total / 3600)
329
- const m = Math.floor((total % 3600) / 60)
350
+ if (total > 3600) {
351
+ return React.createElement('span', { className: 'dsub-sub', style: { fontVariantNumeric: 'tabular-nums' } },
352
+ t('resetLabel') + ' ' + formatRelativeReset(props.resetAt, t('lang'), now))
353
+ }
354
+ const m = Math.floor(total / 60)
330
355
  const sec = total % 60
331
356
  const pad = (n) => String(n).padStart(2, '0')
332
357
  return React.createElement('span', { className: 'dsub-sub', style: { fontVariantNumeric: 'tabular-nums' } },
333
- t('resetLabel') + ' ' + pad(h) + ':' + pad(m) + ':' + pad(sec))
358
+ t('resetLabel') + ' ' + pad(m) + ':' + pad(sec))
334
359
  }
335
360
 
336
361
  function badgeFor(account, t) {
@@ -607,6 +632,19 @@ const cssId = 'dsh-subscriptions/settings.module.css'
607
632
  setAccounts(data.accounts || [])
608
633
  }
609
634
 
635
+ const importLocalCli = async (provider, index) => {
636
+ setErr('')
637
+ const res = await fetch('/dsh-subscriptions/import-local', {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({ provider, index }),
641
+ })
642
+ const data = await res.json().catch(() => ({}))
643
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
644
+ await load()
645
+ setInfo(t('importLocalSuccess'))
646
+ }
647
+
610
648
  const importToken = async (provider, index) => {
611
649
  setErr('')
612
650
  const tok = (tokenDraft[provider + ':' + index] || '').trim()
@@ -752,6 +790,11 @@ const cssId = 'dsh-subscriptions/settings.module.css'
752
790
  type: 'button', className: 'dsub-mini',
753
791
  onClick: () => importToken(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
754
792
  }, t('importToken')),
793
+ React.createElement('button', {
794
+ type: 'button', className: 'dsub-mini',
795
+ title: t('importLocalCli'),
796
+ onClick: () => importLocalCli(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
797
+ }, '📥 CLI'),
755
798
  ),
756
799
  React.createElement('div', { className: 'dsub-row' },
757
800
  React.createElement('input', {
@@ -1120,6 +1163,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1120
1163
  if (p.includes('claude') || p.includes('anthropic')) return { label: 'Claude', cls: 'dsub-brandClaude', icon: '✳' }
1121
1164
  if (p.includes('grok') || p.includes('xai')) return { label: 'Grok', cls: 'dsub-brandGrok', icon: '✦' }
1122
1165
  if (p.includes('antigravity') || p.includes('google') || p.includes('gemini')) return { label: 'AGY', cls: 'dsub-brandAgy', icon: '◆' }
1166
+ if (p.includes('kimi')) return { label: 'Kimi', cls: 'dsub-brandKimi', icon: '🌙' }
1167
+ if (p.includes('glm') || p.includes('zcode')) return { label: 'GLM', cls: 'dsub-brandGlm', icon: '⚡' }
1123
1168
  if (p.includes('ollama')) return { label: 'Ollama', cls: 'dsub-brandOllama', icon: '🦙' }
1124
1169
  return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
1125
1170
  }
@@ -1196,7 +1241,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1196
1241
  React.createElement('div', { className: 'dsub-heroBars' },
1197
1242
  React.createElement('div', null,
1198
1243
  React.createElement('div', { className: 'dsub-barLabelRow' },
1199
- React.createElement('span', { style: { fontWeight: 600 } }, (primaryWin && primaryWin.label) ? primaryWin.label + ' window' : t('subs5hWindow')),
1244
+ React.createElement('span', { style: { fontWeight: 600 } }, ((primaryWin && primaryWin.label) ? primaryWin.label + ' window' : t('subs5hWindow')) + (primaryWin && primaryWin.resetAt ? ' (' + formatRelativeReset(primaryWin.resetAt, t('lang'), now) + ')' : '')),
1200
1245
  React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums', fontWeight: 600 } }, primPct + '% ' + t('subsUsed') + ' (' + (100 - primPct) + '% ' + t('subsFree') + ')'),
1201
1246
  ),
1202
1247
  React.createElement('div', { className: 'dsub-barTrack' },
@@ -0,0 +1,163 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ function homeFile(...parts) {
6
+ return join(homedir(), ...parts)
7
+ }
8
+
9
+ async function readJson(path) {
10
+ try {
11
+ const text = await readFile(path, 'utf8')
12
+ return JSON.parse(text)
13
+ } catch {
14
+ return undefined
15
+ }
16
+ }
17
+
18
+ export async function discoverLocalCliSessions() {
19
+ const detected = {}
20
+
21
+ // 1. Codex CLI
22
+ const codex = await readJson(homeFile('.codex', 'auth.json'))
23
+ if (codex && (codex.access_token || codex.accessToken || (codex.tokens && codex.tokens.access_token))) {
24
+ const tok = codex.tokens || codex
25
+ detected.codex = {
26
+ provider: 'codex',
27
+ path: homeFile('.codex', 'auth.json'),
28
+ email: codex.email || codex.account || '',
29
+ hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
30
+ }
31
+ }
32
+
33
+ // 2. Grok CLI / Hermes
34
+ const grokPaths = [
35
+ homeFile('.grok', 'auth.json'),
36
+ homeFile('.hermes', 'auth.json'),
37
+ ]
38
+ for (const p of grokPaths) {
39
+ const grok = await readJson(p)
40
+ if (grok && (grok.access_token || grok.token || (grok.tokens && grok.tokens.access_token))) {
41
+ detected.grok = {
42
+ provider: 'grok',
43
+ path: p,
44
+ email: grok.email || grok.account || '',
45
+ hasRefreshToken: !!(grok.refresh_token || (grok.tokens && grok.tokens.refresh_token)),
46
+ }
47
+ break
48
+ }
49
+ }
50
+
51
+ // 3. Antigravity / Gemini CLI
52
+ const agyPaths = [
53
+ homeFile('.gemini', 'antigravity-cli', 'antigravity-oauth-token'),
54
+ homeFile('.cli-proxy-api', 'antigravity.json'),
55
+ ]
56
+ for (const p of agyPaths) {
57
+ const agy = await readJson(p)
58
+ const tok = agy && agy.token ? agy.token : agy
59
+ if (tok && (tok.access_token || tok.accessToken)) {
60
+ detected.antigravity = {
61
+ provider: 'antigravity',
62
+ path: p,
63
+ email: agy.email || tok.account || '',
64
+ hasRefreshToken: !!(tok.refresh_token || tok.refreshToken),
65
+ }
66
+ break
67
+ }
68
+ }
69
+
70
+ // 4. Kimi Code Plan
71
+ const kimi = await readJson(homeFile('.kimi-code', 'credentials', 'kimi-code.json'))
72
+ if (kimi && (kimi.access_token || kimi.token)) {
73
+ detected.kimi = {
74
+ provider: 'kimi',
75
+ path: homeFile('.kimi-code', 'credentials', 'kimi-code.json'),
76
+ email: kimi.email || kimi.account || '',
77
+ hasRefreshToken: !!(kimi.refresh_token || kimi.refreshToken),
78
+ }
79
+ }
80
+
81
+ // 5. GLM ZCode
82
+ const glmPaths = [
83
+ homeFile('.zcode', 'v2', 'config.json'),
84
+ homeFile('.zcode', 'cli', 'config.json'),
85
+ homeFile('.zcode', 'config.json'),
86
+ ]
87
+ for (const p of glmPaths) {
88
+ const glm = await readJson(p)
89
+ if (glm && (glm.apiKey || glm.api_key || (glm.provider && glm.provider.apiKey))) {
90
+ detected.glm = {
91
+ provider: 'glm',
92
+ path: p,
93
+ email: 'ZCode CLI Account',
94
+ hasRefreshToken: false,
95
+ }
96
+ break
97
+ }
98
+ }
99
+
100
+ return detected
101
+ }
102
+
103
+ export async function loadLocalCliBlob(provider) {
104
+ const discovered = await discoverLocalCliSessions()
105
+ const info = discovered[provider]
106
+ if (!info) throw new Error(`no local CLI session found for ${provider}`)
107
+
108
+ const raw = await readJson(info.path)
109
+ if (!raw) throw new Error(`failed to read local CLI file: ${info.path}`)
110
+
111
+ if (provider === 'codex') {
112
+ const tok = raw.tokens || raw
113
+ return {
114
+ accessToken: tok.access_token || tok.accessToken || '',
115
+ refreshToken: tok.refresh_token || tok.refreshToken || '',
116
+ expiresAt: tok.expires_at || tok.expiresAt || (Date.now() + 3600 * 1000),
117
+ email: raw.email || raw.account || '',
118
+ accountId: raw.account_id || raw.accountId || '',
119
+ }
120
+ }
121
+
122
+ if (provider === 'grok') {
123
+ const tok = raw.tokens || raw
124
+ return {
125
+ accessToken: tok.access_token || tok.token || '',
126
+ refreshToken: tok.refresh_token || '',
127
+ expiresAt: tok.expires_at || tok.expiresAt || (Date.now() + 3600 * 1000),
128
+ email: raw.email || raw.account || '',
129
+ }
130
+ }
131
+
132
+ if (provider === 'antigravity') {
133
+ const tok = raw.token || raw
134
+ return {
135
+ accessToken: tok.access_token || tok.accessToken || '',
136
+ refreshToken: tok.refresh_token || tok.refreshToken || '',
137
+ expiresAt: tok.expiry || tok.expires_at || tok.expiresAt || (Date.now() + 3600 * 1000),
138
+ email: raw.email || tok.account || '',
139
+ projectId: raw.project_id || raw.projectId || tok.project_id || '',
140
+ }
141
+ }
142
+
143
+ if (provider === 'kimi') {
144
+ return {
145
+ accessToken: raw.access_token || raw.token || '',
146
+ refreshToken: raw.refresh_token || '',
147
+ expiresAt: raw.expires_at || (Date.now() + 86400 * 1000),
148
+ email: raw.email || raw.account || '',
149
+ }
150
+ }
151
+
152
+ if (provider === 'glm') {
153
+ const apiKey = raw.apiKey || raw.api_key || (raw.provider && raw.provider.apiKey) || ''
154
+ return {
155
+ accessToken: apiKey,
156
+ refreshToken: '',
157
+ expiresAt: Date.now() + 30 * 86400 * 1000,
158
+ email: 'ZCode CLI',
159
+ }
160
+ }
161
+
162
+ throw new Error(`unsupported local CLI import for provider ${provider}`)
163
+ }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { discoverLocalCliSessions, loadLocalCliBlob } from './import-auth.js'
1
2
  import { readFileSync } from 'node:fs'
2
3
  import z from '@deepseek-ai/schemastery'
3
4
  import { PROVIDERS, oauthRef, parseOauthRef, isProvider, displayName, droppedCredentialRefs } from './refs.js'
@@ -1123,6 +1124,53 @@ export function apply(ctx, config) {
1123
1124
  },
1124
1125
  }), 'dsh-subscriptions: /check')
1125
1126
 
1127
+ ctx.effect(() => ctx.webServer.register({
1128
+ kind: 'exact',
1129
+ path: '/dsh-subscriptions/discover-local',
1130
+ handler: async (req, res) => {
1131
+ if (req.method !== 'GET') {
1132
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
1133
+ return
1134
+ }
1135
+ try {
1136
+ const detected = await discoverLocalCliSessions()
1137
+ writeJson(res, 200, { ok: true, detected })
1138
+ } catch (e) {
1139
+ writeJson(res, 500, { ok: false, error: { message: String(e && e.message || e) } })
1140
+ }
1141
+ },
1142
+ }), 'dsh-subscriptions: /discover-local')
1143
+
1144
+ ctx.effect(() => ctx.webServer.register({
1145
+ kind: 'exact',
1146
+ path: '/dsh-subscriptions/import-local',
1147
+ handler: async (req, res) => {
1148
+ if (req.method !== 'POST') {
1149
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
1150
+ return
1151
+ }
1152
+ if (!isTrustedSettingsRequest(req)) {
1153
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
1154
+ return
1155
+ }
1156
+ const body = await readBody(req).catch(() => null)
1157
+ if (!body || !body.provider) {
1158
+ writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'missing provider' } })
1159
+ return
1160
+ }
1161
+ try {
1162
+ const blob = await loadLocalCliBlob(body.provider)
1163
+ const slot = normalizeSlots(live().slots).find((s) => s.provider === body.provider && s.index === (Number(body.index) || 1))
1164
+ const ref = slot ? slot.ref : `${body.provider.toUpperCase()}_OAUTH_${Number(body.index) || 1}`
1165
+ await store.saveBlob(ref, blob)
1166
+ writeJson(res, 200, { ok: true, ref, provider: body.provider, email: blob.email || '' })
1167
+ } catch (e) {
1168
+ writeJson(res, 400, { ok: false, error: { message: String(e && e.message || e) } })
1169
+ }
1170
+ },
1171
+ }), 'dsh-subscriptions: /import-local')
1172
+
1173
+
1126
1174
  // HTTP-прокси к API провайдера через subscriptions.request.
1127
1175
  // Same-origin only, allowlist путей, ротация и квота как у моделей.
1128
1176
  // Токен наружу не отдаётся — наружу только ответ провайдера.
package/lib/refs.js CHANGED
@@ -4,6 +4,8 @@ export const BUILTIN_PROVIDERS = Object.freeze([
4
4
  'claude',
5
5
  'grok',
6
6
  'antigravity',
7
+ 'kimi',
8
+ 'glm',
7
9
  ])
8
10
 
9
11
  const dynamicIds = new Set()
@@ -39,6 +41,8 @@ const DISPLAY = {
39
41
  claude: 'Claude',
40
42
  grok: 'Grok',
41
43
  antigravity: 'Antigravity',
44
+ kimi: 'Moonshot Kimi',
45
+ glm: 'Zhipu GLM',
42
46
  }
43
47
 
44
48
  export function displayName(provider) {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Format a future timestamp as a relative remaining duration down to the minute.
3
+ * Supports en and ru.
4
+ */
5
+
6
+ export const RELATIVE_UNITS = {
7
+ en: {
8
+ soon: 'just now',
9
+ prefix: 'in ',
10
+ suffix: '',
11
+ minute: '{n}m',
12
+ hour: '{n}h',
13
+ day: '{n}d',
14
+ },
15
+ ru: {
16
+ soon: 'только что',
17
+ prefix: 'через ',
18
+ suffix: '',
19
+ minute: '{n} мин',
20
+ hour: '{n} ч',
21
+ day: '{n} дн',
22
+ },
23
+ }
24
+
25
+ function fill(template, n) {
26
+ return String(template).replace('{n}', String(n))
27
+ }
28
+
29
+ export function formatRelativeReset(resetAt, lang = 'ru', now = Date.now()) {
30
+ if (typeof resetAt !== 'number' || !Number.isFinite(resetAt) || resetAt <= 0) return ''
31
+ const delta = resetAt - now
32
+ const units = RELATIVE_UNITS[lang] || RELATIVE_UNITS.ru
33
+ if (delta <= 0) return units.soon
34
+
35
+ const totalMinutes = Math.max(1, Math.round(delta / 60_000))
36
+ const days = Math.floor(totalMinutes / 1440)
37
+ const hours = Math.floor((totalMinutes % 1440) / 60)
38
+ const minutes = totalMinutes % 60
39
+
40
+ const bits = []
41
+ if (days) bits.push(fill(units.day, days))
42
+ if (hours) bits.push(fill(units.hour, hours))
43
+ if (minutes || bits.length === 0) bits.push(fill(units.minute, minutes))
44
+
45
+ const durationStr = bits.join(' ')
46
+ return `${units.prefix}${durationStr}${units.suffix}`.trim()
47
+ }
@@ -1,4 +1,4 @@
1
- import { randomUUID } from 'node:crypto'
1
+ import { randomUUID, createHash } from 'node:crypto'
2
2
  import { buildAuthorizeUrl } from '../oauth.js'
3
3
  import { codexResponsesBody, modelCatalog } from '../messages.js'
4
4
  import { chatgptAccountId, emailFromToken } from '../jwt.js'
@@ -206,19 +206,49 @@ export async function usage(blob, cfg, fetchImpl) {
206
206
  export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
207
207
  const base = (config.baseUrl || defaults().baseUrl).replace(/\/$/, '')
208
208
  const body = codexResponsesBody(options, INSTRUCTIONS, config)
209
+ const sessionId = deriveSessionId(options)
210
+
211
+ // Prompt Cache Affinity
212
+ body.prompt_cache_key = sessionId
213
+ delete body.session_id
214
+ delete body.prompt_cache_retention
215
+ delete body.prompt_cache_options
216
+
217
+ const isFast = !!(config && config.fastMode) || String((options && options.model) || '').toLowerCase().endsWith('-fast')
218
+ if (isFast) {
219
+ body.service_tier = 'priority'
220
+ }
221
+
222
+ const codexHeaders = {
223
+ 'Content-Type': 'application/json',
224
+ Accept: 'text/event-stream',
225
+ 'session-id': sessionId,
226
+ 'x-client-request-id': sessionId,
227
+ ...(isFast ? { 'x-codex-routing-hint': `model=${(options && options.model) || 'gpt-5.6-luna'};tier=priority` } : {}),
228
+ }
229
+
209
230
  const res = await fetchImpl(`${base}/responses`, {
210
231
  method: 'POST',
211
232
  headers: {
212
233
  ...headers,
213
- ...identityHeaders(blob, config, {
214
- 'Content-Type': 'application/json',
215
- Accept: 'text/event-stream',
216
- 'session-id': randomUUID(),
217
- }),
234
+ ...identityHeaders(blob, config, codexHeaders),
218
235
  },
219
236
  body: JSON.stringify(body),
220
237
  signal,
221
238
  })
222
239
  if (!res.ok) throw httpError(res.status, await res.text())
223
240
  yield* codexResponsesStream(res.body)
224
- }
241
+ }
242
+ export function deriveSessionId(options) {
243
+ const explicit = (options && (options.sessionId || options.session_id || options.conversationId || options.prompt_cache_key))
244
+ if (typeof explicit === 'string' && explicit.trim()) {
245
+ return explicit.trim().replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 64)
246
+ }
247
+ const firstUserMsg = (options && options.messages || []).find((m) => m.role === 'user')
248
+ const text = (firstUserMsg && (typeof firstUserMsg.content === 'string' ? firstUserMsg.content : JSON.stringify(firstUserMsg.content))) || ''
249
+ if (text) {
250
+ const hash = createHash('sha256').update(text.slice(0, 500)).digest('hex').slice(0, 32)
251
+ return `dsh-sub-${hash}`
252
+ }
253
+ return randomUUID()
254
+ }
@@ -0,0 +1,113 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm'
2
+ import { openaiMessages, openaiTools } from '../messages.js'
3
+ import { openaiChatStream, readJson, httpError } from '../wire.js'
4
+ import { asUsageSnapshot } from '../usage.js'
5
+
6
+ export const id = 'glm'
7
+
8
+ export const GLM_CODING_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions'
9
+ export const GLM_QUOTA_URL = 'https://api.z.ai/api/monitor/usage/quota/limit'
10
+ export const GLM_USER_AGENT = 'ZCode/3.10.1'
11
+
12
+ export const GLM_MODELS = [
13
+ {
14
+ id: 'glm-4-plus',
15
+ name: 'GLM-4-Plus (Coding Plan)',
16
+ contextWindow: 131072,
17
+ maxTokens: 4096,
18
+ inputModalities: ['text'],
19
+ reasoning: { efforts: [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }, { id: 'max', name: 'Max' }] },
20
+ },
21
+ {
22
+ id: 'glm-4-flash',
23
+ name: 'GLM-4-Flash (High Speed)',
24
+ contextWindow: 131072,
25
+ maxTokens: 4096,
26
+ inputModalities: ['text', 'image'],
27
+ },
28
+ {
29
+ id: 'glm-zero-preview',
30
+ name: 'GLM Zero (Thinking)',
31
+ contextWindow: 131072,
32
+ maxTokens: 8192,
33
+ inputModalities: ['text'],
34
+ },
35
+ ]
36
+
37
+ export function providerInfo() {
38
+ return { id, name: 'Zhipu GLM' }
39
+ }
40
+
41
+ export function defaults() {
42
+ return {
43
+ codingUrl: GLM_CODING_URL,
44
+ quotaUrl: GLM_QUOTA_URL,
45
+ models: GLM_MODELS.map((m) => m.id),
46
+ }
47
+ }
48
+
49
+ export function authorizeUrl() {
50
+ return 'https://chat.z.ai/api/oauth/authorize'
51
+ }
52
+
53
+ export async function listModels() {
54
+ return GLM_MODELS
55
+ }
56
+
57
+ export async function usage(blob, config, fetchImpl) {
58
+ try {
59
+ const impl = fetchImpl || fetch
60
+ const url = (config && config.quotaUrl) || GLM_QUOTA_URL
61
+ const token = blob && (blob.accessToken || blob.access_token)
62
+ if (!token) return null
63
+ const res = await impl(url, {
64
+ headers: {
65
+ Authorization: `Bearer ${token}`,
66
+ 'User-Agent': GLM_USER_AGENT,
67
+ Accept: 'application/json',
68
+ },
69
+ })
70
+ if (!res.ok) return null
71
+ const json = await readJson(res)
72
+ if (json && json.data && json.data.percentage != null) {
73
+ return asUsageSnapshot(Math.round(json.data.percentage))
74
+ }
75
+ return null
76
+ } catch {
77
+ return null
78
+ }
79
+ }
80
+
81
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
82
+ const impl = fetchImpl || fetch
83
+ const url = (config && config.codingUrl) || GLM_CODING_URL
84
+ const token = blob && (blob.accessToken || blob.access_token)
85
+ if (!token) throw new LlmError('GLM not authenticated', 'AUTH')
86
+
87
+ const body = {
88
+ model: options.model || 'glm-4-plus',
89
+ messages: openaiMessages(options),
90
+ stream: true,
91
+ ...(options.maxTokens != null ? { max_tokens: options.maxTokens } : {}),
92
+ ...(options.temperature != null ? { temperature: options.temperature } : {}),
93
+ }
94
+
95
+ const tools = openaiTools(options)
96
+ if (tools && tools.length) body.tools = tools
97
+
98
+ const res = await impl(url, {
99
+ method: 'POST',
100
+ headers: {
101
+ ...headers,
102
+ 'Content-Type': 'application/json',
103
+ Authorization: `Bearer ${token}`,
104
+ 'User-Agent': GLM_USER_AGENT,
105
+ 'X-Coding-Plan-Boost': '1.5',
106
+ },
107
+ body: JSON.stringify(body),
108
+ signal,
109
+ })
110
+
111
+ if (!res.ok) throw httpError(res.status, await res.text())
112
+ yield* openaiChatStream(res.body)
113
+ }
@@ -197,11 +197,32 @@ export async function check(blob, cfg, fetchImpl) {
197
197
 
198
198
  export async function usage(blob, cfg, fetchImpl) {
199
199
  try {
200
- const res = await (fetchImpl || fetch)(BILLING, {
200
+ const impl = fetchImpl || fetch
201
+ const res = await impl(BILLING, {
201
202
  headers: { ...identityHeaders(blob, cfg), Accept: 'application/json' },
202
203
  })
203
204
  const json = await readJson(res)
204
- const snap = asUsageSnapshot(grokBillingPercent(json))
205
+ let pct = grokBillingPercent(json)
206
+ if (pct == null) {
207
+ // Fallback to gRPC-web GetGrokCreditsConfig
208
+ try {
209
+ const gRes = await impl('https://grok.com/rest/app-chat/get-grok-credits-config', {
210
+ method: 'POST',
211
+ headers: {
212
+ ...identityHeaders(blob, cfg),
213
+ 'Content-Type': 'application/grpc-web+proto',
214
+ 'X-User-Agent': 'grpc-web-javascript/0.1',
215
+ },
216
+ body: Buffer.from([0, 0, 0, 0, 0]),
217
+ })
218
+ if (gRes.ok) {
219
+ const buf = await gRes.arrayBuffer()
220
+ const decoded = decodeGrokCreditsFrame(Buffer.from(buf))
221
+ if (decoded != null) pct = decoded
222
+ }
223
+ } catch {}
224
+ }
225
+ const snap = asUsageSnapshot(pct)
205
226
  if (snap) snap.windows = usageWindows(json)
206
227
  return snap
207
228
  } catch {
@@ -233,4 +254,82 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
233
254
  })
234
255
  if (!res.ok) throw httpError(res.status, await res.text())
235
256
  yield* codexResponsesStream(res.body)
236
- }
257
+ }
258
+
259
+ function readVarint(bytes, offset) {
260
+ let value = 0, shift = 0, index = offset
261
+ while (index < bytes.length) {
262
+ const byte = bytes[index]
263
+ index += 1
264
+ value += (byte & 0x7f) * (2 ** shift)
265
+ if ((byte & 0x80) === 0) return { value, next: index }
266
+ shift += 7
267
+ if (shift > 63) return undefined
268
+ }
269
+ return undefined
270
+ }
271
+ function readLength(bytes, offset, size) {
272
+ if (offset + size > bytes.length) return undefined
273
+ return { bytes: bytes.subarray(offset, offset + size), next: offset + size }
274
+ }
275
+ function decodeFields(bytes) {
276
+ const fields = new Map()
277
+ let offset = 0
278
+ while (offset < bytes.length) {
279
+ const tag = readVarint(bytes, offset)
280
+ if (!tag) break
281
+ const fieldNumber = Math.floor(tag.value / 8)
282
+ const wireType = tag.value % 8
283
+ offset = tag.next
284
+ if (fieldNumber <= 0) return undefined
285
+ let value
286
+ if (wireType === 0) {
287
+ const next = readVarint(bytes, offset)
288
+ if (!next) return undefined
289
+ value = { wireType, value: next.value }
290
+ offset = next.next
291
+ } else if (wireType === 5) {
292
+ const next = readLength(bytes, offset, 4)
293
+ if (!next) return undefined
294
+ value = { wireType, bytes: next.bytes }
295
+ offset = next.next
296
+ } else if (wireType === 1) {
297
+ const next = readLength(bytes, offset, 8)
298
+ if (!next) return undefined
299
+ value = { wireType, bytes: next.bytes }
300
+ offset = next.next
301
+ } else if (wireType === 2) {
302
+ const length = readVarint(bytes, offset)
303
+ if (!length) return undefined
304
+ const next = readLength(bytes, length.next, length.value)
305
+ if (!next) return undefined
306
+ value = { wireType, bytes: next.bytes }
307
+ offset = next.next
308
+ } else return undefined
309
+ if (!fields.has(fieldNumber)) fields.set(fieldNumber, value)
310
+ }
311
+ return fields
312
+ }
313
+ function float32Le(field) {
314
+ if (!field || field.wireType !== 5 || !field.bytes || field.bytes.length < 4) return undefined
315
+ const val = Buffer.from(field.bytes).readFloatLE(0)
316
+ return Number.isFinite(val) ? val : undefined
317
+ }
318
+ export function decodeGrokCreditsFrame(buffer) {
319
+ try {
320
+ const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)
321
+ if (!bytes.length) return undefined
322
+ const payload = bytes.length > 5 && bytes[0] === 0 ? bytes.subarray(5) : bytes
323
+ const top = decodeFields(payload)
324
+ if (!top) return undefined
325
+ const nested = top.get(1)?.wireType === 2 ? decodeFields(top.get(1).bytes) : undefined
326
+ const credits = nested ?? top
327
+ if (!credits) return undefined
328
+ const ratio = float32Le(credits.get(1))
329
+ if (ratio == null) return undefined
330
+ const pct = ratio <= 1 ? Math.round(ratio * 100) : Math.round(ratio)
331
+ return Math.max(0, Math.min(100, pct))
332
+ } catch {
333
+ return undefined
334
+ }
335
+ }
@@ -2,9 +2,11 @@ import * as codex from './codex.js'
2
2
  import * as claude from './claude.js'
3
3
  import * as grok from './grok.js'
4
4
  import * as antigravity from './antigravity.js'
5
+ import * as kimi from './kimi.js'
6
+ import * as glm from './glm.js'
5
7
  import { createVendorFromProfile } from '../vendor-factory.js'
6
8
 
7
- const builtins = { codex, claude, grok, antigravity }
9
+ const builtins = { codex, claude, grok, antigravity, kimi, glm }
8
10
 
9
11
  const customVendors = new Map()
10
12
 
@@ -0,0 +1,120 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm'
2
+ import { openaiMessages, openaiTools } from '../messages.js'
3
+ import { openaiChatStream, readJson, httpError } from '../wire.js'
4
+ import { asUsageSnapshot } from '../usage.js'
5
+
6
+ export const id = 'kimi'
7
+
8
+ export const KIMI_CLIENT_ID = '17e5f671-d194-4dfb-9706-5516cb48c098'
9
+ export const KIMI_DEVICE_URL = 'https://auth.kimi.com/api/oauth/device_authorization'
10
+ export const KIMI_TOKEN_URL = 'https://auth.kimi.com/api/oauth/token'
11
+ export const KIMI_API_BASE = 'https://api.kimi.com/coding/v1'
12
+
13
+ export const KIMI_MODELS = [
14
+ {
15
+ id: 'kimi-for-coding',
16
+ name: 'Kimi for Coding (256k)',
17
+ contextWindow: 262144,
18
+ maxTokens: 32000,
19
+ inputModalities: ['text', 'image'],
20
+ reasoning: { efforts: [{ id: 'off', name: 'Off' }, { id: 'low', name: 'Low' }, { id: 'medium', name: 'Medium' }, { id: 'high', name: 'High' }, { id: 'max', name: 'Max' }] },
21
+ },
22
+ {
23
+ id: 'kimi-for-coding-highspeed',
24
+ name: 'Kimi for Coding High Speed',
25
+ contextWindow: 262144,
26
+ maxTokens: 32000,
27
+ inputModalities: ['text', 'image'],
28
+ },
29
+ {
30
+ id: 'k3',
31
+ name: 'Kimi K3 (Fast)',
32
+ contextWindow: 131072,
33
+ maxTokens: 16384,
34
+ inputModalities: ['text'],
35
+ },
36
+ ]
37
+
38
+ export function providerInfo() {
39
+ return { id, name: 'Moonshot Kimi' }
40
+ }
41
+
42
+ export function defaults() {
43
+ return {
44
+ clientId: KIMI_CLIENT_ID,
45
+ deviceUrl: KIMI_DEVICE_URL,
46
+ tokenUrl: KIMI_TOKEN_URL,
47
+ apiBase: KIMI_API_BASE,
48
+ models: KIMI_MODELS.map((m) => m.id),
49
+ }
50
+ }
51
+
52
+ export function authorizeUrl() {
53
+ return 'https://auth.kimi.com/device'
54
+ }
55
+
56
+ export async function listModels() {
57
+ return KIMI_MODELS
58
+ }
59
+
60
+ export async function usage(blob, config, fetchImpl) {
61
+ try {
62
+ const impl = fetchImpl || fetch
63
+ const base = (config && config.apiBase) || KIMI_API_BASE
64
+ const token = blob && (blob.accessToken || blob.access_token)
65
+ if (!token) return null
66
+ const res = await impl(`${base}/usages`, {
67
+ headers: {
68
+ Authorization: `Bearer ${token}`,
69
+ Accept: 'application/json',
70
+ },
71
+ })
72
+ if (!res.ok) return null
73
+ const json = await readJson(res)
74
+ // Kimi usages returns remaining fraction or total/used
75
+ if (json && json.usage_percent != null) return asUsageSnapshot(json.usage_percent)
76
+ if (json && json.used != null && json.total != null && json.total > 0) {
77
+ return asUsageSnapshot(Math.round((json.used / json.total) * 100))
78
+ }
79
+ return null
80
+ } catch {
81
+ return null
82
+ }
83
+ }
84
+
85
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
86
+ const impl = fetchImpl || fetch
87
+ const base = (config && config.apiBase) || KIMI_API_BASE
88
+ const token = blob && (blob.accessToken || blob.access_token)
89
+ if (!token) throw new LlmError('Kimi not authenticated', 'AUTH')
90
+
91
+ const body = {
92
+ model: options.model || 'kimi-for-coding',
93
+ messages: openaiMessages(options),
94
+ stream: true,
95
+ ...(options.maxTokens != null ? { max_tokens: options.maxTokens } : {}),
96
+ ...(options.temperature != null ? { temperature: options.temperature } : {}),
97
+ }
98
+
99
+ if (options.reasoningEffort && options.reasoningEffort !== 'off') {
100
+ body.thinking = { effort: options.reasoningEffort }
101
+ }
102
+
103
+ const tools = openaiTools(options)
104
+ if (tools && tools.length) body.tools = tools
105
+
106
+ const res = await impl(`${base}/chat/completions`, {
107
+ method: 'POST',
108
+ headers: {
109
+ ...headers,
110
+ 'Content-Type': 'application/json',
111
+ Authorization: `Bearer ${token}`,
112
+ 'User-Agent': 'dsh-plugin-oauth-subs',
113
+ },
114
+ body: JSON.stringify(body),
115
+ signal,
116
+ })
117
+
118
+ if (!res.ok) throw httpError(res.status, await res.text())
119
+ yield* openaiChatStream(res.body)
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.4.23",
3
+ "version": "0.5.1",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",