@agi-cli/sdk 0.1.111 → 0.1.112

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.111",
3
+ "version": "0.1.112",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "ntishxyz",
6
6
  "license": "MIT",
@@ -68,3 +68,12 @@ export {
68
68
  openAuthUrl,
69
69
  createApiKey,
70
70
  } from './oauth.ts';
71
+
72
+ export {
73
+ authorizeOpenAI,
74
+ exchangeOpenAI,
75
+ refreshOpenAIToken,
76
+ openOpenAIAuthUrl,
77
+ obtainOpenAIApiKey,
78
+ type OpenAIOAuthResult,
79
+ } from './openai-oauth.ts';
@@ -0,0 +1,283 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes, createHash } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+
5
+ const OPENAI_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
6
+ const OPENAI_ISSUER = 'https://auth.openai.com';
7
+ const OPENAI_CALLBACK_PORT = 1455;
8
+
9
+ function generatePKCE() {
10
+ const verifier = randomBytes(32)
11
+ .toString('base64')
12
+ .replace(/\+/g, '-')
13
+ .replace(/\//g, '_')
14
+ .replace(/=/g, '');
15
+
16
+ const challenge = createHash('sha256')
17
+ .update(verifier)
18
+ .digest('base64')
19
+ .replace(/\+/g, '-')
20
+ .replace(/\//g, '_')
21
+ .replace(/=/g, '');
22
+
23
+ return { verifier, challenge };
24
+ }
25
+
26
+ function generateState() {
27
+ return randomBytes(32)
28
+ .toString('base64')
29
+ .replace(/\+/g, '-')
30
+ .replace(/\//g, '_')
31
+ .replace(/=/g, '');
32
+ }
33
+
34
+ async function openBrowser(url: string) {
35
+ const platform = process.platform;
36
+ let command: string;
37
+
38
+ switch (platform) {
39
+ case 'darwin':
40
+ command = `open "${url}"`;
41
+ break;
42
+ case 'win32':
43
+ command = `start "${url}"`;
44
+ break;
45
+ default:
46
+ command = `xdg-open "${url}"`;
47
+ break;
48
+ }
49
+
50
+ return new Promise<void>((resolve, reject) => {
51
+ const child = spawn(command, [], { shell: true });
52
+ child.on('error', reject);
53
+ child.on('exit', (code) => {
54
+ if (code === 0) resolve();
55
+ else reject(new Error(`Failed to open browser (exit code ${code})`));
56
+ });
57
+ });
58
+ }
59
+
60
+ export type OpenAIOAuthResult = {
61
+ url: string;
62
+ verifier: string;
63
+ waitForCallback: () => Promise<string>;
64
+ close: () => void;
65
+ };
66
+
67
+ export async function authorizeOpenAI(): Promise<OpenAIOAuthResult> {
68
+ const pkce = generatePKCE();
69
+ const state = generateState();
70
+ const redirectUri = `http://localhost:${OPENAI_CALLBACK_PORT}/auth/callback`;
71
+
72
+ const params = new URLSearchParams({
73
+ response_type: 'code',
74
+ client_id: OPENAI_CLIENT_ID,
75
+ redirect_uri: redirectUri,
76
+ scope: 'openid profile email offline_access',
77
+ code_challenge: pkce.challenge,
78
+ code_challenge_method: 'S256',
79
+ id_token_add_organizations: 'true',
80
+ codex_cli_simplified_flow: 'true',
81
+ state: state,
82
+ });
83
+
84
+ const authUrl = `${OPENAI_ISSUER}/oauth/authorize?${params.toString()}`;
85
+
86
+ let resolveCallback: (code: string) => void;
87
+ let rejectCallback: (error: Error) => void;
88
+ const callbackPromise = new Promise<string>((resolve, reject) => {
89
+ resolveCallback = resolve;
90
+ rejectCallback = reject;
91
+ });
92
+
93
+ const server = createServer((req, res) => {
94
+ const reqUrl = new URL(
95
+ req.url || '/',
96
+ `http://localhost:${OPENAI_CALLBACK_PORT}`,
97
+ );
98
+
99
+ if (reqUrl.pathname === '/auth/callback') {
100
+ const code = reqUrl.searchParams.get('code');
101
+ const returnedState = reqUrl.searchParams.get('state');
102
+ const error = reqUrl.searchParams.get('error');
103
+
104
+ if (error) {
105
+ res.writeHead(400, { 'Content-Type': 'text/html' });
106
+ res.end(
107
+ `<html><body><h1>Authentication Failed</h1><p>${error}</p></body></html>`,
108
+ );
109
+ rejectCallback(new Error(`OAuth error: ${error}`));
110
+ return;
111
+ }
112
+
113
+ if (returnedState !== state) {
114
+ res.writeHead(400, { 'Content-Type': 'text/html' });
115
+ res.end(
116
+ '<html><body><h1>Invalid State</h1><p>State mismatch. Please try again.</p></body></html>',
117
+ );
118
+ rejectCallback(new Error('State mismatch'));
119
+ return;
120
+ }
121
+
122
+ if (code) {
123
+ res.writeHead(200, { 'Content-Type': 'text/html' });
124
+ res.end(`
125
+ <html>
126
+ <head><title>AGI - Authentication Successful</title></head>
127
+ <body style="font-family: system-ui; text-align: center; padding: 50px;">
128
+ <h1>✅ Authentication Successful</h1>
129
+ <p>You can close this window and return to the terminal.</p>
130
+ </body>
131
+ </html>
132
+ `);
133
+ resolveCallback(code);
134
+ } else {
135
+ res.writeHead(400, { 'Content-Type': 'text/html' });
136
+ res.end('<html><body><h1>Missing Code</h1></body></html>');
137
+ rejectCallback(new Error('No authorization code received'));
138
+ }
139
+ } else {
140
+ res.writeHead(404);
141
+ res.end('Not found');
142
+ }
143
+ });
144
+
145
+ await new Promise<void>((resolve, reject) => {
146
+ server.on('error', (err: NodeJS.ErrnoException) => {
147
+ if (err.code === 'EADDRINUSE') {
148
+ reject(
149
+ new Error(
150
+ `Port ${OPENAI_CALLBACK_PORT} is already in use. Make sure no other OAuth flow is running (including the official Codex CLI).`,
151
+ ),
152
+ );
153
+ } else {
154
+ reject(err);
155
+ }
156
+ });
157
+ server.listen(OPENAI_CALLBACK_PORT, '127.0.0.1', () => resolve());
158
+ });
159
+
160
+ return {
161
+ url: authUrl,
162
+ verifier: pkce.verifier,
163
+ waitForCallback: () => callbackPromise,
164
+ close: () => {
165
+ server.close();
166
+ },
167
+ };
168
+ }
169
+
170
+ export async function exchangeOpenAI(code: string, verifier: string) {
171
+ const redirectUri = `http://localhost:${OPENAI_CALLBACK_PORT}/auth/callback`;
172
+
173
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/x-www-form-urlencoded',
177
+ },
178
+ body: new URLSearchParams({
179
+ grant_type: 'authorization_code',
180
+ code,
181
+ redirect_uri: redirectUri,
182
+ client_id: OPENAI_CLIENT_ID,
183
+ code_verifier: verifier,
184
+ }).toString(),
185
+ });
186
+
187
+ if (!response.ok) {
188
+ const error = await response.text();
189
+ throw new Error(`Token exchange failed: ${error}`);
190
+ }
191
+
192
+ const json = (await response.json()) as {
193
+ id_token: string;
194
+ access_token: string;
195
+ refresh_token: string;
196
+ expires_in?: number;
197
+ };
198
+
199
+ let accountId: string | undefined;
200
+ try {
201
+ const payload = JSON.parse(
202
+ Buffer.from(json.access_token.split('.')[1], 'base64').toString(),
203
+ );
204
+ accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id;
205
+ } catch {}
206
+
207
+ return {
208
+ idToken: json.id_token,
209
+ access: json.access_token,
210
+ refresh: json.refresh_token,
211
+ expires: Date.now() + (json.expires_in || 3600) * 1000,
212
+ accountId,
213
+ };
214
+ }
215
+
216
+ export async function refreshOpenAIToken(refreshToken: string) {
217
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify({
223
+ client_id: OPENAI_CLIENT_ID,
224
+ grant_type: 'refresh_token',
225
+ refresh_token: refreshToken,
226
+ scope: 'openid profile email',
227
+ }),
228
+ });
229
+
230
+ if (!response.ok) {
231
+ throw new Error('Failed to refresh OpenAI token');
232
+ }
233
+
234
+ const json = (await response.json()) as {
235
+ id_token?: string;
236
+ access_token?: string;
237
+ refresh_token?: string;
238
+ expires_in?: number;
239
+ };
240
+
241
+ return {
242
+ idToken: json.id_token,
243
+ access: json.access_token || '',
244
+ refresh: json.refresh_token || refreshToken,
245
+ expires: Date.now() + (json.expires_in || 3600) * 1000,
246
+ };
247
+ }
248
+
249
+ export async function openOpenAIAuthUrl(url: string) {
250
+ try {
251
+ await openBrowser(url);
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+
258
+ export async function obtainOpenAIApiKey(idToken: string): Promise<string> {
259
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
260
+ method: 'POST',
261
+ headers: {
262
+ 'Content-Type': 'application/x-www-form-urlencoded',
263
+ },
264
+ body: new URLSearchParams({
265
+ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
266
+ client_id: OPENAI_CLIENT_ID,
267
+ requested_token: 'openai-api-key',
268
+ subject_token: idToken,
269
+ subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
270
+ }).toString(),
271
+ });
272
+
273
+ if (!response.ok) {
274
+ const error = await response.text();
275
+ throw new Error(`API key exchange failed: ${error}`);
276
+ }
277
+
278
+ const json = (await response.json()) as {
279
+ access_token: string;
280
+ };
281
+
282
+ return json.access_token;
283
+ }
@@ -3,7 +3,12 @@ import { anthropic, createAnthropic } from '@ai-sdk/anthropic';
3
3
  import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
4
4
  import { createOpenRouter } from '@openrouter/ai-sdk-provider';
5
5
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
6
- import { catalog, createSolforgeModel } from '../../../providers/src/index.ts';
6
+ import {
7
+ catalog,
8
+ createSolforgeModel,
9
+ createOpenAIOAuthModel,
10
+ } from '../../../providers/src/index.ts';
11
+ import type { OAuth } from '../../../types/src/index.ts';
7
12
 
8
13
  export type ProviderName =
9
14
  | 'openai'
@@ -19,6 +24,8 @@ export type ModelConfig = {
19
24
  apiKey?: string;
20
25
  customFetch?: typeof fetch;
21
26
  baseURL?: string;
27
+ oauth?: OAuth;
28
+ projectRoot?: string;
22
29
  };
23
30
 
24
31
  export async function resolveModel(
@@ -27,6 +34,19 @@ export async function resolveModel(
27
34
  config: ModelConfig = {},
28
35
  ) {
29
36
  if (provider === 'openai') {
37
+ if (config.oauth) {
38
+ return createOpenAIOAuthModel(model, {
39
+ oauth: config.oauth,
40
+ projectRoot: config.projectRoot,
41
+ });
42
+ }
43
+ if (config.customFetch) {
44
+ const instance = createOpenAI({
45
+ apiKey: config.apiKey || 'oauth-token',
46
+ fetch: config.customFetch,
47
+ });
48
+ return instance(model);
49
+ }
30
50
  if (config.apiKey) {
31
51
  const instance = createOpenAI({ apiKey: config.apiKey });
32
52
  return instance(model);
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  providerIds,
41
41
  defaultModelFor,
42
42
  hasModel,
43
+ getFastModel,
43
44
  } from './providers/src/index.ts';
44
45
  export {
45
46
  isProviderAuthorized,
@@ -60,6 +61,11 @@ export type {
60
61
  SolforgeAuth,
61
62
  SolforgeProviderOptions,
62
63
  } from './providers/src/index.ts';
64
+ export {
65
+ createOpenAIOAuthFetch,
66
+ createOpenAIOAuthModel,
67
+ } from './providers/src/index.ts';
68
+ export type { OpenAIOAuthConfig } from './providers/src/index.ts';
63
69
 
64
70
  // =======================
65
71
  // Authentication (from internal auth module)
@@ -75,6 +81,14 @@ export {
75
81
  openAuthUrl,
76
82
  createApiKey,
77
83
  } from './auth/src/index.ts';
84
+ export {
85
+ authorizeOpenAI,
86
+ exchangeOpenAI,
87
+ refreshOpenAIToken,
88
+ openOpenAIAuthUrl,
89
+ obtainOpenAIApiKey,
90
+ } from './auth/src/index.ts';
91
+ export type { OpenAIOAuthResult } from './auth/src/index.ts';
78
92
 
79
93
  // =======================
80
94
  // Configuration (from internal config module)
@@ -559,7 +559,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
559
559
  label: 'GPT-5.1 Codex mini',
560
560
  modalities: {
561
561
  input: ['text', 'image'],
562
- output: ['text', 'image'],
562
+ output: ['text'],
563
563
  },
564
564
  toolCall: true,
565
565
  reasoning: true,
@@ -629,6 +629,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
629
629
  output: 16384,
630
630
  },
631
631
  },
632
+ {
633
+ id: 'gpt-5.2-codex',
634
+ label: 'GPT-5.2 Codex',
635
+ modalities: {
636
+ input: ['text', 'image'],
637
+ output: ['text'],
638
+ },
639
+ toolCall: true,
640
+ reasoning: true,
641
+ attachment: true,
642
+ temperature: false,
643
+ knowledge: '2025-08-31',
644
+ releaseDate: '2025-12-11',
645
+ lastUpdated: '2025-12-11',
646
+ openWeights: false,
647
+ cost: {
648
+ input: 1.75,
649
+ output: 14,
650
+ cacheRead: 0.175,
651
+ },
652
+ limit: {
653
+ context: 400000,
654
+ output: 128000,
655
+ },
656
+ },
632
657
  {
633
658
  id: 'gpt-5.2-pro',
634
659
  label: 'GPT-5.2 Pro',
@@ -4328,6 +4353,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
4328
4353
  output: 16384,
4329
4354
  },
4330
4355
  },
4356
+ {
4357
+ id: 'openai/gpt-5.2-codex',
4358
+ label: 'GPT-5.2-Codex',
4359
+ modalities: {
4360
+ input: ['text', 'image'],
4361
+ output: ['text'],
4362
+ },
4363
+ toolCall: true,
4364
+ reasoning: true,
4365
+ attachment: true,
4366
+ temperature: true,
4367
+ knowledge: '2025-08-31',
4368
+ releaseDate: '2026-01-14',
4369
+ lastUpdated: '2026-01-14',
4370
+ openWeights: false,
4371
+ cost: {
4372
+ input: 1.75,
4373
+ output: 14,
4374
+ cacheRead: 0.175,
4375
+ },
4376
+ limit: {
4377
+ context: 400000,
4378
+ output: 128000,
4379
+ },
4380
+ },
4331
4381
  {
4332
4382
  id: 'openai/gpt-5.2-pro',
4333
4383
  label: 'GPT-5.2 Pro',
@@ -4877,6 +4927,30 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
4877
4927
  output: 66536,
4878
4928
  },
4879
4929
  },
4930
+ {
4931
+ id: 'qwen/qwen3-coder-30b-a3b-instruct',
4932
+ label: 'Qwen3 Coder 30B A3B Instruct',
4933
+ modalities: {
4934
+ input: ['text'],
4935
+ output: ['text'],
4936
+ },
4937
+ toolCall: true,
4938
+ reasoning: false,
4939
+ attachment: false,
4940
+ temperature: true,
4941
+ knowledge: '2025-04',
4942
+ releaseDate: '2025-07-31',
4943
+ lastUpdated: '2025-07-31',
4944
+ openWeights: true,
4945
+ cost: {
4946
+ input: 0.07,
4947
+ output: 0.27,
4948
+ },
4949
+ limit: {
4950
+ context: 160000,
4951
+ output: 65536,
4952
+ },
4953
+ },
4880
4954
  {
4881
4955
  id: 'qwen/qwen3-coder-flash',
4882
4956
  label: 'Qwen3 Coder Flash',
@@ -5698,8 +5772,8 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
5698
5772
  attachment: true,
5699
5773
  temperature: true,
5700
5774
  knowledge: '2025-03-31',
5701
- releaseDate: '2025-11-01',
5702
- lastUpdated: '2025-11-01',
5775
+ releaseDate: '2025-11-24',
5776
+ lastUpdated: '2025-11-24',
5703
5777
  openWeights: false,
5704
5778
  cost: {
5705
5779
  input: 5,
@@ -5916,8 +5990,8 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
5916
5990
  attachment: true,
5917
5991
  temperature: false,
5918
5992
  knowledge: '2024-09-30',
5919
- releaseDate: '2025-08-07',
5920
- lastUpdated: '2025-08-07',
5993
+ releaseDate: '2025-09-15',
5994
+ lastUpdated: '2025-09-15',
5921
5995
  openWeights: false,
5922
5996
  cost: {
5923
5997
  input: 1.07,
@@ -5972,8 +6046,8 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
5972
6046
  attachment: true,
5973
6047
  temperature: false,
5974
6048
  knowledge: '2024-09-30',
5975
- releaseDate: '2025-11-12',
5976
- lastUpdated: '2025-11-12',
6049
+ releaseDate: '2025-11-13',
6050
+ lastUpdated: '2025-11-13',
5977
6051
  openWeights: false,
5978
6052
  cost: {
5979
6053
  input: 1.07,
@@ -6000,8 +6074,8 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
6000
6074
  attachment: true,
6001
6075
  temperature: false,
6002
6076
  knowledge: '2024-09-30',
6003
- releaseDate: '2025-11-12',
6004
- lastUpdated: '2025-11-12',
6077
+ releaseDate: '2025-11-13',
6078
+ lastUpdated: '2025-11-13',
6005
6079
  openWeights: false,
6006
6080
  cost: {
6007
6081
  input: 1.07,
@@ -6028,8 +6102,8 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
6028
6102
  attachment: true,
6029
6103
  temperature: false,
6030
6104
  knowledge: '2024-09-30',
6031
- releaseDate: '2025-11-12',
6032
- lastUpdated: '2025-11-12',
6105
+ releaseDate: '2025-11-13',
6106
+ lastUpdated: '2025-11-13',
6033
6107
  openWeights: false,
6034
6108
  cost: {
6035
6109
  input: 1.25,
@@ -6044,6 +6118,34 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
6044
6118
  npm: '@ai-sdk/openai',
6045
6119
  },
6046
6120
  },
6121
+ {
6122
+ id: 'gpt-5.1-codex-mini',
6123
+ label: 'GPT-5.1 Codex Mini',
6124
+ modalities: {
6125
+ input: ['text', 'image'],
6126
+ output: ['text'],
6127
+ },
6128
+ toolCall: true,
6129
+ reasoning: true,
6130
+ attachment: true,
6131
+ temperature: false,
6132
+ knowledge: '2024-09-30',
6133
+ releaseDate: '2025-11-13',
6134
+ lastUpdated: '2025-11-13',
6135
+ openWeights: false,
6136
+ cost: {
6137
+ input: 0.25,
6138
+ output: 2,
6139
+ cacheRead: 0.025,
6140
+ },
6141
+ limit: {
6142
+ context: 400000,
6143
+ output: 128000,
6144
+ },
6145
+ provider: {
6146
+ npm: '@ai-sdk/openai',
6147
+ },
6148
+ },
6047
6149
  {
6048
6150
  id: 'gpt-5.2',
6049
6151
  label: 'GPT-5.2',
@@ -6056,8 +6158,36 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
6056
6158
  attachment: true,
6057
6159
  temperature: false,
6058
6160
  knowledge: '2025-08-31',
6059
- releaseDate: '2025-11-12',
6060
- lastUpdated: '2025-11-12',
6161
+ releaseDate: '2025-12-11',
6162
+ lastUpdated: '2025-12-11',
6163
+ openWeights: false,
6164
+ cost: {
6165
+ input: 1.75,
6166
+ output: 14,
6167
+ cacheRead: 0.175,
6168
+ },
6169
+ limit: {
6170
+ context: 400000,
6171
+ output: 128000,
6172
+ },
6173
+ provider: {
6174
+ npm: '@ai-sdk/openai',
6175
+ },
6176
+ },
6177
+ {
6178
+ id: 'gpt-5.2-codex',
6179
+ label: 'GPT-5.2 Codex',
6180
+ modalities: {
6181
+ input: ['text', 'image'],
6182
+ output: ['text'],
6183
+ },
6184
+ toolCall: true,
6185
+ reasoning: true,
6186
+ attachment: true,
6187
+ temperature: false,
6188
+ knowledge: '2025-08-31',
6189
+ releaseDate: '2026-01-14',
6190
+ lastUpdated: '2026-01-14',
6061
6191
  openWeights: false,
6062
6192
  cost: {
6063
6193
  input: 1.75,
@@ -6146,6 +6276,34 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
6146
6276
  output: 262144,
6147
6277
  },
6148
6278
  },
6279
+ {
6280
+ id: 'minimax-m2.1-free',
6281
+ label: 'MiniMax M2.1',
6282
+ modalities: {
6283
+ input: ['text'],
6284
+ output: ['text'],
6285
+ },
6286
+ toolCall: true,
6287
+ reasoning: true,
6288
+ attachment: false,
6289
+ temperature: true,
6290
+ knowledge: '2025-01',
6291
+ releaseDate: '2025-12-23',
6292
+ lastUpdated: '2025-12-23',
6293
+ openWeights: true,
6294
+ cost: {
6295
+ input: 0,
6296
+ output: 0,
6297
+ cacheRead: 0,
6298
+ },
6299
+ limit: {
6300
+ context: 204800,
6301
+ output: 131072,
6302
+ },
6303
+ provider: {
6304
+ npm: '@ai-sdk/anthropic',
6305
+ },
6306
+ },
6149
6307
  {
6150
6308
  id: 'qwen3-coder',
6151
6309
  label: 'Qwen3 Coder',
@@ -6360,4 +6518,187 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
6360
6518
  api: 'https://api.z.ai/api/paas/v4',
6361
6519
  doc: 'https://docs.z.ai/guides/overview/pricing',
6362
6520
  },
6521
+ 'zai-coding': {
6522
+ id: 'zai-coding',
6523
+ models: [
6524
+ {
6525
+ id: 'glm-4.5',
6526
+ label: 'GLM-4.5',
6527
+ modalities: {
6528
+ input: ['text'],
6529
+ output: ['text'],
6530
+ },
6531
+ toolCall: true,
6532
+ reasoning: true,
6533
+ attachment: false,
6534
+ temperature: true,
6535
+ knowledge: '2025-04',
6536
+ releaseDate: '2025-07-28',
6537
+ lastUpdated: '2025-07-28',
6538
+ openWeights: true,
6539
+ cost: {
6540
+ input: 0,
6541
+ output: 0,
6542
+ cacheRead: 0,
6543
+ },
6544
+ limit: {
6545
+ context: 131072,
6546
+ output: 98304,
6547
+ },
6548
+ },
6549
+ {
6550
+ id: 'glm-4.5-air',
6551
+ label: 'GLM-4.5-Air',
6552
+ modalities: {
6553
+ input: ['text'],
6554
+ output: ['text'],
6555
+ },
6556
+ toolCall: true,
6557
+ reasoning: true,
6558
+ attachment: false,
6559
+ temperature: true,
6560
+ knowledge: '2025-04',
6561
+ releaseDate: '2025-07-28',
6562
+ lastUpdated: '2025-07-28',
6563
+ openWeights: true,
6564
+ cost: {
6565
+ input: 0,
6566
+ output: 0,
6567
+ cacheRead: 0,
6568
+ },
6569
+ limit: {
6570
+ context: 131072,
6571
+ output: 98304,
6572
+ },
6573
+ },
6574
+ {
6575
+ id: 'glm-4.5-flash',
6576
+ label: 'GLM-4.5-Flash',
6577
+ modalities: {
6578
+ input: ['text'],
6579
+ output: ['text'],
6580
+ },
6581
+ toolCall: true,
6582
+ reasoning: true,
6583
+ attachment: false,
6584
+ temperature: true,
6585
+ knowledge: '2025-04',
6586
+ releaseDate: '2025-07-28',
6587
+ lastUpdated: '2025-07-28',
6588
+ openWeights: true,
6589
+ cost: {
6590
+ input: 0,
6591
+ output: 0,
6592
+ cacheRead: 0,
6593
+ },
6594
+ limit: {
6595
+ context: 131072,
6596
+ output: 98304,
6597
+ },
6598
+ },
6599
+ {
6600
+ id: 'glm-4.5v',
6601
+ label: 'GLM-4.5V',
6602
+ modalities: {
6603
+ input: ['text', 'image', 'video'],
6604
+ output: ['text'],
6605
+ },
6606
+ toolCall: true,
6607
+ reasoning: true,
6608
+ attachment: true,
6609
+ temperature: true,
6610
+ knowledge: '2025-04',
6611
+ releaseDate: '2025-08-11',
6612
+ lastUpdated: '2025-08-11',
6613
+ openWeights: true,
6614
+ cost: {
6615
+ input: 0,
6616
+ output: 0,
6617
+ },
6618
+ limit: {
6619
+ context: 64000,
6620
+ output: 16384,
6621
+ },
6622
+ },
6623
+ {
6624
+ id: 'glm-4.6',
6625
+ label: 'GLM-4.6',
6626
+ modalities: {
6627
+ input: ['text'],
6628
+ output: ['text'],
6629
+ },
6630
+ toolCall: true,
6631
+ reasoning: true,
6632
+ attachment: false,
6633
+ temperature: true,
6634
+ knowledge: '2025-04',
6635
+ releaseDate: '2025-09-30',
6636
+ lastUpdated: '2025-09-30',
6637
+ openWeights: true,
6638
+ cost: {
6639
+ input: 0,
6640
+ output: 0,
6641
+ cacheRead: 0,
6642
+ },
6643
+ limit: {
6644
+ context: 204800,
6645
+ output: 131072,
6646
+ },
6647
+ },
6648
+ {
6649
+ id: 'glm-4.6v',
6650
+ label: 'GLM-4.6V',
6651
+ modalities: {
6652
+ input: ['text', 'image', 'video'],
6653
+ output: ['text'],
6654
+ },
6655
+ toolCall: true,
6656
+ reasoning: true,
6657
+ attachment: true,
6658
+ temperature: true,
6659
+ knowledge: '2025-04',
6660
+ releaseDate: '2025-12-08',
6661
+ lastUpdated: '2025-12-08',
6662
+ openWeights: true,
6663
+ cost: {
6664
+ input: 0,
6665
+ output: 0,
6666
+ },
6667
+ limit: {
6668
+ context: 128000,
6669
+ output: 32768,
6670
+ },
6671
+ },
6672
+ {
6673
+ id: 'glm-4.7',
6674
+ label: 'GLM-4.7',
6675
+ modalities: {
6676
+ input: ['text'],
6677
+ output: ['text'],
6678
+ },
6679
+ toolCall: true,
6680
+ reasoning: true,
6681
+ attachment: false,
6682
+ temperature: true,
6683
+ knowledge: '2025-04',
6684
+ releaseDate: '2025-12-22',
6685
+ lastUpdated: '2025-12-22',
6686
+ openWeights: true,
6687
+ cost: {
6688
+ input: 0,
6689
+ output: 0,
6690
+ cacheRead: 0,
6691
+ },
6692
+ limit: {
6693
+ context: 204800,
6694
+ output: 131072,
6695
+ },
6696
+ },
6697
+ ],
6698
+ label: 'Z.AI Coding Plan',
6699
+ env: ['ZHIPU_API_KEY'],
6700
+ npm: '@ai-sdk/openai-compatible',
6701
+ api: 'https://api.z.ai/api/coding/paas/v4',
6702
+ doc: 'https://docs.z.ai/devpack/overview',
6703
+ },
6363
6704
  } as const satisfies Partial<Record<ProviderId, ProviderCatalogEntry>>;
@@ -11,6 +11,7 @@ export {
11
11
  providerIds,
12
12
  defaultModelFor,
13
13
  hasModel,
14
+ getFastModel,
14
15
  } from './utils.ts';
15
16
  export { validateProviderModel } from './validate.ts';
16
17
  export { estimateModelCostUsd } from './pricing.ts';
@@ -23,3 +24,8 @@ export type {
23
24
  SolforgeAuth,
24
25
  SolforgeProviderOptions,
25
26
  } from './solforge-client.ts';
27
+ export {
28
+ createOpenAIOAuthFetch,
29
+ createOpenAIOAuthModel,
30
+ } from './openai-oauth-client.ts';
31
+ export type { OpenAIOAuthConfig } from './openai-oauth-client.ts';
@@ -0,0 +1,206 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import type { OAuth } from '../../types/src/index.ts';
3
+ import { refreshOpenAIToken } from '../../auth/src/openai-oauth.ts';
4
+ import { setAuth, getAuth } from '../../auth/src/index.ts';
5
+
6
+ const CHATGPT_BACKEND_URL = 'https://chatgpt.com/backend-api';
7
+ const OPENAI_API_URL = 'https://api.openai.com/v1';
8
+
9
+ const DEFAULT_INSTRUCTIONS = `You are a helpful coding assistant. Be concise and direct.`;
10
+
11
+ export type OpenAIOAuthConfig = {
12
+ oauth: OAuth;
13
+ projectRoot?: string;
14
+ instructions?: string;
15
+ reasoningEffort?: 'none' | 'low' | 'medium' | 'high' | 'xhigh';
16
+ reasoningSummary?: 'auto' | 'detailed';
17
+ };
18
+
19
+ async function ensureValidToken(
20
+ oauth: OAuth,
21
+ projectRoot?: string,
22
+ ): Promise<{ access: string; accountId?: string }> {
23
+ const bufferMs = 5 * 60 * 1000;
24
+ if (oauth.expires > Date.now() + bufferMs) {
25
+ return { access: oauth.access, accountId: oauth.accountId };
26
+ }
27
+
28
+ try {
29
+ const newTokens = await refreshOpenAIToken(oauth.refresh);
30
+ const updatedOAuth: OAuth = {
31
+ type: 'oauth',
32
+ access: newTokens.access,
33
+ refresh: newTokens.refresh,
34
+ expires: newTokens.expires,
35
+ accountId: oauth.accountId,
36
+ idToken: newTokens.idToken,
37
+ };
38
+ await setAuth('openai', updatedOAuth, projectRoot, 'global');
39
+ return { access: newTokens.access, accountId: oauth.accountId };
40
+ } catch {
41
+ return { access: oauth.access, accountId: oauth.accountId };
42
+ }
43
+ }
44
+
45
+ function stripIdsFromInput(input: unknown): unknown {
46
+ if (Array.isArray(input)) {
47
+ return input
48
+ .filter((item) => {
49
+ if (item && typeof item === 'object' && 'type' in item) {
50
+ if (item.type === 'item_reference') return false;
51
+ }
52
+ return true;
53
+ })
54
+ .map((item) => {
55
+ if (item && typeof item === 'object') {
56
+ const result: Record<string, unknown> = {};
57
+ for (const [key, value] of Object.entries(item)) {
58
+ if (key === 'id') continue;
59
+ result[key] = stripIdsFromInput(value);
60
+ }
61
+ return result;
62
+ }
63
+ return item;
64
+ });
65
+ }
66
+ if (input && typeof input === 'object') {
67
+ const result: Record<string, unknown> = {};
68
+ for (const [key, value] of Object.entries(input)) {
69
+ if (key === 'id') continue;
70
+ result[key] = stripIdsFromInput(value);
71
+ }
72
+ return result;
73
+ }
74
+ return input;
75
+ }
76
+
77
+ function rewriteUrl(url: string): string {
78
+ if (url.includes('/responses')) {
79
+ return url
80
+ .replace(OPENAI_API_URL, CHATGPT_BACKEND_URL)
81
+ .replace('/responses', '/codex/responses');
82
+ }
83
+ return url.replace(OPENAI_API_URL, CHATGPT_BACKEND_URL);
84
+ }
85
+
86
+ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
87
+ let currentOAuth = config.oauth;
88
+ const instructions = config.instructions || DEFAULT_INSTRUCTIONS;
89
+
90
+ const customFetch = async (
91
+ input: Parameters<typeof fetch>[0],
92
+ init?: Parameters<typeof fetch>[1],
93
+ ): Promise<Response> => {
94
+ const { access: accessToken, accountId } = await ensureValidToken(
95
+ currentOAuth,
96
+ config.projectRoot,
97
+ );
98
+
99
+ const originalUrl =
100
+ typeof input === 'string'
101
+ ? input
102
+ : input instanceof URL
103
+ ? input.href
104
+ : input.url;
105
+ const targetUrl = rewriteUrl(originalUrl);
106
+
107
+ let body = init?.body;
108
+ if (body && typeof body === 'string') {
109
+ try {
110
+ const parsed = JSON.parse(body);
111
+
112
+ parsed.store = false;
113
+ parsed.stream = true;
114
+ parsed.instructions = instructions;
115
+
116
+ if (parsed.input) {
117
+ parsed.input = stripIdsFromInput(parsed.input);
118
+ }
119
+
120
+ if (!parsed.include) {
121
+ parsed.include = ['reasoning.encrypted_content'];
122
+ } else if (
123
+ Array.isArray(parsed.include) &&
124
+ !parsed.include.includes('reasoning.encrypted_content')
125
+ ) {
126
+ parsed.include.push('reasoning.encrypted_content');
127
+ }
128
+
129
+ if (!parsed.reasoning) {
130
+ const providerOpts = parsed.providerOptions?.openai || {};
131
+ parsed.reasoning = {
132
+ effort:
133
+ providerOpts.reasoningEffort ||
134
+ config.reasoningEffort ||
135
+ 'medium',
136
+ summary:
137
+ providerOpts.reasoningSummary ||
138
+ config.reasoningSummary ||
139
+ 'auto',
140
+ };
141
+ } else {
142
+ const providerOpts = parsed.providerOptions?.openai || {};
143
+ if (!parsed.reasoning.effort) {
144
+ parsed.reasoning.effort =
145
+ providerOpts.reasoningEffort ||
146
+ config.reasoningEffort ||
147
+ 'medium';
148
+ }
149
+ if (!parsed.reasoning.summary) {
150
+ parsed.reasoning.summary =
151
+ providerOpts.reasoningSummary ||
152
+ config.reasoningSummary ||
153
+ 'auto';
154
+ }
155
+ }
156
+
157
+ delete parsed.max_output_tokens;
158
+ delete parsed.max_completion_tokens;
159
+
160
+ body = JSON.stringify(parsed);
161
+ } catch {}
162
+ }
163
+
164
+ const headers = new Headers(init?.headers);
165
+ headers.delete('x-api-key');
166
+ headers.set('Authorization', `Bearer ${accessToken}`);
167
+ headers.set('OpenAI-Beta', 'responses=experimental');
168
+ headers.set('originator', 'codex_cli_rs');
169
+ headers.set('accept', 'text/event-stream');
170
+ if (accountId) {
171
+ headers.set('chatgpt-account-id', accountId);
172
+ }
173
+
174
+ const response = await fetch(targetUrl, {
175
+ ...init,
176
+ body,
177
+ headers,
178
+ });
179
+
180
+ if (response.status === 401) {
181
+ const refreshed = await getAuth('openai', config.projectRoot);
182
+ if (refreshed?.type === 'oauth') {
183
+ currentOAuth = refreshed;
184
+ }
185
+ }
186
+
187
+ return response;
188
+ };
189
+
190
+ return customFetch as typeof fetch;
191
+ }
192
+
193
+ export function createOpenAIOAuthModel(
194
+ model: string,
195
+ config: OpenAIOAuthConfig,
196
+ ) {
197
+ const customFetch = createOpenAIOAuthFetch(config);
198
+
199
+ const provider = createOpenAI({
200
+ apiKey: 'chatgpt-oauth',
201
+ baseURL: CHATGPT_BACKEND_URL,
202
+ fetch: customFetch,
203
+ });
204
+
205
+ return provider(model);
206
+ }
@@ -22,3 +22,42 @@ export function hasModel(
22
22
  if (!model) return false;
23
23
  return listModels(provider).includes(model);
24
24
  }
25
+
26
+ const PREFERRED_FAST_MODELS: Partial<Record<ProviderId, string[]>> = {
27
+ openai: ['gpt-4o-mini', 'gpt-4.1-nano', 'gpt-4.1-mini'],
28
+ anthropic: [
29
+ 'claude-3-5-haiku-latest',
30
+ 'claude-3-5-haiku-20241022',
31
+ 'claude-haiku-4-5',
32
+ ],
33
+ google: [
34
+ 'gemini-2.0-flash-lite',
35
+ 'gemini-2.0-flash',
36
+ 'gemini-2.5-flash-lite',
37
+ ],
38
+ openrouter: [
39
+ 'openai/gpt-4o-mini',
40
+ 'google/gemini-2.0-flash-001',
41
+ 'anthropic/claude-3.5-haiku',
42
+ ],
43
+ opencode: ['gpt-5-nano', 'claude-3-5-haiku', 'gemini-3-flash'],
44
+ zai: ['glm-4.5-flash', 'glm-4.5-air'],
45
+ };
46
+
47
+ export function getFastModel(provider: ProviderId): string | undefined {
48
+ const providerModels = catalog[provider]?.models ?? [];
49
+ if (!providerModels.length) return undefined;
50
+
51
+ const preferred = PREFERRED_FAST_MODELS[provider] ?? [];
52
+ for (const modelId of preferred) {
53
+ if (providerModels.some((m) => m.id === modelId)) {
54
+ return modelId;
55
+ }
56
+ }
57
+
58
+ const sorted = [...providerModels]
59
+ .filter((m) => m.cost?.input !== undefined && m.toolCall !== false)
60
+ .sort((a, b) => (a.cost?.input ?? Infinity) - (b.cost?.input ?? Infinity));
61
+
62
+ return sorted[0]?.id ?? providerModels[0]?.id;
63
+ }
@@ -18,6 +18,8 @@ export type OAuth = {
18
18
  access: string;
19
19
  refresh: string;
20
20
  expires: number;
21
+ accountId?: string;
22
+ idToken?: string;
21
23
  };
22
24
 
23
25
  /**