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.
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 +226 -41
  45. package/server/services/ai/settings.js +11 -1
  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
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,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
- 'Editor-Version': process.env.OPENAI_CODEX_EDITOR_VERSION || 'vscode/1.99.0',
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 content = normalizeInputContent(msg.content);
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: msg.role === 'assistant' ? 'assistant' : 'user',
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 (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
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
- if (tools && tools.length > 0) {
222
- request.tools = this.formatTools(tools);
223
- request.tool_choice = options.toolChoice || 'auto';
224
- }
368
+ return request;
369
+ }
225
370
 
226
- 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
+ }
227
382
 
228
- if (options.temperature !== undefined && options.temperature !== null) {
229
- 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];
230
394
  }
231
-
232
- const reasoningEffort = options.reasoningEffort || options.reasoning_effort;
233
- if (reasoningEffort || this._isReasoningModel(model)) {
234
- request.reasoning = {
235
- effort: reasoningEffort || 'medium',
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
- ...request,
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
- ...request,
278
- stream: true,
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 your ChatGPT/Codex subscription as an AI provider.',
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',