@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 +49 -4
- package/lib/import-auth.js +163 -0
- package/lib/index.js +48 -0
- package/lib/refs.js +4 -0
- package/lib/relative-time.js +47 -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': 'Основное окно',
|
|
@@ -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
|
-
|
|
329
|
-
|
|
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(
|
|
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
|
+
}
|
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