@goodandready/dsh-subscriptions 0.1.0 → 0.2.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/README.md CHANGED
@@ -59,7 +59,7 @@ Settings GET never returns access or refresh tokens — only
59
59
  | `codex` | ChatGPT / Codex | Vendor-public Codex CLI client id |
60
60
  | `claude` | Claude Pro/Max | Vendor-public Claude Code client id |
61
61
  | `grok` | xAI / SuperGrok | Vendor-public Grok CLI client id |
62
- | `antigravity` | Google Antigravity | Vendor-public Antigravity client id |
62
+ | `antigravity` | Google Antigravity | Your OAuth client id + secret in Config |
63
63
 
64
64
  Override `codexClientId`, `claudeClientId`, and the other empty Config fields
65
65
  if you register your own OAuth app.
@@ -67,16 +67,16 @@ if you register your own OAuth app.
67
67
  Live requests use the vendor subscription surfaces, not API-key hosts:
68
68
  Codex `chatgpt.com/backend-api/codex/responses`, Claude Messages with the
69
69
  OAuth beta header, Grok `cli-chat-proxy.grok.com` with CLI identity headers,
70
- and Gemini/Antigravity Cloud Code Assist (`loadCodeAssist` then
70
+ and Antigravity Cloud Code Assist (`loadCodeAssist` then
71
71
  `streamGenerateContent`). Usage endpoints, when they answer, feed the 100%
72
72
  skip. If a live model list fails, the built-in catalog is used.
73
- Installed-app Google client secrets are
74
- vendor-published (not user secrets); override `geminiClientSecret` /
75
- `antigravityClientSecret` when you use your own client.
73
+ Antigravity uses a confidential Google OAuth client: set `antigravityClientId`
74
+ and `antigravityClientSecret` in plugin Config (Settings) nothing is baked
75
+ into the repository.
76
76
 
77
77
  Default model catalogs are built-in lists you can replace with
78
- `codexModels`, `claudeModels`, `grokModels`, `geminiModels`,
79
- `antigravityModels` in the plugin Config.
78
+ `codexModels`, `claudeModels`, `grokModels`, `antigravityModels`
79
+ in the plugin Config.
80
80
 
81
81
  ## Rotation
82
82
 
package/lib/images.js ADDED
@@ -0,0 +1,111 @@
1
+ // Генерация картинок на подписке.
2
+ //
3
+ // Здесь только протокол: куда идти, что послать и как прочитать ответ. Всё
4
+ // остальное — сохранение файла, вложение в разговор, карточка — дело плагина
5
+ // генерации; этот плагин лишь одалживает свой аккаунт.
6
+ //
7
+ // Токен наружу не отдаётся: плагин объявляет службу внутри процесса, а не
8
+ // маршрут в сети. Харнесс на этой машине доступен без пароля, и ручка,
9
+ // раздающая живой токен подписки, была бы дырой пошире тех, что мы закрывали.
10
+
11
+ /** Куда уходит запрос у подписки ChatGPT. */
12
+ export const CODEX_URL = 'https://chatgpt.com/backend-api/codex/images/generations'
13
+ /** Модель, которую отдаёт этот адрес. */
14
+ export const CODEX_MODEL = 'gpt-image-2'
15
+ /** Куда уходит запрос у подписки Grok. */
16
+ export const GROK_URL = 'https://api.x.ai/v1/images/generations'
17
+ /** Модель, которую отдаёт этот адрес. */
18
+ export const GROK_MODEL = 'grok-imagine-image-2.0'
19
+
20
+ /** Размеры, которые понимает ChatGPT. */
21
+ export const SIZES = ['1024x1024', '1024x1536', '1536x1024', 'auto']
22
+
23
+ /** Grok мыслит не размерами, а соотношением сторон. */
24
+ const GROK_ASPECT = {
25
+ '1024x1024': '1:1',
26
+ '1024x1536': '2:3',
27
+ '1536x1024': '3:2',
28
+ auto: 'auto',
29
+ }
30
+
31
+ export function codexBody({ prompt, size, quality }) {
32
+ const text = String(prompt || '').trim()
33
+ if (!text) throw new Error('нужен непустой запрос')
34
+ return {
35
+ prompt: text,
36
+ model: CODEX_MODEL,
37
+ ...(size ? { size } : {}),
38
+ ...(quality ? { quality } : {}),
39
+ }
40
+ }
41
+
42
+ export function grokBody({ prompt, size, quality }) {
43
+ const text = String(prompt || '').trim()
44
+ if (!text) throw new Error('нужен непустой запрос')
45
+ // У Grok качество всего двух ступеней: высокое складывается со средним.
46
+ const level = quality === 'low' ? 'low'
47
+ : (quality === 'medium' || quality === 'high') ? 'medium'
48
+ : undefined
49
+ return {
50
+ prompt: text,
51
+ model: GROK_MODEL,
52
+ response_format: 'b64_json',
53
+ ...(size && GROK_ASPECT[size] ? { aspect_ratio: GROK_ASPECT[size] } : {}),
54
+ ...(level ? { quality: level } : {}),
55
+ }
56
+ }
57
+
58
+ /** Разбор ответа: обе стороны отвечают одинаково. */
59
+ export function parseImages(payload) {
60
+ const body = payload && typeof payload === 'object' ? payload : {}
61
+ const rows = Array.isArray(body.data) ? body.data : []
62
+ const images = []
63
+ for (const row of rows) {
64
+ if (!row || typeof row !== 'object') continue
65
+ if (typeof row.b64_json !== 'string' || !row.b64_json) continue
66
+ images.push({
67
+ b64_json: row.b64_json,
68
+ ...(typeof row.revised_prompt === 'string' && row.revised_prompt
69
+ ? { revisedPrompt: row.revised_prompt }
70
+ : {}),
71
+ })
72
+ }
73
+ if (!images.length) throw new Error('в ответе нет картинок')
74
+ return images
75
+ }
76
+
77
+ /**
78
+ * Один запрос к нужному адресу с заголовками этого провайдера.
79
+ *
80
+ * @param options {{provider, prompt, size, quality, session, fetchImpl, signal}}
81
+ * session — уже освежённый блок токенов: accessToken и, для ChatGPT, accountId.
82
+ */
83
+ export async function generateOnce(options) {
84
+ const { provider, session, fetchImpl, signal } = options
85
+ const isCodex = provider === 'codex'
86
+ const url = isCodex ? CODEX_URL : GROK_URL
87
+ const body = isCodex ? codexBody(options) : grokBody(options)
88
+ const headers = isCodex
89
+ ? {
90
+ authorization: `Bearer ${session.accessToken}`,
91
+ // ChatGPT различает аккаунты отдельным заголовком, без него отвечает отказом.
92
+ 'chatgpt-account-id': session.accountId || '',
93
+ originator: 'codex_cli_rs',
94
+ 'content-type': 'application/json',
95
+ accept: 'application/json',
96
+ }
97
+ : {
98
+ authorization: `Bearer ${session.accessToken}`,
99
+ 'content-type': 'application/json',
100
+ accept: 'application/json',
101
+ }
102
+
103
+ const res = await fetchImpl(url, { method: 'POST', headers, body: JSON.stringify(body), signal })
104
+ const payload = await res.json().catch(() => ({}))
105
+ if (!res.ok) {
106
+ const detail = payload && payload.error
107
+ && (payload.error.message || payload.error.code || payload.error)
108
+ throw new Error(`${provider} HTTP ${res.status}${detail ? ': ' + String(detail).slice(0, 200) : ''}`)
109
+ }
110
+ return parseImages(payload)
111
+ }
package/lib/index.js CHANGED
@@ -5,6 +5,8 @@ import { parseCallbackInput, requestOrigin, webCallbackUri } from './oauth.js'
5
5
  import { writeJson, writeHtml, readBody, isTrustedSettingsRequest, queryOf } from './http.js'
6
6
  import { getVendor } from './vendors/index.js'
7
7
  import { SubscriptionAdapter } from './adapter.js'
8
+ import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
9
+ import { parseBlob } from './blob.js'
8
10
  import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
9
11
  import {
10
12
  inspectGoogleAccount,
@@ -77,6 +79,46 @@ export function apply(ctx, config) {
77
79
 
78
80
  const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
79
81
  const store = createAccountStore({ credentials: ctx.credentials, getConfig: live, fetchImpl: fetch })
82
+
83
+ // Служба генерации картинок на подписке.
84
+ //
85
+ // Наружу отдаётся действие, а не токен: маршрут в сети раздавал бы живой
86
+ // ключ доступа каждому, кто дотянется до харнесса, а служба живёт внутри
87
+ // процесса и видна только другим плагинам. Обновлением токена по-прежнему
88
+ // занимается один хозяин — этот плагин.
89
+ ctx.effect(() => ctx.provide('subscriptionImages', {
90
+ /** Провайдеры, у которых есть вход прямо сейчас. */
91
+ async available() {
92
+ const logged = await store.loggedInProviders()
93
+ return ['codex', 'grok'].filter((name) => logged && logged[name])
94
+ },
95
+ sizes: IMAGE_SIZES,
96
+ /**
97
+ * @param request {{provider, prompt, size, quality, signal}}
98
+ * @returns [{ b64_json, revisedPrompt? }]
99
+ */
100
+ async generate(request) {
101
+ const provider = request && request.provider
102
+ if (provider !== 'codex' && provider !== 'grok') {
103
+ throw new Error(`неизвестный провайдер подписки: ${provider}`)
104
+ }
105
+ const accounts = await store.listAccounts(provider)
106
+ const slot = (accounts || []).find((row) => row && row.ref)
107
+ if (!slot) throw new Error(`нет входа в ${provider}: войдите в разделе «Подписки»`)
108
+ const raw = await store.resolveRaw(slot.ref)
109
+ if (!raw) throw new Error(`нет входа в ${provider}: войдите в разделе «Подписки»`)
110
+ const session = await store.ensureFresh(provider, parseBlob(raw), slot.ref)
111
+ return generateOnce({
112
+ provider,
113
+ prompt: request.prompt,
114
+ size: request.size,
115
+ quality: request.quality,
116
+ session,
117
+ fetchImpl: fetch,
118
+ signal: request.signal,
119
+ })
120
+ },
121
+ }), 'dsh-subscriptions: служба генерации картинок')
80
122
  const pending = new Map()
81
123
  const adapter = new SubscriptionAdapter({
82
124
  listAccounts: (provider) => store.listAccounts(provider),
@@ -3,7 +3,6 @@ import { buildAuthorizeUrl } from '../oauth.js'
3
3
  import { googleContents } from '../messages.js'
4
4
  import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth } from '../wire.js'
5
5
  import { emailFromToken } from '../jwt.js'
6
- import { builtinAntigravityOAuth } from './antigravity-oauth.js'
7
6
  import {
8
7
  CODE_ASSIST_STREAM,
9
8
  antigravityMetadata,
@@ -32,11 +31,10 @@ export function providerInfo() {
32
31
  }
33
32
 
34
33
  export function defaults() {
35
- const builtin = builtinAntigravityOAuth()
36
34
  return {
37
- clientId: builtin.clientId,
38
- clientSecret: builtin.clientSecret,
39
- redirectUri: builtin.redirectUri,
35
+ clientId: '',
36
+ clientSecret: '',
37
+ redirectUri: 'https://antigravity.google/oauth-callback',
40
38
  models: ['gemini-3.5-flash-low', 'gemini-3-flash', 'gemini-2.5-flash'],
41
39
  }
42
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -1,10 +0,0 @@
1
- export function builtinAntigravityOAuth() {
2
- // Public Antigravity IDE OAuth app (same credentials as desktop IDE).
3
- const clientId = String.fromCharCode(49,48,55,49,48,48,54,48,54,48,53,57,49,45,116,109,104,115,115,105,110,50,104,50,49,108,99,114,101,50,51,53,118,116,111,108,111,106,104,52,103,52,48,51,101,112,46,97,112,112,115,46,103,111,111,103,108,101,117,115,101,114,99,111,110,116,101,110,116,46,99,111,109)
4
- const clientSecret = String.fromCharCode(71,79,67,83,80,88,45,75,53,56,70,87,82,52,56,54,76,100,76,74,49,109,76,66,56,115,88,67,52,122,54,113,68,65,102)
5
- return {
6
- clientId,
7
- clientSecret,
8
- redirectUri: 'https://antigravity.google/oauth-callback',
9
- }
10
- }