@goodandready/dsh-subscriptions 0.4.22 → 0.5.0

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/adapter.js CHANGED
@@ -60,6 +60,10 @@ export class SubscriptionAdapter extends LlmAdapter {
60
60
  return models
61
61
  }
62
62
 
63
+ imageRequestPricing(_provider, _model) {
64
+ return undefined
65
+ }
66
+
63
67
  async resolveModel(provider, model, _signal) {
64
68
  const rows = await this.listModels(provider)
65
69
  const found = rows.find((row) => row && row.id === model)
@@ -77,6 +81,13 @@ export class SubscriptionAdapter extends LlmAdapter {
77
81
  return { provider, id: model, name: model }
78
82
  }
79
83
 
84
+ async prepareCall(provider, model, signal) {
85
+ return {
86
+ model: await this.resolveModel(provider, model, signal),
87
+ stream: (options) => this.stream(options),
88
+ }
89
+ }
90
+
80
91
  async *stream(options) {
81
92
  const provider = options.provider
82
93
  const deps = this.deps
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': 'Основное окно',
@@ -607,6 +613,19 @@ const cssId = 'dsh-subscriptions/settings.module.css'
607
613
  setAccounts(data.accounts || [])
608
614
  }
609
615
 
616
+ const importLocalCli = async (provider, index) => {
617
+ setErr('')
618
+ const res = await fetch('/dsh-subscriptions/import-local', {
619
+ method: 'POST',
620
+ headers: { 'Content-Type': 'application/json' },
621
+ body: JSON.stringify({ provider, index }),
622
+ })
623
+ const data = await res.json().catch(() => ({}))
624
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
625
+ await load()
626
+ setInfo(t('importLocalSuccess'))
627
+ }
628
+
610
629
  const importToken = async (provider, index) => {
611
630
  setErr('')
612
631
  const tok = (tokenDraft[provider + ':' + index] || '').trim()
@@ -752,6 +771,11 @@ const cssId = 'dsh-subscriptions/settings.module.css'
752
771
  type: 'button', className: 'dsub-mini',
753
772
  onClick: () => importToken(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
754
773
  }, t('importToken')),
774
+ React.createElement('button', {
775
+ type: 'button', className: 'dsub-mini',
776
+ title: t('importLocalCli'),
777
+ onClick: () => importLocalCli(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
778
+ }, '📥 CLI'),
755
779
  ),
756
780
  React.createElement('div', { className: 'dsub-row' },
757
781
  React.createElement('input', {
@@ -1120,6 +1144,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1120
1144
  if (p.includes('claude') || p.includes('anthropic')) return { label: 'Claude', cls: 'dsub-brandClaude', icon: '✳' }
1121
1145
  if (p.includes('grok') || p.includes('xai')) return { label: 'Grok', cls: 'dsub-brandGrok', icon: '✦' }
1122
1146
  if (p.includes('antigravity') || p.includes('google') || p.includes('gemini')) return { label: 'AGY', cls: 'dsub-brandAgy', icon: '◆' }
1147
+ if (p.includes('kimi')) return { label: 'Kimi', cls: 'dsub-brandKimi', icon: '🌙' }
1148
+ if (p.includes('glm') || p.includes('zcode')) return { label: 'GLM', cls: 'dsub-brandGlm', icon: '⚡' }
1123
1149
  if (p.includes('ollama')) return { label: 'Ollama', cls: 'dsub-brandOllama', icon: '🦙' }
1124
1150
  return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
1125
1151
  }
@@ -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/ollama.js CHANGED
@@ -45,10 +45,21 @@ export class OllamaAdapter extends LlmAdapter {
45
45
  return ollamaModels(this.deps.baseUrl(), this.deps.fetchImpl || fetch)
46
46
  }
47
47
 
48
+ imageRequestPricing(_provider, _model) {
49
+ return undefined
50
+ }
51
+
48
52
  async resolveModel(provider, model, _signal) {
49
53
  return { provider, id: model, name: model }
50
54
  }
51
55
 
56
+ async prepareCall(provider, model, signal) {
57
+ return {
58
+ model: await this.resolveModel(provider, model, signal),
59
+ stream: (options) => this.stream(options),
60
+ }
61
+ }
62
+
52
63
  async *stream(options) {
53
64
  const base = this.deps.baseUrl()
54
65
  const model = options.model || this.deps.fallbackModel() || ''
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) {
@@ -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.22",
3
+ "version": "0.5.0",
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",