jbai-cli 1.7.0 → 1.8.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/bin/jbai-claude.js +58 -65
- package/bin/jbai-codex.js +97 -91
- package/bin/jbai-gemini.js +53 -60
- package/bin/jbai-opencode.js +147 -156
- package/bin/jbai-proxy.js +47 -0
- package/bin/jbai.js +47 -6
- package/bin/test-models.js +56 -3
- package/lib/config.js +110 -6
- package/lib/ensure-token.js +31 -0
- package/package.json +1 -1
package/bin/jbai-claude.js
CHANGED
|
@@ -2,76 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
|
|
4
4
|
const config = require('../lib/config');
|
|
5
|
+
const { ensureToken } = require('../lib/ensure-token');
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
process.
|
|
10
|
-
|
|
7
|
+
(async () => {
|
|
8
|
+
const token = await ensureToken();
|
|
9
|
+
const endpoints = config.getEndpoints();
|
|
10
|
+
let args = process.argv.slice(2);
|
|
11
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
12
|
+
args = handoffConfig.args;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Check for super mode (--super, --yolo, -s)
|
|
15
|
+
const superFlags = ['--super', '--yolo', '-s'];
|
|
16
|
+
const superMode = args.some(a => superFlags.includes(a));
|
|
17
|
+
args = args.filter(a => !superFlags.includes(a));
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
args = handoffConfig.args;
|
|
19
|
+
// Check if model specified
|
|
20
|
+
const hasModel = args.includes('--model') || args.includes('-m');
|
|
21
|
+
let finalArgs = hasModel ? args : ['--model', config.MODELS.claude.default, ...args];
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Add super mode flags
|
|
24
|
+
if (superMode) {
|
|
25
|
+
finalArgs = ['--dangerously-skip-permissions', ...finalArgs];
|
|
26
|
+
console.log('🚀 Super mode: --dangerously-skip-permissions enabled');
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Set environment for Claude Code
|
|
30
|
+
// Use ANTHROPIC_CUSTOM_HEADERS for the Grazie auth header
|
|
31
|
+
// Remove /v1 from endpoint - Claude Code adds it automatically
|
|
32
|
+
const baseUrl = endpoints.anthropic.replace(/\/v1$/, '');
|
|
33
|
+
const env = {
|
|
34
|
+
...process.env,
|
|
35
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
36
|
+
ANTHROPIC_API_KEY: 'placeholder',
|
|
37
|
+
ANTHROPIC_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
|
|
38
|
+
};
|
|
30
39
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
finalArgs = ['--dangerously-skip-permissions', ...finalArgs];
|
|
34
|
-
console.log('🚀 Super mode: --dangerously-skip-permissions enabled');
|
|
35
|
-
}
|
|
40
|
+
// Remove any existing auth token that might conflict
|
|
41
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
50
|
-
|
|
51
|
-
const child = runWithHandoff({
|
|
52
|
-
command: 'claude',
|
|
53
|
-
args: finalArgs,
|
|
54
|
-
env,
|
|
55
|
-
toolName: 'jbai-claude',
|
|
56
|
-
handoffDefaults: {
|
|
57
|
-
enabled: !handoffConfig.disabled,
|
|
58
|
-
grazieToken: token,
|
|
59
|
-
grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
60
|
-
grazieModel: config.MODELS.claude.default,
|
|
61
|
-
cwd: process.cwd(),
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (child && typeof child.on === 'function') {
|
|
66
|
-
child.on('error', (err) => {
|
|
67
|
-
if (err.code === 'ENOENT') {
|
|
68
|
-
const tool = config.TOOLS.claude;
|
|
69
|
-
console.error(`❌ ${tool.name} not found.\n`);
|
|
70
|
-
console.error(`Install with: ${tool.install}`);
|
|
71
|
-
console.error(`Or run: jbai install claude`);
|
|
72
|
-
} else {
|
|
73
|
-
console.error(`Error: ${err.message}`);
|
|
74
|
-
}
|
|
75
|
-
process.exit(1);
|
|
43
|
+
const child = runWithHandoff({
|
|
44
|
+
command: 'claude',
|
|
45
|
+
args: finalArgs,
|
|
46
|
+
env,
|
|
47
|
+
toolName: 'jbai-claude',
|
|
48
|
+
handoffDefaults: {
|
|
49
|
+
enabled: !handoffConfig.disabled,
|
|
50
|
+
grazieToken: token,
|
|
51
|
+
grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
52
|
+
grazieModel: config.MODELS.claude.default,
|
|
53
|
+
cwd: process.cwd(),
|
|
54
|
+
},
|
|
76
55
|
});
|
|
77
|
-
|
|
56
|
+
|
|
57
|
+
if (child && typeof child.on === 'function') {
|
|
58
|
+
child.on('error', (err) => {
|
|
59
|
+
if (err.code === 'ENOENT') {
|
|
60
|
+
const tool = config.TOOLS.claude;
|
|
61
|
+
console.error(`❌ ${tool.name} not found.\n`);
|
|
62
|
+
console.error(`Install with: ${tool.install}`);
|
|
63
|
+
console.error(`Or run: jbai install claude`);
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`Error: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
})();
|
package/bin/jbai-codex.js
CHANGED
|
@@ -5,47 +5,39 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const config = require('../lib/config');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if our provider is configured
|
|
42
|
-
let configContent = '';
|
|
43
|
-
if (fs.existsSync(codexConfig)) {
|
|
44
|
-
configContent = fs.readFileSync(codexConfig, 'utf-8');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!configContent.includes(`[model_providers.${providerName}]`)) {
|
|
48
|
-
const providerConfig = `
|
|
8
|
+
const { ensureToken } = require('../lib/ensure-token');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const token = await ensureToken();
|
|
12
|
+
const endpoints = config.getEndpoints();
|
|
13
|
+
const environment = config.getEnvironment();
|
|
14
|
+
const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
|
|
15
|
+
const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
|
|
16
|
+
let args = process.argv.slice(2);
|
|
17
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
18
|
+
args = handoffConfig.args;
|
|
19
|
+
|
|
20
|
+
// Check for super mode (--super, --yolo, -s)
|
|
21
|
+
const superFlags = ['--super', '--yolo', '-s'];
|
|
22
|
+
const superMode = args.some(a => superFlags.includes(a));
|
|
23
|
+
args = args.filter(a => !superFlags.includes(a));
|
|
24
|
+
|
|
25
|
+
// Ensure Codex config exists
|
|
26
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
27
|
+
const codexConfig = path.join(codexDir, 'config.toml');
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(codexDir)) {
|
|
30
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if our provider is configured
|
|
34
|
+
let configContent = '';
|
|
35
|
+
if (fs.existsSync(codexConfig)) {
|
|
36
|
+
configContent = fs.readFileSync(codexConfig, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!configContent.includes(`[model_providers.${providerName}]`)) {
|
|
40
|
+
const providerConfig = `
|
|
49
41
|
# JetBrains AI (${environment})
|
|
50
42
|
[model_providers.${providerName}]
|
|
51
43
|
name = "JetBrains AI (${environment})"
|
|
@@ -53,54 +45,68 @@ base_url = "${endpoints.openai}"
|
|
|
53
45
|
env_http_headers = { "Grazie-Authenticate-JWT" = "${envVarName}" }
|
|
54
46
|
wire_api = "responses"
|
|
55
47
|
`;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
48
|
+
fs.appendFileSync(codexConfig, providerConfig);
|
|
49
|
+
console.log(`✅ Added ${providerName} provider to Codex config`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Point Codex model picker to our proxy (serves our model list instead of chatgpt.com)
|
|
53
|
+
const proxyModelUrl = 'http://localhost:18080/';
|
|
54
|
+
if (!configContent.includes('chatgpt_base_url')) {
|
|
55
|
+
fs.appendFileSync(codexConfig, `\nchatgpt_base_url = "${proxyModelUrl}"\n`);
|
|
56
|
+
console.log('✅ Set chatgpt_base_url to jbai-proxy for model picker');
|
|
57
|
+
} else if (!configContent.includes(proxyModelUrl)) {
|
|
58
|
+
// Update existing chatgpt_base_url
|
|
59
|
+
configContent = fs.readFileSync(codexConfig, 'utf-8');
|
|
60
|
+
const updated = configContent.replace(/^chatgpt_base_url\s*=\s*"[^"]*"/m, `chatgpt_base_url = "${proxyModelUrl}"`);
|
|
61
|
+
fs.writeFileSync(codexConfig, updated);
|
|
62
|
+
console.log('✅ Updated chatgpt_base_url to jbai-proxy');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hasModel = args.includes('--model');
|
|
66
|
+
const finalArgs = ['-c', `model_provider=${providerName}`];
|
|
67
|
+
|
|
68
|
+
if (!hasModel) {
|
|
69
|
+
finalArgs.push('--model', config.MODELS.codex?.default || config.MODELS.openai.default);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add super mode flags (full-auto)
|
|
73
|
+
if (superMode) {
|
|
74
|
+
finalArgs.push('--full-auto');
|
|
75
|
+
console.log('🚀 Super mode: --full-auto enabled');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
finalArgs.push(...args);
|
|
79
|
+
|
|
80
|
+
const childEnv = {
|
|
81
|
+
...process.env,
|
|
82
|
+
[envVarName]: token
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const child = runWithHandoff({
|
|
86
|
+
command: 'codex',
|
|
87
|
+
args: finalArgs,
|
|
88
|
+
env: childEnv,
|
|
89
|
+
toolName: 'jbai-codex',
|
|
90
|
+
handoffDefaults: {
|
|
91
|
+
enabled: !handoffConfig.disabled,
|
|
92
|
+
grazieToken: token,
|
|
93
|
+
grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
94
|
+
grazieModel: config.MODELS.claude.default,
|
|
95
|
+
cwd: process.cwd(),
|
|
96
|
+
},
|
|
105
97
|
});
|
|
106
|
-
|
|
98
|
+
|
|
99
|
+
if (child && typeof child.on === 'function') {
|
|
100
|
+
child.on('error', (err) => {
|
|
101
|
+
if (err.code === 'ENOENT') {
|
|
102
|
+
const tool = config.TOOLS.codex;
|
|
103
|
+
console.error(`❌ ${tool.name} not found.\n`);
|
|
104
|
+
console.error(`Install with: ${tool.install}`);
|
|
105
|
+
console.error(`Or run: jbai install codex`);
|
|
106
|
+
} else {
|
|
107
|
+
console.error(`Error: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
})();
|
package/bin/jbai-gemini.js
CHANGED
|
@@ -8,70 +8,63 @@
|
|
|
8
8
|
|
|
9
9
|
const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
|
|
10
10
|
const config = require('../lib/config');
|
|
11
|
+
const { ensureToken } = require('../lib/ensure-token');
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
process.
|
|
16
|
-
|
|
13
|
+
(async () => {
|
|
14
|
+
const token = await ensureToken();
|
|
15
|
+
const endpoints = config.getEndpoints();
|
|
16
|
+
let args = process.argv.slice(2);
|
|
17
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
18
|
+
args = handoffConfig.args;
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Check for super mode (--super, --yolo, -s)
|
|
21
|
+
const superFlags = ['--super', '--yolo', '-s'];
|
|
22
|
+
const superMode = args.some(a => superFlags.includes(a));
|
|
23
|
+
args = args.filter(a => !superFlags.includes(a));
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
args = handoffConfig.args;
|
|
25
|
+
// Check if model specified
|
|
26
|
+
const hasModel = args.includes('--model') || args.includes('-m');
|
|
27
|
+
let finalArgs = hasModel ? args : ['--model', config.MODELS.gemini.default, ...args];
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
// Add super mode flags (auto-confirm) - Gemini uses --yolo
|
|
30
|
+
if (superMode) {
|
|
31
|
+
finalArgs = ['--yolo', ...finalArgs];
|
|
32
|
+
console.log('🚀 Super mode: --yolo (auto-confirm) enabled');
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Set environment for Gemini CLI
|
|
36
|
+
// Uses GEMINI_CLI_CUSTOM_HEADERS for auth (supported since Nov 2025)
|
|
37
|
+
const env = {
|
|
38
|
+
...process.env,
|
|
39
|
+
GEMINI_BASE_URL: endpoints.google,
|
|
40
|
+
GEMINI_API_KEY: 'placeholder',
|
|
41
|
+
GEMINI_CLI_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
|
|
42
|
+
};
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
GEMINI_CLI_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const child = runWithHandoff({
|
|
53
|
-
command: 'gemini',
|
|
54
|
-
args: finalArgs,
|
|
55
|
-
env,
|
|
56
|
-
toolName: 'jbai-gemini',
|
|
57
|
-
handoffDefaults: {
|
|
58
|
-
enabled: !handoffConfig.disabled,
|
|
59
|
-
grazieToken: token,
|
|
60
|
-
grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
61
|
-
grazieModel: config.MODELS.claude.default,
|
|
62
|
-
cwd: process.cwd(),
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if (child && typeof child.on === 'function') {
|
|
67
|
-
child.on('error', (err) => {
|
|
68
|
-
if (err.code === 'ENOENT') {
|
|
69
|
-
console.error(`❌ Gemini CLI not found.\n`);
|
|
70
|
-
console.error(`Install with: npm install -g @google/gemini-cli`);
|
|
71
|
-
console.error(`Or run: jbai install gemini`);
|
|
72
|
-
} else {
|
|
73
|
-
console.error(`Error: ${err.message}`);
|
|
74
|
-
}
|
|
75
|
-
process.exit(1);
|
|
44
|
+
const child = runWithHandoff({
|
|
45
|
+
command: 'gemini',
|
|
46
|
+
args: finalArgs,
|
|
47
|
+
env,
|
|
48
|
+
toolName: 'jbai-gemini',
|
|
49
|
+
handoffDefaults: {
|
|
50
|
+
enabled: !handoffConfig.disabled,
|
|
51
|
+
grazieToken: token,
|
|
52
|
+
grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
53
|
+
grazieModel: config.MODELS.claude.default,
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
},
|
|
76
56
|
});
|
|
77
|
-
|
|
57
|
+
|
|
58
|
+
if (child && typeof child.on === 'function') {
|
|
59
|
+
child.on('error', (err) => {
|
|
60
|
+
if (err.code === 'ENOENT') {
|
|
61
|
+
console.error(`❌ Gemini CLI not found.\n`);
|
|
62
|
+
console.error(`Install with: npm install -g @google/gemini-cli`);
|
|
63
|
+
console.error(`Or run: jbai install gemini`);
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`Error: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
})();
|
package/bin/jbai-opencode.js
CHANGED
|
@@ -5,164 +5,155 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const config = require('../lib/config');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
args = args.filter(a => !superFlags.includes(a));
|
|
30
|
-
|
|
31
|
-
// Setup OpenCode config with JetBrains provider
|
|
32
|
-
const configDir = path.join(os.homedir(), '.config', 'opencode');
|
|
33
|
-
const configFile = path.join(configDir, 'opencode.json');
|
|
34
|
-
|
|
35
|
-
if (!fs.existsSync(configDir)) {
|
|
36
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Create or update OpenCode config with JetBrains provider
|
|
40
|
-
const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
|
|
41
|
-
let opencodeConfig = {};
|
|
42
|
-
|
|
43
|
-
if (fs.existsSync(configFile)) {
|
|
44
|
-
try {
|
|
45
|
-
opencodeConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
|
|
46
|
-
} catch {
|
|
47
|
-
opencodeConfig = {};
|
|
8
|
+
const { ensureToken } = require('../lib/ensure-token');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const token = await ensureToken();
|
|
12
|
+
const endpoints = config.getEndpoints();
|
|
13
|
+
const environment = config.getEnvironment();
|
|
14
|
+
let args = process.argv.slice(2);
|
|
15
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
16
|
+
args = handoffConfig.args;
|
|
17
|
+
|
|
18
|
+
// Check for super mode (--super, --yolo, -s)
|
|
19
|
+
const superFlags = ['--super', '--yolo', '-s'];
|
|
20
|
+
const superMode = args.some(a => superFlags.includes(a));
|
|
21
|
+
args = args.filter(a => !superFlags.includes(a));
|
|
22
|
+
|
|
23
|
+
// Setup OpenCode config with JetBrains provider
|
|
24
|
+
const configDir = path.join(os.homedir(), '.config', 'opencode');
|
|
25
|
+
const configFile = path.join(configDir, 'opencode.json');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(configDir)) {
|
|
28
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
48
29
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Provider names for OpenAI and Anthropic
|
|
60
|
-
const anthropicProviderName = environment === 'staging' ? 'jbai-anthropic-staging' : 'jbai-anthropic';
|
|
61
|
-
|
|
62
|
-
// Add/update JetBrains OpenAI provider with custom header (using env var reference)
|
|
63
|
-
// Use OpenAI SDK to support max_completion_tokens for GPT-5.x
|
|
64
|
-
opencodeConfig.provider[providerName] = {
|
|
65
|
-
npm: '@ai-sdk/openai',
|
|
66
|
-
name: `JetBrains AI OpenAI (${environment})`,
|
|
67
|
-
options: {
|
|
68
|
-
baseURL: endpoints.openai,
|
|
69
|
-
apiKey: `{env:${envVarName}}`,
|
|
70
|
-
headers: {
|
|
71
|
-
'Grazie-Authenticate-JWT': `{env:${envVarName}}`
|
|
30
|
+
|
|
31
|
+
// Create or update OpenCode config with JetBrains provider
|
|
32
|
+
const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
|
|
33
|
+
let opencodeConfig = {};
|
|
34
|
+
|
|
35
|
+
if (fs.existsSync(configFile)) {
|
|
36
|
+
try {
|
|
37
|
+
opencodeConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
|
|
38
|
+
} catch {
|
|
39
|
+
opencodeConfig = {};
|
|
72
40
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Ensure provider section exists
|
|
44
|
+
if (!opencodeConfig.provider) {
|
|
45
|
+
opencodeConfig.provider = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Environment variable name for the token
|
|
49
|
+
const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
|
|
50
|
+
|
|
51
|
+
// Provider names for OpenAI and Anthropic
|
|
52
|
+
const anthropicProviderName = environment === 'staging' ? 'jbai-anthropic-staging' : 'jbai-anthropic';
|
|
53
|
+
|
|
54
|
+
// Add/update JetBrains OpenAI provider with custom header (using env var reference)
|
|
55
|
+
// Use OpenAI SDK to support max_completion_tokens for GPT-5.x
|
|
56
|
+
opencodeConfig.provider[providerName] = {
|
|
57
|
+
npm: '@ai-sdk/openai',
|
|
58
|
+
name: `JetBrains AI OpenAI (${environment})`,
|
|
59
|
+
options: {
|
|
60
|
+
baseURL: endpoints.openai,
|
|
61
|
+
apiKey: `{env:${envVarName}}`,
|
|
62
|
+
headers: {
|
|
63
|
+
'Grazie-Authenticate-JWT': `{env:${envVarName}}`
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
models: {}
|
|
87
67
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
68
|
+
|
|
69
|
+
// Add OpenAI models
|
|
70
|
+
// OpenCode requires an output limit in the config.
|
|
71
|
+
// For O-series models we keep a larger context window but still set output.
|
|
72
|
+
config.MODELS.openai.available.forEach(model => {
|
|
73
|
+
const isOSeries = /^o[1-9]/.test(model);
|
|
74
|
+
opencodeConfig.provider[providerName].models[model] = {
|
|
75
|
+
name: model,
|
|
76
|
+
limit: isOSeries
|
|
77
|
+
? { context: 200000, output: 8192 }
|
|
78
|
+
: { context: 128000, output: 8192 }
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Add JetBrains Anthropic provider for Claude models
|
|
83
|
+
opencodeConfig.provider[anthropicProviderName] = {
|
|
84
|
+
npm: '@ai-sdk/anthropic',
|
|
85
|
+
name: `JetBrains AI Anthropic (${environment})`,
|
|
86
|
+
options: {
|
|
87
|
+
baseURL: endpoints.anthropic,
|
|
88
|
+
apiKey: `{env:${envVarName}}`,
|
|
89
|
+
headers: {
|
|
90
|
+
'Grazie-Authenticate-JWT': `{env:${envVarName}}`
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
models: {}
|
|
109
94
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Write config
|
|
118
|
-
fs.writeFileSync(configFile, JSON.stringify(opencodeConfig, null, 2));
|
|
119
|
-
|
|
120
|
-
// Check if model specified
|
|
121
|
-
const hasModel = args.includes('--model') || args.includes('-m');
|
|
122
|
-
let finalArgs = [];
|
|
123
|
-
|
|
124
|
-
if (!hasModel) {
|
|
125
|
-
// Use provider/model format for OpenCode
|
|
126
|
-
finalArgs.push('--model', `${providerName}/${config.MODELS.openai.default}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Note: OpenCode run is already non-interactive, no super mode needed
|
|
130
|
-
if (superMode) {
|
|
131
|
-
console.log('ℹ️ OpenCode run is already non-interactive (no super mode flag needed)');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
finalArgs.push(...args);
|
|
135
|
-
|
|
136
|
-
// Set the token in environment variable for OpenCode config to reference
|
|
137
|
-
const childEnv = {
|
|
138
|
-
...process.env,
|
|
139
|
-
[envVarName]: token
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const child = runWithHandoff({
|
|
143
|
-
command: 'opencode',
|
|
144
|
-
args: finalArgs,
|
|
145
|
-
env: childEnv,
|
|
146
|
-
toolName: 'jbai-opencode',
|
|
147
|
-
handoffDefaults: {
|
|
148
|
-
enabled: !handoffConfig.disabled,
|
|
149
|
-
grazieToken: token,
|
|
150
|
-
grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
151
|
-
grazieModel: config.MODELS.claude.default,
|
|
152
|
-
cwd: process.cwd(),
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (child && typeof child.on === 'function') {
|
|
157
|
-
child.on('error', (err) => {
|
|
158
|
-
if (err.code === 'ENOENT') {
|
|
159
|
-
const tool = config.TOOLS.opencode;
|
|
160
|
-
console.error(`❌ ${tool.name} not found.\n`);
|
|
161
|
-
console.error(`Install with: ${tool.install}`);
|
|
162
|
-
console.error(`Or run: jbai install opencode`);
|
|
163
|
-
} else {
|
|
164
|
-
console.error(`Error: ${err.message}`);
|
|
165
|
-
}
|
|
166
|
-
process.exit(1);
|
|
95
|
+
|
|
96
|
+
// Add Claude models to Anthropic provider
|
|
97
|
+
config.MODELS.claude.available.forEach(model => {
|
|
98
|
+
opencodeConfig.provider[anthropicProviderName].models[model] = {
|
|
99
|
+
name: model,
|
|
100
|
+
limit: { context: 200000, output: 8192 }
|
|
101
|
+
};
|
|
167
102
|
});
|
|
168
|
-
|
|
103
|
+
|
|
104
|
+
// NOTE: Gemini models are NOT available via Grazie OpenAI-compatible proxy.
|
|
105
|
+
// The /user/v5/llm/google/v1/vertex endpoint only works with native Google API
|
|
106
|
+
// format (used by jbai-gemini), not OpenAI chat/completions format.
|
|
107
|
+
// Use `jbai gemini` instead for Gemini models.
|
|
108
|
+
|
|
109
|
+
// Enable max permissions (yolo mode) by default
|
|
110
|
+
opencodeConfig.yolo = true;
|
|
111
|
+
|
|
112
|
+
// Write config
|
|
113
|
+
fs.writeFileSync(configFile, JSON.stringify(opencodeConfig, null, 2));
|
|
114
|
+
|
|
115
|
+
// Check if model specified
|
|
116
|
+
const hasModel = args.includes('--model') || args.includes('-m');
|
|
117
|
+
let finalArgs = ['--yolo'];
|
|
118
|
+
|
|
119
|
+
if (!hasModel) {
|
|
120
|
+
// Use provider/model format for OpenCode
|
|
121
|
+
finalArgs.push('--model', `${providerName}/${config.MODELS.openai.default}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
finalArgs.push(...args);
|
|
125
|
+
|
|
126
|
+
// Set the token in environment variable for OpenCode config to reference
|
|
127
|
+
const childEnv = {
|
|
128
|
+
...process.env,
|
|
129
|
+
[envVarName]: token
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const child = runWithHandoff({
|
|
133
|
+
command: 'opencode',
|
|
134
|
+
args: finalArgs,
|
|
135
|
+
env: childEnv,
|
|
136
|
+
toolName: 'jbai-opencode',
|
|
137
|
+
handoffDefaults: {
|
|
138
|
+
enabled: !handoffConfig.disabled,
|
|
139
|
+
grazieToken: token,
|
|
140
|
+
grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
141
|
+
grazieModel: config.MODELS.claude.default,
|
|
142
|
+
cwd: process.cwd(),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (child && typeof child.on === 'function') {
|
|
147
|
+
child.on('error', (err) => {
|
|
148
|
+
if (err.code === 'ENOENT') {
|
|
149
|
+
const tool = config.TOOLS.opencode;
|
|
150
|
+
console.error(`❌ ${tool.name} not found.\n`);
|
|
151
|
+
console.error(`Install with: ${tool.install}`);
|
|
152
|
+
console.error(`Or run: jbai install opencode`);
|
|
153
|
+
} else {
|
|
154
|
+
console.error(`Error: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
})();
|
package/bin/jbai-proxy.js
CHANGED
|
@@ -107,9 +107,17 @@ function resolveRoute(method, urlPath) {
|
|
|
107
107
|
function buildModelsResponse() {
|
|
108
108
|
const models = [];
|
|
109
109
|
const now = Math.floor(Date.now() / 1000);
|
|
110
|
+
const seen = new Set();
|
|
110
111
|
|
|
111
112
|
for (const m of config.MODELS.openai.available) {
|
|
112
113
|
models.push({ id: m, object: 'model', created: now, owned_by: 'openai' });
|
|
114
|
+
seen.add(m);
|
|
115
|
+
}
|
|
116
|
+
// Include codex-only models (responses API only) that aren't already in the openai list
|
|
117
|
+
for (const m of config.MODELS.codex.available) {
|
|
118
|
+
if (!seen.has(m)) {
|
|
119
|
+
models.push({ id: m, object: 'model', created: now, owned_by: 'openai' });
|
|
120
|
+
}
|
|
113
121
|
}
|
|
114
122
|
for (const m of config.MODELS.claude.available) {
|
|
115
123
|
models.push({ id: m, object: 'model', created: now, owned_by: 'anthropic' });
|
|
@@ -121,6 +129,34 @@ function buildModelsResponse() {
|
|
|
121
129
|
return { object: 'list', data: models };
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
// Codex CLI model picker response (matches chatgpt.com/backend-api/codex/models format)
|
|
133
|
+
function buildCodexModelsResponse() {
|
|
134
|
+
const descriptions = {
|
|
135
|
+
'gpt-5.3-codex-api-preview': 'Latest GPT-5.3 Codex model. Designed for long-running, detailed software engineering tasks.',
|
|
136
|
+
'gpt-5.2-codex': 'Latest frontier agentic coding model.',
|
|
137
|
+
'gpt-5.2-pro-2025-12-11': 'GPT-5.2 Pro for deep reasoning and complex tasks.',
|
|
138
|
+
'gpt-5.2-2025-12-11': 'Latest frontier model with improvements across knowledge, reasoning and coding.',
|
|
139
|
+
'gpt-5.2': 'Latest frontier model (alias).',
|
|
140
|
+
'gpt-5.1-codex-max': 'Codex-optimized flagship for deep and fast reasoning.',
|
|
141
|
+
'gpt-5.1-codex': 'Codex-optimized for software engineering tasks.',
|
|
142
|
+
'gpt-5.1-codex-mini': 'Optimized for codex. Cheaper, faster, but less capable.',
|
|
143
|
+
'gpt-5.1-2025-11-13': 'GPT-5.1 general-purpose model.',
|
|
144
|
+
'gpt-5-codex': 'GPT-5 Codex for agentic coding.',
|
|
145
|
+
'gpt-5-2025-08-07': 'GPT-5 general-purpose model.',
|
|
146
|
+
'o4-mini-2025-04-16': 'O4-mini reasoning model.',
|
|
147
|
+
'o3-2025-04-16': 'O3 reasoning model.',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const models = config.MODELS.codex.available.map((id, i) => ({
|
|
151
|
+
slug: id,
|
|
152
|
+
name: id,
|
|
153
|
+
description: descriptions[id] || id,
|
|
154
|
+
default_active: i === 0,
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
return { models };
|
|
158
|
+
}
|
|
159
|
+
|
|
124
160
|
// ---------------------------------------------------------------------------
|
|
125
161
|
// Proxy handler
|
|
126
162
|
// ---------------------------------------------------------------------------
|
|
@@ -144,6 +180,17 @@ function proxy(req, res) {
|
|
|
144
180
|
const route = resolveRoute(req.method, urlPath);
|
|
145
181
|
|
|
146
182
|
if (!route) {
|
|
183
|
+
// Codex CLI model picker endpoint (overrides chatgpt.com/backend-api/codex/models)
|
|
184
|
+
if (urlPath === '/backend-api/codex/models') {
|
|
185
|
+
res.writeHead(200, {
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
'Access-Control-Allow-Origin': '*',
|
|
188
|
+
});
|
|
189
|
+
res.end(JSON.stringify(buildCodexModelsResponse()));
|
|
190
|
+
log(`[codex] GET /backend-api/codex/models → 200 (${Date.now() - startTime}ms)`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
147
194
|
// Health / info endpoint
|
|
148
195
|
if (urlPath === '/' || urlPath === '/health') {
|
|
149
196
|
const token = getToken();
|
package/bin/jbai.js
CHANGED
|
@@ -41,7 +41,7 @@ jbai-cli v${VERSION} - JetBrains AI Platform CLI Tools
|
|
|
41
41
|
COMMANDS:
|
|
42
42
|
jbai token Show token status
|
|
43
43
|
jbai token set Set token interactively
|
|
44
|
-
jbai token refresh
|
|
44
|
+
jbai token refresh Auto-refresh token via API (no UI needed)
|
|
45
45
|
jbai test Test API endpoints (incl. Codex /responses)
|
|
46
46
|
jbai handoff Continue task in Orca Lab
|
|
47
47
|
jbai env [staging|production] Switch environment
|
|
@@ -98,13 +98,21 @@ async function showTokenStatus() {
|
|
|
98
98
|
const expiry = config.getTokenExpiry(token);
|
|
99
99
|
if (expiry) {
|
|
100
100
|
const now = new Date();
|
|
101
|
-
const
|
|
101
|
+
const hoursLeft = Math.round((expiry - now) / (1000 * 60 * 60));
|
|
102
102
|
|
|
103
103
|
if (config.isTokenExpired(token)) {
|
|
104
|
+
const refreshable = config.isTokenRefreshable(token);
|
|
104
105
|
console.log(`⚠️ Token EXPIRED: ${expiry.toLocaleString()}`);
|
|
105
|
-
|
|
106
|
+
if (refreshable) {
|
|
107
|
+
console.log(` Run: jbai token refresh (auto-refresh via API)`);
|
|
108
|
+
} else {
|
|
109
|
+
console.log(` Token expired >2 weeks ago. Get a new one: ${config.getEndpoints().tokenUrl}`);
|
|
110
|
+
}
|
|
111
|
+
} else if (config.isTokenExpiringSoon(token)) {
|
|
112
|
+
console.log(`⏳ Expiring soon: ${expiry.toLocaleString()} (${hoursLeft}h left)`);
|
|
113
|
+
console.log(` Run: jbai token refresh (auto-refresh via API)`);
|
|
106
114
|
} else {
|
|
107
|
-
console.log(`✅ Expires: ${expiry.toLocaleString()} (${
|
|
115
|
+
console.log(`✅ Expires: ${expiry.toLocaleString()} (${hoursLeft}h left)`);
|
|
108
116
|
}
|
|
109
117
|
}
|
|
110
118
|
}
|
|
@@ -137,13 +145,44 @@ async function setToken() {
|
|
|
137
145
|
});
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
async function
|
|
148
|
+
async function refreshTokenCommand() {
|
|
141
149
|
const token = config.getToken();
|
|
142
150
|
if (!token) {
|
|
143
151
|
console.log('❌ No token found. Run: jbai token set');
|
|
144
152
|
return;
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
if (!config.isTokenExpired(token) && !config.isTokenExpiringSoon(token)) {
|
|
156
|
+
const expiry = config.getTokenExpiry(token);
|
|
157
|
+
const hoursLeft = Math.round((expiry - Date.now()) / (1000 * 60 * 60));
|
|
158
|
+
console.log(`✅ Token is still valid (${hoursLeft}h left). Refreshing anyway...`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (config.isTokenExpired(token) && !config.isTokenRefreshable(token)) {
|
|
162
|
+
console.log('❌ Token expired more than 2 weeks ago. Cannot auto-refresh.');
|
|
163
|
+
console.log(` Get a new token: ${config.getEndpoints().tokenUrl}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
console.log('🔄 Refreshing token via API...');
|
|
169
|
+
const newToken = await config.refreshTokenApi(token);
|
|
170
|
+
config.setToken(newToken);
|
|
171
|
+
console.log('✅ Token refreshed successfully!');
|
|
172
|
+
showTokenStatus();
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.log(`❌ Refresh failed: ${e.message}`);
|
|
175
|
+
console.log(` Get a new token manually: ${config.getEndpoints().tokenUrl}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function testEndpoints() {
|
|
180
|
+
const token = await config.getValidToken();
|
|
181
|
+
if (!token) {
|
|
182
|
+
console.log('❌ No token found. Run: jbai token set');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
147
186
|
const endpoints = config.getEndpoints();
|
|
148
187
|
console.log(`Testing JetBrains AI Platform (${config.getEnvironment()})\n`);
|
|
149
188
|
const defaultOpenAIModel = config.MODELS.openai.default;
|
|
@@ -567,8 +606,10 @@ const [,, command, ...args] = process.argv;
|
|
|
567
606
|
|
|
568
607
|
switch (command) {
|
|
569
608
|
case 'token':
|
|
570
|
-
if (args[0] === 'set'
|
|
609
|
+
if (args[0] === 'set') {
|
|
571
610
|
setToken();
|
|
611
|
+
} else if (args[0] === 'refresh') {
|
|
612
|
+
refreshTokenCommand();
|
|
572
613
|
} else {
|
|
573
614
|
showTokenStatus();
|
|
574
615
|
}
|
package/bin/test-models.js
CHANGED
|
@@ -154,6 +154,33 @@ async function testGeminiModel(model) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
async function testCodexModel(model) {
|
|
158
|
+
try {
|
|
159
|
+
// Codex models use the OpenAI Responses API (not chat/completions)
|
|
160
|
+
const result = await httpPost(
|
|
161
|
+
`${endpoints.openai}/responses`,
|
|
162
|
+
{
|
|
163
|
+
model: model,
|
|
164
|
+
input: 'Say OK',
|
|
165
|
+
max_output_tokens: 50
|
|
166
|
+
},
|
|
167
|
+
{ 'Grazie-Authenticate-JWT': token }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (result.status === 200) {
|
|
171
|
+
return { success: true, message: 'OK' };
|
|
172
|
+
} else if (result.status === 404) {
|
|
173
|
+
return { success: false, message: `404 Not Found`, error: result.data.error?.message || 'Model not found' };
|
|
174
|
+
} else if (result.status === 400) {
|
|
175
|
+
return { success: false, message: `400 Bad Request`, error: result.data.error?.message || 'Invalid request' };
|
|
176
|
+
} else {
|
|
177
|
+
return { success: false, message: `Status ${result.status}`, error: result.data.error?.message || JSON.stringify(result.data).substring(0, 100) };
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return { success: false, message: 'Error', error: e.message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
157
184
|
async function runTests() {
|
|
158
185
|
console.log(`\n${'='.repeat(70)}`);
|
|
159
186
|
console.log(`${colors.cyan}JBAI-CLI E2E MODEL COMPATIBILITY TESTING${colors.reset}`);
|
|
@@ -162,6 +189,7 @@ async function runTests() {
|
|
|
162
189
|
|
|
163
190
|
const results = {
|
|
164
191
|
openai: { working: [], failing: [] },
|
|
192
|
+
codex: { working: [], failing: [] },
|
|
165
193
|
anthropic: { working: [], failing: [] },
|
|
166
194
|
gemini: { working: [], failing: [] }
|
|
167
195
|
};
|
|
@@ -198,6 +226,26 @@ async function runTests() {
|
|
|
198
226
|
}
|
|
199
227
|
}
|
|
200
228
|
|
|
229
|
+
// Test Codex-only models (responses API only, not in openai.available)
|
|
230
|
+
const openaiSet = new Set(config.MODELS.openai.available);
|
|
231
|
+
const codexOnlyModels = config.MODELS.codex.available.filter(m => !openaiSet.has(m));
|
|
232
|
+
if (codexOnlyModels.length > 0) {
|
|
233
|
+
console.log(`\n${colors.cyan}Testing Codex-only models (jbai-codex, responses API)...${colors.reset}`);
|
|
234
|
+
console.log('-'.repeat(50));
|
|
235
|
+
for (const model of codexOnlyModels) {
|
|
236
|
+
process.stdout.write(` ${model.padEnd(35)} `);
|
|
237
|
+
const result = await testCodexModel(model);
|
|
238
|
+
if (result.success) {
|
|
239
|
+
console.log(`${colors.green}✓ ${result.message}${colors.reset}`);
|
|
240
|
+
results.codex.working.push(model);
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`${colors.red}✗ ${result.message}${colors.reset}`);
|
|
243
|
+
console.log(` ${colors.dim}${result.error}${colors.reset}`);
|
|
244
|
+
results.codex.failing.push({ model, error: result.error });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
201
249
|
// Test Gemini models
|
|
202
250
|
console.log(`\n${colors.cyan}Testing Gemini models (jbai-gemini)...${colors.reset}`);
|
|
203
251
|
console.log('-'.repeat(50));
|
|
@@ -220,16 +268,17 @@ async function runTests() {
|
|
|
220
268
|
console.log(`${'='.repeat(70)}`);
|
|
221
269
|
|
|
222
270
|
const totalWorking = results.anthropic.working.length + results.openai.working.length +
|
|
223
|
-
results.gemini.working.length;
|
|
271
|
+
results.codex.working.length + results.gemini.working.length;
|
|
224
272
|
const totalFailing = results.anthropic.failing.length + results.openai.failing.length +
|
|
225
|
-
results.gemini.failing.length;
|
|
273
|
+
results.codex.failing.length + results.gemini.failing.length;
|
|
226
274
|
|
|
227
275
|
console.log(`\n${colors.green}Working Models: ${totalWorking}${colors.reset}`);
|
|
228
276
|
console.log(`${colors.red}Failing Models: ${totalFailing}${colors.reset}`);
|
|
229
277
|
|
|
230
278
|
console.log(`\n${colors.cyan}By Provider:${colors.reset}`);
|
|
231
279
|
console.log(` Claude (Anthropic): ${colors.green}${results.anthropic.working.length}${colors.reset} working, ${colors.red}${results.anthropic.failing.length}${colors.reset} failing`);
|
|
232
|
-
console.log(` OpenAI/GPT:
|
|
280
|
+
console.log(` OpenAI/GPT (Chat): ${colors.green}${results.openai.working.length}${colors.reset} working, ${colors.red}${results.openai.failing.length}${colors.reset} failing`);
|
|
281
|
+
console.log(` Codex (Responses): ${colors.green}${results.codex.working.length}${colors.reset} working, ${colors.red}${results.codex.failing.length}${colors.reset} failing`);
|
|
233
282
|
console.log(` Gemini (Google): ${colors.green}${results.gemini.working.length}${colors.reset} working, ${colors.red}${results.gemini.failing.length}${colors.reset} failing`);
|
|
234
283
|
|
|
235
284
|
if (totalFailing > 0) {
|
|
@@ -243,6 +292,10 @@ async function runTests() {
|
|
|
243
292
|
console.log(`\n OpenAI models:`);
|
|
244
293
|
results.openai.failing.forEach(f => console.log(` - ${f.model}`));
|
|
245
294
|
}
|
|
295
|
+
if (results.codex.failing.length > 0) {
|
|
296
|
+
console.log(`\n Codex models:`);
|
|
297
|
+
results.codex.failing.forEach(f => console.log(` - ${f.model}`));
|
|
298
|
+
}
|
|
246
299
|
if (results.gemini.failing.length > 0) {
|
|
247
300
|
console.log(`\n Gemini models:`);
|
|
248
301
|
results.gemini.failing.forEach(f => console.log(` - ${f.model}`));
|
package/lib/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const https = require('https');
|
|
4
5
|
|
|
5
6
|
const CONFIG_DIR = path.join(os.homedir(), '.jbai');
|
|
6
7
|
const TOKEN_FILE = path.join(CONFIG_DIR, 'token');
|
|
@@ -32,7 +33,9 @@ const MODELS = {
|
|
|
32
33
|
claude: {
|
|
33
34
|
default: 'claude-sonnet-4-5-20250929',
|
|
34
35
|
available: [
|
|
35
|
-
// Claude 4.
|
|
36
|
+
// Claude 4.6 series (latest)
|
|
37
|
+
'claude-opus-4-6',
|
|
38
|
+
// Claude 4.5 series
|
|
36
39
|
'claude-opus-4-5-20251101',
|
|
37
40
|
'claude-sonnet-4-5-20250929',
|
|
38
41
|
'claude-haiku-4-5-20251001',
|
|
@@ -75,12 +78,26 @@ const MODELS = {
|
|
|
75
78
|
]
|
|
76
79
|
},
|
|
77
80
|
// Codex CLI uses OpenAI models via the "responses" API (wire_api = "responses")
|
|
78
|
-
//
|
|
81
|
+
// Includes chat-capable models PLUS codex-only models (responses API only)
|
|
79
82
|
codex: {
|
|
80
|
-
default: 'gpt-5.
|
|
83
|
+
default: 'gpt-5.3-codex-api-preview',
|
|
81
84
|
available: [
|
|
85
|
+
// Codex-specific models (responses API only, NOT available via chat/completions)
|
|
86
|
+
'gpt-5.3-codex-api-preview',
|
|
87
|
+
// GPT-5.x chat models (also work via responses API)
|
|
82
88
|
'gpt-5.2-2025-12-11',
|
|
83
|
-
'gpt-5.2'
|
|
89
|
+
'gpt-5.2',
|
|
90
|
+
'gpt-5.1-2025-11-13',
|
|
91
|
+
'gpt-5-2025-08-07',
|
|
92
|
+
'gpt-5.2-codex',
|
|
93
|
+
'gpt-5.2-pro-2025-12-11',
|
|
94
|
+
'gpt-5.1-codex-max',
|
|
95
|
+
'gpt-5.1-codex',
|
|
96
|
+
'gpt-5.1-codex-mini',
|
|
97
|
+
'gpt-5-codex',
|
|
98
|
+
// O-series (also work via responses API)
|
|
99
|
+
'o4-mini-2025-04-16',
|
|
100
|
+
'o3-2025-04-16'
|
|
84
101
|
]
|
|
85
102
|
},
|
|
86
103
|
gemini: {
|
|
@@ -98,7 +115,7 @@ const MODELS = {
|
|
|
98
115
|
'gemini-2.0-flash-lite-001'
|
|
99
116
|
]
|
|
100
117
|
}
|
|
101
|
-
// NOTE: Other providers (DeepSeek, Mistral, Qwen, XAI, Meta) are available
|
|
118
|
+
// NOTE: Other providers (DeepSeek, Mistral, Qwen, XAI, Meta, Grok) are available
|
|
102
119
|
// via Grazie native Chat API but NOT via the OpenAI-compatible proxy.
|
|
103
120
|
// They are not supported by CLI tools that use OpenAI API format.
|
|
104
121
|
};
|
|
@@ -176,6 +193,88 @@ function isTokenExpired(token) {
|
|
|
176
193
|
return expiry < new Date();
|
|
177
194
|
}
|
|
178
195
|
|
|
196
|
+
function isTokenExpiringSoon(token, thresholdMs = 60 * 60 * 1000) {
|
|
197
|
+
const expiry = getTokenExpiry(token);
|
|
198
|
+
if (!expiry) return true;
|
|
199
|
+
return (expiry.getTime() - Date.now()) < thresholdMs;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Max age for refresh: token expired less than 2 weeks ago
|
|
203
|
+
function isTokenRefreshable(token) {
|
|
204
|
+
const expiry = getTokenExpiry(token);
|
|
205
|
+
if (!expiry) return false;
|
|
206
|
+
const twoWeeksMs = 14 * 24 * 60 * 60 * 1000;
|
|
207
|
+
return (Date.now() - expiry.getTime()) < twoWeeksMs;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function refreshTokenApi(token) {
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const endpoints = ENDPOINTS[getEnvironment()];
|
|
213
|
+
const url = new URL(`${endpoints.base}/user/v5/auth/jwt/refresh/v3`);
|
|
214
|
+
|
|
215
|
+
const req = https.request({
|
|
216
|
+
hostname: url.hostname,
|
|
217
|
+
port: 443,
|
|
218
|
+
path: url.pathname,
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'Grazie-Authenticate-JWT': token
|
|
223
|
+
}
|
|
224
|
+
}, (res) => {
|
|
225
|
+
let body = '';
|
|
226
|
+
res.on('data', chunk => body += chunk);
|
|
227
|
+
res.on('end', () => {
|
|
228
|
+
if (res.statusCode !== 200) {
|
|
229
|
+
reject(new Error(`Refresh failed (HTTP ${res.statusCode}): ${body}`));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const json = JSON.parse(body);
|
|
234
|
+
if (!json.token) {
|
|
235
|
+
reject(new Error('No token in refresh response'));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
resolve(json.token);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
reject(new Error(`Failed to parse refresh response: ${body}`));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
req.on('error', reject);
|
|
246
|
+
req.write('');
|
|
247
|
+
req.end();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function refreshToken() {
|
|
252
|
+
const token = getToken();
|
|
253
|
+
if (!token) {
|
|
254
|
+
throw new Error('No token found. Run: jbai token set');
|
|
255
|
+
}
|
|
256
|
+
if (!isTokenExpired(token) && !isTokenExpiringSoon(token)) {
|
|
257
|
+
return token; // still valid, no refresh needed
|
|
258
|
+
}
|
|
259
|
+
if (isTokenExpired(token) && !isTokenRefreshable(token)) {
|
|
260
|
+
throw new Error('Token expired more than 2 weeks ago. Get a new one from the UI.');
|
|
261
|
+
}
|
|
262
|
+
const newToken = await refreshTokenApi(token);
|
|
263
|
+
setToken(newToken);
|
|
264
|
+
return newToken;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function getValidToken() {
|
|
268
|
+
const token = getToken();
|
|
269
|
+
if (!token) return null;
|
|
270
|
+
if (!isTokenExpiringSoon(token)) return token;
|
|
271
|
+
try {
|
|
272
|
+
return await refreshToken();
|
|
273
|
+
} catch {
|
|
274
|
+
return token; // return current token as fallback
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
179
278
|
const TOOLS = {
|
|
180
279
|
claude: {
|
|
181
280
|
name: 'Claude Code',
|
|
@@ -216,5 +315,10 @@ module.exports = {
|
|
|
216
315
|
getEndpoints,
|
|
217
316
|
parseJWT,
|
|
218
317
|
getTokenExpiry,
|
|
219
|
-
isTokenExpired
|
|
318
|
+
isTokenExpired,
|
|
319
|
+
isTokenExpiringSoon,
|
|
320
|
+
isTokenRefreshable,
|
|
321
|
+
refreshTokenApi,
|
|
322
|
+
refreshToken,
|
|
323
|
+
getValidToken
|
|
220
324
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const config = require('./config');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get a valid token, auto-refreshing if expiring soon.
|
|
5
|
+
* Exits the process if no token or refresh fails on an expired token.
|
|
6
|
+
*/
|
|
7
|
+
async function ensureToken() {
|
|
8
|
+
let token = config.getToken();
|
|
9
|
+
if (!token) {
|
|
10
|
+
console.error('❌ No token found. Run: jbai token set');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (config.isTokenExpiringSoon(token)) {
|
|
15
|
+
try {
|
|
16
|
+
console.error('🔄 Token expiring soon, auto-refreshing...');
|
|
17
|
+
token = await config.refreshToken();
|
|
18
|
+
console.error('✅ Token refreshed');
|
|
19
|
+
} catch (e) {
|
|
20
|
+
if (config.isTokenExpired(token)) {
|
|
21
|
+
console.error(`❌ Token expired and refresh failed: ${e.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
// Token not expired yet, continue with current
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { ensureToken };
|