@renxqoo/renx-code 0.0.7 → 0.0.9
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 +103 -43
- package/package.json +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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Renx
|
|
2
2
|
|
|
3
|
-
`renx` is a terminal AI coding assistant.
|
|
3
|
+
`renx` is a terminal AI coding assistant for reading code, editing files, fixing errors, and working directly inside your current project.
|
|
4
4
|
|
|
5
5
|
- npm package: `@renxqoo/renx-code`
|
|
6
6
|
- command: `renx`
|
|
@@ -23,24 +23,24 @@ Start:
|
|
|
23
23
|
renx
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## Quick Start
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Run `renx` inside any project directory:
|
|
28
|
+
Open a project directory and run:
|
|
31
29
|
|
|
32
30
|
```bash
|
|
31
|
+
cd your-project
|
|
33
32
|
renx
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
`renx` uses the current terminal directory as the workspace by default.
|
|
37
36
|
|
|
38
|
-
Common things you can do:
|
|
37
|
+
Common things you can ask it to do:
|
|
39
38
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
39
|
+
- explain a codebase or a single file
|
|
40
|
+
- fix build or runtime errors
|
|
41
|
+
- modify files directly
|
|
42
|
+
- help write features, tests, and scripts
|
|
43
|
+
- investigate logs and terminal output
|
|
44
44
|
|
|
45
45
|
Built-in commands:
|
|
46
46
|
|
|
@@ -71,37 +71,19 @@ Default user data locations:
|
|
|
71
71
|
- `RENX_HOME/task/`
|
|
72
72
|
- `RENX_HOME/data.db`
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Runtime:
|
|
77
|
-
|
|
78
|
-
- `RENX_HOME`
|
|
79
|
-
- `AGENT_MODEL`
|
|
80
|
-
- `AGENT_MAX_STEPS`
|
|
81
|
-
- `AGENT_MAX_RETRY_COUNT`
|
|
82
|
-
- `AGENT_TOOL_CONFIRMATION_MODE`
|
|
83
|
-
- `AGENT_CONVERSATION_ID`
|
|
84
|
-
- `AGENT_SESSION_ID`
|
|
85
|
-
|
|
86
|
-
Logging:
|
|
87
|
-
|
|
88
|
-
- `AGENT_LOG_LEVEL`
|
|
89
|
-
- `AGENT_LOG_FORMAT`
|
|
90
|
-
- `AGENT_LOG_CONSOLE`
|
|
91
|
-
- `AGENT_LOG_FILE_ENABLED`
|
|
92
|
-
|
|
93
|
-
File history:
|
|
74
|
+
Project config example:
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
- `AGENT_FILE_HISTORY_MAX_TOTAL_MB`
|
|
76
|
+
```text
|
|
77
|
+
your-project/.renx/config.json
|
|
78
|
+
```
|
|
99
79
|
|
|
100
|
-
|
|
80
|
+
Global config example:
|
|
101
81
|
|
|
102
|
-
|
|
82
|
+
```text
|
|
83
|
+
~/.renx/config.json
|
|
84
|
+
```
|
|
103
85
|
|
|
104
|
-
## Config Example
|
|
86
|
+
## Basic Config Example
|
|
105
87
|
|
|
106
88
|
```json
|
|
107
89
|
{
|
|
@@ -127,14 +109,92 @@ Provider API keys are still passed through their own environment variables, for
|
|
|
127
109
|
}
|
|
128
110
|
```
|
|
129
111
|
|
|
130
|
-
|
|
112
|
+
## Custom Model Config
|
|
131
113
|
|
|
132
|
-
|
|
133
|
-
|
|
114
|
+
You can define your own model in global or project `config.json` and then point `agent.defaultModel` to it.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"agent": {
|
|
121
|
+
"defaultModel": "my-openai-compatible"
|
|
122
|
+
},
|
|
123
|
+
"models": {
|
|
124
|
+
"my-openai-compatible": {
|
|
125
|
+
"provider": "openai",
|
|
126
|
+
"name": "My OpenAI Compatible",
|
|
127
|
+
"baseURL": "https://example.com/v1",
|
|
128
|
+
"endpointPath": "/chat/completions",
|
|
129
|
+
"envApiKey": "MY_API_KEY",
|
|
130
|
+
"envBaseURL": "MY_API_BASE",
|
|
131
|
+
"model": "my-model",
|
|
132
|
+
"max_tokens": 8000,
|
|
133
|
+
"LLMMAX_TOKENS": 128000,
|
|
134
|
+
"features": ["streaming", "function-calling"]
|
|
135
|
+
},
|
|
136
|
+
"gpt-5.4": {
|
|
137
|
+
"baseURL": "https://my-proxy.example.com/v1"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
134
141
|
```
|
|
135
142
|
|
|
136
|
-
|
|
143
|
+
This supports both:
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
- adding a completely new model
|
|
146
|
+
- overriding part of a built-in model, such as `baseURL`
|
|
147
|
+
|
|
148
|
+
## Environment Variables
|
|
149
|
+
|
|
150
|
+
Runtime:
|
|
151
|
+
|
|
152
|
+
- `RENX_HOME`
|
|
153
|
+
- `AGENT_MODEL`
|
|
154
|
+
- `AGENT_MAX_STEPS`
|
|
155
|
+
- `AGENT_MAX_RETRY_COUNT`
|
|
156
|
+
- `AGENT_TOOL_CONFIRMATION_MODE`
|
|
157
|
+
- `AGENT_CONVERSATION_ID`
|
|
158
|
+
- `AGENT_SESSION_ID`
|
|
159
|
+
|
|
160
|
+
Logging:
|
|
161
|
+
|
|
162
|
+
- `AGENT_LOG_LEVEL`
|
|
163
|
+
- `AGENT_LOG_FORMAT`
|
|
164
|
+
- `AGENT_LOG_CONSOLE`
|
|
165
|
+
- `AGENT_LOG_FILE_ENABLED`
|
|
166
|
+
|
|
167
|
+
File history:
|
|
168
|
+
|
|
169
|
+
- `AGENT_FILE_HISTORY_ENABLED`
|
|
170
|
+
- `AGENT_FILE_HISTORY_MAX_PER_FILE`
|
|
171
|
+
- `AGENT_FILE_HISTORY_MAX_AGE_DAYS`
|
|
172
|
+
- `AGENT_FILE_HISTORY_MAX_TOTAL_MB`
|
|
173
|
+
|
|
174
|
+
Custom models:
|
|
175
|
+
|
|
176
|
+
- `RENX_CUSTOM_MODELS_JSON`
|
|
177
|
+
|
|
178
|
+
Provider API keys are passed through their own environment variables, for example:
|
|
179
|
+
|
|
180
|
+
- `OPENAI_API_KEY`
|
|
181
|
+
- `GLM_API_KEY`
|
|
182
|
+
- `QWEN_API_KEY`
|
|
183
|
+
|
|
184
|
+
## Publish
|
|
185
|
+
|
|
186
|
+
Useful local release commands:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm run pack:dry
|
|
190
|
+
npm run pack:tgz
|
|
191
|
+
npm run publish:patch
|
|
192
|
+
npm run publish:minor
|
|
193
|
+
npm run publish:major
|
|
140
194
|
```
|
|
195
|
+
|
|
196
|
+
Version rules:
|
|
197
|
+
|
|
198
|
+
- `publish:patch`: small fix, `0.0.1`
|
|
199
|
+
- `publish:minor`: new feature, `0.1.0`
|
|
200
|
+
- `publish:major`: breaking change or refactor, `1.0.0`
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ describe('loadConfigToEnv', () => {
|
|
|
25
25
|
delete process.env.AGENT_TOOL_CONFIRMATION_MODE;
|
|
26
26
|
delete process.env.AGENT_MODEL;
|
|
27
27
|
delete process.env.AGENT_MAX_STEPS;
|
|
28
|
+
delete process.env.RENX_CUSTOM_MODELS_JSON;
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
afterEach(() => {
|
|
@@ -126,4 +127,112 @@ describe('loadConfigToEnv', () => {
|
|
|
126
127
|
expect(process.env.AGENT_MODEL).toBe('qwen3.5-max');
|
|
127
128
|
expect(process.env.AGENT_MAX_STEPS).toBe('100');
|
|
128
129
|
});
|
|
130
|
+
|
|
131
|
+
it('should merge custom models into RENX_CUSTOM_MODELS_JSON', () => {
|
|
132
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(globalDir, 'config.json'),
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
models: {
|
|
137
|
+
'shared-model': {
|
|
138
|
+
provider: 'openai',
|
|
139
|
+
name: 'Shared Model',
|
|
140
|
+
baseURL: 'https://global.example.com/v1',
|
|
141
|
+
endpointPath: '/chat/completions',
|
|
142
|
+
envApiKey: 'SHARED_API_KEY',
|
|
143
|
+
envBaseURL: 'SHARED_API_BASE',
|
|
144
|
+
model: 'shared-global',
|
|
145
|
+
max_tokens: 4096,
|
|
146
|
+
LLMMAX_TOKENS: 64000,
|
|
147
|
+
features: ['streaming'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const projectConfigDir = path.join(tmpDir, '.renx');
|
|
154
|
+
fs.mkdirSync(projectConfigDir, { recursive: true });
|
|
155
|
+
fs.writeFileSync(
|
|
156
|
+
path.join(projectConfigDir, 'config.json'),
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
models: {
|
|
159
|
+
'shared-model': {
|
|
160
|
+
baseURL: 'https://project.example.com/v1',
|
|
161
|
+
model: 'shared-project',
|
|
162
|
+
},
|
|
163
|
+
'project-model': {
|
|
164
|
+
provider: 'openai',
|
|
165
|
+
name: 'Project Model',
|
|
166
|
+
baseURL: 'https://project-only.example.com/v1',
|
|
167
|
+
endpointPath: '/responses',
|
|
168
|
+
envApiKey: 'PROJECT_API_KEY',
|
|
169
|
+
envBaseURL: 'PROJECT_API_BASE',
|
|
170
|
+
model: 'project-model',
|
|
171
|
+
max_tokens: 8000,
|
|
172
|
+
LLMMAX_TOKENS: 128000,
|
|
173
|
+
features: ['streaming', 'function-calling'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
loadConfigToEnv({ projectRoot: tmpDir, globalDir });
|
|
180
|
+
|
|
181
|
+
const models = JSON.parse(process.env.RENX_CUSTOM_MODELS_JSON ?? '{}') as Record<
|
|
182
|
+
string,
|
|
183
|
+
Record<string, unknown>
|
|
184
|
+
>;
|
|
185
|
+
|
|
186
|
+
expect(models['shared-model']).toMatchObject({
|
|
187
|
+
provider: 'openai',
|
|
188
|
+
baseURL: 'https://project.example.com/v1',
|
|
189
|
+
model: 'shared-project',
|
|
190
|
+
});
|
|
191
|
+
expect(models['project-model']).toMatchObject({
|
|
192
|
+
endpointPath: '/responses',
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should keep existing RENX_CUSTOM_MODELS_JSON values over config files', () => {
|
|
197
|
+
process.env.RENX_CUSTOM_MODELS_JSON = JSON.stringify({
|
|
198
|
+
'shared-model': {
|
|
199
|
+
baseURL: 'https://env.example.com/v1',
|
|
200
|
+
model: 'env-model',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
205
|
+
fs.writeFileSync(
|
|
206
|
+
path.join(globalDir, 'config.json'),
|
|
207
|
+
JSON.stringify({
|
|
208
|
+
models: {
|
|
209
|
+
'shared-model': {
|
|
210
|
+
provider: 'openai',
|
|
211
|
+
name: 'Shared Model',
|
|
212
|
+
baseURL: 'https://global.example.com/v1',
|
|
213
|
+
endpointPath: '/chat/completions',
|
|
214
|
+
envApiKey: 'SHARED_API_KEY',
|
|
215
|
+
envBaseURL: 'SHARED_API_BASE',
|
|
216
|
+
model: 'shared-global',
|
|
217
|
+
max_tokens: 4096,
|
|
218
|
+
LLMMAX_TOKENS: 64000,
|
|
219
|
+
features: ['streaming'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
loadConfigToEnv({ projectRoot: tmpDir, globalDir });
|
|
226
|
+
|
|
227
|
+
const models = JSON.parse(process.env.RENX_CUSTOM_MODELS_JSON ?? '{}') as Record<
|
|
228
|
+
string,
|
|
229
|
+
Record<string, unknown>
|
|
230
|
+
>;
|
|
231
|
+
expect(models['shared-model']).toMatchObject({
|
|
232
|
+
baseURL: 'https://env.example.com/v1',
|
|
233
|
+
model: 'env-model',
|
|
234
|
+
provider: 'openai',
|
|
235
|
+
envApiKey: 'SHARED_API_KEY',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
129
238
|
});
|
|
@@ -144,6 +144,74 @@ describe('Renx Config Loader', () => {
|
|
|
144
144
|
expect(config.sources.global).toBe(path.join(globalDir, 'config.json'));
|
|
145
145
|
expect(config.sources.project).toBe(path.join(projectConfigDir, 'config.json'));
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
it('should merge custom models from global and project config', () => {
|
|
149
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
path.join(globalDir, 'config.json'),
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
models: {
|
|
154
|
+
'shared-model': {
|
|
155
|
+
provider: 'openai',
|
|
156
|
+
name: 'Shared Model',
|
|
157
|
+
baseURL: 'https://global.example.com/v1',
|
|
158
|
+
endpointPath: '/chat/completions',
|
|
159
|
+
envApiKey: 'SHARED_API_KEY',
|
|
160
|
+
envBaseURL: 'SHARED_API_BASE',
|
|
161
|
+
model: 'shared-global',
|
|
162
|
+
max_tokens: 4096,
|
|
163
|
+
LLMMAX_TOKENS: 32000,
|
|
164
|
+
features: ['streaming'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const projectConfigDir = path.join(tmpDir, '.renx');
|
|
171
|
+
fs.mkdirSync(projectConfigDir, { recursive: true });
|
|
172
|
+
fs.writeFileSync(
|
|
173
|
+
path.join(projectConfigDir, 'config.json'),
|
|
174
|
+
JSON.stringify({
|
|
175
|
+
models: {
|
|
176
|
+
'shared-model': {
|
|
177
|
+
baseURL: 'https://project.example.com/v1',
|
|
178
|
+
model: 'shared-project',
|
|
179
|
+
},
|
|
180
|
+
'project-model': {
|
|
181
|
+
provider: 'openai',
|
|
182
|
+
name: 'Project Model',
|
|
183
|
+
baseURL: 'https://project-only.example.com/v1',
|
|
184
|
+
endpointPath: '/responses',
|
|
185
|
+
envApiKey: 'PROJECT_API_KEY',
|
|
186
|
+
envBaseURL: 'PROJECT_API_BASE',
|
|
187
|
+
model: 'project-model',
|
|
188
|
+
max_tokens: 8000,
|
|
189
|
+
LLMMAX_TOKENS: 128000,
|
|
190
|
+
features: ['streaming', 'function-calling'],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const config = loadConfig({
|
|
197
|
+
projectRoot: tmpDir,
|
|
198
|
+
globalDir,
|
|
199
|
+
loadEnv: false,
|
|
200
|
+
env: { RENX_HOME: renxHome },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(config.models['shared-model']).toMatchObject({
|
|
204
|
+
provider: 'openai',
|
|
205
|
+
name: 'Shared Model',
|
|
206
|
+
baseURL: 'https://project.example.com/v1',
|
|
207
|
+
model: 'shared-project',
|
|
208
|
+
envApiKey: 'SHARED_API_KEY',
|
|
209
|
+
});
|
|
210
|
+
expect(config.models['project-model']).toMatchObject({
|
|
211
|
+
provider: 'openai',
|
|
212
|
+
endpointPath: '/responses',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
147
215
|
});
|
|
148
216
|
|
|
149
217
|
describe('loadConfig with env overrides', () => {
|
|
@@ -190,6 +258,52 @@ describe('Renx Config Loader', () => {
|
|
|
190
258
|
expect(config.log.format).toBe('json');
|
|
191
259
|
expect(config.agent.confirmationMode).toBe('auto-deny');
|
|
192
260
|
});
|
|
261
|
+
|
|
262
|
+
it('should let RENX_CUSTOM_MODELS_JSON override file-based model config', () => {
|
|
263
|
+
const projectConfigDir = path.join(tmpDir, '.renx');
|
|
264
|
+
fs.mkdirSync(projectConfigDir, { recursive: true });
|
|
265
|
+
fs.writeFileSync(
|
|
266
|
+
path.join(projectConfigDir, 'config.json'),
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
models: {
|
|
269
|
+
'custom-openai': {
|
|
270
|
+
provider: 'openai',
|
|
271
|
+
name: 'Custom OpenAI',
|
|
272
|
+
baseURL: 'https://file.example.com/v1',
|
|
273
|
+
endpointPath: '/chat/completions',
|
|
274
|
+
envApiKey: 'CUSTOM_API_KEY',
|
|
275
|
+
envBaseURL: 'CUSTOM_API_BASE',
|
|
276
|
+
model: 'file-model',
|
|
277
|
+
max_tokens: 4096,
|
|
278
|
+
LLMMAX_TOKENS: 64000,
|
|
279
|
+
features: ['streaming'],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const config = loadConfig({
|
|
286
|
+
projectRoot: tmpDir,
|
|
287
|
+
globalDir,
|
|
288
|
+
env: {
|
|
289
|
+
RENX_HOME: renxHome,
|
|
290
|
+
RENX_CUSTOM_MODELS_JSON: JSON.stringify({
|
|
291
|
+
'custom-openai': {
|
|
292
|
+
baseURL: 'https://env.example.com/v1',
|
|
293
|
+
model: 'env-model',
|
|
294
|
+
features: ['streaming', 'function-calling'],
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(config.models['custom-openai']).toMatchObject({
|
|
301
|
+
provider: 'openai',
|
|
302
|
+
baseURL: 'https://env.example.com/v1',
|
|
303
|
+
model: 'env-model',
|
|
304
|
+
features: ['streaming', 'function-calling'],
|
|
305
|
+
});
|
|
306
|
+
});
|
|
193
307
|
});
|
|
194
308
|
|
|
195
309
|
describe('writeProjectConfig', () => {
|
|
@@ -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;
|