@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.
- package/README.md +40 -45
- package/bin/renx.cjs +12 -7
- package/package.json +16 -4
- package/src/files/workspace-files.ts +4 -6
- package/vendor/agent-root/src/agent/tool/base-tool.ts +1 -1
- package/vendor/agent-root/src/agent/tool/bash.ts +1 -1
- package/vendor/agent-root/src/agent/tool/file-edit-tool.ts +1 -1
- package/vendor/agent-root/src/agent/tool/file-history-list.ts +1 -1
- package/vendor/agent-root/src/agent/tool/file-history-restore.ts +1 -1
- package/vendor/agent-root/src/agent/tool/file-read-tool.ts +1 -1
- package/vendor/agent-root/src/agent/tool/glob.ts +1 -1
- package/vendor/agent-root/src/agent/tool/grep.ts +2 -2
- package/vendor/agent-root/src/agent/tool/lsp.ts +1 -1
- package/vendor/agent-root/src/agent/tool/skill-tool.ts +1 -1
- package/vendor/agent-root/src/agent/tool/tool-manager.ts +8 -9
- package/vendor/agent-root/src/agent/tool/web-fetch.ts +1 -1
- package/vendor/agent-root/src/agent/tool/web-search.ts +1 -1
- package/vendor/agent-root/src/config/__tests__/load-config-to-env.test.ts +109 -0
- package/vendor/agent-root/src/config/__tests__/loader.test.ts +114 -0
- package/vendor/agent-root/src/config/index.ts +1 -0
- package/vendor/agent-root/src/config/loader.ts +67 -4
- package/vendor/agent-root/src/config/types.ts +26 -0
- package/vendor/agent-root/src/providers/__tests__/registry.test.ts +82 -8
- package/vendor/agent-root/src/providers/index.ts +1 -1
- package/vendor/agent-root/src/providers/registry/model-config.ts +291 -44
- package/vendor/agent-root/src/providers/registry/provider-factory.ts +8 -4
- package/vendor/agent-root/src/providers/registry.ts +8 -8
- package/vendor/agent-root/src/providers/types/index.ts +1 -1
- 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 {
|
|
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(
|
|
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
|
|
73
|
-
expect(MODEL_CONFIGS['gpt-5.3'].baseURL).toBe('https://
|
|
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(
|
|
77
|
-
expect(MODEL_CONFIGS['gpt-5.3'].LLMMAX_TOKENS).toBe(
|
|
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
|
|
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://
|
|
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(
|
|
145
|
-
expect(provider.config.LLMMAX_TOKENS).toBe(
|
|
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';
|