neoagent 2.3.1-beta.99 → 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.
Files changed (52) hide show
  1. package/.env.example +6 -3
  2. package/flutter_app/lib/main.dart +1 -0
  3. package/flutter_app/lib/main_integrations.dart +21 -2
  4. package/flutter_app/lib/main_models.dart +60 -0
  5. package/flutter_app/lib/main_theme.dart +31 -2
  6. package/flutter_app/macos/Runner/AppDelegate.swift +11 -1
  7. package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
  8. package/flutter_app/macos/Runner/Release.entitlements +4 -0
  9. package/flutter_app/pubspec.lock +5 -5
  10. package/lib/manager.js +164 -2
  11. package/package.json +1 -1
  12. package/server/db/database.js +85 -0
  13. package/server/public/.last_build_id +1 -1
  14. package/server/public/assets/NOTICES +971 -1066
  15. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  16. package/server/public/assets/shaders/ink_sparkle.frag +1 -1
  17. package/server/public/assets/shaders/stretch_effect.frag +1 -1
  18. package/server/public/canvaskit/canvaskit.js +2 -2
  19. package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
  20. package/server/public/canvaskit/canvaskit.wasm +0 -0
  21. package/server/public/canvaskit/chromium/canvaskit.js +2 -2
  22. package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
  23. package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
  24. package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
  25. package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
  26. package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
  27. package/server/public/canvaskit/skwasm.js +14 -14
  28. package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
  29. package/server/public/canvaskit/skwasm.wasm +0 -0
  30. package/server/public/canvaskit/skwasm_heavy.js +14 -14
  31. package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
  32. package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
  33. package/server/public/canvaskit/wimp.js +94 -95
  34. package/server/public/canvaskit/wimp.js.symbols +11325 -11177
  35. package/server/public/canvaskit/wimp.wasm +0 -0
  36. package/server/public/flutter_bootstrap.js +2 -2
  37. package/server/public/main.dart.js +83866 -82074
  38. package/server/routes/integrations.js +2 -2
  39. package/server/routes/memory.js +73 -0
  40. package/server/services/ai/engine.js +65 -26
  41. package/server/services/ai/models.js +21 -0
  42. package/server/services/ai/preModelCompaction.js +191 -0
  43. package/server/services/ai/providers/claudeCode.js +273 -0
  44. package/server/services/ai/providers/openaiCodex.js +212 -40
  45. package/server/services/ai/settings.js +12 -2
  46. package/server/services/integrations/google/provider.js +78 -0
  47. package/server/services/integrations/manager.js +29 -13
  48. package/server/services/manager.js +25 -0
  49. package/server/services/memory/ingestion.js +486 -0
  50. package/server/services/memory/manager.js +422 -0
  51. package/server/services/memory/openhuman_uplift.test.js +98 -0
  52. 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
- const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
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 normalizeInputContent(content) {
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: 'input_text', text: content }];
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: 'input_text', text }] : [];
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: 'input_text', text: part });
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: 'input_text', text: part.text });
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: 'input_text', text: part.text });
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 baseURL = config.baseUrl || process.env.OPENAI_CODEX_BASE_URL || DEFAULT_BASE_URL;
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 (!baseURL.includes('chatgpt.com/backend-api/codex') && !baseURL.includes('api.openai.com')) {
208
+ if (!this.usesCodexBackend && !baseURL.includes('api.openai.com')) {
132
209
  console.warn(`[OpenAICodex] Using non-official base URL: ${baseURL}`);
133
- } else if (baseURL.includes('chatgpt.com/backend-api/codex')) {
210
+ } else if (this.usesCodexBackend) {
134
211
  console.info(`[OpenAICodex] Using ChatGPT Codex endpoint: ${baseURL}`);
135
212
  }
136
213
 
@@ -145,11 +222,25 @@ class OpenAICodexProvider extends BaseProvider {
145
222
  'gpt-5.4',
146
223
  'gpt-5.4-mini',
147
224
  ]);
148
- const defaultHeaders = baseURL.includes('chatgpt.com/backend-api/codex')
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
149
237
  ? {
150
- 'Editor-Version': process.env.OPENAI_CODEX_EDITOR_VERSION || 'vscode/1.99.0',
151
- 'Editor-Plugin-Version': process.env.OPENAI_CODEX_EDITOR_PLUGIN_VERSION || 'neoagent/1.0.0',
152
- 'User-Agent': process.env.OPENAI_CODEX_USER_AGENT || 'NeoAgent/1.0.0',
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 } : {}),
153
244
  }
154
245
  : undefined;
155
246
 
@@ -157,6 +248,7 @@ class OpenAICodexProvider extends BaseProvider {
157
248
  apiKey: config.apiKey || process.env.OPENAI_CODEX_ACCESS_TOKEN,
158
249
  baseURL,
159
250
  defaultHeaders,
251
+ ...(config.fetch ? { fetch: config.fetch } : {}),
160
252
  });
161
253
  }
162
254
 
@@ -198,11 +290,12 @@ class OpenAICodexProvider extends BaseProvider {
198
290
  continue;
199
291
  }
200
292
 
201
- const content = normalizeInputContent(msg.content);
293
+ const role = msg.role === 'assistant' ? 'assistant' : 'user';
294
+ const content = normalizeMessageContent(msg.content, role);
202
295
  if (content.length > 0) {
203
296
  input.push({
204
297
  type: 'message',
205
- role: msg.role === 'assistant' ? 'assistant' : 'user',
298
+ role,
206
299
  content,
207
300
  });
208
301
  }
@@ -227,40 +320,120 @@ class OpenAICodexProvider extends BaseProvider {
227
320
  input,
228
321
  };
229
322
 
230
- if (instructions.length > 0) {
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
231
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
+ }
232
366
  }
233
367
 
234
- if (tools && tools.length > 0) {
235
- request.tools = this.formatTools(tools);
236
- request.tool_choice = options.toolChoice || 'auto';
237
- }
368
+ return request;
369
+ }
238
370
 
239
- request.max_output_tokens = options.maxTokens || 16384;
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
+ }
240
382
 
241
- if (options.temperature !== undefined && options.temperature !== null) {
242
- request.temperature = options.temperature;
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];
243
394
  }
244
-
245
- const reasoningEffort = options.reasoningEffort || options.reasoning_effort;
246
- if (reasoningEffort || this._isReasoningModel(model)) {
247
- request.reasoning = {
248
- effort: reasoningEffort || 'medium',
249
- };
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
+ }
250
403
  }
251
-
252
404
  return request;
253
405
  }
254
406
 
255
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
+
256
429
  const model = options.model || this.config.model || this.getDefaultModel();
257
430
  const request = this._buildRequest(messages, tools, options, model);
258
431
  let response;
259
432
  try {
260
- response = await this.client.responses.create({
261
- model,
262
- ...request,
263
- });
433
+ response = await this.client.responses.create(
434
+ { model, ...request },
435
+ { headers: this._requestHeaders() },
436
+ );
264
437
  } catch (err) {
265
438
  throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
266
439
  }
@@ -285,11 +458,10 @@ class OpenAICodexProvider extends BaseProvider {
285
458
  const request = this._buildRequest(messages, tools, options, model);
286
459
  let stream;
287
460
  try {
288
- stream = await this.client.responses.create({
289
- model,
290
- ...request,
291
- stream: true,
292
- });
461
+ stream = await this.client.responses.create(
462
+ { model, ...request, stream: true },
463
+ { headers: this._requestHeaders() },
464
+ );
293
465
  } catch (err) {
294
466
  throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
295
467
  }
@@ -72,12 +72,22 @@ const AI_PROVIDER_DEFINITIONS = Object.freeze({
72
72
  'openai-codex': {
73
73
  id: 'openai-codex',
74
74
  label: 'OpenAI Codex',
75
- description: 'Use Codex models through OpenAI authentication.',
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
- defaultBaseUrl: 'https://api.openai.com/v1'
80
+ defaultBaseUrl: 'https://chatgpt.com/backend-api/codex'
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: ''
81
91
  },
82
92
  ollama: {
83
93
  id: 'ollama',