@khanglvm/llm-router 1.1.1 → 1.3.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.
@@ -1,12 +1,67 @@
1
1
  /**
2
- * OAuth authentication for ChatGPT Codex subscription providers.
3
- * Implements browser-based login and device code flow.
2
+ * OAuth authentication for subscription providers.
3
+ * Supports ChatGPT Codex and Claude Code OAuth flows.
4
4
  */
5
5
 
6
6
  import http from 'node:http';
7
7
  import crypto from 'node:crypto';
8
- import { CODEX_OAUTH_CONFIG } from './subscription-constants.js';
9
- import { saveTokens, loadTokens, isTokenExpired, deleteTokens, listTokenProfiles as listTokenProfilesFromStore } from './subscription-tokens.js';
8
+ import { spawn } from 'node:child_process';
9
+ import {
10
+ CODEX_OAUTH_CONFIG,
11
+ CLAUDE_CODE_OAUTH_CONFIG
12
+ } from './subscription-constants.js';
13
+ import {
14
+ saveTokens,
15
+ loadTokens,
16
+ isTokenExpired,
17
+ deleteTokens,
18
+ listTokenProfiles as listTokenProfilesFromStore
19
+ } from './subscription-tokens.js';
20
+
21
+ const SUBSCRIPTION_TYPE_CHATGPT_CODEX = 'chatgpt-codex';
22
+ const SUBSCRIPTION_TYPE_CLAUDE_CODE = 'claude-code';
23
+ const CLAUDE_PROFILE_PREFIX = `${SUBSCRIPTION_TYPE_CLAUDE_CODE}__`;
24
+
25
+ function normalizeSubscriptionType(value, { allowEmpty = false } = {}) {
26
+ const normalized = String(value || '').trim().toLowerCase();
27
+ if (!normalized) {
28
+ return allowEmpty ? '' : SUBSCRIPTION_TYPE_CHATGPT_CODEX;
29
+ }
30
+ if (normalized === SUBSCRIPTION_TYPE_CHATGPT_CODEX) return SUBSCRIPTION_TYPE_CHATGPT_CODEX;
31
+ if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) return SUBSCRIPTION_TYPE_CLAUDE_CODE;
32
+ throw new Error(`Unsupported subscription type '${value}'.`);
33
+ }
34
+
35
+ function resolveOAuthConfig(subscriptionType) {
36
+ const normalized = normalizeSubscriptionType(subscriptionType);
37
+ if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
38
+ return CLAUDE_CODE_OAUTH_CONFIG;
39
+ }
40
+ return CODEX_OAUTH_CONFIG;
41
+ }
42
+
43
+ function toTokenProfileKey(profileId, subscriptionType) {
44
+ const normalizedProfile = String(profileId || 'default').trim() || 'default';
45
+ const normalizedType = normalizeSubscriptionType(subscriptionType);
46
+ if (normalizedType === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
47
+ return `${CLAUDE_PROFILE_PREFIX}${normalizedProfile}`;
48
+ }
49
+ return normalizedProfile;
50
+ }
51
+
52
+ function fromTokenProfileKey(profileKey) {
53
+ const value = String(profileKey || '').trim();
54
+ if (value.startsWith(CLAUDE_PROFILE_PREFIX)) {
55
+ return {
56
+ profileId: value.slice(CLAUDE_PROFILE_PREFIX.length) || 'default',
57
+ subscriptionType: SUBSCRIPTION_TYPE_CLAUDE_CODE
58
+ };
59
+ }
60
+ return {
61
+ profileId: value || 'default',
62
+ subscriptionType: SUBSCRIPTION_TYPE_CHATGPT_CODEX
63
+ };
64
+ }
10
65
 
11
66
  /**
12
67
  * Generate PKCE code verifier and challenge.
@@ -27,22 +82,60 @@ function generateState() {
27
82
  return crypto.randomBytes(16).toString('hex');
28
83
  }
29
84
 
85
+ function tryOpenBrowser(url) {
86
+ const target = String(url || '').trim();
87
+ if (!target) return false;
88
+
89
+ try {
90
+ let child;
91
+ if (process.platform === 'darwin') {
92
+ child = spawn('open', [target], { stdio: 'ignore', detached: true });
93
+ } else if (process.platform === 'win32') {
94
+ child = spawn('cmd', ['/c', 'start', '', target], { stdio: 'ignore', detached: true });
95
+ } else {
96
+ child = spawn('xdg-open', [target], { stdio: 'ignore', detached: true });
97
+ }
98
+ child.unref();
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function normalizeTokenData(data, fallbackRefreshToken = undefined) {
106
+ return {
107
+ accessToken: data.access_token,
108
+ refreshToken: data.refresh_token || fallbackRefreshToken,
109
+ expiresAt: Date.now() + (Number(data.expires_in || 0) * 1000),
110
+ tokenType: data.token_type || 'Bearer',
111
+ scope: data.scope
112
+ };
113
+ }
114
+
30
115
  /**
31
116
  * Refresh an access token using refresh token.
32
117
  * @param {string} refreshToken - OAuth refresh token
118
+ * @param {Object} [options] - Options
119
+ * @param {string} [options.subscriptionType] - Subscription type
33
120
  * @returns {Promise<Object>} New token data
34
121
  */
35
- export async function refreshAccessToken(refreshToken) {
36
- const response = await fetch(CODEX_OAUTH_CONFIG.tokenUrl, {
122
+ export async function refreshAccessToken(refreshToken, options = {}) {
123
+ const config = resolveOAuthConfig(options.subscriptionType);
124
+ const body = {
125
+ grant_type: 'refresh_token',
126
+ refresh_token: refreshToken,
127
+ client_id: config.clientId
128
+ };
129
+ if (typeof config.scopes === 'string' && config.scopes.trim()) {
130
+ body.scope = config.scopes;
131
+ }
132
+
133
+ const response = await fetch(config.tokenUrl, {
37
134
  method: 'POST',
38
135
  headers: {
39
136
  'Content-Type': 'application/json'
40
137
  },
41
- body: JSON.stringify({
42
- grant_type: 'refresh_token',
43
- refresh_token: refreshToken,
44
- client_id: CODEX_OAUTH_CONFIG.clientId
45
- })
138
+ body: JSON.stringify(body)
46
139
  });
47
140
 
48
141
  if (!response.ok) {
@@ -51,38 +144,36 @@ export async function refreshAccessToken(refreshToken) {
51
144
  }
52
145
 
53
146
  const data = await response.json();
54
-
55
- return {
56
- accessToken: data.access_token,
57
- refreshToken: data.refresh_token || refreshToken,
58
- expiresAt: Date.now() + (data.expires_in * 1000),
59
- tokenType: data.token_type || 'Bearer',
60
- scope: data.scope
61
- };
147
+ return normalizeTokenData(data, refreshToken);
62
148
  }
63
149
 
64
150
  /**
65
151
  * Get valid access token for a profile, refreshing if needed.
66
152
  * @param {string} profileId - Provider profile ID
153
+ * @param {Object} [options] - Options
154
+ * @param {string} [options.subscriptionType] - Subscription type
67
155
  * @returns {Promise<string|null>} Valid access token or null
68
156
  */
69
- export async function getValidAccessToken(profileId) {
70
- const tokens = await loadTokens(profileId);
157
+ export async function getValidAccessToken(profileId, options = {}) {
158
+ const config = resolveOAuthConfig(options.subscriptionType);
159
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
160
+ const tokens = await loadTokens(tokenProfileKey);
71
161
  if (!tokens) return null;
72
162
 
73
163
  // Check if token needs refresh
74
- if (isTokenExpired(tokens, CODEX_OAUTH_CONFIG.tokenRefreshBufferMs)) {
164
+ if (isTokenExpired(tokens, config.tokenRefreshBufferMs)) {
75
165
  if (!tokens.refreshToken) {
76
- return null; // Cannot refresh without refresh token
166
+ return null;
77
167
  }
78
168
 
79
169
  try {
80
- const newTokens = await refreshAccessToken(tokens.refreshToken);
81
- await saveTokens(profileId, newTokens);
170
+ const newTokens = await refreshAccessToken(tokens.refreshToken, {
171
+ subscriptionType: options.subscriptionType
172
+ });
173
+ await saveTokens(tokenProfileKey, newTokens);
82
174
  return newTokens.accessToken;
83
175
  } catch {
84
- // Refresh failed, tokens are invalid
85
- await deleteTokens(profileId);
176
+ await deleteTokens(tokenProfileKey);
86
177
  return null;
87
178
  }
88
179
  }
@@ -95,21 +186,30 @@ export async function getValidAccessToken(profileId) {
95
186
  * @param {string} code - Authorization code
96
187
  * @param {string} codeVerifier - PKCE code verifier
97
188
  * @param {string} redirectUri - Redirect URI used in auth request
189
+ * @param {Object} [options] - Options
190
+ * @param {string} [options.subscriptionType] - Subscription type
191
+ * @param {string} [options.state] - OAuth state
98
192
  * @returns {Promise<Object>} Token data
99
193
  */
100
- async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
101
- const response = await fetch(CODEX_OAUTH_CONFIG.tokenUrl, {
194
+ async function exchangeCodeForTokens(code, codeVerifier, redirectUri, options = {}) {
195
+ const config = resolveOAuthConfig(options.subscriptionType);
196
+ const body = {
197
+ grant_type: 'authorization_code',
198
+ code,
199
+ code_verifier: codeVerifier,
200
+ redirect_uri: redirectUri,
201
+ client_id: config.clientId
202
+ };
203
+ if (typeof options.state === 'string' && options.state.trim()) {
204
+ body.state = options.state.trim();
205
+ }
206
+
207
+ const response = await fetch(config.tokenUrl, {
102
208
  method: 'POST',
103
209
  headers: {
104
210
  'Content-Type': 'application/json'
105
211
  },
106
- body: JSON.stringify({
107
- grant_type: 'authorization_code',
108
- code,
109
- code_verifier: codeVerifier,
110
- redirect_uri: redirectUri,
111
- client_id: CODEX_OAUTH_CONFIG.clientId
112
- })
212
+ body: JSON.stringify(body)
113
213
  });
114
214
 
115
215
  if (!response.ok) {
@@ -118,47 +218,57 @@ async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
118
218
  }
119
219
 
120
220
  const data = await response.json();
121
-
122
- return {
123
- accessToken: data.access_token,
124
- refreshToken: data.refresh_token,
125
- expiresAt: Date.now() + (data.expires_in * 1000),
126
- tokenType: data.token_type || 'Bearer',
127
- scope: data.scope
128
- };
221
+ return normalizeTokenData(data);
129
222
  }
130
223
 
131
224
  /**
132
225
  * Start browser-based OAuth login.
133
226
  * @param {string} profileId - Provider profile ID
134
227
  * @param {Object} [options] - Options
228
+ * @param {string} [options.subscriptionType] - Subscription type
135
229
  * @param {number} [options.port] - Callback server port
136
230
  * @param {function} [options.onUrl] - Callback when auth URL is ready
137
231
  * @returns {Promise<boolean>} Success status
138
232
  */
139
233
  export async function loginWithBrowser(profileId, options = {}) {
140
- const port = options.port || CODEX_OAUTH_CONFIG.callbackPort;
141
- const redirectUri = `http://localhost:${port}${CODEX_OAUTH_CONFIG.callbackPath}`;
142
-
234
+ const config = resolveOAuthConfig(options.subscriptionType);
235
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
236
+ const port = options.port || config.callbackPort;
237
+ const redirectUri = `http://localhost:${port}${config.callbackPath}`;
238
+
143
239
  const pkce = generatePKCE();
144
240
  const state = generateState();
145
-
146
- const authUrl = new URL(CODEX_OAUTH_CONFIG.authorizeUrl);
241
+
242
+ const authUrl = new URL(config.authorizeUrl);
147
243
  authUrl.searchParams.set('response_type', 'code');
148
- authUrl.searchParams.set('client_id', CODEX_OAUTH_CONFIG.clientId);
244
+ authUrl.searchParams.set('client_id', config.clientId);
149
245
  authUrl.searchParams.set('redirect_uri', redirectUri);
150
- authUrl.searchParams.set('scope', CODEX_OAUTH_CONFIG.scopes);
151
- authUrl.searchParams.set('audience', CODEX_OAUTH_CONFIG.audience);
246
+ authUrl.searchParams.set('scope', config.scopes);
152
247
  authUrl.searchParams.set('state', state);
153
248
  authUrl.searchParams.set('code_challenge', pkce.challenge);
154
249
  authUrl.searchParams.set('code_challenge_method', 'S256');
250
+ if (config.authorizeParams && typeof config.authorizeParams === 'object') {
251
+ for (const [key, value] of Object.entries(config.authorizeParams)) {
252
+ if (value !== undefined && value !== null && String(value).trim() !== '') {
253
+ authUrl.searchParams.set(key, String(value));
254
+ }
255
+ }
256
+ }
155
257
 
156
258
  return new Promise((resolve, reject) => {
259
+ let completed = false;
260
+ const finish = (fn) => {
261
+ if (completed) return;
262
+ completed = true;
263
+ clearTimeout(timeout);
264
+ fn();
265
+ };
266
+
157
267
  const server = http.createServer(async (req, res) => {
158
268
  try {
159
269
  const url = new URL(req.url, `http://localhost:${port}`);
160
-
161
- if (url.pathname !== CODEX_OAUTH_CONFIG.callbackPath) {
270
+
271
+ if (url.pathname !== config.callbackPath) {
162
272
  res.writeHead(404);
163
273
  res.end('Not found');
164
274
  return;
@@ -172,7 +282,7 @@ export async function loginWithBrowser(profileId, options = {}) {
172
282
  res.writeHead(400, { 'Content-Type': 'text/html' });
173
283
  res.end(`<h1>Authentication failed</h1><p>${error}</p>`);
174
284
  server.close();
175
- reject(new Error(`OAuth error: ${error}`));
285
+ finish(() => reject(new Error(`OAuth error: ${error}`)));
176
286
  return;
177
287
  }
178
288
 
@@ -180,38 +290,39 @@ export async function loginWithBrowser(profileId, options = {}) {
180
290
  res.writeHead(400, { 'Content-Type': 'text/html' });
181
291
  res.end('<h1>Invalid callback</h1><p>Missing or invalid state/code</p>');
182
292
  server.close();
183
- reject(new Error('Invalid OAuth callback'));
293
+ finish(() => reject(new Error('Invalid OAuth callback')));
184
294
  return;
185
295
  }
186
296
 
187
- // Exchange code for tokens
188
- const tokens = await exchangeCodeForTokens(code, pkce.verifier, redirectUri);
189
- await saveTokens(profileId, tokens);
297
+ const tokens = await exchangeCodeForTokens(code, pkce.verifier, redirectUri, {
298
+ subscriptionType: options.subscriptionType,
299
+ state
300
+ });
301
+ await saveTokens(tokenProfileKey, tokens);
190
302
 
191
303
  res.writeHead(200, { 'Content-Type': 'text/html' });
192
304
  res.end('<h1>Success!</h1><p>You can close this window and return to the terminal.</p>');
193
-
194
305
  server.close();
195
- resolve(true);
306
+ finish(() => resolve(true));
196
307
  } catch (err) {
197
308
  res.writeHead(500, { 'Content-Type': 'text/html' });
198
309
  res.end(`<h1>Error</h1><p>${err.message}</p>`);
199
310
  server.close();
200
- reject(err);
311
+ finish(() => reject(err));
201
312
  }
202
313
  });
203
314
 
204
315
  server.listen(port, () => {
205
316
  const authUrlStr = authUrl.toString();
317
+ const openedBrowser = options.autoOpen !== false ? tryOpenBrowser(authUrlStr) : false;
206
318
  if (options.onUrl) {
207
- options.onUrl(authUrlStr);
319
+ options.onUrl(authUrlStr, { openedBrowser });
208
320
  }
209
321
  });
210
322
 
211
- // Timeout after 5 minutes
212
- setTimeout(() => {
323
+ const timeout = setTimeout(() => {
213
324
  server.close();
214
- reject(new Error('Login timed out after 5 minutes'));
325
+ finish(() => reject(new Error('Login timed out after 5 minutes')));
215
326
  }, 5 * 60 * 1000);
216
327
  });
217
328
  }
@@ -219,20 +330,26 @@ export async function loginWithBrowser(profileId, options = {}) {
219
330
  /**
220
331
  * Start device code OAuth login (for headless environments).
221
332
  * @param {string} profileId - Provider profile ID
333
+ * @param {Object} [options] - Options
334
+ * @param {string} [options.subscriptionType] - Subscription type
222
335
  * @param {function} [options.onCode] - Callback when device code is ready
223
336
  * @returns {Promise<boolean>} Success status
224
337
  */
225
338
  export async function loginWithDeviceCode(profileId, options = {}) {
226
- // Request device code
227
- const response = await fetch(CODEX_OAUTH_CONFIG.deviceCodeUrl, {
339
+ const config = resolveOAuthConfig(options.subscriptionType);
340
+ if (!config.deviceCodeUrl) {
341
+ throw new Error(`Device code OAuth flow is not supported for subscription type '${normalizeSubscriptionType(options.subscriptionType)}'.`);
342
+ }
343
+
344
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
345
+ const response = await fetch(config.deviceCodeUrl, {
228
346
  method: 'POST',
229
347
  headers: {
230
348
  'Content-Type': 'application/x-www-form-urlencoded'
231
349
  },
232
350
  body: new URLSearchParams({
233
- client_id: CODEX_OAUTH_CONFIG.clientId,
234
- scope: CODEX_OAUTH_CONFIG.scopes,
235
- audience: CODEX_OAUTH_CONFIG.audience
351
+ client_id: config.clientId,
352
+ scope: config.scopes
236
353
  }).toString()
237
354
  });
238
355
 
@@ -248,18 +365,16 @@ export async function loginWithDeviceCode(profileId, options = {}) {
248
365
  const expiresIn = data.expires_in;
249
366
  const interval = data.interval || 5;
250
367
 
251
- // Notify user
252
368
  if (options.onCode) {
253
369
  options.onCode({ userCode, verificationUri, expiresIn });
254
370
  }
255
371
 
256
- // Poll for token
257
372
  const startTime = Date.now();
258
373
  while (Date.now() - startTime < expiresIn * 1000) {
259
- await new Promise(r => setTimeout(r, interval * 1000));
374
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
260
375
 
261
376
  try {
262
- const tokenResponse = await fetch(CODEX_OAUTH_CONFIG.tokenUrl, {
377
+ const tokenResponse = await fetch(config.tokenUrl, {
263
378
  method: 'POST',
264
379
  headers: {
265
380
  'Content-Type': 'application/json'
@@ -267,29 +382,22 @@ export async function loginWithDeviceCode(profileId, options = {}) {
267
382
  body: JSON.stringify({
268
383
  grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
269
384
  device_code: deviceCode,
270
- client_id: CODEX_OAUTH_CONFIG.clientId
385
+ client_id: config.clientId
271
386
  })
272
387
  });
273
388
 
274
389
  if (tokenResponse.ok) {
275
390
  const tokenData = await tokenResponse.json();
276
- const tokens = {
277
- accessToken: tokenData.access_token,
278
- refreshToken: tokenData.refresh_token,
279
- expiresAt: Date.now() + (tokenData.expires_in * 1000),
280
- tokenType: tokenData.token_type || 'Bearer',
281
- scope: tokenData.scope
282
- };
283
- await saveTokens(profileId, tokens);
391
+ await saveTokens(tokenProfileKey, normalizeTokenData(tokenData));
284
392
  return true;
285
393
  }
286
394
 
287
395
  const errorData = await tokenResponse.json();
288
396
  if (errorData.error === 'authorization_pending') {
289
- continue; // Keep polling
397
+ continue;
290
398
  }
291
399
  if (errorData.error === 'slow_down') {
292
- await new Promise(r => setTimeout(r, interval * 1000));
400
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
293
401
  continue;
294
402
  }
295
403
  throw new Error(`Token polling error: ${errorData.error}`);
@@ -307,32 +415,41 @@ export async function loginWithDeviceCode(profileId, options = {}) {
307
415
  /**
308
416
  * Logout (delete tokens) for a profile.
309
417
  * @param {string} profileId - Provider profile ID
418
+ * @param {Object} [options] - Options
419
+ * @param {string} [options.subscriptionType] - Subscription type
310
420
  */
311
- export async function logout(profileId) {
312
- await deleteTokens(profileId);
421
+ export async function logout(profileId, options = {}) {
422
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
423
+ await deleteTokens(tokenProfileKey);
313
424
  }
314
425
 
315
426
  /**
316
427
  * Check authentication status for a profile.
317
428
  * @param {string} profileId - Provider profile ID
429
+ * @param {Object} [options] - Options
430
+ * @param {string} [options.subscriptionType] - Subscription type
318
431
  * @returns {Promise<Object>} Status object
319
432
  */
320
- export async function getAuthStatus(profileId) {
321
- const tokens = await loadTokens(profileId);
322
-
433
+ export async function getAuthStatus(profileId, options = {}) {
434
+ const config = resolveOAuthConfig(options.subscriptionType);
435
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
436
+ const tokens = await loadTokens(tokenProfileKey);
437
+
323
438
  if (!tokens) {
324
439
  return {
325
440
  authenticated: false,
326
441
  profileId,
442
+ subscriptionType: normalizeSubscriptionType(options.subscriptionType),
327
443
  reason: 'No tokens found'
328
444
  };
329
445
  }
330
446
 
331
- const expired = isTokenExpired(tokens, CODEX_OAUTH_CONFIG.tokenRefreshBufferMs);
332
-
447
+ const expired = isTokenExpired(tokens, config.tokenRefreshBufferMs);
448
+
333
449
  return {
334
450
  authenticated: !expired,
335
451
  profileId,
452
+ subscriptionType: normalizeSubscriptionType(options.subscriptionType),
336
453
  expiresAt: tokens.expiresAt,
337
454
  expiresAtIso: new Date(tokens.expiresAt).toISOString(),
338
455
  expired,
@@ -342,8 +459,24 @@ export async function getAuthStatus(profileId) {
342
459
 
343
460
  /**
344
461
  * List all token profiles with stored subscription credentials.
462
+ * When subscriptionType is provided, returns only profiles for that type.
463
+ * @param {Object} [options] - Options
464
+ * @param {string} [options.subscriptionType] - Subscription type filter
345
465
  * @returns {Promise<string[]>} Profile IDs
346
466
  */
347
- export async function listTokenProfiles() {
348
- return listTokenProfilesFromStore();
467
+ export async function listTokenProfiles(options = {}) {
468
+ const requestedType = normalizeSubscriptionType(options.subscriptionType, { allowEmpty: true });
469
+ const profileKeys = await listTokenProfilesFromStore();
470
+ const visibleProfiles = [];
471
+ const seen = new Set();
472
+
473
+ for (const profileKey of profileKeys) {
474
+ const parsed = fromTokenProfileKey(profileKey);
475
+ if (requestedType && parsed.subscriptionType !== requestedType) continue;
476
+ if (seen.has(parsed.profileId)) continue;
477
+ seen.add(parsed.profileId);
478
+ visibleProfiles.push(parsed.profileId);
479
+ }
480
+
481
+ return visibleProfiles;
349
482
  }
@@ -1,29 +1,65 @@
1
1
  /**
2
2
  * Hardcoded Codex subscription models.
3
- * These are official ChatGPT Codex models that users cannot edit.
4
- * Updated via llm-router version releases to reflect OpenAI changes.
3
+ * These are used as the default seed list for ChatGPT subscription providers.
4
+ * Users can still customize the final saved model list.
5
5
  */
6
6
  export const CODEX_SUBSCRIPTION_MODELS = Object.freeze([
7
7
  'gpt-5.3-codex',
8
- 'gpt-5.2',
8
+ 'gpt-5.2-codex',
9
9
  'gpt-5.1-codex-mini'
10
10
  ]);
11
11
 
12
+ /**
13
+ * Hardcoded Claude Code subscription models.
14
+ * These defaults mirror current Claude Code model naming.
15
+ * Users can still customize the final saved model list.
16
+ */
17
+ export const CLAUDE_CODE_SUBSCRIPTION_MODELS = Object.freeze([
18
+ 'claude-sonnet-4-6',
19
+ 'claude-opus-4-6',
20
+ 'claude-haiku-4-5'
21
+ ]);
22
+
12
23
  /**
13
24
  * OAuth configuration for ChatGPT Codex subscription.
14
25
  */
15
26
  export const CODEX_OAUTH_CONFIG = Object.freeze({
16
- authorizeUrl: 'https://auth.openai.com/authorize',
27
+ authorizeUrl: 'https://auth.openai.com/oauth/authorize',
17
28
  tokenUrl: 'https://auth.openai.com/oauth/token',
18
29
  deviceCodeUrl: 'https://auth.openai.com/oauth/device/code',
19
30
  callbackPort: 1455,
20
- callbackPath: '/callback',
31
+ callbackPath: '/auth/callback',
21
32
  scopes: 'openid profile email offline_access',
22
- clientId: 'pdlLIX2Y72MIl2rhLhTE9VV9bN905kBh', // Public Codex CLI client ID
23
- audience: 'https://api.openai.com/v1',
33
+ clientId: 'app_EMoamEEZ73f0CkXaXp7hrann', // Matches current codex-cli browser login flow
34
+ authorizeParams: Object.freeze({
35
+ id_token_add_organizations: 'true',
36
+ codex_cli_simplified_flow: 'true',
37
+ originator: 'codex_cli_rs'
38
+ }),
24
39
  tokenRefreshBufferMs: 5 * 60 * 1000 // 5 minutes before expiration
25
40
  });
26
41
 
42
+ /**
43
+ * OAuth configuration for Claude Code subscription.
44
+ * Values align with the current Claude Code CLI runtime.
45
+ */
46
+ export const CLAUDE_CODE_OAUTH_CONFIG = Object.freeze({
47
+ authorizeUrl: 'https://claude.ai/oauth/authorize',
48
+ tokenUrl: 'https://platform.claude.com/v1/oauth/token',
49
+ callbackPort: 1456,
50
+ callbackPath: '/callback',
51
+ manualRedirectUrl: 'https://platform.claude.com/oauth/code/callback',
52
+ scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers',
53
+ clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
54
+ authorizeParams: Object.freeze({
55
+ code: 'true'
56
+ }),
57
+ oauthBeta: 'oauth-2025-04-20',
58
+ apiBaseUrl: 'https://api.anthropic.com',
59
+ messagesPath: '/v1/messages?beta=true',
60
+ tokenRefreshBufferMs: 5 * 60 * 1000
61
+ });
62
+
27
63
  /**
28
64
  * Token storage directory relative to home.
29
65
  */