@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
|
@@ -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';
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 集中管理所有模型的配置信息,可从外部加载
|
|
2
|
+
* Central model configuration storage.
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
|
-
import type { ModelConfig, ModelId } from '../types';
|
|
5
|
+
import type { ModelConfig, ModelId, BuiltinModelId, ProviderType } from '../types';
|
|
6
|
+
|
|
7
|
+
const CUSTOM_MODELS_ENV_VAR = 'RENX_CUSTOM_MODELS_JSON';
|
|
8
|
+
|
|
9
|
+
export type ModelDefinition = Omit<ModelConfig, 'apiKey'>;
|
|
10
|
+
type PartialModelDefinition = Partial<ModelDefinition>;
|
|
11
|
+
|
|
12
|
+
const VALID_PROVIDERS: ProviderType[] = [
|
|
13
|
+
'anthropic',
|
|
14
|
+
'kimi',
|
|
15
|
+
'deepseek',
|
|
16
|
+
'glm',
|
|
17
|
+
'minimax',
|
|
18
|
+
'openai',
|
|
19
|
+
'openrouter',
|
|
20
|
+
'qwen',
|
|
21
|
+
];
|
|
8
22
|
|
|
9
23
|
/**
|
|
10
|
-
*
|
|
24
|
+
* Built-in model definitions.
|
|
11
25
|
*/
|
|
12
|
-
export const MODEL_DEFINITIONS: Record<
|
|
13
|
-
// Anthropic 系列
|
|
26
|
+
export const MODEL_DEFINITIONS: Record<BuiltinModelId, ModelDefinition> = {
|
|
14
27
|
'claude-opus-4.6': {
|
|
15
28
|
id: 'claude-opus-4.6',
|
|
16
29
|
provider: 'anthropic',
|
|
17
30
|
name: 'Claude Opus 4.6',
|
|
18
|
-
baseURL: '',
|
|
31
|
+
baseURL: 'https://api.anthropic.com',
|
|
19
32
|
endpointPath: '/v1/messages',
|
|
20
33
|
envApiKey: 'ANTHROPIC_API_KEY',
|
|
21
34
|
envBaseURL: 'ANTHROPIC_API_BASE',
|
|
@@ -25,8 +38,6 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
25
38
|
features: ['streaming', 'function-calling', 'vision'],
|
|
26
39
|
modalities: { image: true },
|
|
27
40
|
},
|
|
28
|
-
|
|
29
|
-
// GLM 系列
|
|
30
41
|
'glm-4.7': {
|
|
31
42
|
id: 'glm-4.7',
|
|
32
43
|
provider: 'glm',
|
|
@@ -41,7 +52,6 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
41
52
|
features: ['streaming', 'function-calling', 'vision'],
|
|
42
53
|
modalities: { image: true },
|
|
43
54
|
},
|
|
44
|
-
// GLM 系列
|
|
45
55
|
'glm-5': {
|
|
46
56
|
id: 'glm-5',
|
|
47
57
|
provider: 'glm',
|
|
@@ -56,7 +66,6 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
56
66
|
features: ['streaming', 'function-calling', 'vision'],
|
|
57
67
|
modalities: { image: true },
|
|
58
68
|
},
|
|
59
|
-
// MiniMax 系列
|
|
60
69
|
'minimax-2.5': {
|
|
61
70
|
id: 'minimax-2.5',
|
|
62
71
|
provider: 'minimax',
|
|
@@ -70,7 +79,6 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
70
79
|
LLMMAX_TOKENS: 200 * 1000,
|
|
71
80
|
features: ['streaming', 'function-calling'],
|
|
72
81
|
},
|
|
73
|
-
// Kimi 系列
|
|
74
82
|
'kimi-k2.5': {
|
|
75
83
|
id: 'kimi-k2.5',
|
|
76
84
|
provider: 'kimi',
|
|
@@ -86,7 +94,6 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
86
94
|
temperature: 0.6,
|
|
87
95
|
thinking: false,
|
|
88
96
|
},
|
|
89
|
-
// DeepSeek 系列
|
|
90
97
|
'deepseek-reasoner': {
|
|
91
98
|
id: 'deepseek-reasoner',
|
|
92
99
|
provider: 'deepseek',
|
|
@@ -100,12 +107,11 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
100
107
|
LLMMAX_TOKENS: 128 * 1000,
|
|
101
108
|
features: ['streaming', 'function-calling'],
|
|
102
109
|
},
|
|
103
|
-
// Qwen 系列
|
|
104
110
|
'qwen3.5-plus': {
|
|
105
111
|
id: 'qwen3.5-plus',
|
|
106
112
|
provider: 'qwen',
|
|
107
113
|
name: 'Qwen 3.5 Plus',
|
|
108
|
-
baseURL: 'https://
|
|
114
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
109
115
|
endpointPath: '/chat/completions',
|
|
110
116
|
envApiKey: 'QWEN_API_KEY',
|
|
111
117
|
envBaseURL: 'QWEN_API_BASE',
|
|
@@ -115,12 +121,11 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
115
121
|
features: ['streaming', 'function-calling'],
|
|
116
122
|
modalities: { image: true },
|
|
117
123
|
},
|
|
118
|
-
// Qwen 系列
|
|
119
124
|
'qwen3.5-max': {
|
|
120
125
|
id: 'qwen3.5-max',
|
|
121
126
|
provider: 'qwen',
|
|
122
127
|
name: 'Qwen 3.5 Max',
|
|
123
|
-
baseURL: 'https://
|
|
128
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
124
129
|
endpointPath: '/chat/completions',
|
|
125
130
|
envApiKey: 'QWEN_API_KEY',
|
|
126
131
|
envBaseURL: 'QWEN_API_BASE',
|
|
@@ -133,12 +138,12 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
133
138
|
id: 'qwen-kimi-k2.5',
|
|
134
139
|
provider: 'qwen',
|
|
135
140
|
name: 'qwen kimi k2.5',
|
|
136
|
-
baseURL: 'https://
|
|
141
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
137
142
|
endpointPath: '/chat/completions',
|
|
138
143
|
envApiKey: 'QWEN_API_KEY',
|
|
139
144
|
envBaseURL: 'QWEN_API_BASE',
|
|
140
145
|
model: 'kimi-k2.5',
|
|
141
|
-
max_tokens:
|
|
146
|
+
max_tokens: 1000 * 32,
|
|
142
147
|
LLMMAX_TOKENS: 200 * 1000,
|
|
143
148
|
features: ['streaming', 'function-calling'],
|
|
144
149
|
},
|
|
@@ -146,12 +151,12 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
146
151
|
id: 'qwen-glm-5',
|
|
147
152
|
provider: 'qwen',
|
|
148
153
|
name: 'Qwen GLM 5',
|
|
149
|
-
baseURL: 'https://
|
|
154
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
150
155
|
endpointPath: '/chat/completions',
|
|
151
156
|
envApiKey: 'QWEN_API_KEY',
|
|
152
157
|
envBaseURL: 'QWEN_API_BASE',
|
|
153
158
|
model: 'glm-5',
|
|
154
|
-
max_tokens:
|
|
159
|
+
max_tokens: 1000 * 32,
|
|
155
160
|
LLMMAX_TOKENS: 200 * 1000,
|
|
156
161
|
features: ['streaming', 'function-calling'],
|
|
157
162
|
},
|
|
@@ -159,40 +164,26 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
159
164
|
id: 'qwen-minimax-2.5',
|
|
160
165
|
provider: 'qwen',
|
|
161
166
|
name: 'Qwen MiniMax 2.5',
|
|
162
|
-
baseURL: 'https://
|
|
167
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
163
168
|
endpointPath: '/chat/completions',
|
|
164
169
|
envApiKey: 'QWEN_API_KEY',
|
|
165
170
|
envBaseURL: 'QWEN_API_BASE',
|
|
166
171
|
model: 'MiniMax-M2.5',
|
|
167
|
-
max_tokens:
|
|
172
|
+
max_tokens: 1000 * 32,
|
|
168
173
|
LLMMAX_TOKENS: 200 * 1000,
|
|
169
174
|
features: ['streaming', 'function-calling'],
|
|
170
175
|
},
|
|
171
|
-
// 'claude-4.6': {
|
|
172
|
-
// id: 'wr-claude-4.6',
|
|
173
|
-
// provider: 'openai',
|
|
174
|
-
// name: 'Claude Opus 4.6',
|
|
175
|
-
// baseURL: '',
|
|
176
|
-
// endpointPath: '/chat/completions',
|
|
177
|
-
// envApiKey: 'ANTHROPIC_API_KEY',
|
|
178
|
-
// envBaseURL: 'ANTHROPIC_API_BASE',
|
|
179
|
-
// model: 'claude-opus-4-6',
|
|
180
|
-
// max_tokens: 16384,
|
|
181
|
-
// LLMMAX_TOKENS: 1000 * 1000,
|
|
182
|
-
// features: ['streaming', 'function-calling', 'vision'],
|
|
183
|
-
// modalities: { image: true },
|
|
184
|
-
// },
|
|
185
176
|
'gpt-5.3': {
|
|
186
177
|
id: 'gpt-5.3',
|
|
187
178
|
provider: 'openai',
|
|
188
179
|
name: 'GPT-5.3',
|
|
189
|
-
baseURL: 'https://
|
|
180
|
+
baseURL: 'https://api.openai.com/v1',
|
|
190
181
|
endpointPath: '/responses',
|
|
191
182
|
envApiKey: 'OPENAI_API_KEY',
|
|
192
183
|
envBaseURL: 'OPENAI_API_BASE',
|
|
193
184
|
model: 'gpt-5.3-codex',
|
|
194
|
-
max_tokens:
|
|
195
|
-
LLMMAX_TOKENS:
|
|
185
|
+
max_tokens: 1000 * 32,
|
|
186
|
+
LLMMAX_TOKENS: 258 * 1000,
|
|
196
187
|
model_reasoning_effort: 'high',
|
|
197
188
|
features: ['streaming', 'function-calling', 'reasoning'],
|
|
198
189
|
modalities: { image: true },
|
|
@@ -201,12 +192,12 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
201
192
|
id: 'gpt-5.4',
|
|
202
193
|
provider: 'openai',
|
|
203
194
|
name: 'GPT-5.4',
|
|
204
|
-
baseURL: 'https://
|
|
195
|
+
baseURL: 'https://api.openai.com/v1',
|
|
205
196
|
endpointPath: '/responses',
|
|
206
197
|
envApiKey: 'OPENAI_API_KEY',
|
|
207
198
|
envBaseURL: 'OPENAI_API_BASE',
|
|
208
199
|
model: 'gpt-5.4',
|
|
209
|
-
max_tokens:
|
|
200
|
+
max_tokens: 1000 * 32,
|
|
210
201
|
LLMMAX_TOKENS: 200 * 1000,
|
|
211
202
|
model_reasoning_effort: 'high',
|
|
212
203
|
features: ['streaming', 'function-calling'],
|
|
@@ -221,10 +212,266 @@ export const MODEL_DEFINITIONS: Record<ModelId, Omit<ModelConfig, 'apiKey'>> = {
|
|
|
221
212
|
envApiKey: 'OPENROUTER_API_KEY',
|
|
222
213
|
envBaseURL: 'OPENROUTER_API_BASE',
|
|
223
214
|
model: 'openrouter/hunter-alpha',
|
|
224
|
-
max_tokens:
|
|
215
|
+
max_tokens: 1000 * 32,
|
|
225
216
|
LLMMAX_TOKENS: 200 * 1000,
|
|
226
217
|
model_reasoning_effort: 'high',
|
|
227
218
|
features: ['streaming', 'function-calling'],
|
|
228
219
|
modalities: { image: true },
|
|
229
220
|
},
|
|
230
221
|
};
|
|
222
|
+
|
|
223
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
224
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isValidProvider(value: unknown): value is ProviderType {
|
|
228
|
+
return typeof value === 'string' && VALID_PROVIDERS.includes(value as ProviderType);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isValidNumber(value: unknown): value is number {
|
|
232
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isValidReasoningEffort(
|
|
236
|
+
value: unknown
|
|
237
|
+
): value is NonNullable<ModelDefinition['model_reasoning_effort']> {
|
|
238
|
+
return value === 'low' || value === 'medium' || value === 'high';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function sanitizeModalities(value: unknown): ModelDefinition['modalities'] | undefined {
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!isPlainObject(value)) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const result: NonNullable<ModelDefinition['modalities']> = {};
|
|
251
|
+
|
|
252
|
+
if (typeof value.image === 'boolean') {
|
|
253
|
+
result.image = value.image;
|
|
254
|
+
}
|
|
255
|
+
if (typeof value.audio === 'boolean') {
|
|
256
|
+
result.audio = value.audio;
|
|
257
|
+
}
|
|
258
|
+
if (typeof value.video === 'boolean') {
|
|
259
|
+
result.video = value.video;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return Object.keys(result).length > 0 ? result : {};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sanitizePartialModelDefinition(
|
|
266
|
+
modelId: string,
|
|
267
|
+
value: unknown
|
|
268
|
+
): PartialModelDefinition | null {
|
|
269
|
+
if (!isPlainObject(value)) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const sanitized: PartialModelDefinition = { id: modelId as ModelId };
|
|
274
|
+
|
|
275
|
+
if (value.provider !== undefined) {
|
|
276
|
+
if (!isValidProvider(value.provider)) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
sanitized.provider = value.provider;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (value.name !== undefined) {
|
|
283
|
+
if (typeof value.name !== 'string' || value.name.trim() === '') {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
sanitized.name = value.name;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (value.endpointPath !== undefined) {
|
|
290
|
+
if (typeof value.endpointPath !== 'string' || value.endpointPath.trim() === '') {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
sanitized.endpointPath = value.endpointPath;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (value.envApiKey !== undefined) {
|
|
297
|
+
if (typeof value.envApiKey !== 'string' || value.envApiKey.trim() === '') {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
sanitized.envApiKey = value.envApiKey;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (value.envBaseURL !== undefined) {
|
|
304
|
+
if (typeof value.envBaseURL !== 'string' || value.envBaseURL.trim() === '') {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
sanitized.envBaseURL = value.envBaseURL;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (value.baseURL !== undefined) {
|
|
311
|
+
if (typeof value.baseURL !== 'string' || value.baseURL.trim() === '') {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
sanitized.baseURL = value.baseURL;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (value.model !== undefined) {
|
|
318
|
+
if (typeof value.model !== 'string' || value.model.trim() === '') {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
sanitized.model = value.model;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (value.max_tokens !== undefined) {
|
|
325
|
+
if (!isValidNumber(value.max_tokens)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
sanitized.max_tokens = value.max_tokens;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (value.LLMMAX_TOKENS !== undefined) {
|
|
332
|
+
if (!isValidNumber(value.LLMMAX_TOKENS)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
sanitized.LLMMAX_TOKENS = value.LLMMAX_TOKENS;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (value.features !== undefined) {
|
|
339
|
+
if (!Array.isArray(value.features) || value.features.some((feature) => typeof feature !== 'string')) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
sanitized.features = [...value.features];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (value.modalities !== undefined) {
|
|
346
|
+
const modalities = sanitizeModalities(value.modalities);
|
|
347
|
+
if (modalities === undefined) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
sanitized.modalities = modalities;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (value.temperature !== undefined) {
|
|
354
|
+
if (typeof value.temperature !== 'number' || !Number.isFinite(value.temperature)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
sanitized.temperature = value.temperature;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (value.tool_stream !== undefined) {
|
|
361
|
+
if (typeof value.tool_stream !== 'boolean') {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
sanitized.tool_stream = value.tool_stream;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (value.thinking !== undefined) {
|
|
368
|
+
if (typeof value.thinking !== 'boolean') {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
sanitized.thinking = value.thinking;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (value.timeout !== undefined) {
|
|
375
|
+
if (!isValidNumber(value.timeout)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
sanitized.timeout = value.timeout;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (value.model_reasoning_effort !== undefined) {
|
|
382
|
+
if (!isValidReasoningEffort(value.model_reasoning_effort)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
sanitized.model_reasoning_effort = value.model_reasoning_effort;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return sanitized;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isCompleteModelDefinition(value: PartialModelDefinition): value is ModelDefinition {
|
|
392
|
+
return (
|
|
393
|
+
typeof value.id === 'string' &&
|
|
394
|
+
isValidProvider(value.provider) &&
|
|
395
|
+
typeof value.name === 'string' &&
|
|
396
|
+
typeof value.endpointPath === 'string' &&
|
|
397
|
+
typeof value.envApiKey === 'string' &&
|
|
398
|
+
typeof value.envBaseURL === 'string' &&
|
|
399
|
+
typeof value.baseURL === 'string' &&
|
|
400
|
+
typeof value.model === 'string' &&
|
|
401
|
+
isValidNumber(value.max_tokens) &&
|
|
402
|
+
isValidNumber(value.LLMMAX_TOKENS) &&
|
|
403
|
+
Array.isArray(value.features) &&
|
|
404
|
+
value.features.every((feature) => typeof feature === 'string')
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function mergeModelDefinition(
|
|
409
|
+
modelId: string,
|
|
410
|
+
base: PartialModelDefinition,
|
|
411
|
+
override: PartialModelDefinition
|
|
412
|
+
): ModelDefinition | null {
|
|
413
|
+
const merged: PartialModelDefinition = {
|
|
414
|
+
...base,
|
|
415
|
+
...override,
|
|
416
|
+
id: modelId as ModelId,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (base.modalities || override.modalities) {
|
|
420
|
+
merged.modalities = {
|
|
421
|
+
...(base.modalities ?? {}),
|
|
422
|
+
...(override.modalities ?? {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!isCompleteModelDefinition(merged)) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return merged;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function readCustomModelDefinitionsFromEnv(
|
|
434
|
+
env: NodeJS.ProcessEnv = process.env
|
|
435
|
+
): Record<string, PartialModelDefinition> {
|
|
436
|
+
const raw = env[CUSTOM_MODELS_ENV_VAR];
|
|
437
|
+
if (!raw) {
|
|
438
|
+
return {};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
443
|
+
if (!isPlainObject(parsed)) {
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const result: Record<string, PartialModelDefinition> = {};
|
|
448
|
+
for (const [modelId, modelConfig] of Object.entries(parsed)) {
|
|
449
|
+
const sanitized = sanitizePartialModelDefinition(modelId, modelConfig);
|
|
450
|
+
if (sanitized) {
|
|
451
|
+
result[modelId] = sanitized;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return result;
|
|
456
|
+
} catch {
|
|
457
|
+
return {};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function getResolvedModelDefinitions(
|
|
462
|
+
env: NodeJS.ProcessEnv = process.env
|
|
463
|
+
): Record<ModelId, ModelDefinition> {
|
|
464
|
+
const customDefinitions = readCustomModelDefinitionsFromEnv(env);
|
|
465
|
+
const resolved: Record<string, ModelDefinition> = { ...MODEL_DEFINITIONS };
|
|
466
|
+
|
|
467
|
+
for (const [modelId, customDefinition] of Object.entries(customDefinitions)) {
|
|
468
|
+
const baseDefinition = resolved[modelId] ?? ({ id: modelId as ModelId } as PartialModelDefinition);
|
|
469
|
+
const mergedDefinition = mergeModelDefinition(modelId, baseDefinition, customDefinition);
|
|
470
|
+
|
|
471
|
+
if (mergedDefinition) {
|
|
472
|
+
resolved[modelId] = mergedDefinition;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return resolved as Record<ModelId, ModelDefinition>;
|
|
477
|
+
}
|
|
@@ -9,7 +9,7 @@ import { AnthropicAdapter } from '../adapters/anthropic';
|
|
|
9
9
|
import { OpenAICompatibleProvider, OpenAICompatibleConfig } from '../openai-compatible';
|
|
10
10
|
import type { BaseProviderConfig, ModelId } from '../types';
|
|
11
11
|
import type { BaseAPIAdapter } from '../adapters/base';
|
|
12
|
-
import {
|
|
12
|
+
import { getResolvedModelDefinitions } from './model-config';
|
|
13
13
|
import { KimiAdapter } from '../adapters/kimi';
|
|
14
14
|
import { ResponsesAdapter } from '../adapters/responses';
|
|
15
15
|
|
|
@@ -32,7 +32,7 @@ export class ProviderFactory {
|
|
|
32
32
|
throw new Error('ModelId is required.');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const modelConfig =
|
|
35
|
+
const modelConfig = getResolvedModelDefinitions()[modelId];
|
|
36
36
|
if (!modelConfig) {
|
|
37
37
|
throw new Error(`Unknown model: ${modelId}`);
|
|
38
38
|
}
|
|
@@ -73,7 +73,7 @@ export class ProviderFactory {
|
|
|
73
73
|
* @returns OpenAI Compatible Provider 实例
|
|
74
74
|
*/
|
|
75
75
|
static create(modelId: ModelId, config: BaseProviderConfig): OpenAICompatibleProvider {
|
|
76
|
-
const modelConfig =
|
|
76
|
+
const modelConfig = getResolvedModelDefinitions()[modelId];
|
|
77
77
|
if (!modelConfig) {
|
|
78
78
|
throw new Error(`Unknown model: ${modelId}`);
|
|
79
79
|
}
|
|
@@ -92,7 +92,11 @@ export class ProviderFactory {
|
|
|
92
92
|
modelId: ModelId,
|
|
93
93
|
logger?: OpenAICompatibleConfig['logger']
|
|
94
94
|
): BaseAPIAdapter {
|
|
95
|
-
const modelConfig =
|
|
95
|
+
const modelConfig = getResolvedModelDefinitions()[modelId];
|
|
96
|
+
if (!modelConfig) {
|
|
97
|
+
throw new Error(`Unknown model: ${modelId}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
if (modelConfig.provider === 'anthropic') {
|
|
97
101
|
return new AnthropicAdapter({
|
|
98
102
|
defaultModel: modelConfig.model,
|