@goodandready/dsh-subscriptions 0.4.23 → 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/client.js +26 -0
- package/lib/import-auth.js +163 -0
- package/lib/index.js +48 -0
- package/lib/refs.js +4 -0
- package/lib/vendors/codex.js +37 -7
- package/lib/vendors/glm.js +113 -0
- package/lib/vendors/grok.js +102 -3
- package/lib/vendors/index.js +3 -1
- package/lib/vendors/kimi.js +120 -0
- package/package.json +1 -1
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/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) {
|
package/lib/vendors/codex.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/vendors/grok.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/vendors/index.js
CHANGED
|
@@ -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