@foxden-app/foxclaw 0.5.63 → 0.5.64

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 CHANGED
@@ -27,8 +27,19 @@ CODEX_APP_SERVER_STATE_PATH=
27
27
  CODEX_APP_SERVER_LOG_PATH=
28
28
  CODEX_APP_SYNC_ON_OPEN=true
29
29
  CODEX_APP_SYNC_ON_TURN_COMPLETE=false
30
- STORE_PATH=
31
- LOG_LEVEL=info
30
+ # Optional OpenAI-compatible model providers for Codex app-server.
31
+ # Keys stay in environment variables; CODEX_API_PROVIDERS only references their names.
32
+ # Codex currently requires Responses-compatible endpoints. If your source URL is
33
+ # /v1/chat/completions, FoxClaw normalizes the base URL for display/injection but
34
+ # the upstream service still needs /v1/responses compatibility or a proxy shim.
35
+ # Compact form: id|url|ENV_KEY|model|display name
36
+ # CODEX_API_PROVIDERS=shop|https://example.com/v1|FOXCLAW_CODEX_API_KEY_SHOP|gpt-5.5|Shop Proxy
37
+ # CODEX_API_DEFAULT_PROVIDER=shop
38
+ # FOXCLAW_CODEX_API_KEY_SHOP=sk-...
39
+ # JSON form:
40
+ # CODEX_API_PROVIDERS=[{"id":"shop","baseUrl":"https://example.com/v1","apiKeyEnv":"FOXCLAW_CODEX_API_KEY_SHOP","model":"gpt-5.5","displayName":"Shop Proxy"}]
41
+ STORE_PATH=
42
+ LOG_LEVEL=info
32
43
  DEFAULT_CWD=/absolute/path/to/workspace
33
44
  DEFAULT_APPROVAL_POLICY=on-request
34
45
  DEFAULT_SANDBOX_MODE=workspace-write
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.64 - 2026-06-29
6
+
7
+ ### 中文
8
+ - 新增 OpenAI-compatible API provider 配置支持:`CODEX_API_PROVIDERS` 可声明 provider id、base URL、key 环境变量、默认模型,并在启动 Codex app-server 时注入原生 `model_providers.*` 配置。
9
+ - 新增 `CODEX_API_DEFAULT_PROVIDER`,用于显式把 Codex 默认 `model_provider` 切到已配置的 API provider;未设置时只登记 provider,不会悄悄替换现有 ChatGPT/Codex 登录态。
10
+ - `/config` 现在展示 API provider 摘要、默认 provider、key 环境变量是否存在,并对 `/v1/chat/completions` 来源提示 Codex 当前需要 Responses-compatible 端点;API key 不会出现在面板或 Codex 配置 override 中。
11
+
12
+ ### English
13
+ - Added OpenAI-compatible API provider configuration. `CODEX_API_PROVIDERS` can declare provider id, base URL, API-key environment variable, and default model, then FoxClaw injects native Codex `model_providers.*` app-server config.
14
+ - Added `CODEX_API_DEFAULT_PROVIDER` to explicitly switch Codex's default `model_provider` to a configured API provider. Without it, providers are registered but the existing ChatGPT/Codex auth flow is not silently replaced.
15
+ - `/config` now shows API provider summaries, the default provider, whether each key env var is present, and warns when the source URL was `/v1/chat/completions` because current Codex requires a Responses-compatible endpoint. API keys are not printed in panels or Codex config overrides.
16
+
5
17
  ## 0.5.63 - 2026-06-28
6
18
 
7
19
  ### 中文
package/dist/config.d.ts CHANGED
@@ -31,6 +31,8 @@ export interface AppConfig {
31
31
  codexAppServerLogPath: string;
32
32
  codexAuthDir: string | null;
33
33
  codexHome: string | null;
34
+ codexApiProviders: CodexApiProviderConfig[];
35
+ codexApiDefaultProvider: string | null;
34
36
  codexAppSyncOnOpen: boolean;
35
37
  codexAppSyncOnTurnComplete: boolean;
36
38
  storePath: string;
@@ -79,6 +81,18 @@ export interface AppConfig {
79
81
  voiceTextLimit: number;
80
82
  voiceTtsTimeoutMs: number;
81
83
  }
84
+ export interface CodexApiProviderConfig {
85
+ id: string;
86
+ name: string;
87
+ baseUrl: string;
88
+ apiKeyEnv: string;
89
+ model: string | null;
90
+ wireApi: 'responses';
91
+ sourceEndpoint: string | null;
92
+ chatCompletionsOnly: boolean;
93
+ }
82
94
  export declare function loadConfig(): AppConfig;
95
+ export declare function buildCodexApiProviderOverrides(providers: readonly CodexApiProviderConfig[], defaultProviderId?: string | null): string[];
96
+ export declare function parseCodexApiProviders(raw: string | undefined): CodexApiProviderConfig[];
83
97
  export declare function selectDefaultRuntimeBotToken(configuredTokens: string[], legacyToken: string | null): string | null;
84
98
  export declare function ensureAppDirs(config: AppConfig): void;
package/dist/config.js CHANGED
@@ -68,6 +68,8 @@ export function loadConfig() {
68
68
  codexAppServerLogPath: process.env.CODEX_APP_SERVER_LOG_PATH || DEFAULT_CODEX_APP_SERVER_LOG_PATH,
69
69
  codexAuthDir: process.env.CODEX_AUTH_DIR?.trim() || null,
70
70
  codexHome: process.env.CODEX_HOME?.trim() || null,
71
+ codexApiProviders: parseCodexApiProviders(process.env.CODEX_API_PROVIDERS),
72
+ codexApiDefaultProvider: optionalSanitizedProviderId(process.env.CODEX_API_DEFAULT_PROVIDER),
71
73
  codexAppSyncOnOpen: boolEnv('CODEX_APP_SYNC_ON_OPEN', true),
72
74
  codexAppSyncOnTurnComplete: boolEnv('CODEX_APP_SYNC_ON_TURN_COMPLETE', false),
73
75
  storePath: process.env.STORE_PATH || DEFAULT_STORE_PATH,
@@ -116,6 +118,124 @@ export function loadConfig() {
116
118
  ensureAppDirs(config);
117
119
  return config;
118
120
  }
121
+ export function buildCodexApiProviderOverrides(providers, defaultProviderId = null) {
122
+ const overrides = [];
123
+ for (const provider of providers) {
124
+ overrides.push(`model_providers.${provider.id}=${tomlInlineTable({
125
+ name: provider.name,
126
+ base_url: provider.baseUrl,
127
+ env_key: provider.apiKeyEnv,
128
+ wire_api: provider.wireApi,
129
+ })}`);
130
+ }
131
+ if (defaultProviderId) {
132
+ const selected = providers.find(provider => provider.id === defaultProviderId);
133
+ if (!selected) {
134
+ throw new Error(`CODEX_API_DEFAULT_PROVIDER does not match any configured provider: ${defaultProviderId}`);
135
+ }
136
+ overrides.push(`model_provider=${tomlString(selected.id)}`);
137
+ if (selected.model) {
138
+ overrides.push(`model=${tomlString(selected.model)}`);
139
+ }
140
+ }
141
+ return overrides;
142
+ }
143
+ export function parseCodexApiProviders(raw) {
144
+ if (!raw?.trim())
145
+ return [];
146
+ const trimmed = raw.trim();
147
+ if (trimmed.startsWith('[')) {
148
+ const parsed = JSON.parse(trimmed);
149
+ if (!Array.isArray(parsed)) {
150
+ throw new Error('CODEX_API_PROVIDERS JSON must be an array');
151
+ }
152
+ return parsed.map((entry, index) => normalizeCodexApiProvider(entry, index));
153
+ }
154
+ return trimmed
155
+ .split(',')
156
+ .map((entry) => entry.trim())
157
+ .filter(Boolean)
158
+ .map((entry, index) => {
159
+ const parts = entry.split('|').map((part) => part.trim());
160
+ if (parts.length < 3 || parts.length > 5) {
161
+ throw new Error('CODEX_API_PROVIDERS entries must use id|url|env_key[|model][|name]');
162
+ }
163
+ const [id, url, apiKeyEnv, model, name] = parts;
164
+ return normalizeCodexApiProvider({ id, url, apiKeyEnv, model, displayName: name }, index);
165
+ });
166
+ }
167
+ function normalizeCodexApiProvider(entry, index) {
168
+ if (!entry || typeof entry !== 'object') {
169
+ throw new Error(`CODEX_API_PROVIDERS[${index}] must be an object`);
170
+ }
171
+ const record = entry;
172
+ const id = sanitizeCodexProviderId(stringField(record, ['id'], index));
173
+ const sourceEndpoint = optionalStringField(record, ['endpoint', 'api', 'url', 'baseUrl', 'base_url']);
174
+ if (!sourceEndpoint) {
175
+ throw new Error(`CODEX_API_PROVIDERS[${index}] is missing url/baseUrl`);
176
+ }
177
+ const normalized = normalizeOpenAiCompatibleBaseUrl(sourceEndpoint);
178
+ const apiKeyEnv = stringField(record, ['apiKeyEnv', 'api_key_env', 'envKey', 'env_key'], index);
179
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(apiKeyEnv)) {
180
+ throw new Error(`CODEX_API_PROVIDERS[${index}] env key is not a valid environment variable name`);
181
+ }
182
+ const displayName = optionalStringField(record, ['displayName', 'display_name', 'label']) ?? id;
183
+ return {
184
+ id,
185
+ name: displayName,
186
+ baseUrl: normalized.baseUrl,
187
+ apiKeyEnv,
188
+ model: optionalStringField(record, ['model']),
189
+ wireApi: 'responses',
190
+ sourceEndpoint,
191
+ chatCompletionsOnly: normalized.chatCompletionsEndpoint,
192
+ };
193
+ }
194
+ function normalizeOpenAiCompatibleBaseUrl(input) {
195
+ const trimmed = input.trim().replace(/\/+$/, '');
196
+ if (!trimmed) {
197
+ throw new Error('CODEX_API_PROVIDERS contains an empty URL');
198
+ }
199
+ const chatSuffix = '/chat/completions';
200
+ if (trimmed.endsWith(chatSuffix)) {
201
+ return { baseUrl: trimmed.slice(0, -chatSuffix.length), chatCompletionsEndpoint: true };
202
+ }
203
+ return { baseUrl: trimmed, chatCompletionsEndpoint: false };
204
+ }
205
+ function stringField(record, keys, index) {
206
+ const value = optionalStringField(record, keys);
207
+ if (!value) {
208
+ throw new Error(`CODEX_API_PROVIDERS[${index}] is missing ${keys[0]}`);
209
+ }
210
+ return value;
211
+ }
212
+ function optionalStringField(record, keys) {
213
+ for (const key of keys) {
214
+ const value = record[key];
215
+ if (typeof value === 'string' && value.trim()) {
216
+ return value.trim();
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+ function sanitizeCodexProviderId(value) {
222
+ const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
223
+ if (!sanitized) {
224
+ throw new Error('CODEX_API_PROVIDERS contains an invalid provider id');
225
+ }
226
+ return sanitized;
227
+ }
228
+ function optionalSanitizedProviderId(value) {
229
+ if (!value?.trim())
230
+ return null;
231
+ return sanitizeCodexProviderId(value);
232
+ }
233
+ function tomlInlineTable(values) {
234
+ return `{ ${Object.entries(values).map(([key, value]) => `${key} = ${tomlString(value)}`).join(', ')} }`;
235
+ }
236
+ function tomlString(value) {
237
+ return JSON.stringify(value);
238
+ }
119
239
  export function selectDefaultRuntimeBotToken(configuredTokens, legacyToken) {
120
240
  if (configuredTokens.length === 0 || !legacyToken)
121
241
  return null;
@@ -9471,8 +9471,29 @@ function formatConfigMessage(locale, result, appConfig, authPoolStats) {
9471
9471
  lines.push(t(locale, 'config_panel_ttl', { value: formatDurationMs(locale, appConfig.telegramPanelTtlMs) }));
9472
9472
  lines.push(`TELEGRAM_PANEL_TTL_MS=${appConfig.telegramPanelTtlMs}`);
9473
9473
  lines.push(formatCodexAuthPoolSummary(locale, authPoolStats));
9474
+ lines.push(...formatCodexApiProviderConfigLines(locale, appConfig));
9474
9475
  return lines.join('\n');
9475
9476
  }
9477
+ function formatCodexApiProviderConfigLines(locale, appConfig) {
9478
+ if (appConfig.codexApiProviders.length === 0) {
9479
+ return [t(locale, 'config_api_providers_none')];
9480
+ }
9481
+ const lines = [t(locale, 'config_api_providers_title')];
9482
+ lines.push(t(locale, 'config_api_default_provider', { value: appConfig.codexApiDefaultProvider ?? '-' }));
9483
+ for (const provider of appConfig.codexApiProviders) {
9484
+ lines.push(t(locale, 'config_api_provider_line', {
9485
+ id: provider.id,
9486
+ model: provider.model ?? '-',
9487
+ base: provider.baseUrl,
9488
+ env: provider.apiKeyEnv,
9489
+ key: process.env[provider.apiKeyEnv]?.trim() ? t(locale, 'yes') : t(locale, 'no'),
9490
+ }));
9491
+ if (provider.chatCompletionsOnly) {
9492
+ lines.push(t(locale, 'config_api_provider_chat_warning', { id: provider.id }));
9493
+ }
9494
+ }
9495
+ return lines;
9496
+ }
9476
9497
  function formatCodexAuthPoolSummary(locale, stats) {
9477
9498
  return t(locale, 'auth_pool_summary', {
9478
9499
  total: stats.totalSeen,
package/dist/i18n.d.ts CHANGED
@@ -623,6 +623,11 @@ declare const MESSAGES: {
623
623
  readonly config_delete_tool_details_updated: "Delete operation details after final reply set to: {value}";
624
624
  readonly config_delete_tool_details_usage: "Usage: /config delete_tool_details <on|off>";
625
625
  readonly config_panel_ttl: "Interactive panel cleanup: {value}";
626
+ readonly config_api_providers_none: "API providers: none configured";
627
+ readonly config_api_providers_title: "API providers:";
628
+ readonly config_api_default_provider: "Default API provider: {value}";
629
+ readonly config_api_provider_line: "- {id}: model {model}, base {base}, key env {env} present {key}";
630
+ readonly config_api_provider_chat_warning: " {id}: source was /chat/completions; Codex requires a Responses-compatible endpoint.";
626
631
  readonly config_env_update_failed: "Runtime setting changed, but updating {value} failed: {error}";
627
632
  readonly requirements_title: "Config requirements";
628
633
  readonly requirements_empty: "No config requirements are configured.";
@@ -1329,6 +1334,11 @@ declare const MESSAGES: {
1329
1334
  readonly config_delete_tool_details_updated: "最终回复后删除操作明细已设置为:{value}";
1330
1335
  readonly config_delete_tool_details_usage: "用法:/config delete_tool_details <on|off>";
1331
1336
  readonly config_panel_ttl: "交互面板自动清理:{value}";
1337
+ readonly config_api_providers_none: "API Provider:未配置";
1338
+ readonly config_api_providers_title: "API Provider:";
1339
+ readonly config_api_default_provider: "默认 API Provider:{value}";
1340
+ readonly config_api_provider_line: "- {id}:模型 {model},base {base},key 环境变量 {env} 存在 {key}";
1341
+ readonly config_api_provider_chat_warning: " {id}:来源是 /chat/completions;Codex 需要兼容 Responses 的端点。";
1332
1342
  readonly config_env_update_failed: "运行时设置已改变,但更新 {value} 失败:{error}";
1333
1343
  readonly requirements_title: "配置要求";
1334
1344
  readonly requirements_empty: "当前没有配置要求。";
package/dist/i18n.js CHANGED
@@ -621,6 +621,11 @@ const MESSAGES = {
621
621
  config_delete_tool_details_updated: 'Delete operation details after final reply set to: {value}',
622
622
  config_delete_tool_details_usage: 'Usage: /config delete_tool_details <on|off>',
623
623
  config_panel_ttl: 'Interactive panel cleanup: {value}',
624
+ config_api_providers_none: 'API providers: none configured',
625
+ config_api_providers_title: 'API providers:',
626
+ config_api_default_provider: 'Default API provider: {value}',
627
+ config_api_provider_line: '- {id}: model {model}, base {base}, key env {env} present {key}',
628
+ config_api_provider_chat_warning: ' {id}: source was /chat/completions; Codex requires a Responses-compatible endpoint.',
624
629
  config_env_update_failed: 'Runtime setting changed, but updating {value} failed: {error}',
625
630
  requirements_title: 'Config requirements',
626
631
  requirements_empty: 'No config requirements are configured.',
@@ -1327,6 +1332,11 @@ const MESSAGES = {
1327
1332
  config_delete_tool_details_updated: '最终回复后删除操作明细已设置为:{value}',
1328
1333
  config_delete_tool_details_usage: '用法:/config delete_tool_details <on|off>',
1329
1334
  config_panel_ttl: '交互面板自动清理:{value}',
1335
+ config_api_providers_none: 'API Provider:未配置',
1336
+ config_api_providers_title: 'API Provider:',
1337
+ config_api_default_provider: '默认 API Provider:{value}',
1338
+ config_api_provider_line: '- {id}:模型 {model},base {base},key 环境变量 {env} 存在 {key}',
1339
+ config_api_provider_chat_warning: ' {id}:来源是 /chat/completions;Codex 需要兼容 Responses 的端点。',
1330
1340
  config_env_update_failed: '运行时设置已改变,但更新 {value} 失败:{error}',
1331
1341
  requirements_title: '配置要求',
1332
1342
  requirements_empty: '当前没有配置要求。',
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import process from 'node:process';
7
7
  import { createInterface } from 'node:readline/promises';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
10
+ import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
11
11
  import { createAuthRefreshNotificationAggregator, } from './auth/notifications.js';
12
12
  import { installBundledCodexSkills } from './codex_skills.js';
13
13
  import { acquireProcessLock, LockHeldError } from './lock.js';
@@ -448,6 +448,7 @@ async function runServeCli() {
448
448
  ]);
449
449
  const config = loadConfig();
450
450
  const logger = new Logger(config.logLevel, config.logPath);
451
+ const codexApiProviderOverrides = buildCodexApiProviderOverrides(config.codexApiProviders, config.codexApiDefaultProvider);
451
452
  const authNotificationAggregator = createAuthRefreshNotificationAggregator(logger);
452
453
  attachIlinkRuntimeFromBridgeLogger(logger, config.wxIlinkRouteTag);
453
454
  const processLock = acquireProcessLock(config.lockPath);
@@ -496,7 +497,10 @@ async function runServeCli() {
496
497
  const childEnv = sharedDefaultRuntime
497
498
  ? (config.codexHome ? { CODEX_HOME: config.codexHome } : null)
498
499
  : { CODEX_HOME: home };
499
- const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']);
500
+ const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, [
501
+ ...codexApiProviderOverrides,
502
+ ...(sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']),
503
+ ]);
500
504
  seeds.push({ id, home, authDir, sharedDefaultRuntime, config: runtimeConfig, bot, app });
501
505
  }
502
506
  let authSync = null;
@@ -670,7 +674,7 @@ async function runServeCli() {
670
674
  runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
671
675
  }
672
676
  if (config.wxEnabled) {
673
- const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
677
+ const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
674
678
  const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
675
679
  activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
676
680
  managedApps.push(weixinApp);
@@ -764,7 +768,7 @@ async function runServeCli() {
764
768
  const singleCodexHome = config.codexHome ?? path.join(os.homedir(), '.codex');
765
769
  installBundledCodexSkills(packageRoot, singleCodexHome);
766
770
  const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
767
- const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
771
+ const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
768
772
  const telegramMessaging = new TelegramMessagingPort(bot);
769
773
  const weixinMessaging = config.wxEnabled
770
774
  ? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.63",
3
+ "version": "0.5.64",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",