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.
- 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 +212 -40
- package/server/services/ai/settings.js +12 -2
- 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
|
-
const DEFAULT_BASE_URL = 'https://
|
|
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,11 +222,25 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
145
222
|
'gpt-5.4',
|
|
146
223
|
'gpt-5.4-mini',
|
|
147
224
|
]);
|
|
148
|
-
|
|
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
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'User-Agent':
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
request.tool_choice = options.toolChoice || 'auto';
|
|
237
|
-
}
|
|
368
|
+
return request;
|
|
369
|
+
}
|
|
238
370
|
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
|
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://
|
|
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',
|