@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.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/cordis.patch.yml +8 -0
- package/lib/accounts.js +198 -0
- package/lib/adapter.js +110 -0
- package/lib/blob.js +43 -0
- package/lib/client.js +248 -0
- package/lib/code-assist.js +250 -0
- package/lib/gemini-schema.js +67 -0
- package/lib/google-validation.js +117 -0
- package/lib/http.js +44 -0
- package/lib/index.js +422 -0
- package/lib/jwt.js +28 -0
- package/lib/messages.js +258 -0
- package/lib/oauth.js +68 -0
- package/lib/pkce.js +12 -0
- package/lib/refs.js +59 -0
- package/lib/responses-stream.js +175 -0
- package/lib/rotate.js +31 -0
- package/lib/sse.js +44 -0
- package/lib/stream-rotate.js +41 -0
- package/lib/usage.js +51 -0
- package/lib/vendors/antigravity-oauth.js +10 -0
- package/lib/vendors/antigravity.js +152 -0
- package/lib/vendors/claude.js +119 -0
- package/lib/vendors/codex.js +163 -0
- package/lib/vendors/grok.js +223 -0
- package/lib/vendors/index.js +17 -0
- package/lib/wire.js +200 -0
- package/package.json +63 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@goodandready/dsh-subscriptions',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
const React = require('react')
|
|
7
|
+
|
|
8
|
+
const CSS =
|
|
9
|
+
'.dsub-wrap{display:flex;flex-direction:column;gap:22px;padding:4px 0;max-width:760px}' +
|
|
10
|
+
'.dsub-block{display:flex;flex-direction:column;gap:10px}' +
|
|
11
|
+
'.dsub-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
12
|
+
'.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
13
|
+
'.dsub-card{display:flex;flex-direction:column;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}' +
|
|
14
|
+
'.dsub-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
15
|
+
'.dsub-row input,.dsub-field input,.dsub-row select{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
|
|
16
|
+
'.dsub-grow{flex:1;min-width:180px}' +
|
|
17
|
+
'.dsub-field{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
18
|
+
'.dsub-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;height:28px;padding:0 8px;cursor:pointer}' +
|
|
19
|
+
'.dsub-save{background:var(--dsw-alias-brand-primary);color:#fff;border:none;border-radius:6px;padding:7px 14px;font-size:13px;cursor:pointer}' +
|
|
20
|
+
'.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
21
|
+
'.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
|
|
22
|
+
'.dsub-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap}' +
|
|
23
|
+
'.dsub-badge-on{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
|
|
24
|
+
'.dsub-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
|
|
25
|
+
'.dsub-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0}' +
|
|
26
|
+
'.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
|
|
27
|
+
'.dsub-verify a{color:var(--dsw-alias-brand-primary)}'
|
|
28
|
+
|
|
29
|
+
const cssId = 'dsh-subscriptions/settings.module.css'
|
|
30
|
+
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + cssId + '"]')) {
|
|
31
|
+
const tag = document.createElement('style')
|
|
32
|
+
tag.textContent = CSS
|
|
33
|
+
tag.setAttribute('data-plugin', 'dsh-subscriptions')
|
|
34
|
+
tag.dataset.pluginCss = cssId
|
|
35
|
+
document.head.appendChild(tag)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function badgeFor(account) {
|
|
39
|
+
if (!account || !account.configured) return 'Not connected'
|
|
40
|
+
if (account.validationUrl) return 'Verify account'
|
|
41
|
+
if (account.cooldownUntil && account.cooldownUntil > Date.now()) return 'Cooling down'
|
|
42
|
+
if (account.usagePercent != null && account.usagePercent >= 100) return 'Usage 100%'
|
|
43
|
+
return 'Connected'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function SubsSection() {
|
|
47
|
+
const [draft, setDraft] = React.useState(null)
|
|
48
|
+
const [accounts, setAccounts] = React.useState([])
|
|
49
|
+
const [providers, setProviders] = React.useState([])
|
|
50
|
+
const [paste, setPaste] = React.useState({})
|
|
51
|
+
const [saved, setSaved] = React.useState(false)
|
|
52
|
+
const [err, setErr] = React.useState('')
|
|
53
|
+
|
|
54
|
+
const applyPayload = (data) => {
|
|
55
|
+
setDraft(JSON.parse(JSON.stringify((data && data.config) || {})))
|
|
56
|
+
setAccounts((data && data.accounts) || [])
|
|
57
|
+
setProviders((data && data.providers) || [])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reload = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
61
|
+
.then((res) => res.json())
|
|
62
|
+
.then(applyPayload)
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
let alive = true
|
|
66
|
+
reload().catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
67
|
+
return () => { alive = false }
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
if (!draft) return React.createElement('div', { className: 'dsub-wrap' }, 'Loading\u2026')
|
|
71
|
+
|
|
72
|
+
const slots = Array.isArray(draft.slots) ? draft.slots : []
|
|
73
|
+
const setSlots = (next) => setDraft((d) => Object.assign({}, d, { slots: next }))
|
|
74
|
+
const accountOf = (provider, index) => accounts.find((a) => a.provider === provider && a.index === index) || {}
|
|
75
|
+
|
|
76
|
+
const save = async () => {
|
|
77
|
+
setErr(''); setSaved(false)
|
|
78
|
+
const res = await fetch('/dsh-subscriptions/config', {
|
|
79
|
+
method: 'PUT',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify(draft),
|
|
82
|
+
})
|
|
83
|
+
const data = await res.json().catch(() => ({}))
|
|
84
|
+
if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
85
|
+
applyPayload(data)
|
|
86
|
+
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const connect = async (provider, index) => {
|
|
90
|
+
setErr('')
|
|
91
|
+
const res = await fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(provider) + '&index=' + encodeURIComponent(index), { cache: 'no-store' })
|
|
92
|
+
const data = await res.json().catch(() => ({}))
|
|
93
|
+
if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
94
|
+
if (data.url) window.open(data.url, '_blank', 'noopener')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const complete = async (provider, index) => {
|
|
98
|
+
setErr('')
|
|
99
|
+
const key = provider + ':' + index
|
|
100
|
+
const url = paste[key] || ''
|
|
101
|
+
const res = await fetch('/dsh-subscriptions/oauth/complete', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ provider: provider, index: index, url: url }),
|
|
105
|
+
})
|
|
106
|
+
const data = await res.json().catch(() => ({}))
|
|
107
|
+
if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
108
|
+
setAccounts(data.accounts || [])
|
|
109
|
+
setPaste((p) => Object.assign({}, p, { [key]: '' }))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const logout = async (provider, index) => {
|
|
113
|
+
setErr('')
|
|
114
|
+
const res = await fetch('/dsh-subscriptions/logout', {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ provider: provider, index: index }),
|
|
118
|
+
})
|
|
119
|
+
const data = await res.json().catch(() => ({}))
|
|
120
|
+
if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
121
|
+
setAccounts(data.accounts || [])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const addSlot = (provider) => {
|
|
125
|
+
const used = slots.filter((s) => s.provider === provider).map((s) => s.index)
|
|
126
|
+
let index = 1
|
|
127
|
+
while (used.indexOf(index) >= 0) index++
|
|
128
|
+
setSlots(slots.concat([{ provider: provider, index: index, label: '' }]))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const names = providers.length ? providers : [
|
|
132
|
+
{ id: 'codex', name: 'ChatGPT Codex' },
|
|
133
|
+
{ id: 'claude', name: 'Claude' },
|
|
134
|
+
{ id: 'grok', name: 'Grok' },
|
|
135
|
+
{ id: 'antigravity', name: 'Antigravity' },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
return React.createElement('div', { className: 'dsub-wrap' },
|
|
139
|
+
React.createElement('div', { className: 'dsub-block' },
|
|
140
|
+
React.createElement('div', { className: 'dsub-h' }, 'Subscriptions'),
|
|
141
|
+
React.createElement('div', { className: 'dsub-sub' },
|
|
142
|
+
'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.'),
|
|
143
|
+
React.createElement('label', { className: 'dsub-row' },
|
|
144
|
+
React.createElement('input', {
|
|
145
|
+
type: 'checkbox',
|
|
146
|
+
checked: !!draft.useWebCallback,
|
|
147
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { useWebCallback: e.target.checked })),
|
|
148
|
+
}),
|
|
149
|
+
React.createElement('span', null, 'Use this Web UI origin as OAuth redirect_uri'),
|
|
150
|
+
),
|
|
151
|
+
React.createElement('div', { className: 'dsub-sub' },
|
|
152
|
+
'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.'),
|
|
153
|
+
),
|
|
154
|
+
names.map((prov) => {
|
|
155
|
+
const rows = slots
|
|
156
|
+
.map((slot, i) => ({ slot: slot, i: i }))
|
|
157
|
+
.filter((row) => row.slot.provider === prov.id)
|
|
158
|
+
return React.createElement('div', { className: 'dsub-block', key: prov.id },
|
|
159
|
+
React.createElement('div', { className: 'dsub-h' }, prov.name),
|
|
160
|
+
rows.map((row) => {
|
|
161
|
+
const account = accountOf(row.slot.provider, row.slot.index)
|
|
162
|
+
const key = row.slot.provider + ':' + row.slot.index
|
|
163
|
+
const on = !!account.configured
|
|
164
|
+
return React.createElement('div', { className: 'dsub-card', key: key },
|
|
165
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
166
|
+
React.createElement('input', {
|
|
167
|
+
className: 'dsub-grow',
|
|
168
|
+
value: row.slot.label || '',
|
|
169
|
+
placeholder: account.label || ('Account ' + row.slot.index),
|
|
170
|
+
onChange: (e) => {
|
|
171
|
+
const next = slots.slice()
|
|
172
|
+
next[row.i] = Object.assign({}, next[row.i], { label: e.target.value })
|
|
173
|
+
setSlots(next)
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
React.createElement('span', { className: 'dsub-badge' + (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : '')) }, badgeFor(account)),
|
|
177
|
+
React.createElement('button', {
|
|
178
|
+
type: 'button', className: 'dsub-mini',
|
|
179
|
+
onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
|
|
180
|
+
}, on ? 'Reconnect' : 'Connect'),
|
|
181
|
+
React.createElement('button', {
|
|
182
|
+
type: 'button', className: 'dsub-mini',
|
|
183
|
+
disabled: !on,
|
|
184
|
+
onClick: () => logout(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
|
|
185
|
+
}, 'Disconnect'),
|
|
186
|
+
React.createElement('button', {
|
|
187
|
+
type: 'button', className: 'dsub-mini', title: 'Remove slot',
|
|
188
|
+
onClick: () => {
|
|
189
|
+
const slot = row.slot
|
|
190
|
+
Promise.resolve(on ? logout(slot.provider, slot.index) : null)
|
|
191
|
+
.then(() => setSlots(slots.filter((_, k) => k !== row.i)))
|
|
192
|
+
.catch((e) => setErr(String(e.message || e)))
|
|
193
|
+
},
|
|
194
|
+
}, '\u00d7'),
|
|
195
|
+
),
|
|
196
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
197
|
+
React.createElement('input', {
|
|
198
|
+
className: 'dsub-grow',
|
|
199
|
+
value: paste[key] || '',
|
|
200
|
+
placeholder: 'Paste redirected URL or code',
|
|
201
|
+
onChange: (e) => setPaste((p) => Object.assign({}, p, { [key]: e.target.value })),
|
|
202
|
+
}),
|
|
203
|
+
React.createElement('button', {
|
|
204
|
+
type: 'button', className: 'dsub-mini',
|
|
205
|
+
onClick: () => complete(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
|
|
206
|
+
}, 'Submit code'),
|
|
207
|
+
),
|
|
208
|
+
account.accountNotice ? React.createElement('div', { className: 'dsub-verify' }, account.accountNotice) : null,
|
|
209
|
+
account.validationUrl ? React.createElement('div', { className: 'dsub-verify' },
|
|
210
|
+
'Google requires one-time account verification. ',
|
|
211
|
+
React.createElement('a', { href: account.validationUrl, target: '_blank', rel: 'noopener noreferrer' }, 'Open verification link'),
|
|
212
|
+
' then reconnect.',
|
|
213
|
+
) : null,
|
|
214
|
+
account.paidTierName ? React.createElement('span', { className: 'dsub-sub' }, 'Plan: ' + account.paidTierName) : null,
|
|
215
|
+
account.ref ? React.createElement('span', { className: 'dsub-sub' }, 'Stored as ' + account.ref) : null,
|
|
216
|
+
)
|
|
217
|
+
}),
|
|
218
|
+
React.createElement('button', {
|
|
219
|
+
type: 'button', className: 'dsub-mini',
|
|
220
|
+
onClick: () => addSlot(prov.id),
|
|
221
|
+
}, '+ Add account'),
|
|
222
|
+
)
|
|
223
|
+
}),
|
|
224
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
225
|
+
React.createElement('button', {
|
|
226
|
+
type: 'button', className: 'dsub-save',
|
|
227
|
+
onClick: () => save().catch((e) => setErr(String(e.message || e))),
|
|
228
|
+
}, 'Save'),
|
|
229
|
+
saved ? React.createElement('span', { className: 'dsub-ok' }, 'Saved') : null,
|
|
230
|
+
err ? React.createElement('span', { className: 'dsub-bad' }, err) : null,
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function registerSettings(ctx) {
|
|
236
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
237
|
+
{ name: 'settings.section', id: '@goodandready/dsh-subscriptions', order: 28, label: () => 'Subscriptions', inject: () => ({ ctx: ctx }) },
|
|
238
|
+
SubsSection,
|
|
239
|
+
))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
exports.inject = ['slots']
|
|
243
|
+
exports.apply = function apply(ctx) {
|
|
244
|
+
registerSettings(ctx)
|
|
245
|
+
}
|
|
246
|
+
return module.exports
|
|
247
|
+
},
|
|
248
|
+
})
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { httpError, readJson } from './wire.js'
|
|
3
|
+
import { validationFromLoadCodeAssist, validationFromHttpError, noticeFromLoadCodeAssist, validationRequiredError } from './google-validation.js'
|
|
4
|
+
import { asUsageSnapshot, deepestUsedPercent } from './usage.js'
|
|
5
|
+
|
|
6
|
+
export const CODE_ASSIST_PROD = 'https://cloudcode-pa.googleapis.com/v1internal'
|
|
7
|
+
export const CODE_ASSIST_STREAM = 'https://daily-cloudcode-pa.googleapis.com/v1internal'
|
|
8
|
+
export const CODE_ASSIST = CODE_ASSIST_PROD
|
|
9
|
+
|
|
10
|
+
function antigravityPlatform() {
|
|
11
|
+
if (process.platform === 'win32') return 'WINDOWS'
|
|
12
|
+
if (process.platform === 'darwin') return 'MACOS'
|
|
13
|
+
return 'LINUX'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function antigravityMetadata(projectId) {
|
|
17
|
+
const meta = {
|
|
18
|
+
ideType: 'ANTIGRAVITY',
|
|
19
|
+
platform: antigravityPlatform(),
|
|
20
|
+
pluginType: 'GEMINI',
|
|
21
|
+
}
|
|
22
|
+
if (projectId) meta.duetProject = projectId
|
|
23
|
+
return meta
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function antigravityIdentityHeaders(projectId) {
|
|
27
|
+
const uaPlat = process.platform === 'darwin' ? 'darwin/amd64' : process.platform === 'win32' ? 'windows/amd64' : 'linux/amd64'
|
|
28
|
+
return {
|
|
29
|
+
'user-agent': `antigravity/1.18.3 ${uaPlat}`,
|
|
30
|
+
'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
|
|
31
|
+
'Client-Metadata': JSON.stringify(antigravityMetadata(projectId)),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assistHeaders(token, extra) {
|
|
36
|
+
return {
|
|
37
|
+
Authorization: `Bearer ${token}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
Accept: 'application/json',
|
|
40
|
+
...(extra || {}),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function postAssist(fetchImpl, method, token, body, extraHeaders) {
|
|
45
|
+
const res = await fetchImpl(`${CODE_ASSIST}:${method}`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: assistHeaders(token, extraHeaders),
|
|
48
|
+
body: JSON.stringify(body || {}),
|
|
49
|
+
})
|
|
50
|
+
return readJson(res)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sleep(ms) {
|
|
54
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectFrom(obj) {
|
|
58
|
+
if (!obj || typeof obj !== 'object') return ''
|
|
59
|
+
if (typeof obj.cloudaicompanionProject === 'string') return obj.cloudaicompanionProject
|
|
60
|
+
const nested = obj.cloudaicompanionProject
|
|
61
|
+
if (nested && typeof nested === 'object') return String(nested.id || nested.name || '')
|
|
62
|
+
if (obj.response && obj.response.cloudaicompanionProject) {
|
|
63
|
+
const r = obj.response.cloudaicompanionProject
|
|
64
|
+
return typeof r === 'string' ? r : String(r.id || r.name || '')
|
|
65
|
+
}
|
|
66
|
+
return ''
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function accountFromLoad(load) {
|
|
70
|
+
const current = load && load.currentTier && typeof load.currentTier === 'object' ? load.currentTier : {}
|
|
71
|
+
const paidTierId = load?.paidTier?.id ? String(load.paidTier.id) : ''
|
|
72
|
+
return {
|
|
73
|
+
projectId: projectFrom(load),
|
|
74
|
+
tierId: String(current.id || paidTierId || 'free-tier'),
|
|
75
|
+
paidTierId,
|
|
76
|
+
paidTierName: load?.paidTier?.name ? String(load.paidTier.name) : '',
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function resolveProjectId(fetchImpl, token, metadata, extraHeaders, currentProjectId) {
|
|
81
|
+
const load = await postAssist(fetchImpl, 'loadCodeAssist', token, { metadata }, extraHeaders)
|
|
82
|
+
const validation = validationFromLoadCodeAssist(load)
|
|
83
|
+
if (validation) throw validationRequiredError(validation)
|
|
84
|
+
const account = accountFromLoad(load)
|
|
85
|
+
if (account.projectId) return { ...account, load }
|
|
86
|
+
if (currentProjectId) return { ...account, projectId: currentProjectId, load }
|
|
87
|
+
return { ...account, projectId: '', load }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pickOnboardTier(load) {
|
|
91
|
+
const current = load?.currentTier
|
|
92
|
+
const paidTierId = load?.paidTier?.id ? String(load.paidTier.id) : ''
|
|
93
|
+
if (paidTierId === 'g1-pro-tier') return 'standard-tier'
|
|
94
|
+
for (const tier of load?.allowedTiers || []) {
|
|
95
|
+
if (tier?.isDefault && tier?.id) return String(tier.id)
|
|
96
|
+
}
|
|
97
|
+
return String(current?.id || 'free-tier')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function discoverProject(fetchImpl, token, metadata, extraHeaders) {
|
|
101
|
+
const resolved = await resolveProjectId(fetchImpl, token, metadata, extraHeaders, '')
|
|
102
|
+
const { load } = resolved
|
|
103
|
+
const account = accountFromLoad(load)
|
|
104
|
+
if (account.projectId) {
|
|
105
|
+
return {
|
|
106
|
+
projectId: account.projectId,
|
|
107
|
+
tierId: account.paidTierId || account.tierId,
|
|
108
|
+
paidTierId: account.paidTierId,
|
|
109
|
+
paidTierName: account.paidTierName,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const tierId = pickOnboardTier(load)
|
|
113
|
+
const onboardBody = {
|
|
114
|
+
tierId,
|
|
115
|
+
metadata,
|
|
116
|
+
...(tierId !== 'free-tier' && tierId !== 'FREE' ? { cloudaicompanionProject: metadata.duetProject } : {}),
|
|
117
|
+
}
|
|
118
|
+
let op = await postAssist(fetchImpl, 'onboardUser', token, onboardBody, extraHeaders).catch(() => null)
|
|
119
|
+
let n = 0
|
|
120
|
+
while (op && op.done === false && op.name && n < 8) {
|
|
121
|
+
await sleep(400)
|
|
122
|
+
try {
|
|
123
|
+
op = await postAssist(fetchImpl, 'loadCodeAssist', token, { metadata }, extraHeaders)
|
|
124
|
+
} catch {
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
n += 1
|
|
128
|
+
}
|
|
129
|
+
const projectId = projectFrom(op) || account.projectId
|
|
130
|
+
const after = accountFromLoad(op && op.response ? op.response : load)
|
|
131
|
+
return {
|
|
132
|
+
projectId,
|
|
133
|
+
tierId: after.paidTierId || tierId,
|
|
134
|
+
paidTierId: after.paidTierId,
|
|
135
|
+
paidTierName: after.paidTierName,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function catalogFallback(fallbackIds, provider) {
|
|
140
|
+
return (fallbackIds || []).map((row) => {
|
|
141
|
+
if (typeof row === 'string') return { provider, id: row, name: row }
|
|
142
|
+
const id = row && (row.id || row.name)
|
|
143
|
+
if (!id) return null
|
|
144
|
+
return { provider, id, name: row.name || id }
|
|
145
|
+
}).filter(Boolean)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function catalogRows(json) {
|
|
149
|
+
const raw = json?.models ?? json?.availableModels ?? json?.data ?? []
|
|
150
|
+
if (Array.isArray(raw)) return raw.map((row) => (typeof row === 'string' ? { id: row } : row))
|
|
151
|
+
if (raw && typeof raw === 'object') {
|
|
152
|
+
return Object.entries(raw).map(([id, row]) => ({ ...(row || {}), id }))
|
|
153
|
+
}
|
|
154
|
+
return []
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function fetchAvailableModels(fetchImpl, token, extraHeaders, fallbackIds, provider) {
|
|
158
|
+
try {
|
|
159
|
+
const json = await postAssist(fetchImpl, 'fetchAvailableModels', token, {}, extraHeaders)
|
|
160
|
+
const out = []
|
|
161
|
+
for (const row of catalogRows(json)) {
|
|
162
|
+
if (!row) continue
|
|
163
|
+
if (typeof row === 'string') {
|
|
164
|
+
if (row && !row.startsWith('chat_')) out.push({ provider, id: row, name: row })
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (row.isInternal) continue
|
|
168
|
+
const raw = String(row.id || row.name || row.model || '').replace(/^models\//, '')
|
|
169
|
+
if (!raw || raw.startsWith('chat_')) continue
|
|
170
|
+
const display = row.displayName ?? row.display_name
|
|
171
|
+
if (display === '') continue
|
|
172
|
+
out.push({
|
|
173
|
+
provider,
|
|
174
|
+
id: raw,
|
|
175
|
+
name: display || raw,
|
|
176
|
+
...(row.recommended ? { description: 'Recommended' } : {}),
|
|
177
|
+
...(row.maxTokens ? { contextWindow: row.maxTokens } : {}),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
if (out.length) {
|
|
181
|
+
out.sort((a, b) => Number(Boolean(b.description)) - Number(Boolean(a.description)) || a.name.localeCompare(b.name))
|
|
182
|
+
return out
|
|
183
|
+
}
|
|
184
|
+
} catch { /* catalog fallback */ }
|
|
185
|
+
return catalogFallback(fallbackIds, provider)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function retrieveQuotaPercent(fetchImpl, token, extraHeaders) {
|
|
189
|
+
try {
|
|
190
|
+
const json = await postAssist(fetchImpl, 'retrieveUserQuota', token, {}, extraHeaders)
|
|
191
|
+
return asUsageSnapshot(deepestUsedPercent(json))
|
|
192
|
+
} catch {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const G1_CREDIT_TYPE = 'GOOGLE_ONE_AI'
|
|
198
|
+
|
|
199
|
+
export function streamEnvelope({ projectId, model, request, userAgent, sessionId, paidTierId }) {
|
|
200
|
+
const session = sessionId || randomUUID()
|
|
201
|
+
const body = { ...(request || {}) }
|
|
202
|
+
if (!body.session_id) body.session_id = session
|
|
203
|
+
const envelope = {
|
|
204
|
+
...(projectId ? { project: projectId } : {}),
|
|
205
|
+
model,
|
|
206
|
+
user_prompt_id: randomUUID(),
|
|
207
|
+
request: body,
|
|
208
|
+
userAgent,
|
|
209
|
+
}
|
|
210
|
+
if (paidTierId === 'g1-pro-tier') envelope.enabled_credit_types = [G1_CREDIT_TYPE]
|
|
211
|
+
return { envelope, sessionId: session }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { httpError }
|
|
215
|
+
|
|
216
|
+
export async function inspectGoogleAccount(fetchImpl, token, { metadata, extraHeaders, projectId }) {
|
|
217
|
+
try {
|
|
218
|
+
const { projectId: resolvedProject, load } = await resolveProjectId(
|
|
219
|
+
fetchImpl,
|
|
220
|
+
token,
|
|
221
|
+
metadata,
|
|
222
|
+
extraHeaders,
|
|
223
|
+
projectId || '',
|
|
224
|
+
)
|
|
225
|
+
const validation = validationFromLoadCodeAssist(load)
|
|
226
|
+
if (validation) return { validation, notice: null, projectId: resolvedProject }
|
|
227
|
+
const notice = noticeFromLoadCodeAssist(load)
|
|
228
|
+
const account = accountFromLoad(load)
|
|
229
|
+
return {
|
|
230
|
+
validation: null,
|
|
231
|
+
notice,
|
|
232
|
+
projectId: resolvedProject,
|
|
233
|
+
paidTierId: account.paidTierId,
|
|
234
|
+
paidTierName: account.paidTierName,
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
|
238
|
+
return {
|
|
239
|
+
validation: {
|
|
240
|
+
message: String(err.message || ''),
|
|
241
|
+
validationUrl: err.validationUrl,
|
|
242
|
+
learnMoreUrl: err.learnMoreUrl || '',
|
|
243
|
+
},
|
|
244
|
+
notice: null,
|
|
245
|
+
projectId: projectId || '',
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { validation: null, notice: null, projectId: projectId || '' }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const TYPE_MAP = {
|
|
2
|
+
string: 'string',
|
|
3
|
+
number: 'number',
|
|
4
|
+
integer: 'integer',
|
|
5
|
+
boolean: 'boolean',
|
|
6
|
+
array: 'array',
|
|
7
|
+
object: 'object',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function asType(value) {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
const nullable = value.includes('null')
|
|
13
|
+
const first = value.find((item) => item && item !== 'null')
|
|
14
|
+
return { type: TYPE_MAP[String(first || 'string').toLowerCase()] || 'string', nullable }
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'string' && TYPE_MAP[value.toLowerCase()]) {
|
|
17
|
+
return { type: TYPE_MAP[value.toLowerCase()], nullable: false }
|
|
18
|
+
}
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toGeminiSchema(schema, depth) {
|
|
23
|
+
const level = Number(depth) || 0
|
|
24
|
+
if (level > 12) return { type: 'string' }
|
|
25
|
+
if (!schema || typeof schema !== 'object') return { type: 'string' }
|
|
26
|
+
if (Array.isArray(schema)) return toGeminiSchema(schema[0] || { type: 'string' }, level + 1)
|
|
27
|
+
|
|
28
|
+
const typed = asType(schema.type)
|
|
29
|
+
let type = typed ? typed.type : null
|
|
30
|
+
if (!type) {
|
|
31
|
+
if (schema.properties) type = 'object'
|
|
32
|
+
else if (schema.items) type = 'array'
|
|
33
|
+
else type = 'string'
|
|
34
|
+
}
|
|
35
|
+
const out = { type }
|
|
36
|
+
if (typed && typed.nullable) out.nullable = true
|
|
37
|
+
if (schema.description) out.description = String(schema.description)
|
|
38
|
+
if (type === 'object') {
|
|
39
|
+
const props = schema.properties
|
|
40
|
+
if (props && typeof props === 'object' && !Array.isArray(props)) {
|
|
41
|
+
out.properties = {}
|
|
42
|
+
for (const [key, value] of Object.entries(props)) {
|
|
43
|
+
out.properties[key] = toGeminiSchema(value, level + 1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(schema.required)) {
|
|
47
|
+
out.required = schema.required.filter((item) => typeof item === 'string')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (type === 'array') {
|
|
51
|
+
const items = schema.items
|
|
52
|
+
out.items = toGeminiSchema(Array.isArray(items) ? items[0] : (items || { type: 'string' }), level + 1)
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(schema.enum) && schema.enum.length) {
|
|
55
|
+
out.enum = schema.enum.map((item) => String(item))
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function geminiFunctionDeclarations(tools) {
|
|
61
|
+
if (!Array.isArray(tools) || !tools.length) return undefined
|
|
62
|
+
return tools.map((tool) => ({
|
|
63
|
+
name: tool.name,
|
|
64
|
+
description: tool.description || '',
|
|
65
|
+
parameters: toGeminiSchema(tool.parameters || { type: 'object' }),
|
|
66
|
+
}))
|
|
67
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const CLOUDCODE_DOMAINS = [
|
|
2
|
+
'cloudcode-pa.googleapis.com',
|
|
3
|
+
'staging-cloudcode-pa.googleapis.com',
|
|
4
|
+
'autopush-cloudcode-pa.googleapis.com',
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
export function isCloudCodeDomain(domain) {
|
|
8
|
+
const sanitized = String(domain || '').replace(/[^a-zA-Z0-9.-]/g, '')
|
|
9
|
+
return CLOUDCODE_DOMAINS.includes(sanitized)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseGoogleApiError(bodyText) {
|
|
13
|
+
try {
|
|
14
|
+
const json = JSON.parse(String(bodyText || ''))
|
|
15
|
+
const err = json.error || json
|
|
16
|
+
if (!err || typeof err !== 'object') return null
|
|
17
|
+
return {
|
|
18
|
+
code: err.code,
|
|
19
|
+
message: String(err.message || ''),
|
|
20
|
+
status: String(err.status || ''),
|
|
21
|
+
details: Array.isArray(err.details) ? err.details : [],
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function metadataUrl(metadata) {
|
|
29
|
+
if (!metadata || typeof metadata !== 'object') return ''
|
|
30
|
+
return String(
|
|
31
|
+
metadata.validation_url
|
|
32
|
+
|| metadata.validation_link
|
|
33
|
+
|| metadata.validationUrl
|
|
34
|
+
|| '',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validationFromLoadCodeAssist(load) {
|
|
39
|
+
if (!load) return null
|
|
40
|
+
const tiers = load.ineligibleTiers
|
|
41
|
+
if (Array.isArray(tiers) && tiers.length) {
|
|
42
|
+
const tier = tiers.find((row) => row && (
|
|
43
|
+
row.reasonCode === 'VALIDATION_REQUIRED'
|
|
44
|
+
|| row.reason === 'VALIDATION_REQUIRED'
|
|
45
|
+
) && (row.validationUrl || row.validation_url))
|
|
46
|
+
if (tier) {
|
|
47
|
+
return {
|
|
48
|
+
message: String(tier.reasonMessage || 'Verify your account to continue.'),
|
|
49
|
+
validationUrl: String(tier.validationUrl || tier.validation_url),
|
|
50
|
+
learnMoreUrl: String(tier.learnMoreUrl || tier.learn_more_url || ''),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function noticeFromLoadCodeAssist(load) {
|
|
58
|
+
if (!load || load.currentTier) return null
|
|
59
|
+
const tiers = load.ineligibleTiers
|
|
60
|
+
if (!Array.isArray(tiers) || !tiers.length) return null
|
|
61
|
+
const tier = tiers[0]
|
|
62
|
+
if (!tier) return null
|
|
63
|
+
const code = String(tier.reasonCode || tier.reason || '')
|
|
64
|
+
const message = String(tier.reasonMessage || '')
|
|
65
|
+
if (!message) return null
|
|
66
|
+
if (code === 'VALIDATION_REQUIRED') return null
|
|
67
|
+
return { code, message }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validationFromHttpError(status, bodyText) {
|
|
71
|
+
if (status !== 403) return null
|
|
72
|
+
const api = parseGoogleApiError(bodyText)
|
|
73
|
+
if (!api) return null
|
|
74
|
+
const info = api.details.find((d) => d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo')
|
|
75
|
+
if (!info || info.reason !== 'VALIDATION_REQUIRED' || !isCloudCodeDomain(info.domain)) return null
|
|
76
|
+
|
|
77
|
+
const help = api.details.find((d) => d['@type'] === 'type.googleapis.com/google.rpc.Help')
|
|
78
|
+
let validationUrl = metadataUrl(info.metadata)
|
|
79
|
+
let message = String(info.metadata?.validation_error_message || api.message || 'Verify your account to continue.')
|
|
80
|
+
if (help?.links?.length) {
|
|
81
|
+
const verify = help.links.find((link) => {
|
|
82
|
+
const desc = String(link.description || '').toLowerCase()
|
|
83
|
+
return desc.includes('verify') || !desc.includes('learn more')
|
|
84
|
+
}) || help.links[0]
|
|
85
|
+
validationUrl = validationUrl || String(verify?.url || '')
|
|
86
|
+
message = String(verify?.description || message)
|
|
87
|
+
}
|
|
88
|
+
if (!validationUrl) return null
|
|
89
|
+
let learnMoreUrl = String(info.metadata?.validation_learn_more_url || '')
|
|
90
|
+
if (!learnMoreUrl && help?.links?.length) {
|
|
91
|
+
const more = help.links.find((link) => {
|
|
92
|
+
if (String(link.description || '').toLowerCase().trim() === 'learn more') return true
|
|
93
|
+
try { return new URL(link.url).hostname === 'support.google.com' } catch { return false }
|
|
94
|
+
})
|
|
95
|
+
if (more) learnMoreUrl = String(more.url || '')
|
|
96
|
+
}
|
|
97
|
+
return { message, validationUrl, learnMoreUrl }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function validationRequiredError(details) {
|
|
101
|
+
const err = new Error(`${details.message} Verify at ${details.validationUrl}`)
|
|
102
|
+
err.code = 'VALIDATION_REQUIRED'
|
|
103
|
+
err.validationUrl = details.validationUrl
|
|
104
|
+
if (details.learnMoreUrl) err.learnMoreUrl = details.learnMoreUrl
|
|
105
|
+
return err
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
export function googleRateLimitMessage(status, bodyText) {
|
|
110
|
+
if (status !== 429) return ''
|
|
111
|
+
const api = parseGoogleApiError(bodyText)
|
|
112
|
+
const msg = String(api?.message || bodyText || '')
|
|
113
|
+
if (/resource has been exhausted|RESOURCE_EXHAUSTED|quota/i.test(msg)) {
|
|
114
|
+
return 'Google Antigravity quota or rate limit reached. Wait a few minutes, check usage in antigravity.google, or upgrade Google AI Pro.'
|
|
115
|
+
}
|
|
116
|
+
return ''
|
|
117
|
+
}
|