@goodandready/dsh-subscriptions 0.1.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.
@@ -0,0 +1,223 @@
1
+ import { buildAuthorizeUrl } from '../oauth.js'
2
+ import { modelCatalog, codexResponsesBody } from '../messages.js'
3
+ import { formTokenRequest, codexResponsesStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
4
+ import { emailFromToken } from '../jwt.js'
5
+ import { asUsageSnapshot, grokBillingPercent } from '../usage.js'
6
+
7
+ export const id = 'grok'
8
+
9
+ const AUTH = 'https://auth.x.ai/oauth2/authorize'
10
+ const TOKEN = 'https://auth.x.ai/oauth2/token'
11
+ const SCOPE = 'openid profile email offline_access grok-cli:access api:access conversations:read conversations:write'
12
+ const BILLING = 'https://cli-chat-proxy.grok.com/v1/billing?format=credits'
13
+ const MODELS = 'https://api.x.ai/v1/models'
14
+ const CLI_MODELS = 'https://cli-chat-proxy.grok.com/v1/models'
15
+ const CATALOG_TTL_MS = 5 * 60 * 1000
16
+ const catalogCache = new Map()
17
+
18
+ async function cliCatalogCached(blob, cfg, fetchImpl) {
19
+ const key = String(blob.accessToken || '').slice(-24)
20
+ const hit = catalogCache.get(key)
21
+ if (hit && Date.now() - hit.at < CATALOG_TTL_MS) return hit.map
22
+ const map = await cliCatalog(blob, cfg, fetchImpl)
23
+ if (map.size > 0) catalogCache.set(key, { at: Date.now(), map })
24
+ return map
25
+ }
26
+
27
+ function grokReasoningBody(modelId, effort, catalogMap) {
28
+ const entry = catalogMap && catalogMap.get(modelId)
29
+ const reasoning = entry && entry.reasoning
30
+ if (!reasoning || !reasoning.efforts || !reasoning.efforts.length) return undefined
31
+ const allowed = new Set(reasoning.efforts.map((row) => row.id))
32
+ const pick = effort != null && effort !== '' ? String(effort) : ''
33
+ if (pick && allowed.has(pick)) return { effort: pick }
34
+ return undefined
35
+ }
36
+
37
+ function grokBodyForModel(modelId, body) {
38
+ if (!/multi-agent/i.test(String(modelId || ''))) return body
39
+ const next = { ...body }
40
+ delete next.tools
41
+ return next
42
+ }
43
+
44
+
45
+ export function providerInfo() {
46
+ return { id, name: 'Grok' }
47
+ }
48
+
49
+ export function defaults() {
50
+ return {
51
+ clientId: 'b1a00492-073a-47ea-816f-4c329264a828',
52
+ redirectUri: 'http://127.0.0.1:56121/callback',
53
+ baseUrl: 'https://api.x.ai/v1',
54
+ clientVersion: '0.2.103',
55
+ models: [
56
+ { id: 'grok-4', name: 'Grok 4' },
57
+ { id: 'grok-4-fast-reasoning', name: 'Grok 4 Fast Reasoning' },
58
+ { id: 'grok-code-fast-1', name: 'Grok Code Fast 1' },
59
+ ],
60
+ }
61
+ }
62
+
63
+ function isChatModel(modelId) {
64
+ return !/imagine|image-|video|embed/i.test(String(modelId || ''))
65
+ }
66
+
67
+ function grokModalities(modelId) {
68
+ return /code|embed/i.test(String(modelId || '')) ? ['text'] : ['text', 'image']
69
+ }
70
+
71
+ function identityHeaders(blob, config) {
72
+ return {
73
+ Authorization: `Bearer ${blob.accessToken}`,
74
+ 'X-XAI-Token-Auth': 'xai-grok-cli',
75
+ 'x-grok-client-identifier': 'grok-shell',
76
+ 'x-grok-client-version': config.clientVersion || defaults().clientVersion,
77
+ 'User-Agent': 'xai-grok-cli',
78
+ }
79
+ }
80
+
81
+ function streamHeaders(blob, config, extra) {
82
+ const base = String(config.baseUrl || defaults().baseUrl)
83
+ const headers = {
84
+ Authorization: `Bearer ${blob.accessToken}`,
85
+ ...(extra || {}),
86
+ }
87
+ if (/grok\.com/i.test(base)) Object.assign(headers, identityHeaders(blob, config))
88
+ return headers
89
+ }
90
+
91
+ export function authorizeUrl(cfg, pkce) {
92
+ return buildAuthorizeUrl({
93
+ authUrl: AUTH,
94
+ clientId: cfg.clientId,
95
+ redirectUri: cfg.redirectUri,
96
+ challenge: pkce.challenge,
97
+ state: pkce.state,
98
+ scope: SCOPE,
99
+ })
100
+ }
101
+
102
+ export async function exchangeCode(cfg, pkce, code, fetchImpl) {
103
+ const json = await formTokenRequest(TOKEN, {
104
+ grant_type: 'authorization_code',
105
+ client_id: cfg.clientId,
106
+ code,
107
+ redirect_uri: cfg.redirectUri,
108
+ code_verifier: pkce.verifier,
109
+ }, fetchImpl)
110
+ const blob = tokenBlobFromOAuth(json)
111
+ return { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || 'Grok' }
112
+ }
113
+
114
+ export async function refresh(cfg, blob, fetchImpl) {
115
+ const json = await formTokenRequest(TOKEN, {
116
+ grant_type: 'refresh_token',
117
+ client_id: cfg.clientId,
118
+ refresh_token: blob.refreshToken,
119
+ }, fetchImpl)
120
+ return tokenBlobFromOAuth(json, { label: blob.label, email: blob.email })
121
+ }
122
+
123
+ async function cliCatalog(blob, cfg, fetchImpl) {
124
+ const res = await fetchImpl(CLI_MODELS, {
125
+ headers: { ...identityHeaders(blob, cfg), Accept: 'application/json' },
126
+ })
127
+ const json = await readJson(res)
128
+ const map = new Map()
129
+ for (const entry of json.data || []) {
130
+ if (!entry || !entry.id) continue
131
+ const efforts = (entry.reasoning_efforts || [])
132
+ .map((level) => (typeof level === 'string' ? { id: level, name: level } : (level && level.value && {
133
+ id: level.value,
134
+ name: level.label || level.value,
135
+ })))
136
+ .filter(Boolean)
137
+ const reasoning = entry.supports_reasoning_effort === true && efforts.length
138
+ ? {
139
+ efforts,
140
+ ...(entry.reasoning_effort && efforts.some((row) => row.id === entry.reasoning_effort)
141
+ ? { defaultEffort: entry.reasoning_effort }
142
+ : {}),
143
+ }
144
+ : undefined
145
+ map.set(entry.id, {
146
+ name: entry.name || entry.id,
147
+ ...(entry.description ? { description: entry.description } : {}),
148
+ ...(entry.context_window ? { contextWindow: entry.context_window } : {}),
149
+ ...(reasoning ? { reasoning } : {}),
150
+ })
151
+ }
152
+ return map
153
+ }
154
+
155
+ export async function listModels(blob, cfg, fetchImpl) {
156
+ const fallback = modelCatalog(id, (cfg.models || defaults().models).map((row) => (
157
+ typeof row === 'string' ? row : { ...row, inputModalities: grokModalities(row.id) }
158
+ )))
159
+ const impl = fetchImpl || fetch
160
+ try {
161
+ const [res, extra] = await Promise.all([
162
+ impl(MODELS, { headers: { Authorization: `Bearer ${blob.accessToken}`, Accept: 'application/json' } }),
163
+ cliCatalog(blob, cfg, impl).catch(() => new Map()),
164
+ ])
165
+ const json = await readJson(res)
166
+ const rows = []
167
+ for (const entry of json.data || []) {
168
+ const modelId = entry && entry.id
169
+ if (!modelId || !isChatModel(modelId)) continue
170
+ const enrich = extra.get(modelId) || {}
171
+ const note = /multi-agent/i.test(modelId)
172
+ ? 'Chat only in Harness until xAI grants multi-agent tool beta.'
173
+ : ''
174
+ rows.push({
175
+ id: modelId,
176
+ name: enrich.name || modelId,
177
+ inputModalities: grokModalities(modelId),
178
+ ...enrich,
179
+ ...(note ? { description: enrich.description ? `${enrich.description} ${note}` : note } : {}),
180
+ })
181
+ }
182
+ if (!rows.length) throw new Error('empty grok catalog')
183
+ return modelCatalog(id, rows)
184
+ } catch {
185
+ return fallback
186
+ }
187
+ }
188
+
189
+ export async function usage(blob, cfg, fetchImpl) {
190
+ try {
191
+ const res = await (fetchImpl || fetch)(BILLING, {
192
+ headers: { ...identityHeaders(blob, cfg), Accept: 'application/json' },
193
+ })
194
+ const json = await readJson(res)
195
+ return asUsageSnapshot(grokBillingPercent(json))
196
+ } catch {
197
+ return null
198
+ }
199
+ }
200
+
201
+ export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
202
+ const base = (config.baseUrl || defaults().baseUrl).replace(/\/$/, '')
203
+ let body = codexResponsesBody(options, '')
204
+ if (!body.instructions) delete body.instructions
205
+ const catalog = await cliCatalogCached(blob, config, fetchImpl).catch(() => new Map())
206
+ const reasoning = grokReasoningBody(options.model, options.reasoningEffort, catalog)
207
+ if (reasoning) body.reasoning = reasoning
208
+ body = grokBodyForModel(options.model, body)
209
+ const res = await fetchImpl(`${base}/responses`, {
210
+ method: 'POST',
211
+ headers: {
212
+ ...headers,
213
+ ...streamHeaders(blob, config, {
214
+ 'Content-Type': 'application/json',
215
+ Accept: 'text/event-stream',
216
+ }),
217
+ },
218
+ body: JSON.stringify(body),
219
+ signal,
220
+ })
221
+ if (!res.ok) throw httpError(res.status, await res.text())
222
+ yield* codexResponsesStream(res.body)
223
+ }
@@ -0,0 +1,17 @@
1
+ import * as codex from './codex.js'
2
+ import * as claude from './claude.js'
3
+ import * as grok from './grok.js'
4
+ import * as antigravity from './antigravity.js'
5
+
6
+ export const vendors = {
7
+ codex,
8
+ claude,
9
+ grok,
10
+ antigravity,
11
+ }
12
+
13
+ export function getVendor(provider) {
14
+ const vendor = vendors[provider]
15
+ if (!vendor) throw new Error(`unknown provider: ${provider}`)
16
+ return vendor
17
+ }
package/lib/wire.js ADDED
@@ -0,0 +1,200 @@
1
+ import { iterateSse, jsonSse } from './sse.js'
2
+ import { validationFromHttpError, validationRequiredError, googleRateLimitMessage } from './google-validation.js'
3
+
4
+ export function httpError(status, bodyText, code) {
5
+ const snippet = String(bodyText || '').slice(0, 400)
6
+ const err = new Error(snippet ? `vendor http ${status}: ${snippet}` : `vendor http ${status}`)
7
+ err.status = status
8
+ if (code) {
9
+ err.code = code
10
+ return err
11
+ }
12
+ if (status === 429) err.code = 'RATE_LIMIT'
13
+ else if (status === 402) err.code = 'QUOTA'
14
+ else if (status === 401 || status === 403) err.code = 'VENDOR'
15
+ else if (/quota|rate.?limit|usage.?limit|billing/i.test(snippet)) {
16
+ err.code = status === 400 ? 'QUOTA' : 'RATE_LIMIT'
17
+ } else err.code = 'VENDOR'
18
+ return err
19
+ }
20
+
21
+ export function throwHttpError(status, bodyText) {
22
+ const validation = validationFromHttpError(status, bodyText)
23
+ if (validation) throw validationRequiredError(validation)
24
+ const hint = googleRateLimitMessage(status, bodyText)
25
+ if (hint) {
26
+ const err = httpError(status, bodyText, status === 429 ? 'RATE_LIMIT' : undefined)
27
+ err.message = `${hint} (${err.message})`
28
+ throw err
29
+ }
30
+ throw httpError(status, bodyText)
31
+ }
32
+
33
+ export async function readJson(res) {
34
+ const text = await res.text()
35
+ if (!res.ok) {
36
+ const validation = validationFromHttpError(res.status, text)
37
+ if (validation) throw validationRequiredError(validation)
38
+ throw httpError(res.status, text)
39
+ }
40
+ try { return JSON.parse(text) } catch { return {} }
41
+ }
42
+
43
+ export function tokenBlobFromOAuth(json, extra) {
44
+ const expiresIn = Number(json.expires_in) || 3600
45
+ return {
46
+ accessToken: json.access_token || json.accessToken || '',
47
+ refreshToken: json.refresh_token || json.refreshToken || '',
48
+ expiresAt: Date.now() + expiresIn * 1000,
49
+ idToken: json.id_token || json.idToken || '',
50
+ ...(extra || {}),
51
+ }
52
+ }
53
+
54
+ function openBlock(kind, index) {
55
+ return { kind, index, text: '', id: '', name: '' }
56
+ }
57
+
58
+ export async function* openaiChatStream(body) {
59
+ let next = 0
60
+ let textBlock = null
61
+ const tools = new Map()
62
+ let finish = { kind: 'stop' }
63
+ for await (const data of iterateSse(body)) {
64
+ const json = jsonSse(data)
65
+ if (!json) continue
66
+ const choice = (json.choices && json.choices[0]) || {}
67
+ const delta = choice.delta || json.delta || {}
68
+ if (typeof delta.content === 'string' && delta.content) {
69
+ if (!textBlock) {
70
+ textBlock = openBlock('text', next++)
71
+ yield { type: 'block-start', index: textBlock.index, blockType: 'text' }
72
+ }
73
+ yield { type: 'text-delta', index: textBlock.index, text: delta.content }
74
+ }
75
+ if (typeof delta.reasoning_content === 'string' && delta.reasoning_content) {
76
+ yield { type: 'reasoning-delta', index: 0, text: delta.reasoning_content }
77
+ }
78
+ for (const call of delta.tool_calls || []) {
79
+ let block = tools.get(call.index)
80
+ if (!block) {
81
+ block = openBlock('tool-call', next++)
82
+ tools.set(call.index, block)
83
+ yield { type: 'block-start', index: block.index, blockType: 'tool-call' }
84
+ }
85
+ if (call.id) block.id = call.id
86
+ if (call.function && call.function.name) block.name = call.function.name
87
+ const fragment = (call.function && call.function.arguments) || ''
88
+ yield {
89
+ type: 'tool-call-delta',
90
+ index: block.index,
91
+ id: block.id,
92
+ ...(block.name ? { name: block.name } : {}),
93
+ argumentsDelta: fragment,
94
+ }
95
+ }
96
+ if (json.usage) yield { type: 'usage', usage: json.usage }
97
+ if (choice.finish_reason === 'length') finish = { kind: 'length' }
98
+ else if (choice.finish_reason === 'tool_calls') finish = { kind: 'tool' }
99
+ }
100
+ yield { type: 'finish', reason: finish }
101
+ }
102
+
103
+ export { streamResponses as codexResponsesStream } from './responses-stream.js'
104
+
105
+ export async function* anthropicStream(body) {
106
+ let index = 0
107
+ let textIndex = 0
108
+ for await (const data of iterateSse(body)) {
109
+ const json = jsonSse(data)
110
+ if (!json) continue
111
+ if (json.type === 'content_block_start') {
112
+ const block = json.content_block || {}
113
+ if (block.type === 'text') {
114
+ textIndex = json.index || index++
115
+ yield { type: 'block-start', index: textIndex, blockType: 'text' }
116
+ } else if (block.type === 'tool_use') {
117
+ yield { type: 'block-start', index: json.index || index++, blockType: 'tool-call' }
118
+ yield {
119
+ type: 'tool-call-delta',
120
+ index: json.index || 0,
121
+ id: block.id,
122
+ name: block.name,
123
+ argumentsDelta: '',
124
+ }
125
+ }
126
+ } else if (json.type === 'content_block_delta') {
127
+ const delta = json.delta || {}
128
+ if (delta.type === 'text_delta' && delta.text) {
129
+ yield { type: 'text-delta', index: json.index || textIndex, text: delta.text }
130
+ } else if (delta.type === 'input_json_delta' && delta.partial_json) {
131
+ yield {
132
+ type: 'tool-call-delta',
133
+ index: json.index || 0,
134
+ argumentsDelta: delta.partial_json,
135
+ }
136
+ }
137
+ } else if (json.type === 'message_delta' && json.usage) {
138
+ yield { type: 'usage', usage: json.usage }
139
+ }
140
+ }
141
+ yield { type: 'finish', reason: { kind: 'stop' } }
142
+ }
143
+
144
+ export async function* googleStream(body) {
145
+ let index = 0
146
+ let started = false
147
+ for await (const data of iterateSse(body)) {
148
+ const json = jsonSse(data)
149
+ const wrapped = json && json.response ? json.response : json
150
+ if (!wrapped) continue
151
+ const cand = wrapped.candidates && wrapped.candidates[0]
152
+ const parts = cand && cand.content && Array.isArray(cand.content.parts) ? cand.content.parts : []
153
+ for (const part of parts) {
154
+ if (part.text) {
155
+ if (!started) {
156
+ yield { type: 'block-start', index, blockType: 'text' }
157
+ started = true
158
+ }
159
+ yield { type: 'text-delta', index, text: part.text }
160
+ }
161
+ if (part.functionCall) {
162
+ yield { type: 'block-start', index: index + 1, blockType: 'tool-call' }
163
+ yield {
164
+ type: 'tool-call-delta',
165
+ index: index + 1,
166
+ name: part.functionCall.name,
167
+ argumentsDelta: JSON.stringify(part.functionCall.args || {}),
168
+ }
169
+ }
170
+ }
171
+ if (wrapped.usageMetadata) {
172
+ yield {
173
+ type: 'usage',
174
+ usage: {
175
+ input: wrapped.usageMetadata.promptTokenCount,
176
+ output: wrapped.usageMetadata.candidatesTokenCount,
177
+ },
178
+ }
179
+ }
180
+ }
181
+ yield { type: 'finish', reason: { kind: 'stop' } }
182
+ }
183
+
184
+ export async function formTokenRequest(url, params, fetchImpl, headers) {
185
+ const res = await fetchImpl(url, {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', ...(headers || {}) },
188
+ body: new URLSearchParams(params),
189
+ })
190
+ return readJson(res)
191
+ }
192
+
193
+ export async function jsonTokenRequest(url, body, fetchImpl, headers) {
194
+ const res = await fetchImpl(url, {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(headers || {}) },
197
+ body: JSON.stringify(body),
198
+ })
199
+ return readJson(res)
200
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@goodandready/dsh-subscriptions",
3
+ "version": "0.1.0",
4
+ "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./lib/index.js",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json",
12
+ "./cordis.patch.yml": "./cordis.patch.yml"
13
+ },
14
+ "files": [
15
+ "lib/",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "dsh",
22
+ "dsh-plugin",
23
+ "deepseek-harness",
24
+ "oauth",
25
+ "chatgpt",
26
+ "claude",
27
+ "grok"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/GooDAnDReaDY/dsh-subscriptions.git"
32
+ },
33
+ "homepage": "https://github.com/GooDAnDReaDY/dsh-subscriptions",
34
+ "bugs": {
35
+ "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
+ },
37
+ "scripts": {
38
+ "test": "node --test test/*.test.mjs"
39
+ },
40
+ "dsh": {
41
+ "bundle": {
42
+ "patch": "./cordis.patch.yml"
43
+ },
44
+ "client": {
45
+ "platform": "web",
46
+ "inject": [
47
+ "@deepseek-ai/dsh-client-runtime",
48
+ "@deepseek-ai/dsh-client-ui-slots"
49
+ ]
50
+ }
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": "^4.0.1",
54
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
56
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
57
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
58
+ "@deepseek-ai/schemastery": "^3.18.1"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }