neoagent 2.3.1-beta.98 → 2.4.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/.env.example +6 -3
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_integrations.dart +21 -2
- package/flutter_app/lib/main_models.dart +60 -0
- package/flutter_app/lib/main_theme.dart +31 -2
- package/flutter_app/macos/Runner/AppDelegate.swift +11 -1
- package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
- package/flutter_app/macos/Runner/Release.entitlements +4 -0
- package/flutter_app/pubspec.lock +5 -5
- package/lib/manager.js +164 -2
- package/package.json +1 -1
- package/server/db/database.js +85 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +971 -1066
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/shaders/ink_sparkle.frag +1 -1
- package/server/public/assets/shaders/stretch_effect.frag +1 -1
- package/server/public/canvaskit/canvaskit.js +2 -2
- package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
- package/server/public/canvaskit/canvaskit.wasm +0 -0
- package/server/public/canvaskit/chromium/canvaskit.js +2 -2
- package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
- package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
- package/server/public/canvaskit/skwasm.js +14 -14
- package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
- package/server/public/canvaskit/skwasm.wasm +0 -0
- package/server/public/canvaskit/skwasm_heavy.js +14 -14
- package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
- package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
- package/server/public/canvaskit/wimp.js +94 -95
- package/server/public/canvaskit/wimp.js.symbols +11325 -11177
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +83866 -82074
- package/server/routes/integrations.js +2 -2
- package/server/routes/memory.js +73 -0
- package/server/services/ai/engine.js +65 -26
- package/server/services/ai/models.js +21 -0
- package/server/services/ai/preModelCompaction.js +191 -0
- package/server/services/ai/providers/claudeCode.js +273 -0
- package/server/services/ai/providers/openaiCodex.js +226 -41
- package/server/services/ai/settings.js +11 -1
- package/server/services/integrations/google/provider.js +78 -0
- package/server/services/integrations/manager.js +29 -13
- package/server/services/manager.js +25 -0
- package/server/services/memory/ingestion.js +486 -0
- package/server/services/memory/manager.js +422 -0
- package/server/services/memory/openhuman_uplift.test.js +98 -0
- package/server/services/widgets/focus_widget.js +45 -4
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const Anthropic = require('@anthropic-ai/sdk');
|
|
5
|
+
const { AnthropicProvider } = require('./anthropic');
|
|
6
|
+
|
|
7
|
+
const CLAUDE_CLI_CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
8
|
+
const CLAUDE_CODE_BASE_URL = 'https://api.anthropic.com';
|
|
9
|
+
const CLAUDE_CODE_VERSION = process.env.CLAUDE_CODE_VERSION || '2.1.75';
|
|
10
|
+
const CLAUDE_CODE_OAUTH_BETA = 'claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14';
|
|
11
|
+
const CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
12
|
+
const CLAUDE_CODE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
13
|
+
const CLAUDE_CODE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
14
|
+
const CLAUDE_CODE_SCOPES = 'user:inference user:profile org:create_api_key user:sessions:claude_code user:mcp_servers';
|
|
15
|
+
|
|
16
|
+
function readTokenRecord(data) {
|
|
17
|
+
const tokens = data?.claudeAiOauthTokens || data?.claudeAiOauth || {};
|
|
18
|
+
const access = tokens.accessToken || tokens.access;
|
|
19
|
+
const refresh = tokens.refreshToken || tokens.refresh;
|
|
20
|
+
const expires = tokens.expiresAt || tokens.expires;
|
|
21
|
+
return {
|
|
22
|
+
access: typeof access === 'string' && access ? access : null,
|
|
23
|
+
refresh: typeof refresh === 'string' && refresh ? refresh : null,
|
|
24
|
+
expires: typeof expires === 'number' && Number.isFinite(expires) ? expires : null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readTokenValue(data) {
|
|
29
|
+
return readTokenRecord(data).access;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readClaudeCliTokenRecord() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(CLAUDE_CLI_CREDS_PATH, 'utf8');
|
|
35
|
+
const data = JSON.parse(raw);
|
|
36
|
+
return readTokenRecord(data);
|
|
37
|
+
} catch {
|
|
38
|
+
return { access: null, refresh: null, expires: null };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readClaudeCliToken() {
|
|
43
|
+
return readClaudeCliTokenRecord().access;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mergeAnthropicBeta(existing) {
|
|
47
|
+
if (!existing) return CLAUDE_CODE_OAUTH_BETA;
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
return String(existing)
|
|
50
|
+
.split(',')
|
|
51
|
+
.concat(CLAUDE_CODE_OAUTH_BETA.split(','))
|
|
52
|
+
.map((item) => item.trim())
|
|
53
|
+
.filter((item) => {
|
|
54
|
+
if (!item || seen.has(item)) return false;
|
|
55
|
+
seen.add(item);
|
|
56
|
+
return true;
|
|
57
|
+
})
|
|
58
|
+
.join(',');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeExpiresAt(data) {
|
|
62
|
+
if (typeof data.expires_at === 'number' && Number.isFinite(data.expires_at)) {
|
|
63
|
+
return data.expires_at > 10_000_000_000 ? data.expires_at : data.expires_at * 1000;
|
|
64
|
+
}
|
|
65
|
+
if (typeof data.expiresAt === 'number' && Number.isFinite(data.expiresAt)) {
|
|
66
|
+
return data.expiresAt > 10_000_000_000 ? data.expiresAt : data.expiresAt * 1000;
|
|
67
|
+
}
|
|
68
|
+
if (typeof data.expires_in === 'number' && Number.isFinite(data.expires_in)) {
|
|
69
|
+
return Date.now() + (data.expires_in * 1000);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sanitizeEnvKey(key) {
|
|
75
|
+
return String(key).replace(/[\r\n]/g, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeEnvValue(value) {
|
|
79
|
+
return String(value).replace(/[\r\n]/g, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function persistEnvValue(key, value) {
|
|
83
|
+
if (!value) return;
|
|
84
|
+
try {
|
|
85
|
+
const { ENV_FILE } = require('../../../../runtime/paths');
|
|
86
|
+
const safeKey = sanitizeEnvKey(key);
|
|
87
|
+
const safeValue = sanitizeEnvValue(value);
|
|
88
|
+
const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
|
|
89
|
+
const lines = raw ? raw.split('\n') : [];
|
|
90
|
+
let replaced = false;
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
if (lines[i].startsWith(`${safeKey}=`)) {
|
|
93
|
+
lines[i] = `${safeKey}=${safeValue}`;
|
|
94
|
+
replaced = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!replaced) lines.push(`${safeKey}=${safeValue}`);
|
|
99
|
+
const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
|
|
100
|
+
fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
|
|
101
|
+
fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
|
|
102
|
+
} catch { }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
|
|
106
|
+
if (!refreshToken) return null;
|
|
107
|
+
const response = await fetchImpl(CLAUDE_CODE_TOKEN_URL, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
'Accept': 'application/json',
|
|
112
|
+
'anthropic-version': '2023-06-01',
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
grant_type: 'refresh_token',
|
|
116
|
+
refresh_token: refreshToken,
|
|
117
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const text = await response.text();
|
|
122
|
+
let data = {};
|
|
123
|
+
try {
|
|
124
|
+
data = text ? JSON.parse(text) : {};
|
|
125
|
+
} catch {
|
|
126
|
+
data = {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const detail = data?.error?.message || data?.error_description || data?.error || text || 'Unknown error';
|
|
131
|
+
throw new Error(`Claude Code OAuth refresh failed: HTTP ${response.status} ${detail}`);
|
|
132
|
+
}
|
|
133
|
+
if (!data.access_token) {
|
|
134
|
+
throw new Error('Claude Code OAuth refresh succeeded but no access_token was returned.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
access: data.access_token,
|
|
139
|
+
refresh: data.refresh_token || refreshToken,
|
|
140
|
+
expires: normalizeExpiresAt(data),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isAuthenticationError(err) {
|
|
145
|
+
return err?.status === 401 || err?.error?.type === 'authentication_error';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isInferenceScopeError(err) {
|
|
149
|
+
const message = String(err?.error?.message || err?.message || '');
|
|
150
|
+
const type = String(err?.error?.type || err?.type || '');
|
|
151
|
+
return err?.status === 403
|
|
152
|
+
&& (type === 'permission_error' || message.includes('"permission_error"'))
|
|
153
|
+
&& message.includes('scope requirement')
|
|
154
|
+
&& message.includes('user:inference');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatClaudeCodeCredentialError(err) {
|
|
158
|
+
if (isInferenceScopeError(err)) {
|
|
159
|
+
return new Error(`Claude Code OAuth token is missing inference scope. Re-run \`neoagent login claude-code\` to create a token with ${CLAUDE_CODE_SCOPES}.`);
|
|
160
|
+
}
|
|
161
|
+
return err;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class ClaudeCodeProvider extends AnthropicProvider {
|
|
165
|
+
constructor(config = {}) {
|
|
166
|
+
super(config);
|
|
167
|
+
this.name = 'claude-code';
|
|
168
|
+
this.models = [
|
|
169
|
+
'claude-opus-4-7',
|
|
170
|
+
'claude-sonnet-4-6',
|
|
171
|
+
'claude-haiku-4-5-20251001',
|
|
172
|
+
];
|
|
173
|
+
this.contextWindows = {
|
|
174
|
+
'claude-opus-4-7': 200000,
|
|
175
|
+
'claude-sonnet-4-6': 200000,
|
|
176
|
+
'claude-haiku-4-5-20251001': 200000,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const cliTokenRecord = readClaudeCliTokenRecord();
|
|
180
|
+
const authToken = config.apiKey || process.env.CLAUDE_CODE_OAUTH_TOKEN || cliTokenRecord.access;
|
|
181
|
+
if (!authToken) {
|
|
182
|
+
console.warn('[ClaudeCode] No access token. Run `neoagent login claude-code` to authenticate.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.authToken = authToken || null;
|
|
186
|
+
this.refreshToken = config.refreshToken || process.env.CLAUDE_CODE_REFRESH_TOKEN || cliTokenRecord.refresh || null;
|
|
187
|
+
this.fetchImpl = config.fetch || fetch;
|
|
188
|
+
this.baseURL = config.baseUrl || CLAUDE_CODE_BASE_URL;
|
|
189
|
+
this.defaultHeaders = {
|
|
190
|
+
...(config.defaultHeaders || {}),
|
|
191
|
+
accept: 'application/json',
|
|
192
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
193
|
+
'anthropic-beta': mergeAnthropicBeta(config.defaultHeaders?.['anthropic-beta']),
|
|
194
|
+
'user-agent': `claude-cli/${CLAUDE_CODE_VERSION}`,
|
|
195
|
+
'x-app': 'cli',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this.client = this.createClient(this.authToken, config);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
createClient(authToken, config = this.config) {
|
|
202
|
+
// OAuth tokens use Authorization: Bearer. Claude Code subscription inference
|
|
203
|
+
// also requires the Claude Code beta surface headers used by the official CLI.
|
|
204
|
+
return new Anthropic({
|
|
205
|
+
authToken: authToken || undefined,
|
|
206
|
+
baseURL: this.baseURL,
|
|
207
|
+
defaultHeaders: authToken ? this.defaultHeaders : config.defaultHeaders,
|
|
208
|
+
...(this.fetchImpl ? { fetch: this.fetchImpl } : {}),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async refreshClient() {
|
|
213
|
+
const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl);
|
|
214
|
+
if (!refreshed?.access) return false;
|
|
215
|
+
this.authToken = refreshed.access;
|
|
216
|
+
this.refreshToken = refreshed.refresh || this.refreshToken;
|
|
217
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = this.authToken;
|
|
218
|
+
persistEnvValue('CLAUDE_CODE_OAUTH_TOKEN', this.authToken);
|
|
219
|
+
if (this.refreshToken) {
|
|
220
|
+
process.env.CLAUDE_CODE_REFRESH_TOKEN = this.refreshToken;
|
|
221
|
+
persistEnvValue('CLAUDE_CODE_REFRESH_TOKEN', this.refreshToken);
|
|
222
|
+
}
|
|
223
|
+
this.client = this.createClient(this.authToken);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
convertMessages(messages) {
|
|
228
|
+
const converted = super.convertMessages(messages);
|
|
229
|
+
if (!converted.system) {
|
|
230
|
+
converted.system = [{ type: 'text', text: CLAUDE_CODE_SYSTEM_PROMPT }];
|
|
231
|
+
return converted;
|
|
232
|
+
}
|
|
233
|
+
converted.system = [
|
|
234
|
+
{ type: 'text', text: CLAUDE_CODE_SYSTEM_PROMPT },
|
|
235
|
+
{ type: 'text', text: converted.system },
|
|
236
|
+
];
|
|
237
|
+
return converted;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async chat(messages, tools = [], options = {}) {
|
|
241
|
+
try {
|
|
242
|
+
return await super.chat(messages, tools, options);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
|
|
245
|
+
throw formatClaudeCodeCredentialError(err);
|
|
246
|
+
}
|
|
247
|
+
await this.refreshClient();
|
|
248
|
+
try {
|
|
249
|
+
return await super.chat(messages, tools, options);
|
|
250
|
+
} catch (retryErr) {
|
|
251
|
+
throw formatClaudeCodeCredentialError(retryErr);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async *stream(messages, tools = [], options = {}) {
|
|
257
|
+
try {
|
|
258
|
+
yield* super.stream(messages, tools, options);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
|
|
261
|
+
throw formatClaudeCodeCredentialError(err);
|
|
262
|
+
}
|
|
263
|
+
await this.refreshClient();
|
|
264
|
+
try {
|
|
265
|
+
yield* super.stream(messages, tools, options);
|
|
266
|
+
} catch (retryErr) {
|
|
267
|
+
throw formatClaudeCodeCredentialError(retryErr);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { ClaudeCodeProvider, readClaudeCliToken, refreshClaudeCodeAccessToken, CLAUDE_CODE_SCOPES };
|
|
@@ -1,7 +1,68 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const OpenAI = require('openai');
|
|
2
3
|
const { BaseProvider } = require('./base');
|
|
3
4
|
|
|
4
5
|
const DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
6
|
+
const OPENAI_CODEX_EMPTY_INPUT_TEXT = ' ';
|
|
7
|
+
const NEOAGENT_VERSION = (() => {
|
|
8
|
+
try {
|
|
9
|
+
return require('../../../../package.json').version || 'unknown';
|
|
10
|
+
} catch {
|
|
11
|
+
return 'unknown';
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
// Stable per-process installation ID — Codex backend uses it for request tracking.
|
|
16
|
+
const INSTALLATION_ID = crypto.randomUUID();
|
|
17
|
+
|
|
18
|
+
function decodeJwtPayload(token) {
|
|
19
|
+
try {
|
|
20
|
+
const parts = String(token || '').split('.');
|
|
21
|
+
if (parts.length < 2) return null;
|
|
22
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
23
|
+
return JSON.parse(payload);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isCodexBackendBaseUrl(baseURL) {
|
|
30
|
+
const trimmed = String(baseURL || '').trim();
|
|
31
|
+
if (!trimmed) return false;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(trimmed);
|
|
35
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
36
|
+
return url.hostname === 'chatgpt.com'
|
|
37
|
+
&& (path === '/backend-api' || path === '/backend-api/v1' || path === '/backend-api/codex' || path === '/backend-api/codex/v1');
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isOpenAIApiBaseUrl(baseURL) {
|
|
44
|
+
const trimmed = String(baseURL || '').trim();
|
|
45
|
+
if (!trimmed) return false;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(trimmed);
|
|
49
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
50
|
+
return url.hostname === 'api.openai.com' && (path === '' || path === '/v1');
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeCodexBaseUrl(baseURL) {
|
|
57
|
+
if (!baseURL || isOpenAIApiBaseUrl(baseURL) || isCodexBackendBaseUrl(baseURL)) {
|
|
58
|
+
return DEFAULT_BASE_URL;
|
|
59
|
+
}
|
|
60
|
+
return baseURL;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isNativeCodexResponsesBaseUrl(baseURL) {
|
|
64
|
+
return isCodexBackendBaseUrl(baseURL);
|
|
65
|
+
}
|
|
5
66
|
|
|
6
67
|
function normalizeContent(content) {
|
|
7
68
|
if (content == null) return '';
|
|
@@ -18,33 +79,45 @@ function normalizeContent(content) {
|
|
|
18
79
|
return String(content);
|
|
19
80
|
}
|
|
20
81
|
|
|
21
|
-
function
|
|
82
|
+
function normalizeMessageContent(content, role = 'user') {
|
|
22
83
|
if (content == null) return [];
|
|
84
|
+
const isAssistant = role === 'assistant';
|
|
85
|
+
const textType = isAssistant ? 'output_text' : 'input_text';
|
|
23
86
|
|
|
24
87
|
if (typeof content === 'string') {
|
|
25
|
-
return [{ type:
|
|
88
|
+
return [{ type: textType, text: content, ...(isAssistant ? { annotations: [] } : {}) }];
|
|
26
89
|
}
|
|
27
90
|
|
|
28
91
|
if (!Array.isArray(content)) {
|
|
29
92
|
const text = String(content);
|
|
30
|
-
return text ? [{ type:
|
|
93
|
+
return text ? [{ type: textType, text, ...(isAssistant ? { annotations: [] } : {}) }] : [];
|
|
31
94
|
}
|
|
32
95
|
|
|
33
96
|
const parts = [];
|
|
34
97
|
for (const part of content) {
|
|
35
98
|
if (!part) continue;
|
|
36
99
|
if (typeof part === 'string') {
|
|
37
|
-
if (part.trim()) parts.push({ type:
|
|
100
|
+
if (part.trim()) parts.push({ type: textType, text: part, ...(isAssistant ? { annotations: [] } : {}) });
|
|
38
101
|
continue;
|
|
39
102
|
}
|
|
40
103
|
if (part.type === 'text' && typeof part.text === 'string') {
|
|
41
|
-
parts.push({ type:
|
|
104
|
+
parts.push({ type: textType, text: part.text, ...(isAssistant ? { annotations: [] } : {}) });
|
|
42
105
|
continue;
|
|
43
106
|
}
|
|
44
107
|
if (part.type === 'input_text' && typeof part.text === 'string') {
|
|
45
|
-
parts.push({ type:
|
|
108
|
+
parts.push({ type: textType, text: part.text, ...(isAssistant ? { annotations: [] } : {}) });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (part.type === 'output_text' && typeof part.text === 'string') {
|
|
112
|
+
parts.push({ type: textType, text: part.text, ...(isAssistant ? { annotations: part.annotations || [] } : {}) });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (isAssistant && part.type === 'refusal') {
|
|
116
|
+
const refusal = typeof part.refusal === 'string' ? part.refusal : part.text;
|
|
117
|
+
if (typeof refusal === 'string') parts.push({ type: 'refusal', refusal });
|
|
46
118
|
continue;
|
|
47
119
|
}
|
|
120
|
+
if (isAssistant) continue;
|
|
48
121
|
if (part.type === 'image_url') {
|
|
49
122
|
const imageUrl = typeof part.image_url === 'string' ? part.image_url : part.image_url?.url;
|
|
50
123
|
if (imageUrl) {
|
|
@@ -126,11 +199,15 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
126
199
|
constructor(config = {}) {
|
|
127
200
|
super(config);
|
|
128
201
|
|
|
129
|
-
const
|
|
202
|
+
const configuredBaseURL = config.baseUrl || process.env.OPENAI_CODEX_BASE_URL || DEFAULT_BASE_URL;
|
|
203
|
+
const baseURL = normalizeCodexBaseUrl(configuredBaseURL);
|
|
204
|
+
|
|
205
|
+
this.baseURL = baseURL;
|
|
206
|
+
this.usesCodexBackend = isCodexBackendBaseUrl(baseURL);
|
|
130
207
|
|
|
131
|
-
if (!
|
|
208
|
+
if (!this.usesCodexBackend && !baseURL.includes('api.openai.com')) {
|
|
132
209
|
console.warn(`[OpenAICodex] Using non-official base URL: ${baseURL}`);
|
|
133
|
-
} else if (
|
|
210
|
+
} else if (this.usesCodexBackend) {
|
|
134
211
|
console.info(`[OpenAICodex] Using ChatGPT Codex endpoint: ${baseURL}`);
|
|
135
212
|
}
|
|
136
213
|
|
|
@@ -145,17 +222,46 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
145
222
|
'gpt-5.4',
|
|
146
223
|
'gpt-5.4-mini',
|
|
147
224
|
]);
|
|
225
|
+
|
|
226
|
+
let accountId = process.env.OPENAI_CODEX_ACCOUNT_ID || '';
|
|
227
|
+
if (!accountId && this.usesCodexBackend) {
|
|
228
|
+
const accessToken = config.apiKey || process.env.OPENAI_CODEX_ACCESS_TOKEN || '';
|
|
229
|
+
const payload = decodeJwtPayload(accessToken);
|
|
230
|
+
// account_id lives in access_token directly, or nested under the OIDC namespace in id_token
|
|
231
|
+
accountId = payload?.chatgpt_account_id
|
|
232
|
+
|| payload?.['https://api.openai.com/auth']?.chatgpt_account_id
|
|
233
|
+
|| '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const defaultHeaders = this.usesCodexBackend
|
|
237
|
+
? {
|
|
238
|
+
'originator': 'openclaw',
|
|
239
|
+
'version': NEOAGENT_VERSION,
|
|
240
|
+
'User-Agent': `openclaw/${NEOAGENT_VERSION}`,
|
|
241
|
+
'x-codex-installation-id': INSTALLATION_ID,
|
|
242
|
+
'x-openai-internal-codex-residency': process.env.OPENAI_CODEX_RESIDENCY || 'us',
|
|
243
|
+
...(accountId ? { 'ChatGPT-Account-Id': accountId } : {}),
|
|
244
|
+
}
|
|
245
|
+
: undefined;
|
|
246
|
+
|
|
148
247
|
this.client = new OpenAI({
|
|
149
248
|
apiKey: config.apiKey || process.env.OPENAI_CODEX_ACCESS_TOKEN,
|
|
150
249
|
baseURL,
|
|
151
|
-
defaultHeaders
|
|
152
|
-
|
|
153
|
-
'Editor-Plugin-Version': process.env.OPENAI_CODEX_EDITOR_PLUGIN_VERSION || 'neoagent/1.0.0',
|
|
154
|
-
'User-Agent': process.env.OPENAI_CODEX_USER_AGENT || 'NeoAgent/1.0.0',
|
|
155
|
-
},
|
|
250
|
+
defaultHeaders,
|
|
251
|
+
...(config.fetch ? { fetch: config.fetch } : {}),
|
|
156
252
|
});
|
|
157
253
|
}
|
|
158
254
|
|
|
255
|
+
formatTools(tools) {
|
|
256
|
+
return tools.map((tool) => ({
|
|
257
|
+
type: 'function',
|
|
258
|
+
name: tool.name,
|
|
259
|
+
description: tool.description || '',
|
|
260
|
+
parameters: tool.parameters || { type: 'object', properties: {} },
|
|
261
|
+
strict: false,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
159
265
|
_isReasoningModel(model) {
|
|
160
266
|
if (!model) return false;
|
|
161
267
|
for (const id of this.reasoningModels) {
|
|
@@ -184,11 +290,12 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
184
290
|
continue;
|
|
185
291
|
}
|
|
186
292
|
|
|
187
|
-
const
|
|
293
|
+
const role = msg.role === 'assistant' ? 'assistant' : 'user';
|
|
294
|
+
const content = normalizeMessageContent(msg.content, role);
|
|
188
295
|
if (content.length > 0) {
|
|
189
296
|
input.push({
|
|
190
297
|
type: 'message',
|
|
191
|
-
role
|
|
298
|
+
role,
|
|
192
299
|
content,
|
|
193
300
|
});
|
|
194
301
|
}
|
|
@@ -201,7 +308,6 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
201
308
|
if (!name || !callId) continue;
|
|
202
309
|
input.push({
|
|
203
310
|
type: 'function_call',
|
|
204
|
-
id: callId,
|
|
205
311
|
call_id: callId,
|
|
206
312
|
name,
|
|
207
313
|
arguments: argumentsText,
|
|
@@ -214,40 +320,120 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
214
320
|
input,
|
|
215
321
|
};
|
|
216
322
|
|
|
217
|
-
if (
|
|
323
|
+
if (this.usesCodexBackend) {
|
|
324
|
+
if (input.length === 0 && instructions.length > 0) {
|
|
325
|
+
input.push({
|
|
326
|
+
type: 'message',
|
|
327
|
+
role: 'user',
|
|
328
|
+
content: [{ type: 'input_text', text: OPENAI_CODEX_EMPTY_INPUT_TEXT }],
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// instructions must always be present (even empty) — backend returns 400 if omitted
|
|
218
332
|
request.instructions = instructions.join('\n\n');
|
|
333
|
+
request.store = false;
|
|
334
|
+
// tools fields must always be explicit
|
|
335
|
+
if (tools && tools.length > 0) {
|
|
336
|
+
request.tools = this.formatTools(tools);
|
|
337
|
+
request.tool_choice = options.toolChoice || 'auto';
|
|
338
|
+
} else {
|
|
339
|
+
request.tools = [];
|
|
340
|
+
request.tool_choice = 'auto';
|
|
341
|
+
}
|
|
342
|
+
request.parallel_tool_calls = false;
|
|
343
|
+
// reasoning: both effort and summary required for Codex reasoning models
|
|
344
|
+
if (this._isReasoningModel(model)) {
|
|
345
|
+
const effort = options.reasoningEffort || options.reasoning_effort || 'medium';
|
|
346
|
+
request.reasoning = { effort, summary: 'auto' };
|
|
347
|
+
request.include = ['reasoning.encrypted_content'];
|
|
348
|
+
}
|
|
349
|
+
this._sanitizeNativeCodexRequest(request);
|
|
350
|
+
} else {
|
|
351
|
+
if (instructions.length > 0) {
|
|
352
|
+
request.instructions = instructions.join('\n\n');
|
|
353
|
+
}
|
|
354
|
+
if (tools && tools.length > 0) {
|
|
355
|
+
request.tools = this.formatTools(tools);
|
|
356
|
+
request.tool_choice = options.toolChoice || 'auto';
|
|
357
|
+
}
|
|
358
|
+
request.max_output_tokens = options.maxTokens || 16384;
|
|
359
|
+
if (options.temperature !== undefined && options.temperature !== null) {
|
|
360
|
+
request.temperature = options.temperature;
|
|
361
|
+
}
|
|
362
|
+
const reasoningEffort = options.reasoningEffort || options.reasoning_effort;
|
|
363
|
+
if (reasoningEffort || this._isReasoningModel(model)) {
|
|
364
|
+
request.reasoning = { effort: reasoningEffort || 'medium' };
|
|
365
|
+
}
|
|
219
366
|
}
|
|
220
367
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
request.tool_choice = options.toolChoice || 'auto';
|
|
224
|
-
}
|
|
368
|
+
return request;
|
|
369
|
+
}
|
|
225
370
|
|
|
226
|
-
|
|
371
|
+
_requestHeaders() {
|
|
372
|
+
const requestId = crypto.randomUUID();
|
|
373
|
+
return this.usesCodexBackend
|
|
374
|
+
? {
|
|
375
|
+
'x-client-request-id': requestId,
|
|
376
|
+
'x-openclaw-session-id': requestId,
|
|
377
|
+
'x-openclaw-turn-id': requestId,
|
|
378
|
+
'x-openclaw-turn-attempt': '1',
|
|
379
|
+
}
|
|
380
|
+
: undefined;
|
|
381
|
+
}
|
|
227
382
|
|
|
228
|
-
|
|
229
|
-
|
|
383
|
+
_sanitizeNativeCodexRequest(request) {
|
|
384
|
+
if (!isNativeCodexResponsesBaseUrl(this.baseURL)) return request;
|
|
385
|
+
for (const key of [
|
|
386
|
+
'max_output_tokens',
|
|
387
|
+
'metadata',
|
|
388
|
+
'prompt_cache_retention',
|
|
389
|
+
'service_tier',
|
|
390
|
+
'temperature',
|
|
391
|
+
'top_p',
|
|
392
|
+
]) {
|
|
393
|
+
delete request[key];
|
|
230
394
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
395
|
+
if (request.text && typeof request.text === 'object' && !Array.isArray(request.text)) {
|
|
396
|
+
const text = { ...request.text };
|
|
397
|
+
delete text.format;
|
|
398
|
+
if (Object.keys(text).length > 0) {
|
|
399
|
+
request.text = text;
|
|
400
|
+
} else {
|
|
401
|
+
delete request.text;
|
|
402
|
+
}
|
|
237
403
|
}
|
|
238
|
-
|
|
239
404
|
return request;
|
|
240
405
|
}
|
|
241
406
|
|
|
242
407
|
async chat(messages, tools = [], options = {}) {
|
|
408
|
+
if (this.usesCodexBackend) {
|
|
409
|
+
let final = null;
|
|
410
|
+
let content = '';
|
|
411
|
+
for await (const event of this.stream(messages, tools, options)) {
|
|
412
|
+
if (event.type === 'content') {
|
|
413
|
+
content += event.content || '';
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (event.type === 'tool_calls' || event.type === 'done') {
|
|
417
|
+
final = event;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
content: final?.content || content,
|
|
422
|
+
toolCalls: final?.toolCalls || [],
|
|
423
|
+
finishReason: final?.finishReason || (final?.toolCalls?.length > 0 ? 'tool_calls' : 'stop'),
|
|
424
|
+
usage: final?.usage || null,
|
|
425
|
+
model: final?.model || options.model || this.config.model || this.getDefaultModel(),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
243
429
|
const model = options.model || this.config.model || this.getDefaultModel();
|
|
244
430
|
const request = this._buildRequest(messages, tools, options, model);
|
|
245
431
|
let response;
|
|
246
432
|
try {
|
|
247
|
-
response = await this.client.responses.create(
|
|
248
|
-
model,
|
|
249
|
-
|
|
250
|
-
|
|
433
|
+
response = await this.client.responses.create(
|
|
434
|
+
{ model, ...request },
|
|
435
|
+
{ headers: this._requestHeaders() },
|
|
436
|
+
);
|
|
251
437
|
} catch (err) {
|
|
252
438
|
throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
|
|
253
439
|
}
|
|
@@ -272,11 +458,10 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
272
458
|
const request = this._buildRequest(messages, tools, options, model);
|
|
273
459
|
let stream;
|
|
274
460
|
try {
|
|
275
|
-
stream = await this.client.responses.create(
|
|
276
|
-
model,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
});
|
|
461
|
+
stream = await this.client.responses.create(
|
|
462
|
+
{ model, ...request, stream: true },
|
|
463
|
+
{ headers: this._requestHeaders() },
|
|
464
|
+
);
|
|
280
465
|
} catch (err) {
|
|
281
466
|
throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
|
|
282
467
|
}
|
|
@@ -72,13 +72,23 @@ const AI_PROVIDER_DEFINITIONS = Object.freeze({
|
|
|
72
72
|
'openai-codex': {
|
|
73
73
|
id: 'openai-codex',
|
|
74
74
|
label: 'OpenAI Codex',
|
|
75
|
-
description: 'Use
|
|
75
|
+
description: 'Use Codex models through ChatGPT Codex authentication.',
|
|
76
76
|
envKey: 'OPENAI_CODEX_ACCESS_TOKEN',
|
|
77
77
|
supportsApiKey: true,
|
|
78
78
|
supportsBaseUrl: true,
|
|
79
79
|
defaultEnabled: false,
|
|
80
80
|
defaultBaseUrl: 'https://chatgpt.com/backend-api/codex'
|
|
81
81
|
},
|
|
82
|
+
'claude-code': {
|
|
83
|
+
id: 'claude-code',
|
|
84
|
+
label: 'Claude Code',
|
|
85
|
+
description: 'Claude models via Claude Code subscription. Login with `neoagent login claude-code`.',
|
|
86
|
+
envKey: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
87
|
+
supportsApiKey: true,
|
|
88
|
+
supportsBaseUrl: false,
|
|
89
|
+
defaultEnabled: false,
|
|
90
|
+
defaultBaseUrl: ''
|
|
91
|
+
},
|
|
82
92
|
ollama: {
|
|
83
93
|
id: 'ollama',
|
|
84
94
|
label: 'Ollama',
|