@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/usage.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function numberOrNull(value) {
|
|
2
|
+
const n = Number(value)
|
|
3
|
+
return Number.isFinite(n) ? n : null
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function windowPercent(obj) {
|
|
7
|
+
if (!obj || typeof obj !== 'object') return null
|
|
8
|
+
return numberOrNull(
|
|
9
|
+
obj.utilization ?? obj.used_percent ?? obj.usedPercent ?? obj.creditUsagePercent ?? obj.used_percentage,
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deepestUsedPercent(obj) {
|
|
14
|
+
let max = null
|
|
15
|
+
function walk(value, depth) {
|
|
16
|
+
if (!value || typeof value !== 'object' || depth > 8) return
|
|
17
|
+
const here = windowPercent(value)
|
|
18
|
+
if (here != null) max = max == null ? here : Math.max(max, here)
|
|
19
|
+
const remaining = numberOrNull(value.remainingFraction)
|
|
20
|
+
if (remaining != null) {
|
|
21
|
+
const used = (1 - remaining) * 100
|
|
22
|
+
max = max == null ? used : Math.max(max, used)
|
|
23
|
+
}
|
|
24
|
+
const usedFrac = numberOrNull(value.usedFraction)
|
|
25
|
+
if (usedFrac != null) {
|
|
26
|
+
const used = usedFrac * 100
|
|
27
|
+
max = max == null ? used : Math.max(max, used)
|
|
28
|
+
}
|
|
29
|
+
for (const child of Object.values(value)) {
|
|
30
|
+
if (child && typeof child === 'object') walk(child, depth + 1)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
walk(obj, 0)
|
|
34
|
+
return max
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function grokBillingPercent(json) {
|
|
38
|
+
const cfg = json && json.config && typeof json.config === 'object' ? json.config : json
|
|
39
|
+
const ready = numberOrNull(cfg && cfg.creditUsagePercent)
|
|
40
|
+
if (ready != null) return ready
|
|
41
|
+
const limit = numberOrNull(cfg && (cfg.monthlyLimit ?? cfg.limit))
|
|
42
|
+
const used = numberOrNull(cfg && (cfg.used ?? cfg.usedCredits))
|
|
43
|
+
if (limit && limit > 0 && used != null) return (used / limit) * 100
|
|
44
|
+
return deepestUsedPercent(json)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function asUsageSnapshot(percent) {
|
|
48
|
+
const usedPercent = numberOrNull(percent)
|
|
49
|
+
if (usedPercent == null) return null
|
|
50
|
+
return { usedPercent }
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
|
+
import { googleContents } from '../messages.js'
|
|
4
|
+
import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth } from '../wire.js'
|
|
5
|
+
import { emailFromToken } from '../jwt.js'
|
|
6
|
+
import { builtinAntigravityOAuth } from './antigravity-oauth.js'
|
|
7
|
+
import {
|
|
8
|
+
CODE_ASSIST_STREAM,
|
|
9
|
+
antigravityMetadata,
|
|
10
|
+
antigravityIdentityHeaders,
|
|
11
|
+
discoverProject,
|
|
12
|
+
fetchAvailableModels,
|
|
13
|
+
retrieveQuotaPercent,
|
|
14
|
+
streamEnvelope,
|
|
15
|
+
assistHeaders,
|
|
16
|
+
} from '../code-assist.js'
|
|
17
|
+
|
|
18
|
+
export const id = 'antigravity'
|
|
19
|
+
|
|
20
|
+
const AUTH = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
21
|
+
const TOKEN = 'https://oauth2.googleapis.com/token'
|
|
22
|
+
const SCOPE = [
|
|
23
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
24
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
25
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
26
|
+
'https://www.googleapis.com/auth/cclog',
|
|
27
|
+
'https://www.googleapis.com/auth/experimentsandconfigs',
|
|
28
|
+
].join(' ')
|
|
29
|
+
|
|
30
|
+
export function providerInfo() {
|
|
31
|
+
return { id, name: 'Antigravity' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaults() {
|
|
35
|
+
const builtin = builtinAntigravityOAuth()
|
|
36
|
+
return {
|
|
37
|
+
clientId: builtin.clientId,
|
|
38
|
+
clientSecret: builtin.clientSecret,
|
|
39
|
+
redirectUri: builtin.redirectUri,
|
|
40
|
+
models: ['gemini-3.5-flash-low', 'gemini-3-flash', 'gemini-2.5-flash'],
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function authorizeUrl(cfg, pkce) {
|
|
45
|
+
return buildAuthorizeUrl({
|
|
46
|
+
authUrl: AUTH,
|
|
47
|
+
clientId: cfg.clientId,
|
|
48
|
+
redirectUri: cfg.redirectUri,
|
|
49
|
+
challenge: pkce.challenge,
|
|
50
|
+
state: pkce.state,
|
|
51
|
+
scope: SCOPE,
|
|
52
|
+
extra: { access_type: 'offline', prompt: 'consent' },
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function headersFor(projectId) {
|
|
57
|
+
return antigravityIdentityHeaders(projectId)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function metadataFor(projectId) {
|
|
61
|
+
return antigravityMetadata(projectId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function withProject(blob, fetchImpl, saveBlob) {
|
|
65
|
+
if (blob.projectId && blob.paidTierId) {
|
|
66
|
+
return { ...blob, sessionId: blob.sessionId || randomUUID() }
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const meta = metadataFor(blob.projectId || '')
|
|
70
|
+
const found = await discoverProject(fetchImpl, blob.accessToken, meta, headersFor(blob.projectId || ''))
|
|
71
|
+
const next = {
|
|
72
|
+
...blob,
|
|
73
|
+
projectId: found.projectId || blob.projectId || '',
|
|
74
|
+
paidTierId: found.paidTierId || blob.paidTierId || '',
|
|
75
|
+
paidTierName: found.paidTierName || blob.paidTierName || '',
|
|
76
|
+
sessionId: blob.sessionId || randomUUID(),
|
|
77
|
+
}
|
|
78
|
+
if (saveBlob) await saveBlob(next)
|
|
79
|
+
return next
|
|
80
|
+
} catch (e) {
|
|
81
|
+
if (e && e.code === 'VALIDATION_REQUIRED' && e.validationUrl) {
|
|
82
|
+
return { ...blob, validationUrl: e.validationUrl, validationMessage: String(e.message || '') }
|
|
83
|
+
}
|
|
84
|
+
return blob
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
89
|
+
const json = await formTokenRequest(TOKEN, {
|
|
90
|
+
client_id: cfg.clientId,
|
|
91
|
+
client_secret: cfg.clientSecret || '',
|
|
92
|
+
grant_type: 'authorization_code',
|
|
93
|
+
code,
|
|
94
|
+
redirect_uri: cfg.redirectUri,
|
|
95
|
+
code_verifier: pkce.verifier,
|
|
96
|
+
}, fetchImpl)
|
|
97
|
+
const blob = tokenBlobFromOAuth(json)
|
|
98
|
+
const labeled = { ...blob, email: blob.email || emailFromToken(blob.accessToken), label: blob.label || 'Antigravity' }
|
|
99
|
+
return withProject(labeled, fetchImpl)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function refresh(cfg, blob, fetchImpl) {
|
|
103
|
+
const json = await formTokenRequest(TOKEN, {
|
|
104
|
+
client_id: cfg.clientId,
|
|
105
|
+
client_secret: cfg.clientSecret || '',
|
|
106
|
+
grant_type: 'refresh_token',
|
|
107
|
+
refresh_token: blob.refreshToken,
|
|
108
|
+
}, fetchImpl)
|
|
109
|
+
return tokenBlobFromOAuth(json, {
|
|
110
|
+
label: blob.label,
|
|
111
|
+
email: blob.email,
|
|
112
|
+
projectId: blob.projectId,
|
|
113
|
+
paidTierId: blob.paidTierId,
|
|
114
|
+
paidTierName: blob.paidTierName,
|
|
115
|
+
sessionId: blob.sessionId,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function listModels(blob, cfg, fetchImpl) {
|
|
120
|
+
const fallback = cfg.models || defaults().models
|
|
121
|
+
return fetchAvailableModels(fetchImpl || fetch, blob.accessToken, headersFor(blob.projectId || ''), fallback, id)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function usage(blob, _cfg, fetchImpl) {
|
|
125
|
+
return retrieveQuotaPercent(fetchImpl || fetch, blob.accessToken, headersFor(blob.projectId || ''))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, saveBlob }) {
|
|
129
|
+
const ready = await withProject(blob, fetchImpl, saveBlob)
|
|
130
|
+
const { envelope, sessionId } = streamEnvelope({
|
|
131
|
+
projectId: ready.projectId,
|
|
132
|
+
model: options.model,
|
|
133
|
+
request: googleContents(options),
|
|
134
|
+
userAgent: 'antigravity',
|
|
135
|
+
sessionId: ready.sessionId,
|
|
136
|
+
paidTierId: ready.paidTierId,
|
|
137
|
+
})
|
|
138
|
+
const res = await fetchImpl(`${CODE_ASSIST_STREAM}:streamGenerateContent?alt=sse`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
...headers,
|
|
142
|
+
...assistHeaders(ready.accessToken, { ...headersFor(ready.projectId || ''), Accept: 'text/event-stream' }),
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(envelope),
|
|
145
|
+
signal,
|
|
146
|
+
})
|
|
147
|
+
if (!res.ok) throwHttpError(res.status, await res.text())
|
|
148
|
+
if (saveBlob && sessionId && sessionId !== ready.sessionId) {
|
|
149
|
+
await saveBlob({ ...ready, sessionId })
|
|
150
|
+
}
|
|
151
|
+
yield* googleStream(res.body)
|
|
152
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { buildAuthorizeUrl } from '../oauth.js'
|
|
2
|
+
import { anthropicPayload, modelCatalog } from '../messages.js'
|
|
3
|
+
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
4
|
+
import { emailFromToken } from '../jwt.js'
|
|
5
|
+
import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
|
|
6
|
+
|
|
7
|
+
export const id = 'claude'
|
|
8
|
+
|
|
9
|
+
const AUTH = 'https://claude.ai/oauth/authorize'
|
|
10
|
+
const TOKEN = 'https://platform.claude.com/v1/oauth/token'
|
|
11
|
+
const API = 'https://api.anthropic.com/v1/messages?beta=true'
|
|
12
|
+
const PROFILE = 'https://api.anthropic.com/api/oauth/profile'
|
|
13
|
+
const USAGE = 'https://api.anthropic.com/api/oauth/usage'
|
|
14
|
+
const SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload'
|
|
15
|
+
const IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
|
|
16
|
+
const BETA = 'oauth-2025-04-20,claude-code-20250219'
|
|
17
|
+
|
|
18
|
+
export function providerInfo() {
|
|
19
|
+
return { id, name: 'Claude' }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defaults() {
|
|
23
|
+
return {
|
|
24
|
+
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
25
|
+
redirectUri: 'https://console.anthropic.com/oauth/code/callback',
|
|
26
|
+
models: [
|
|
27
|
+
{ id: 'claude-opus-5', name: 'Claude Opus 5' },
|
|
28
|
+
{ id: 'claude-sonnet-5', name: 'Claude Sonnet 5' },
|
|
29
|
+
{ id: 'claude-fable-5', name: 'Claude Fable 5' },
|
|
30
|
+
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
|
|
31
|
+
],
|
|
32
|
+
systemPrefix: IDENTITY,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function oauthHeaders(token) {
|
|
37
|
+
return {
|
|
38
|
+
Authorization: `Bearer ${token}`,
|
|
39
|
+
Accept: 'application/json',
|
|
40
|
+
'anthropic-version': '2023-06-01',
|
|
41
|
+
'anthropic-beta': BETA,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function authorizeUrl(cfg, pkce) {
|
|
46
|
+
return buildAuthorizeUrl({
|
|
47
|
+
authUrl: AUTH,
|
|
48
|
+
clientId: cfg.clientId,
|
|
49
|
+
redirectUri: cfg.redirectUri,
|
|
50
|
+
challenge: pkce.challenge,
|
|
51
|
+
state: pkce.state,
|
|
52
|
+
scope: SCOPE,
|
|
53
|
+
extra: { code: 'true' },
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function decorate(blob, fetchImpl) {
|
|
58
|
+
let email = blob.email || emailFromToken(blob.accessToken)
|
|
59
|
+
if (!email && fetchImpl && blob.accessToken) {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetchImpl(PROFILE, { headers: oauthHeaders(blob.accessToken) })
|
|
62
|
+
const json = await readJson(res)
|
|
63
|
+
email = json.email || (json.account && json.account.email) || email
|
|
64
|
+
} catch { /* profile is optional */ }
|
|
65
|
+
}
|
|
66
|
+
return { ...blob, email, label: blob.label || email || 'Claude' }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
70
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
71
|
+
grant_type: 'authorization_code',
|
|
72
|
+
client_id: cfg.clientId,
|
|
73
|
+
code,
|
|
74
|
+
redirect_uri: cfg.redirectUri,
|
|
75
|
+
code_verifier: pkce.verifier,
|
|
76
|
+
state: pkce.state,
|
|
77
|
+
}, fetchImpl)
|
|
78
|
+
return decorate(tokenBlobFromOAuth(json), fetchImpl)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function refresh(cfg, blob, fetchImpl) {
|
|
82
|
+
const json = await jsonTokenRequest(TOKEN, {
|
|
83
|
+
grant_type: 'refresh_token',
|
|
84
|
+
client_id: cfg.clientId,
|
|
85
|
+
refresh_token: blob.refreshToken,
|
|
86
|
+
}, fetchImpl)
|
|
87
|
+
return decorate(tokenBlobFromOAuth(json, { label: blob.label, email: blob.email }), fetchImpl)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function listModels(blob, cfg) {
|
|
91
|
+
return modelCatalog(id, cfg.models || defaults().models)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function usage(blob, _cfg, fetchImpl) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await (fetchImpl || fetch)(USAGE, { headers: oauthHeaders(blob.accessToken) })
|
|
97
|
+
const json = await readJson(res)
|
|
98
|
+
return asUsageSnapshot(deepestUsedPercent(json))
|
|
99
|
+
} catch {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
105
|
+
const payload = anthropicPayload(options, config.systemPrefix || IDENTITY)
|
|
106
|
+
const res = await fetchImpl(API, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
...headers,
|
|
110
|
+
...oauthHeaders(blob.accessToken),
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
Accept: 'text/event-stream',
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(payload),
|
|
115
|
+
signal,
|
|
116
|
+
})
|
|
117
|
+
if (!res.ok) throw httpError(res.status, await res.text())
|
|
118
|
+
yield* anthropicStream(res.body)
|
|
119
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
|
+
import { codexResponsesBody, modelCatalog } from '../messages.js'
|
|
4
|
+
import { chatgptAccountId, emailFromToken } from '../jwt.js'
|
|
5
|
+
import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from '../wire.js'
|
|
6
|
+
import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
|
|
7
|
+
|
|
8
|
+
export const id = 'codex'
|
|
9
|
+
|
|
10
|
+
const AUTH = 'https://auth.openai.com/oauth/authorize'
|
|
11
|
+
const TOKEN = 'https://auth.openai.com/oauth/token'
|
|
12
|
+
const USAGE = 'https://chatgpt.com/backend-api/wham/usage'
|
|
13
|
+
const SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke'
|
|
14
|
+
const INSTRUCTIONS = 'You are a coding assistant using a ChatGPT Codex subscription.'
|
|
15
|
+
|
|
16
|
+
export function providerInfo() {
|
|
17
|
+
return { id, name: 'ChatGPT Codex' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defaults() {
|
|
21
|
+
return {
|
|
22
|
+
clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
23
|
+
redirectUri: 'http://localhost:1455/auth/callback',
|
|
24
|
+
baseUrl: 'https://chatgpt.com/backend-api/codex',
|
|
25
|
+
originator: 'codex_cli_rs',
|
|
26
|
+
models: [
|
|
27
|
+
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex' },
|
|
28
|
+
{ id: 'gpt-5.1-codex-mini', name: 'GPT-5.1 Codex Mini' },
|
|
29
|
+
{ id: 'gpt-5.1', name: 'GPT-5.1' },
|
|
30
|
+
],
|
|
31
|
+
clientVersion: '0.147.0',
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function authorizeUrl(cfg, pkce) {
|
|
36
|
+
return buildAuthorizeUrl({
|
|
37
|
+
authUrl: AUTH,
|
|
38
|
+
clientId: cfg.clientId,
|
|
39
|
+
redirectUri: cfg.redirectUri,
|
|
40
|
+
challenge: pkce.challenge,
|
|
41
|
+
state: pkce.state,
|
|
42
|
+
scope: SCOPE,
|
|
43
|
+
extra: {
|
|
44
|
+
id_token_add_organizations: 'true',
|
|
45
|
+
codex_cli_simplified_flow: 'true',
|
|
46
|
+
originator: cfg.originator || 'codex_cli_rs',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function identityHeaders(blob, config, extra) {
|
|
52
|
+
const originator = config.originator || 'codex_cli_rs'
|
|
53
|
+
return {
|
|
54
|
+
Authorization: `Bearer ${blob.accessToken}`,
|
|
55
|
+
originator,
|
|
56
|
+
'chatgpt-account-id': blob.accountId || '',
|
|
57
|
+
'ChatGPT-Account-ID': blob.accountId || '',
|
|
58
|
+
'User-Agent': `${originator}/0.0.1`,
|
|
59
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
60
|
+
...(extra || {}),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function decorate(blob, json) {
|
|
65
|
+
const idToken = (json && json.id_token) || blob.idToken || ''
|
|
66
|
+
return {
|
|
67
|
+
...blob,
|
|
68
|
+
accountId: blob.accountId || chatgptAccountId(idToken) || chatgptAccountId(blob.accessToken),
|
|
69
|
+
email: blob.email || emailFromToken(idToken) || emailFromToken(blob.accessToken),
|
|
70
|
+
label: blob.label || emailFromToken(idToken) || emailFromToken(blob.accessToken) || 'ChatGPT',
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function exchangeCode(cfg, pkce, code, fetchImpl) {
|
|
75
|
+
const json = await formTokenRequest(TOKEN, {
|
|
76
|
+
grant_type: 'authorization_code',
|
|
77
|
+
client_id: cfg.clientId,
|
|
78
|
+
code,
|
|
79
|
+
redirect_uri: cfg.redirectUri,
|
|
80
|
+
code_verifier: pkce.verifier,
|
|
81
|
+
}, fetchImpl)
|
|
82
|
+
return decorate(tokenBlobFromOAuth(json), json)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function refresh(cfg, blob, fetchImpl) {
|
|
86
|
+
const json = await formTokenRequest(TOKEN, {
|
|
87
|
+
grant_type: 'refresh_token',
|
|
88
|
+
client_id: cfg.clientId,
|
|
89
|
+
refresh_token: blob.refreshToken,
|
|
90
|
+
}, fetchImpl)
|
|
91
|
+
return decorate(tokenBlobFromOAuth(json, {
|
|
92
|
+
label: blob.label,
|
|
93
|
+
email: blob.email,
|
|
94
|
+
accountId: blob.accountId,
|
|
95
|
+
}), json)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function listModels(blob, cfg, fetchImpl) {
|
|
99
|
+
const catalog = modelCatalog(id, cfg.models || defaults().models)
|
|
100
|
+
const impl = fetchImpl || fetch
|
|
101
|
+
try {
|
|
102
|
+
const base = (cfg.baseUrl || defaults().baseUrl).replace(/\/$/, '')
|
|
103
|
+
const version = cfg.clientVersion || defaults().clientVersion
|
|
104
|
+
const res = await impl(`${base}/models?client_version=${encodeURIComponent(version)}`, {
|
|
105
|
+
headers: identityHeaders(blob, cfg, { Accept: 'application/json' }),
|
|
106
|
+
})
|
|
107
|
+
const json = await readJson(res)
|
|
108
|
+
const rows = []
|
|
109
|
+
for (const entry of json.models || json.data || []) {
|
|
110
|
+
const slug = entry && (entry.slug || entry.id)
|
|
111
|
+
if (!slug) continue
|
|
112
|
+
if (entry.visibility === 'hide' || entry.visibility === 'none') continue
|
|
113
|
+
const efforts = (entry.supported_reasoning_levels || [])
|
|
114
|
+
.map((level) => (typeof level === 'string' ? level : level && level.effort))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((effort) => ({ id: effort, name: effort }))
|
|
117
|
+
rows.push({
|
|
118
|
+
id: slug,
|
|
119
|
+
name: entry.display_name || slug,
|
|
120
|
+
priority: Number(entry.priority) || 0,
|
|
121
|
+
...(entry.description ? { description: entry.description } : {}),
|
|
122
|
+
...(entry.context_window ? { contextWindow: entry.context_window } : {}),
|
|
123
|
+
...(efforts.length ? { reasoning: { efforts } } : {}),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
if (!rows.length) throw new Error('empty codex catalog')
|
|
127
|
+
rows.sort((a, b) => a.priority - b.priority)
|
|
128
|
+
return modelCatalog(id, rows.map(({ priority, ...row }) => row))
|
|
129
|
+
} catch { /* catalog fallback */ }
|
|
130
|
+
return catalog
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function usage(blob, cfg, fetchImpl) {
|
|
134
|
+
try {
|
|
135
|
+
const res = await (fetchImpl || fetch)(USAGE, {
|
|
136
|
+
headers: identityHeaders(blob, cfg, { Accept: 'application/json' }),
|
|
137
|
+
})
|
|
138
|
+
const json = await readJson(res)
|
|
139
|
+
return asUsageSnapshot(deepestUsedPercent(json))
|
|
140
|
+
} catch {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
146
|
+
const base = (config.baseUrl || defaults().baseUrl).replace(/\/$/, '')
|
|
147
|
+
const body = codexResponsesBody(options, INSTRUCTIONS)
|
|
148
|
+
const res = await fetchImpl(`${base}/responses`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
...headers,
|
|
152
|
+
...identityHeaders(blob, config, {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
Accept: 'text/event-stream',
|
|
155
|
+
'session-id': randomUUID(),
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify(body),
|
|
159
|
+
signal,
|
|
160
|
+
})
|
|
161
|
+
if (!res.ok) throw httpError(res.status, await res.text())
|
|
162
|
+
yield* codexResponsesStream(res.body)
|
|
163
|
+
}
|