banana-code 1.2.0
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/LICENSE +21 -0
- package/README.md +246 -0
- package/banana.js +5464 -0
- package/lib/agenticRunner.js +1884 -0
- package/lib/borderRenderer.js +41 -0
- package/lib/commandRunner.js +205 -0
- package/lib/completer.js +286 -0
- package/lib/config.js +301 -0
- package/lib/contextBuilder.js +324 -0
- package/lib/diffViewer.js +295 -0
- package/lib/fileManager.js +224 -0
- package/lib/historyManager.js +124 -0
- package/lib/hookManager.js +1143 -0
- package/lib/imageHandler.js +268 -0
- package/lib/inlineComplete.js +192 -0
- package/lib/interactivePicker.js +254 -0
- package/lib/lmStudio.js +226 -0
- package/lib/markdownRenderer.js +423 -0
- package/lib/mcpClient.js +288 -0
- package/lib/modelRegistry.js +350 -0
- package/lib/monkeyModels.js +97 -0
- package/lib/oauthOpenAI.js +167 -0
- package/lib/parser.js +134 -0
- package/lib/promptManager.js +96 -0
- package/lib/providerClients.js +1014 -0
- package/lib/providerManager.js +130 -0
- package/lib/providerStore.js +413 -0
- package/lib/statusBar.js +283 -0
- package/lib/streamHandler.js +306 -0
- package/lib/subAgentManager.js +406 -0
- package/lib/tokenCounter.js +132 -0
- package/lib/visionAnalyzer.js +163 -0
- package/lib/watcher.js +138 -0
- package/models.json +57 -0
- package/package.json +42 -0
- package/prompts/base.md +23 -0
- package/prompts/code-agent-glm.md +16 -0
- package/prompts/code-agent-gptoss.md +25 -0
- package/prompts/code-agent-nemotron.md +17 -0
- package/prompts/code-agent-qwen.md +20 -0
- package/prompts/code-agent.md +70 -0
- package/prompts/plan.md +44 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const {
|
|
2
|
+
OpenAICompatibleClient,
|
|
3
|
+
AnthropicClient,
|
|
4
|
+
OpenAICodexClient
|
|
5
|
+
} = require('./providerClients');
|
|
6
|
+
const {
|
|
7
|
+
OPENAI_AUTH_ISSUER,
|
|
8
|
+
OPENAI_CODEX_CLIENT_ID,
|
|
9
|
+
requestDeviceCode,
|
|
10
|
+
completeDeviceCodeLogin,
|
|
11
|
+
refreshOpenAIToken,
|
|
12
|
+
buildTokenRecord,
|
|
13
|
+
isTokenExpired
|
|
14
|
+
} = require('./oauthOpenAI');
|
|
15
|
+
|
|
16
|
+
const PROVIDER_LABELS = {
|
|
17
|
+
local: 'LM Studio',
|
|
18
|
+
anthropic: 'Anthropic',
|
|
19
|
+
openai: 'OpenAI',
|
|
20
|
+
openrouter: 'OpenRouter',
|
|
21
|
+
monkey: 'Monkey Models'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class ProviderManager {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.lmStudio = options.lmStudio;
|
|
27
|
+
this.store = options.store;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getProviderLabel(provider) {
|
|
31
|
+
return PROVIDER_LABELS[provider] || provider;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async beginOpenAIDeviceLogin() {
|
|
35
|
+
return await requestDeviceCode({
|
|
36
|
+
issuer: OPENAI_AUTH_ISSUER,
|
|
37
|
+
clientId: OPENAI_CODEX_CLIENT_ID
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async completeOpenAIDeviceLogin(deviceCode) {
|
|
42
|
+
const tokens = await completeDeviceCodeLogin(deviceCode, {
|
|
43
|
+
timeoutMs: 15 * 60 * 1000
|
|
44
|
+
});
|
|
45
|
+
this.store.connectOpenAI(tokens);
|
|
46
|
+
return tokens;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async _getOpenAIToken() {
|
|
50
|
+
const auth = this.store.getAuth('openai');
|
|
51
|
+
if (auth.accessToken && !isTokenExpired(auth.expiresAt)) {
|
|
52
|
+
return auth.accessToken;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!auth.refreshToken) {
|
|
56
|
+
if (auth.accessToken) return auth.accessToken;
|
|
57
|
+
throw new Error('OpenAI is not connected. Run /connect and choose OpenAI.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const refreshed = await refreshOpenAIToken({
|
|
61
|
+
refreshToken: auth.refreshToken
|
|
62
|
+
});
|
|
63
|
+
const tokenRecord = buildTokenRecord(refreshed);
|
|
64
|
+
this.store.connectOpenAI({
|
|
65
|
+
accessToken: tokenRecord.accessToken || auth.accessToken,
|
|
66
|
+
refreshToken: tokenRecord.refreshToken || auth.refreshToken,
|
|
67
|
+
idToken: tokenRecord.idToken || auth.idToken,
|
|
68
|
+
expiresAt: tokenRecord.expiresAt || auth.expiresAt
|
|
69
|
+
});
|
|
70
|
+
return this.store.getAuth('openai').accessToken;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getClientForProvider(provider) {
|
|
74
|
+
if (provider === 'local') return this.lmStudio;
|
|
75
|
+
|
|
76
|
+
if (provider === 'anthropic') {
|
|
77
|
+
const stored = this.store.getAuth('anthropic').apiKey;
|
|
78
|
+
const apiKey = stored || process.env.ANTHROPIC_API_KEY;
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
throw new Error('Anthropic API key not found. Run /connect and choose Anthropic.');
|
|
81
|
+
}
|
|
82
|
+
return new AnthropicClient({
|
|
83
|
+
apiKey,
|
|
84
|
+
baseUrl: 'https://api.anthropic.com'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (provider === 'monkey') {
|
|
89
|
+
const { MonkeyModelsClient } = require('./monkeyModels');
|
|
90
|
+
const token = process.env.BANANA_MONKEY_TOKEN || this.store?.getAuth?.('monkey')?.apiKey;
|
|
91
|
+
return new MonkeyModelsClient({ token: token || undefined });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (provider === 'openrouter') {
|
|
95
|
+
const stored = this.store.getAuth('openrouter').apiKey;
|
|
96
|
+
const apiKey = stored || process.env.OPENROUTER_API_KEY;
|
|
97
|
+
if (!apiKey) {
|
|
98
|
+
throw new Error('OpenRouter API key not found. Run /connect and choose OpenRouter.');
|
|
99
|
+
}
|
|
100
|
+
return new OpenAICompatibleClient({
|
|
101
|
+
label: 'OpenRouter',
|
|
102
|
+
baseUrl: 'https://openrouter.ai/api',
|
|
103
|
+
apiKey,
|
|
104
|
+
extraHeaders: {
|
|
105
|
+
'HTTP-Referer': 'https://github.com/mrchevyceleb/banana-code',
|
|
106
|
+
'X-Title': 'Banana Code'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (provider === 'openai') {
|
|
112
|
+
const accessToken = await this._getOpenAIToken();
|
|
113
|
+
return new OpenAICodexClient({
|
|
114
|
+
accessToken
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getClientForModel(model) {
|
|
122
|
+
const provider = model?.provider || 'local';
|
|
123
|
+
return await this.getClientForProvider(provider);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
ProviderManager,
|
|
129
|
+
PROVIDER_LABELS
|
|
130
|
+
};
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PATH = path.join(os.homedir(), '.banana', 'providers.json');
|
|
6
|
+
|
|
7
|
+
const PROVIDERS = ['monkey', 'anthropic', 'openai', 'openrouter'];
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PROVIDER_MODELS = {
|
|
10
|
+
monkey: {},
|
|
11
|
+
anthropic: {
|
|
12
|
+
'claude-opus-4.6': {
|
|
13
|
+
name: 'Claude Opus 4.6',
|
|
14
|
+
id: 'claude-opus-4-6',
|
|
15
|
+
contextLimit: 200000,
|
|
16
|
+
supportsThinking: true,
|
|
17
|
+
prompt: 'code-agent'
|
|
18
|
+
},
|
|
19
|
+
'claude-sonnet-4.6': {
|
|
20
|
+
name: 'Claude Sonnet 4.6',
|
|
21
|
+
id: 'claude-sonnet-4-6',
|
|
22
|
+
contextLimit: 200000,
|
|
23
|
+
supportsThinking: true,
|
|
24
|
+
prompt: 'code-agent'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
openai: {
|
|
28
|
+
'codex-5.3-medium': {
|
|
29
|
+
name: 'Codex 5.3 Medium',
|
|
30
|
+
id: 'gpt-5.3-codex',
|
|
31
|
+
reasoningEffort: 'medium',
|
|
32
|
+
contextLimit: 400000,
|
|
33
|
+
supportsThinking: true,
|
|
34
|
+
prompt: 'code-agent'
|
|
35
|
+
},
|
|
36
|
+
'codex-5.3-high': {
|
|
37
|
+
name: 'Codex 5.3 High',
|
|
38
|
+
id: 'gpt-5.3-codex',
|
|
39
|
+
reasoningEffort: 'high',
|
|
40
|
+
contextLimit: 400000,
|
|
41
|
+
supportsThinking: true,
|
|
42
|
+
prompt: 'code-agent'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
openrouter: {
|
|
46
|
+
'claude-opus-4.6': {
|
|
47
|
+
name: 'Claude Opus 4.6 (OpenRouter)',
|
|
48
|
+
id: 'anthropic/claude-opus-4-6',
|
|
49
|
+
contextLimit: 200000,
|
|
50
|
+
supportsThinking: true,
|
|
51
|
+
prompt: 'code-agent'
|
|
52
|
+
},
|
|
53
|
+
'claude-sonnet-4.6': {
|
|
54
|
+
name: 'Claude Sonnet 4.6 (OpenRouter)',
|
|
55
|
+
id: 'anthropic/claude-sonnet-4-6',
|
|
56
|
+
contextLimit: 200000,
|
|
57
|
+
supportsThinking: true,
|
|
58
|
+
prompt: 'code-agent'
|
|
59
|
+
},
|
|
60
|
+
'codex-5.3-medium': {
|
|
61
|
+
name: 'Codex 5.3 Medium (OpenRouter)',
|
|
62
|
+
id: 'openai/gpt-5.3-codex',
|
|
63
|
+
reasoningEffort: 'medium',
|
|
64
|
+
contextLimit: 400000,
|
|
65
|
+
supportsThinking: true,
|
|
66
|
+
prompt: 'code-agent'
|
|
67
|
+
},
|
|
68
|
+
'codex-5.3-high': {
|
|
69
|
+
name: 'Codex 5.3 High (OpenRouter)',
|
|
70
|
+
id: 'openai/gpt-5.3-codex',
|
|
71
|
+
reasoningEffort: 'high',
|
|
72
|
+
contextLimit: 400000,
|
|
73
|
+
supportsThinking: true,
|
|
74
|
+
prompt: 'code-agent'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function defaultProviderRecord(provider) {
|
|
80
|
+
return {
|
|
81
|
+
connected: false,
|
|
82
|
+
auth: {},
|
|
83
|
+
models: JSON.parse(JSON.stringify(DEFAULT_PROVIDER_MODELS[provider] || {})),
|
|
84
|
+
updatedAt: null
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createDefaultData() {
|
|
89
|
+
return {
|
|
90
|
+
version: 1,
|
|
91
|
+
providers: {
|
|
92
|
+
anthropic: defaultProviderRecord('anthropic'),
|
|
93
|
+
openai: defaultProviderRecord('openai'),
|
|
94
|
+
openrouter: defaultProviderRecord('openrouter')
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function canonicalizeProviderModelId(provider, modelId) {
|
|
100
|
+
if (!modelId || typeof modelId !== 'string') return modelId;
|
|
101
|
+
const raw = modelId.trim();
|
|
102
|
+
const lower = raw.toLowerCase();
|
|
103
|
+
if (provider === 'anthropic') {
|
|
104
|
+
if (lower === 'claude-sonnet-4.7' || lower === 'claude-sonnet-4-7') {
|
|
105
|
+
return 'claude-sonnet-4-6';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (provider === 'openai') {
|
|
109
|
+
if (lower === 'codex-5.3-medium' || lower === 'codex-5.3-high'
|
|
110
|
+
|| lower === 'openai/codex-5.3-medium' || lower === 'openai/codex-5.3-high') {
|
|
111
|
+
return 'gpt-5.3-codex';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (provider === 'openrouter') {
|
|
115
|
+
if (lower === 'claude-sonnet-4.7' || lower === 'claude-sonnet-4-7'
|
|
116
|
+
|| lower === 'anthropic/claude-sonnet-4.7' || lower === 'anthropic/claude-sonnet-4-7') {
|
|
117
|
+
return 'anthropic/claude-sonnet-4-6';
|
|
118
|
+
}
|
|
119
|
+
if (lower === 'codex-5.3-medium' || lower === 'codex-5.3-high'
|
|
120
|
+
|| lower === 'openai/codex-5.3-medium' || lower === 'openai/codex-5.3-high') {
|
|
121
|
+
return 'openai/gpt-5.3-codex';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return raw;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class ProviderStore {
|
|
128
|
+
constructor(filePath = DEFAULT_PATH) {
|
|
129
|
+
this.filePath = filePath;
|
|
130
|
+
this.data = createDefaultData();
|
|
131
|
+
this.load();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_ensureDir() {
|
|
135
|
+
const dir = path.dirname(this.filePath);
|
|
136
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_touch(provider) {
|
|
140
|
+
if (!this.data.providers[provider]) {
|
|
141
|
+
this.data.providers[provider] = defaultProviderRecord(provider);
|
|
142
|
+
}
|
|
143
|
+
this.data.providers[provider].updatedAt = new Date().toISOString();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
load() {
|
|
147
|
+
let migrated = false;
|
|
148
|
+
try {
|
|
149
|
+
// Migrate from ~/.ripley/providers.json if upgrading
|
|
150
|
+
if (!fs.existsSync(this.filePath)) {
|
|
151
|
+
const legacyPath = path.join(os.homedir(), '.ripley', 'providers.json');
|
|
152
|
+
if (fs.existsSync(legacyPath)) {
|
|
153
|
+
this._ensureDir();
|
|
154
|
+
fs.copyFileSync(legacyPath, this.filePath);
|
|
155
|
+
} else {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'));
|
|
160
|
+
this.data = createDefaultData();
|
|
161
|
+
if (raw && typeof raw === 'object' && raw.providers && typeof raw.providers === 'object') {
|
|
162
|
+
for (const provider of PROVIDERS) {
|
|
163
|
+
const incoming = raw.providers[provider] || {};
|
|
164
|
+
this.data.providers[provider] = {
|
|
165
|
+
...defaultProviderRecord(provider),
|
|
166
|
+
...incoming,
|
|
167
|
+
auth: { ...(incoming.auth || {}) },
|
|
168
|
+
models: {
|
|
169
|
+
...defaultProviderRecord(provider).models,
|
|
170
|
+
...(incoming.models || {})
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
if (this._migrateLegacyModelIds(provider)) {
|
|
174
|
+
migrated = true;
|
|
175
|
+
}
|
|
176
|
+
if (this._migrateContextLimits(provider)) {
|
|
177
|
+
migrated = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
this.data = createDefaultData();
|
|
183
|
+
}
|
|
184
|
+
if (migrated) this.save();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_migrateLegacyModelIds(provider) {
|
|
188
|
+
const models = this.data.providers?.[provider]?.models;
|
|
189
|
+
if (!models || typeof models !== 'object') return false;
|
|
190
|
+
let changed = false;
|
|
191
|
+
|
|
192
|
+
const sonnetLegacyAlias = 'claude-sonnet-4.7';
|
|
193
|
+
const sonnetCurrentAlias = 'claude-sonnet-4.6';
|
|
194
|
+
if ((provider === 'anthropic' || provider === 'openrouter') && models[sonnetLegacyAlias]) {
|
|
195
|
+
const legacy = models[sonnetLegacyAlias];
|
|
196
|
+
const current = models[sonnetCurrentAlias] || {};
|
|
197
|
+
const currentDefaultName = provider === 'openrouter'
|
|
198
|
+
? 'Claude Sonnet 4.6 (OpenRouter)'
|
|
199
|
+
: 'Claude Sonnet 4.6';
|
|
200
|
+
const currentDefaultId = provider === 'openrouter'
|
|
201
|
+
? 'anthropic/claude-sonnet-4-6'
|
|
202
|
+
: 'claude-sonnet-4-6';
|
|
203
|
+
|
|
204
|
+
models[sonnetCurrentAlias] = {
|
|
205
|
+
...legacy,
|
|
206
|
+
...current,
|
|
207
|
+
name: current.name || currentDefaultName,
|
|
208
|
+
id: canonicalizeProviderModelId(provider, current.id || legacy.id || currentDefaultId)
|
|
209
|
+
};
|
|
210
|
+
delete models[sonnetLegacyAlias];
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const patchSetByProvider = {
|
|
215
|
+
anthropic: {
|
|
216
|
+
'claude-sonnet-4.6': {
|
|
217
|
+
id: 'claude-sonnet-4-6',
|
|
218
|
+
name: 'Claude Sonnet 4.6',
|
|
219
|
+
legacyIds: ['claude-sonnet-4.6', 'claude-sonnet-4-6', 'claude-sonnet-4.7', 'claude-sonnet-4-7']
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
openai: {
|
|
223
|
+
'codex-5.3-medium': {
|
|
224
|
+
id: 'gpt-5.3-codex',
|
|
225
|
+
reasoningEffort: 'medium',
|
|
226
|
+
legacyIds: ['codex-5.3-medium', 'openai/codex-5.3-medium']
|
|
227
|
+
},
|
|
228
|
+
'codex-5.3-high': {
|
|
229
|
+
id: 'gpt-5.3-codex',
|
|
230
|
+
reasoningEffort: 'high',
|
|
231
|
+
legacyIds: ['codex-5.3-high', 'openai/codex-5.3-high']
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
openrouter: {
|
|
235
|
+
'claude-sonnet-4.6': {
|
|
236
|
+
id: 'anthropic/claude-sonnet-4-6',
|
|
237
|
+
name: 'Claude Sonnet 4.6 (OpenRouter)',
|
|
238
|
+
legacyIds: [
|
|
239
|
+
'claude-sonnet-4.6',
|
|
240
|
+
'claude-sonnet-4-6',
|
|
241
|
+
'claude-sonnet-4.7',
|
|
242
|
+
'claude-sonnet-4-7',
|
|
243
|
+
'anthropic/claude-sonnet-4.6',
|
|
244
|
+
'anthropic/claude-sonnet-4-6',
|
|
245
|
+
'anthropic/claude-sonnet-4.7',
|
|
246
|
+
'anthropic/claude-sonnet-4-7'
|
|
247
|
+
]
|
|
248
|
+
},
|
|
249
|
+
'codex-5.3-medium': {
|
|
250
|
+
id: 'openai/gpt-5.3-codex',
|
|
251
|
+
reasoningEffort: 'medium',
|
|
252
|
+
legacyIds: ['codex-5.3-medium', 'openai/codex-5.3-medium']
|
|
253
|
+
},
|
|
254
|
+
'codex-5.3-high': {
|
|
255
|
+
id: 'openai/gpt-5.3-codex',
|
|
256
|
+
reasoningEffort: 'high',
|
|
257
|
+
legacyIds: ['codex-5.3-high', 'openai/codex-5.3-high']
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const patchSet = patchSetByProvider[provider];
|
|
263
|
+
if (!patchSet) return changed;
|
|
264
|
+
|
|
265
|
+
for (const [alias, patch] of Object.entries(patchSet)) {
|
|
266
|
+
const model = models[alias];
|
|
267
|
+
if (!model) continue;
|
|
268
|
+
|
|
269
|
+
const currentId = typeof model.id === 'string' ? model.id.trim().toLowerCase() : '';
|
|
270
|
+
const legacyIds = new Set((patch.legacyIds || []).map((id) => String(id).toLowerCase()));
|
|
271
|
+
if (!currentId || legacyIds.has(currentId)) {
|
|
272
|
+
model.id = patch.id;
|
|
273
|
+
changed = true;
|
|
274
|
+
}
|
|
275
|
+
if (patch.reasoningEffort && !model.reasoningEffort) {
|
|
276
|
+
model.reasoningEffort = patch.reasoningEffort;
|
|
277
|
+
changed = true;
|
|
278
|
+
}
|
|
279
|
+
if (patch.name && (!model.name || /4\.7/.test(model.name))) {
|
|
280
|
+
model.name = patch.name;
|
|
281
|
+
changed = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return changed;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_migrateContextLimits(provider) {
|
|
289
|
+
const models = this.data.providers?.[provider]?.models;
|
|
290
|
+
if (!models || typeof models !== 'object') return false;
|
|
291
|
+
const defaults = DEFAULT_PROVIDER_MODELS[provider] || {};
|
|
292
|
+
let changed = false;
|
|
293
|
+
for (const [alias, defaultModel] of Object.entries(defaults)) {
|
|
294
|
+
const model = models[alias];
|
|
295
|
+
if (!model) continue;
|
|
296
|
+
if (model.contextLimit !== defaultModel.contextLimit) {
|
|
297
|
+
model.contextLimit = defaultModel.contextLimit;
|
|
298
|
+
changed = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return changed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
save() {
|
|
305
|
+
this._ensureDir();
|
|
306
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
listProviders() {
|
|
310
|
+
return PROVIDERS.map((provider) => ({ provider, ...this.getProvider(provider) }));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getProvider(provider) {
|
|
314
|
+
return this.data.providers[provider] || defaultProviderRecord(provider);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
isConnected(provider) {
|
|
318
|
+
return this.getProvider(provider).connected === true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
connectWithApiKey(provider, apiKey) {
|
|
322
|
+
if (!['monkey', 'anthropic', 'openrouter'].includes(provider)) {
|
|
323
|
+
throw new Error(`API key login is not supported for provider "${provider}"`);
|
|
324
|
+
}
|
|
325
|
+
this._touch(provider);
|
|
326
|
+
this.data.providers[provider].connected = true;
|
|
327
|
+
this.data.providers[provider].auth = { apiKey };
|
|
328
|
+
this.save();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
connectOpenAI(auth) {
|
|
332
|
+
this._touch('openai');
|
|
333
|
+
this.data.providers.openai.connected = true;
|
|
334
|
+
this.data.providers.openai.auth = {
|
|
335
|
+
accessToken: auth.accessToken,
|
|
336
|
+
refreshToken: auth.refreshToken,
|
|
337
|
+
idToken: auth.idToken || null,
|
|
338
|
+
expiresAt: auth.expiresAt || null
|
|
339
|
+
};
|
|
340
|
+
this.save();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
disconnect(provider) {
|
|
344
|
+
this.data.providers[provider] = defaultProviderRecord(provider);
|
|
345
|
+
this._touch(provider);
|
|
346
|
+
this.save();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
getAuth(provider) {
|
|
350
|
+
return { ...(this.getProvider(provider).auth || {}) };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
setModelId(provider, alias, modelId, options = {}) {
|
|
354
|
+
if (!PROVIDERS.includes(provider)) {
|
|
355
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
356
|
+
}
|
|
357
|
+
const record = this.getProvider(provider);
|
|
358
|
+
const current = record.models[alias] || {
|
|
359
|
+
name: options.name || alias,
|
|
360
|
+
id: modelId,
|
|
361
|
+
contextLimit: 128000,
|
|
362
|
+
supportsThinking: true,
|
|
363
|
+
prompt: 'code-agent'
|
|
364
|
+
};
|
|
365
|
+
record.models[alias] = {
|
|
366
|
+
...current,
|
|
367
|
+
...options,
|
|
368
|
+
id: canonicalizeProviderModelId(provider, modelId)
|
|
369
|
+
};
|
|
370
|
+
this.data.providers[provider] = record;
|
|
371
|
+
this._touch(provider);
|
|
372
|
+
this.save();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
getModels(provider) {
|
|
376
|
+
return { ...(this.getProvider(provider).models || {}) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getConnectedRemoteModels() {
|
|
380
|
+
const models = [];
|
|
381
|
+
for (const provider of PROVIDERS) {
|
|
382
|
+
const record = this.getProvider(provider);
|
|
383
|
+
const envConnected = (provider === 'anthropic' && !!process.env.ANTHROPIC_API_KEY)
|
|
384
|
+
|| (provider === 'openrouter' && !!process.env.OPENROUTER_API_KEY);
|
|
385
|
+
if (!record.connected && !envConnected) continue;
|
|
386
|
+
|
|
387
|
+
for (const [alias, model] of Object.entries(record.models || {})) {
|
|
388
|
+
const key = `${provider}:${alias}`;
|
|
389
|
+
models.push({
|
|
390
|
+
key,
|
|
391
|
+
provider,
|
|
392
|
+
alias,
|
|
393
|
+
name: model.name || alias,
|
|
394
|
+
id: model.id,
|
|
395
|
+
reasoningEffort: model.reasoningEffort,
|
|
396
|
+
contextLimit: model.contextLimit || 128000,
|
|
397
|
+
supportsThinking: model.supportsThinking !== false,
|
|
398
|
+
prompt: model.prompt || 'code-agent',
|
|
399
|
+
tags: provider === 'openrouter'
|
|
400
|
+
? ['remote', provider, 'tool-calling']
|
|
401
|
+
: ['remote', provider, 'tool-calling', 'vision']
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return models;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
ProviderStore,
|
|
411
|
+
PROVIDERS,
|
|
412
|
+
DEFAULT_PROVIDER_MODELS
|
|
413
|
+
};
|