@renxqoo/renx-code 0.0.6 → 0.0.8

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 (29) hide show
  1. package/README.md +40 -45
  2. package/bin/renx.cjs +12 -7
  3. package/package.json +16 -4
  4. package/src/files/workspace-files.ts +4 -6
  5. package/vendor/agent-root/src/agent/tool/base-tool.ts +1 -1
  6. package/vendor/agent-root/src/agent/tool/bash.ts +1 -1
  7. package/vendor/agent-root/src/agent/tool/file-edit-tool.ts +1 -1
  8. package/vendor/agent-root/src/agent/tool/file-history-list.ts +1 -1
  9. package/vendor/agent-root/src/agent/tool/file-history-restore.ts +1 -1
  10. package/vendor/agent-root/src/agent/tool/file-read-tool.ts +1 -1
  11. package/vendor/agent-root/src/agent/tool/glob.ts +1 -1
  12. package/vendor/agent-root/src/agent/tool/grep.ts +2 -2
  13. package/vendor/agent-root/src/agent/tool/lsp.ts +1 -1
  14. package/vendor/agent-root/src/agent/tool/skill-tool.ts +1 -1
  15. package/vendor/agent-root/src/agent/tool/tool-manager.ts +8 -9
  16. package/vendor/agent-root/src/agent/tool/web-fetch.ts +1 -1
  17. package/vendor/agent-root/src/agent/tool/web-search.ts +1 -1
  18. package/vendor/agent-root/src/config/__tests__/load-config-to-env.test.ts +109 -0
  19. package/vendor/agent-root/src/config/__tests__/loader.test.ts +114 -0
  20. package/vendor/agent-root/src/config/index.ts +1 -0
  21. package/vendor/agent-root/src/config/loader.ts +67 -4
  22. package/vendor/agent-root/src/config/types.ts +26 -0
  23. package/vendor/agent-root/src/providers/__tests__/registry.test.ts +82 -8
  24. package/vendor/agent-root/src/providers/index.ts +1 -1
  25. package/vendor/agent-root/src/providers/registry/model-config.ts +291 -44
  26. package/vendor/agent-root/src/providers/registry/provider-factory.ts +8 -4
  27. package/vendor/agent-root/src/providers/registry.ts +8 -8
  28. package/vendor/agent-root/src/providers/types/index.ts +1 -1
  29. package/vendor/agent-root/src/providers/types/registry.ts +10 -30
@@ -7,10 +7,17 @@ import {
7
7
  resolveRenxLogsDir,
8
8
  resolveRenxStorageRoot,
9
9
  } from './paths';
10
- import type { LoadConfigOptions, LogConfig, RenxConfig, ResolvedConfig } from './types';
10
+ import type {
11
+ ConfigModelDefinition,
12
+ LoadConfigOptions,
13
+ LogConfig,
14
+ RenxConfig,
15
+ ResolvedConfig,
16
+ } from './types';
11
17
 
12
18
  const PROJECT_DIR_NAME = '.renx';
13
19
  const CONFIG_FILENAME = 'config.json';
20
+ const CUSTOM_MODELS_ENV_VAR = 'RENX_CUSTOM_MODELS_JSON';
14
21
 
15
22
  const DEFAULTS: RenxConfig = {
16
23
  log: {
@@ -57,6 +64,37 @@ function readJsonFile<T>(filePath: string): T | null {
57
64
  }
58
65
  }
59
66
 
67
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ }
70
+
71
+ function parseCustomModelsEnv(
72
+ raw: string | undefined
73
+ ): Record<string, ConfigModelDefinition> | null {
74
+ if (!raw) {
75
+ return null;
76
+ }
77
+
78
+ try {
79
+ const parsed = JSON.parse(raw) as unknown;
80
+ if (!isPlainObject(parsed)) {
81
+ return null;
82
+ }
83
+
84
+ const result: Record<string, ConfigModelDefinition> = {};
85
+ for (const [modelId, modelConfig] of Object.entries(parsed)) {
86
+ if (!isPlainObject(modelConfig)) {
87
+ continue;
88
+ }
89
+ result[modelId] = modelConfig as ConfigModelDefinition;
90
+ }
91
+
92
+ return result;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
60
98
  function parseLogLevelValue(raw: string | undefined): LogLevel | null {
61
99
  if (!raw) {
62
100
  return null;
@@ -242,6 +280,11 @@ function applyEnvOverrides(config: RenxConfig, env: NodeJS.ProcessEnv): RenxConf
242
280
  }
243
281
  }
244
282
 
283
+ const customModels = parseCustomModelsEnv(env[CUSTOM_MODELS_ENV_VAR]);
284
+ if (customModels) {
285
+ result.models = deepMerge(result.models ?? {}, customModels);
286
+ }
287
+
245
288
  return result;
246
289
  }
247
290
 
@@ -281,6 +324,7 @@ function resolveConfig(
281
324
  confirmationMode: merged.agent?.confirmationMode ?? 'manual',
282
325
  defaultModel: merged.agent?.defaultModel ?? 'qwen3.5-plus',
283
326
  },
327
+ models: merged.models ?? {},
284
328
  sources,
285
329
  };
286
330
  }
@@ -324,25 +368,30 @@ export function loadConfigToEnv(options: LoadConfigOptions = {}): string[] {
324
368
  const loadedFiles: string[] = [];
325
369
 
326
370
  const protectedEnvKeys = new Set(Object.keys(process.env));
371
+ const protectedCustomModels = parseCustomModelsEnv(process.env[CUSTOM_MODELS_ENV_VAR]) ?? {};
327
372
 
328
373
  const globalConfigPath = ensureGlobalConfigFile(globalDir);
329
374
  const globalConfig = readJsonFile<RenxConfig>(globalConfigPath);
330
375
  if (globalConfig) {
331
- applyConfigToEnv(globalConfig, protectedEnvKeys);
376
+ applyConfigToEnv(globalConfig, protectedEnvKeys, protectedCustomModels);
332
377
  loadedFiles.push(globalConfigPath);
333
378
  }
334
379
 
335
380
  const projectConfigPath = path.join(projectRoot, PROJECT_DIR_NAME, CONFIG_FILENAME);
336
381
  const projectConfig = readJsonFile<RenxConfig>(projectConfigPath);
337
382
  if (projectConfig) {
338
- applyConfigToEnv(projectConfig, protectedEnvKeys);
383
+ applyConfigToEnv(projectConfig, protectedEnvKeys, protectedCustomModels);
339
384
  loadedFiles.push(projectConfigPath);
340
385
  }
341
386
 
342
387
  return loadedFiles;
343
388
  }
344
389
 
345
- function applyConfigToEnv(config: RenxConfig, protectedEnvKeys: Set<string>): void {
390
+ function applyConfigToEnv(
391
+ config: RenxConfig,
392
+ protectedEnvKeys: Set<string>,
393
+ protectedCustomModels: Record<string, ConfigModelDefinition>
394
+ ): void {
346
395
  const setIfUnset = (key: string, value: string | undefined) => {
347
396
  if (value !== undefined && !protectedEnvKeys.has(key)) {
348
397
  process.env[key] = value;
@@ -397,6 +446,20 @@ function applyConfigToEnv(config: RenxConfig, protectedEnvKeys: Set<string>): vo
397
446
  config.agent.maxSteps !== undefined ? String(config.agent.maxSteps) : undefined
398
447
  );
399
448
  }
449
+
450
+ if (config.models && Object.keys(config.models).length > 0) {
451
+ const existingModels = parseCustomModelsEnv(process.env[CUSTOM_MODELS_ENV_VAR]) ?? {};
452
+ const mergedModels = deepMerge(existingModels, config.models);
453
+
454
+ if (protectedEnvKeys.has(CUSTOM_MODELS_ENV_VAR)) {
455
+ process.env[CUSTOM_MODELS_ENV_VAR] = JSON.stringify(
456
+ deepMerge(mergedModels, protectedCustomModels)
457
+ );
458
+ return;
459
+ }
460
+
461
+ process.env[CUSTOM_MODELS_ENV_VAR] = JSON.stringify(mergedModels);
462
+ }
400
463
  }
401
464
 
402
465
  export function getGlobalConfigDir(): string {
@@ -1,4 +1,5 @@
1
1
  import type { LogLevel } from '../logger';
2
+ import type { ProviderType } from '../providers/types';
2
3
 
3
4
  export interface LogConfig {
4
5
  level?: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
@@ -24,10 +25,34 @@ export interface AgentConfig {
24
25
  defaultModel?: string;
25
26
  }
26
27
 
28
+ export interface ConfigModelDefinition {
29
+ provider?: ProviderType;
30
+ name?: string;
31
+ endpointPath?: string;
32
+ envApiKey?: string;
33
+ envBaseURL?: string;
34
+ baseURL?: string;
35
+ model?: string;
36
+ max_tokens?: number;
37
+ LLMMAX_TOKENS?: number;
38
+ features?: string[];
39
+ modalities?: {
40
+ image?: boolean;
41
+ audio?: boolean;
42
+ video?: boolean;
43
+ };
44
+ temperature?: number;
45
+ tool_stream?: boolean;
46
+ thinking?: boolean;
47
+ timeout?: number;
48
+ model_reasoning_effort?: 'low' | 'medium' | 'high';
49
+ }
50
+
27
51
  export interface RenxConfig {
28
52
  log?: LogConfig;
29
53
  storage?: StorageConfig;
30
54
  agent?: AgentConfig;
55
+ models?: Record<string, ConfigModelDefinition>;
31
56
  }
32
57
 
33
58
  export interface ResolvedConfig {
@@ -56,6 +81,7 @@ export interface ResolvedConfig {
56
81
  confirmationMode: 'manual' | 'auto-approve' | 'auto-deny';
57
82
  defaultModel: string;
58
83
  };
84
+ models: Record<string, ConfigModelDefinition>;
59
85
  sources: {
60
86
  global: string | null;
61
87
  project: string | null;
@@ -15,6 +15,7 @@ describe('ProviderRegistry', () => {
15
15
  beforeEach(() => {
16
16
  vi.resetModules();
17
17
  process.env = { ...originalEnv };
18
+ delete process.env.RENX_CUSTOM_MODELS_JSON;
18
19
  });
19
20
 
20
21
  afterEach(() => {
@@ -69,12 +70,12 @@ describe('ProviderRegistry', () => {
69
70
  expect(MODEL_CONFIGS['claude-opus-4.6'].endpointPath).toBe('/v1/messages');
70
71
  });
71
72
 
72
- it('should use responses endpoint for gpt-5.3 compatibility gateway', () => {
73
- expect(MODEL_CONFIGS['gpt-5.3'].baseURL).toBe('https://gmncode.cn/v1');
73
+ it('should use responses endpoint for gpt-5.3 official OpenAI API', () => {
74
+ expect(MODEL_CONFIGS['gpt-5.3'].baseURL).toBe('https://api.openai.com/v1');
74
75
  expect(MODEL_CONFIGS['gpt-5.3'].endpointPath).toBe('/responses');
75
76
  expect(MODEL_CONFIGS['gpt-5.3'].model).toBe('gpt-5.3-codex');
76
- expect(MODEL_CONFIGS['gpt-5.3'].max_tokens).toBe(128 * 1000);
77
- expect(MODEL_CONFIGS['gpt-5.3'].LLMMAX_TOKENS).toBe(400 * 1000);
77
+ expect(MODEL_CONFIGS['gpt-5.3'].max_tokens).toBe(1000 * 32);
78
+ expect(MODEL_CONFIGS['gpt-5.3'].LLMMAX_TOKENS).toBe(258 * 1000);
78
79
  expect(MODEL_CONFIGS['gpt-5.3'].features).toContain('reasoning');
79
80
  });
80
81
  });
@@ -132,20 +133,45 @@ describe('ProviderRegistry', () => {
132
133
  expect(provider.adapter).toBeInstanceOf(StandardAdapter);
133
134
  });
134
135
 
135
- it('should create gpt-5.3 provider with gmn-compatible defaults', () => {
136
+ it('should create gpt-5.3 provider with official OpenAI defaults', () => {
136
137
  process.env.OPENAI_API_KEY = 'test-openai-key';
137
138
  delete process.env.OPENAI_API_BASE;
138
139
 
139
140
  const provider = ProviderRegistry.createFromEnv('gpt-5.3');
140
141
 
141
142
  expect(provider.config.apiKey).toBe('test-openai-key');
142
- expect(provider.config.baseURL).toBe('https://gmncode.cn/v1');
143
+ expect(provider.config.baseURL).toBe('https://api.openai.com/v1');
143
144
  expect(provider.config.model).toBe('gpt-5.3-codex');
144
- expect(provider.config.max_tokens).toBe(128 * 1000);
145
- expect(provider.config.LLMMAX_TOKENS).toBe(400 * 1000);
145
+ expect(provider.config.max_tokens).toBe(1000 * 32);
146
+ expect(provider.config.LLMMAX_TOKENS).toBe(258 * 1000);
146
147
  expect(provider.adapter).toBeInstanceOf(ResponsesAdapter);
147
148
  });
148
149
 
150
+ it('should create provider for a custom model from RENX_CUSTOM_MODELS_JSON', () => {
151
+ process.env.CUSTOM_OPENAI_API_KEY = 'custom-key';
152
+ process.env.RENX_CUSTOM_MODELS_JSON = JSON.stringify({
153
+ 'custom-openai': {
154
+ provider: 'openai',
155
+ name: 'Custom OpenAI',
156
+ baseURL: 'https://custom.example.com/v1',
157
+ endpointPath: '/chat/completions',
158
+ envApiKey: 'CUSTOM_OPENAI_API_KEY',
159
+ envBaseURL: 'CUSTOM_OPENAI_API_BASE',
160
+ model: 'custom-model',
161
+ max_tokens: 4096,
162
+ LLMMAX_TOKENS: 64000,
163
+ features: ['streaming', 'function-calling'],
164
+ },
165
+ });
166
+
167
+ const provider = ProviderRegistry.createFromEnv('custom-openai');
168
+
169
+ expect(provider.config.apiKey).toBe('custom-key');
170
+ expect(provider.config.baseURL).toBe('https://custom.example.com/v1');
171
+ expect(provider.config.model).toBe('custom-model');
172
+ expect(provider.adapter).toBeInstanceOf(StandardAdapter);
173
+ });
174
+
149
175
  it('should use default baseURL when env var not set', () => {
150
176
  process.env.GLM_API_KEY = 'test-key';
151
177
  delete process.env.GLM_API_BASE;
@@ -164,6 +190,20 @@ describe('ProviderRegistry', () => {
164
190
  expect(provider.config.baseURL).toBe('https://custom.example.com');
165
191
  });
166
192
 
193
+ it('should allow custom model config to override a built-in baseURL', () => {
194
+ process.env.OPENAI_API_KEY = 'test-openai-key';
195
+ process.env.RENX_CUSTOM_MODELS_JSON = JSON.stringify({
196
+ 'gpt-5.4': {
197
+ baseURL: 'https://proxy.example.com/v1',
198
+ },
199
+ });
200
+
201
+ const provider = ProviderRegistry.createFromEnv('gpt-5.4');
202
+
203
+ expect(provider.config.baseURL).toBe('https://proxy.example.com/v1');
204
+ expect(provider.adapter).toBeInstanceOf(ResponsesAdapter);
205
+ });
206
+
167
207
  it('should accept config overrides', () => {
168
208
  process.env.GLM_API_KEY = 'test-key';
169
209
 
@@ -355,6 +395,25 @@ describe('ProviderRegistry', () => {
355
395
 
356
396
  expect(Array.isArray(ids)).toBe(true);
357
397
  });
398
+
399
+ it('should include custom model IDs from env config', () => {
400
+ process.env.RENX_CUSTOM_MODELS_JSON = JSON.stringify({
401
+ 'custom-openai': {
402
+ provider: 'openai',
403
+ name: 'Custom OpenAI',
404
+ baseURL: 'https://custom.example.com/v1',
405
+ endpointPath: '/chat/completions',
406
+ envApiKey: 'CUSTOM_OPENAI_API_KEY',
407
+ envBaseURL: 'CUSTOM_OPENAI_API_BASE',
408
+ model: 'custom-model',
409
+ max_tokens: 4096,
410
+ LLMMAX_TOKENS: 64000,
411
+ features: ['streaming'],
412
+ },
413
+ });
414
+
415
+ expect(ProviderRegistry.getModelIds()).toContain('custom-openai');
416
+ });
358
417
  });
359
418
 
360
419
  describe('getModelConfig', () => {
@@ -379,6 +438,21 @@ describe('ProviderRegistry', () => {
379
438
  ProviderRegistry.getModelConfig('unknown' as ModelId);
380
439
  }).toThrow('Unknown model: unknown');
381
440
  });
441
+
442
+ it('should reflect built-in overrides from custom env config', () => {
443
+ process.env.RENX_CUSTOM_MODELS_JSON = JSON.stringify({
444
+ 'gpt-5.4': {
445
+ baseURL: 'https://proxy.example.com/v1',
446
+ name: 'GPT-5.4 Proxy',
447
+ },
448
+ });
449
+
450
+ const config = ProviderRegistry.getModelConfig('gpt-5.4');
451
+
452
+ expect(config.baseURL).toBe('https://proxy.example.com/v1');
453
+ expect(config.name).toBe('GPT-5.4 Proxy');
454
+ expect(config.envApiKey).toBe('OPENAI_API_KEY');
455
+ });
382
456
  });
383
457
 
384
458
  describe('getModelName', () => {
@@ -4,7 +4,7 @@
4
4
 
5
5
  // Registry 相关
6
6
  export { Models, MODEL_CONFIGS, ProviderRegistry } from './registry';
7
- export type { ProviderType, ModelId } from './registry';
7
+ export type { ProviderType, BuiltinModelId, ModelId } from './registry';
8
8
 
9
9
  // Provider 相关
10
10
  export { LLMProvider } from './types';