-
-
Notifications
You must be signed in to change notification settings - Fork 253
Expand file tree
/
Copy pathprovider_config_service.dart
More file actions
200 lines (184 loc) · 7.09 KB
/
Copy pathprovider_config_service.dart
File metadata and controls
200 lines (184 loc) · 7.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import 'dart:convert';
import '../models/ai_provider.dart';
import 'native_bridge.dart';
/// Reads and writes AI provider configuration in openclaw.json.
class ProviderConfigService {
static const _configPath = '/root/.openclaw/openclaw.json';
/// Escape a string for use as a single-quoted shell argument.
static String _shellEscape(String s) {
return "'${s.replaceAll("'", "'\\''")}'";
}
static String? _extractPrimaryModel(dynamic modelsRaw) {
if (modelsRaw is! List || modelsRaw.isEmpty) return null;
final first = modelsRaw.first;
if (first is String) return first;
if (first is Map) {
final id = first['id'];
if (id is String && id.isNotEmpty) return id;
}
return null;
}
/// Read the current config and return a map with:
/// - `activeModel`: the current primary model string (or null)
/// - `providers`: Map<providerId, {apiKey, baseUrl, model, ...}> for configured providers
static Future<Map<String, dynamic>> readConfig() async {
try {
final content = await NativeBridge.readRootfsFile(_configPath);
if (content == null || content.isEmpty) {
return {'activeModel': null, 'providers': <String, dynamic>{}};
}
final config = jsonDecode(content) as Map<String, dynamic>;
// Extract active model
String? activeModel;
final agents = config['agents'] as Map<String, dynamic>?;
if (agents != null) {
final defaults = agents['defaults'] as Map<String, dynamic>?;
if (defaults != null) {
final model = defaults['model'] as Map<String, dynamic>?;
if (model != null) {
activeModel = model['primary'] as String?;
}
}
}
// Extract configured providers
final providers = <String, dynamic>{};
final modelsSection = config['models'] as Map<String, dynamic>?;
if (modelsSection != null) {
final providerEntries = modelsSection['providers'] as Map<String, dynamic>?;
if (providerEntries != null) {
for (final entry in providerEntries.entries) {
if (entry.value is Map) {
final normalized = Map<String, dynamic>.from(entry.value as Map);
final model = _extractPrimaryModel(normalized['models']);
if (model != null) {
normalized['model'] = model;
}
providers[entry.key] = normalized;
}
}
}
}
return {'activeModel': activeModel, 'providers': providers};
} catch (_) {
return {'activeModel': null, 'providers': <String, dynamic>{}};
}
}
/// Save a provider's API key/base URL and set its model as the active model.
/// Tries a Node.js one-liner in proot first, then falls back to a direct
/// file write via NativeBridge.writeRootfsFile if proot/DNS is unavailable.
static Future<void> saveProviderConfig({
required AiProvider provider,
required String apiKey,
required String baseUrl,
required String model,
}) async {
final providerIdJson = jsonEncode(provider.id);
final apiKeyJson = jsonEncode(apiKey);
final baseUrlJson = jsonEncode(baseUrl);
final modelJson = jsonEncode(model);
// Build the provider object with the model as an object containing `id`,
// not a bare string. OpenClaw expects: models: [{ id: "model-name" }].
// Writing a bare string causes config validation failure (#83, #88).
final script = '''
const fs = require("fs");
const p = "$_configPath";
let c = {};
try { c = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
if (!c.models) c.models = {};
if (!c.models.providers) c.models.providers = {};
c.models.providers[$providerIdJson] = {
apiKey: $apiKeyJson,
baseUrl: $baseUrlJson,
models: [{ id: $modelJson }]
};
if (!c.agents) c.agents = {};
if (!c.agents.defaults) c.agents.defaults = {};
if (!c.agents.defaults.model) c.agents.defaults.model = {};
c.agents.defaults.model.primary = $modelJson;
if (!c.gateway) c.gateway = {};
if (!c.gateway.mode) c.gateway.mode = "local";
fs.mkdirSync(require("path").dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(c, null, 2));
''';
try {
await NativeBridge.runInProot(
'node -e ${_shellEscape(script)}',
timeout: 15,
);
} catch (_) {
// Fallback: write config directly via NativeBridge file I/O
await _saveConfigDirect(
providerId: provider.id,
apiKey: apiKey,
baseUrl: baseUrl,
model: model,
);
}
}
/// Direct file-write fallback that doesn't depend on proot or DNS.
static Future<void> _saveConfigDirect({
required String providerId,
required String apiKey,
required String baseUrl,
required String model,
}) async {
Map<String, dynamic> config = {};
try {
final content = await NativeBridge.readRootfsFile(_configPath);
if (content != null && content.isNotEmpty) {
config = jsonDecode(content) as Map<String, dynamic>;
}
} catch (_) {
// Start fresh
}
// Merge provider entry — models must be objects with `id`, not bare strings (#83, #88).
config['models'] ??= <String, dynamic>{};
(config['models'] as Map<String, dynamic>)['providers'] ??= <String, dynamic>{};
((config['models'] as Map<String, dynamic>)['providers'] as Map<String, dynamic>)[providerId] = {
'apiKey': apiKey,
'baseUrl': baseUrl,
'models': [{'id': model}],
};
// Set active model
config['agents'] ??= <String, dynamic>{};
(config['agents'] as Map<String, dynamic>)['defaults'] ??= <String, dynamic>{};
((config['agents'] as Map<String, dynamic>)['defaults'] as Map<String, dynamic>)['model'] ??= <String, dynamic>{};
(((config['agents'] as Map<String, dynamic>)['defaults'] as Map<String, dynamic>)['model'] as Map<String, dynamic>)['primary'] = model;
// Ensure gateway.mode is set (#93, #90)
config['gateway'] ??= <String, dynamic>{};
(config['gateway'] as Map<String, dynamic>)['mode'] ??= 'local';
const encoder = JsonEncoder.withIndent(' ');
await NativeBridge.writeRootfsFile(_configPath, encoder.convert(config));
}
/// Remove a provider's config entry and clear the active model if it
/// belonged to this provider.
static Future<void> removeProviderConfig({
required AiProvider provider,
}) async {
final providerIdJson = jsonEncode(provider.id);
// Build a list of this provider's known model names so we can clear
// the active model if it matches one of them.
final modelsJson = jsonEncode(provider.defaultModels);
final script = '''
const fs = require("fs");
const p = "$_configPath";
let c = {};
try { c = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
if (c.models && c.models.providers) {
delete c.models.providers[$providerIdJson];
}
const known = $modelsJson;
if (c.agents && c.agents.defaults && c.agents.defaults.model) {
const cur = c.agents.defaults.model.primary;
if (cur && known.some(m => cur.includes(m))) {
delete c.agents.defaults.model.primary;
}
}
fs.writeFileSync(p, JSON.stringify(c, null, 2));
''';
await NativeBridge.runInProot(
'node -e ${_shellEscape(script)}',
timeout: 15,
);
}
}