free-coding-models 0.2.1 → 0.2.3
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 +19 -2
- package/bin/free-coding-models.js +36 -9
- package/package.json +1 -1
- package/sources.js +1 -1
- package/src/config.js +42 -2
- package/src/endpoint-installer.js +459 -0
- package/src/key-handler.js +336 -16
- package/src/opencode.js +2 -2
- package/src/overlays.js +204 -3
- package/src/provider-metadata.js +3 -1
- package/src/render-table.js +9 -2
- package/src/utils.js +4 -2
package/src/key-handler.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module encapsulates the full onKeyPress switch used by the TUI,
|
|
7
|
-
* including settings navigation, overlays, profile management, and
|
|
7
|
+
* including settings navigation, install-endpoint flow, overlays, profile management, and
|
|
8
8
|
* OpenCode/OpenClaw launch actions. It also keeps the live key bindings
|
|
9
9
|
* aligned with the highlighted letters shown in the table headers.
|
|
10
10
|
*
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
* - `parseProviderModelIds` — extract model ids from an OpenAI-style `/models` payload
|
|
20
20
|
* - `listProviderTestModels` — build an ordered candidate list for provider key verification
|
|
21
21
|
* - `classifyProviderTestOutcome` — convert attempted HTTP codes into a settings badge state
|
|
22
|
+
* - `buildProviderTestDetail` — turn probe attempts into a readable failure explanation
|
|
22
23
|
* - `createKeyHandler` — returns the async keypress handler
|
|
23
24
|
*
|
|
24
|
-
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, createKeyHandler }
|
|
25
|
+
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, buildProviderTestDetail, createKeyHandler }
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
@@ -31,6 +32,17 @@ const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
|
31
32
|
nvidia: ['deepseek-ai/deepseek-v3.1-terminus', 'openai/gpt-oss-120b'],
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// 📖 Settings key tests retry retryable failures across several models so a
|
|
36
|
+
// 📖 single stale catalog entry or transient timeout does not mark a valid key as dead.
|
|
37
|
+
const SETTINGS_TEST_MAX_ATTEMPTS = 10
|
|
38
|
+
const SETTINGS_TEST_RETRY_DELAY_MS = 4000
|
|
39
|
+
|
|
40
|
+
// 📖 Sleep helper kept local to this module so the Settings key test flow can
|
|
41
|
+
// 📖 back off between retries without leaking timer logic into the rest of the TUI.
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
/**
|
|
35
47
|
* 📖 buildProviderModelsUrl derives the matching `/models` endpoint for providers
|
|
36
48
|
* 📖 that expose an OpenAI-compatible model list next to `/chat/completions`.
|
|
@@ -86,16 +98,52 @@ export function listProviderTestModels(providerKey, src, discoveredModelIds = []
|
|
|
86
98
|
* 📖 - `rate_limited` means the key is valid but the provider is currently throttling
|
|
87
99
|
* 📖 - `no_callable_model` means the provider responded, but none of the attempted models were callable
|
|
88
100
|
* @param {string[]} codes
|
|
89
|
-
* @returns {'ok'|'
|
|
101
|
+
* @returns {'ok'|'auth_error'|'rate_limited'|'no_callable_model'|'fail'}
|
|
90
102
|
*/
|
|
91
103
|
export function classifyProviderTestOutcome(codes) {
|
|
92
104
|
if (codes.includes('200')) return 'ok'
|
|
93
|
-
if (codes.includes('401') || codes.includes('403')) return '
|
|
105
|
+
if (codes.includes('401') || codes.includes('403')) return 'auth_error'
|
|
94
106
|
if (codes.length > 0 && codes.every(code => code === '429')) return 'rate_limited'
|
|
95
107
|
if (codes.length > 0 && codes.every(code => code === '404' || code === '410')) return 'no_callable_model'
|
|
96
108
|
return 'fail'
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
// 📖 buildProviderTestDetail explains why the Settings `T` probe failed, with
|
|
112
|
+
// 📖 enough context for the user to know whether the key, model list, or provider
|
|
113
|
+
// 📖 quota is the problem.
|
|
114
|
+
export function buildProviderTestDetail(providerLabel, outcome, attempts = [], discoveryNote = '') {
|
|
115
|
+
const introByOutcome = {
|
|
116
|
+
missing_key: `${providerLabel} has no saved API key right now, so no authenticated test could be sent.`,
|
|
117
|
+
ok: `${providerLabel} accepted the key.`,
|
|
118
|
+
auth_error: `${providerLabel} rejected the configured key with an authentication error.`,
|
|
119
|
+
rate_limited: `${providerLabel} throttled every probe, so the key may still be valid but is currently rate-limited.`,
|
|
120
|
+
no_callable_model: `${providerLabel} answered the requests, but none of the probed models were callable on its chat endpoint.`,
|
|
121
|
+
fail: `${providerLabel} never returned a successful probe during the retry window.`,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hintsByOutcome = {
|
|
125
|
+
missing_key: 'Save the key with Enter in Settings, then rerun T.',
|
|
126
|
+
ok: attempts.length > 0 ? `Validated on ${attempts[attempts.length - 1].model}.` : 'The provider returned a success response.',
|
|
127
|
+
auth_error: 'This usually means the saved key is invalid, expired, revoked, or truncated before it reached disk.',
|
|
128
|
+
rate_limited: 'Wait for the provider quota window to reset, then rerun T.',
|
|
129
|
+
no_callable_model: 'The provider catalog or repo defaults likely drifted; try another model family or refresh the catalog.',
|
|
130
|
+
fail: 'This can be caused by timeouts, 5xx responses, or a provider-side outage.',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const attemptSummary = attempts.length > 0
|
|
134
|
+
? `Attempts: ${attempts.map(({ attempt, model, code }) => `#${attempt} ${model} -> ${code}`).join(' | ')}`
|
|
135
|
+
: 'Attempts: none'
|
|
136
|
+
|
|
137
|
+
const segments = [
|
|
138
|
+
introByOutcome[outcome] || introByOutcome.fail,
|
|
139
|
+
hintsByOutcome[outcome] || hintsByOutcome.fail,
|
|
140
|
+
discoveryNote,
|
|
141
|
+
attemptSummary,
|
|
142
|
+
].filter(Boolean)
|
|
143
|
+
|
|
144
|
+
return segments.join(' ')
|
|
145
|
+
}
|
|
146
|
+
|
|
99
147
|
export function createKeyHandler(ctx) {
|
|
100
148
|
const {
|
|
101
149
|
state,
|
|
@@ -115,6 +163,10 @@ export function createKeyHandler(ctx) {
|
|
|
115
163
|
saveAsProfile,
|
|
116
164
|
setActiveProfile,
|
|
117
165
|
saveConfig,
|
|
166
|
+
getConfiguredInstallableProviders,
|
|
167
|
+
getInstallTargetModes,
|
|
168
|
+
getProviderCatalogModels,
|
|
169
|
+
installProviderEndpoints,
|
|
118
170
|
syncFavoriteFlags,
|
|
119
171
|
toggleFavoriteModel,
|
|
120
172
|
sortResultsWithPinnedFavorites,
|
|
@@ -168,11 +220,19 @@ export function createKeyHandler(ctx) {
|
|
|
168
220
|
const src = sources[providerKey]
|
|
169
221
|
if (!src) return
|
|
170
222
|
const testKey = getApiKey(state.config, providerKey)
|
|
171
|
-
|
|
223
|
+
const providerLabel = src.name || providerKey
|
|
224
|
+
if (!state.settingsTestDetails) state.settingsTestDetails = {}
|
|
225
|
+
if (!testKey) {
|
|
226
|
+
state.settingsTestResults[providerKey] = 'missing_key'
|
|
227
|
+
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'missing_key')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
172
230
|
|
|
173
231
|
state.settingsTestResults[providerKey] = 'pending'
|
|
232
|
+
state.settingsTestDetails[providerKey] = `Testing ${providerLabel} across up to ${SETTINGS_TEST_MAX_ATTEMPTS} probes...`
|
|
174
233
|
const discoveredModelIds = []
|
|
175
234
|
const modelsUrl = buildProviderModelsUrl(src.url)
|
|
235
|
+
let discoveryNote = ''
|
|
176
236
|
|
|
177
237
|
if (modelsUrl) {
|
|
178
238
|
try {
|
|
@@ -185,30 +245,53 @@ export function createKeyHandler(ctx) {
|
|
|
185
245
|
if (modelsResp.ok) {
|
|
186
246
|
const data = await modelsResp.json()
|
|
187
247
|
discoveredModelIds.push(...parseProviderModelIds(data))
|
|
248
|
+
discoveryNote = discoveredModelIds.length > 0
|
|
249
|
+
? `Live model discovery returned ${discoveredModelIds.length} ids.`
|
|
250
|
+
: 'Live model discovery succeeded but returned no callable ids.'
|
|
251
|
+
} else {
|
|
252
|
+
discoveryNote = `Live model discovery returned HTTP ${modelsResp.status}; falling back to the repo catalog.`
|
|
188
253
|
}
|
|
189
|
-
} catch {
|
|
254
|
+
} catch (err) {
|
|
190
255
|
// 📖 Discovery failure is non-fatal; we still have repo-defined fallbacks.
|
|
256
|
+
discoveryNote = `Live model discovery failed (${err?.name || 'error'}); falling back to the repo catalog.`
|
|
191
257
|
}
|
|
192
258
|
}
|
|
193
259
|
|
|
194
260
|
const candidateModels = listProviderTestModels(providerKey, src, discoveredModelIds)
|
|
195
|
-
if (candidateModels.length === 0) {
|
|
196
|
-
|
|
261
|
+
if (candidateModels.length === 0) {
|
|
262
|
+
state.settingsTestResults[providerKey] = 'fail'
|
|
263
|
+
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'fail', [], discoveryNote || 'No candidate model was available for probing.')
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
const attempts = []
|
|
197
267
|
|
|
198
|
-
for (
|
|
268
|
+
for (let attemptIndex = 0; attemptIndex < SETTINGS_TEST_MAX_ATTEMPTS; attemptIndex++) {
|
|
269
|
+
const testModel = candidateModels[attemptIndex % candidateModels.length]
|
|
199
270
|
const { code } = await ping(testKey, testModel, providerKey, src.url)
|
|
200
|
-
|
|
271
|
+
attempts.push({ attempt: attemptIndex + 1, model: testModel, code })
|
|
272
|
+
|
|
201
273
|
if (code === '200') {
|
|
202
274
|
state.settingsTestResults[providerKey] = 'ok'
|
|
275
|
+
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'ok', attempts, discoveryNote)
|
|
203
276
|
return
|
|
204
277
|
}
|
|
205
|
-
|
|
206
|
-
|
|
278
|
+
|
|
279
|
+
const outcome = classifyProviderTestOutcome(attempts.map(({ code: attemptCode }) => attemptCode))
|
|
280
|
+
if (outcome === 'auth_error') {
|
|
281
|
+
state.settingsTestResults[providerKey] = 'auth_error'
|
|
282
|
+
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'auth_error', attempts, discoveryNote)
|
|
207
283
|
return
|
|
208
284
|
}
|
|
285
|
+
|
|
286
|
+
if (attemptIndex < SETTINGS_TEST_MAX_ATTEMPTS - 1) {
|
|
287
|
+
state.settingsTestDetails[providerKey] = `Testing ${providerLabel}... probe ${attemptIndex + 1}/${SETTINGS_TEST_MAX_ATTEMPTS} failed on ${testModel} (${code}). Retrying in ${SETTINGS_TEST_RETRY_DELAY_MS / 1000}s.`
|
|
288
|
+
await sleep(SETTINGS_TEST_RETRY_DELAY_MS)
|
|
289
|
+
}
|
|
209
290
|
}
|
|
210
291
|
|
|
211
|
-
|
|
292
|
+
const finalOutcome = classifyProviderTestOutcome(attempts.map(({ code }) => code))
|
|
293
|
+
state.settingsTestResults[providerKey] = finalOutcome
|
|
294
|
+
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, finalOutcome, attempts, discoveryNote)
|
|
212
295
|
}
|
|
213
296
|
|
|
214
297
|
// 📖 Manual update checker from settings; keeps status visible in maintenance row.
|
|
@@ -242,6 +325,48 @@ export function createKeyHandler(ctx) {
|
|
|
242
325
|
runUpdate(latestVersion)
|
|
243
326
|
}
|
|
244
327
|
|
|
328
|
+
function resetInstallEndpointsOverlay() {
|
|
329
|
+
state.installEndpointsOpen = false
|
|
330
|
+
state.installEndpointsPhase = 'providers'
|
|
331
|
+
state.installEndpointsCursor = 0
|
|
332
|
+
state.installEndpointsScrollOffset = 0
|
|
333
|
+
state.installEndpointsProviderKey = null
|
|
334
|
+
state.installEndpointsToolMode = null
|
|
335
|
+
state.installEndpointsScope = null
|
|
336
|
+
state.installEndpointsSelectedModelIds = new Set()
|
|
337
|
+
state.installEndpointsErrorMsg = null
|
|
338
|
+
state.installEndpointsResult = null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function runInstallEndpointsFlow() {
|
|
342
|
+
const selectedModelIds = [...state.installEndpointsSelectedModelIds]
|
|
343
|
+
const result = installProviderEndpoints(
|
|
344
|
+
state.config,
|
|
345
|
+
state.installEndpointsProviderKey,
|
|
346
|
+
state.installEndpointsToolMode,
|
|
347
|
+
{
|
|
348
|
+
scope: state.installEndpointsScope,
|
|
349
|
+
modelIds: selectedModelIds,
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
state.installEndpointsResult = {
|
|
354
|
+
type: 'success',
|
|
355
|
+
title: `${result.modelCount} models installed into ${result.toolLabel}`,
|
|
356
|
+
lines: [
|
|
357
|
+
chalk.bold(`Provider:`) + ` ${result.providerLabel}`,
|
|
358
|
+
chalk.bold(`Scope:`) + ` ${result.scope === 'selected' ? 'Selected models' : 'All current models'}`,
|
|
359
|
+
chalk.bold(`Managed Id:`) + ` ${result.providerId}`,
|
|
360
|
+
chalk.bold(`Config:`) + ` ${result.path}`,
|
|
361
|
+
...(result.extraPath ? [chalk.bold(`Secrets:`) + ` ${result.extraPath}`] : []),
|
|
362
|
+
],
|
|
363
|
+
}
|
|
364
|
+
state.installEndpointsPhase = 'result'
|
|
365
|
+
state.installEndpointsCursor = 0
|
|
366
|
+
state.installEndpointsScrollOffset = 0
|
|
367
|
+
state.installEndpointsErrorMsg = null
|
|
368
|
+
}
|
|
369
|
+
|
|
245
370
|
return async (str, key) => {
|
|
246
371
|
if (!key) return
|
|
247
372
|
noteUserActivity()
|
|
@@ -287,6 +412,186 @@ export function createKeyHandler(ctx) {
|
|
|
287
412
|
return
|
|
288
413
|
}
|
|
289
414
|
|
|
415
|
+
// 📖 Install Endpoints overlay: provider → tool → scope → optional model subset.
|
|
416
|
+
if (state.installEndpointsOpen) {
|
|
417
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
418
|
+
|
|
419
|
+
const providerChoices = getConfiguredInstallableProviders(state.config)
|
|
420
|
+
const toolChoices = getInstallTargetModes()
|
|
421
|
+
const modelChoices = state.installEndpointsProviderKey
|
|
422
|
+
? getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
423
|
+
: []
|
|
424
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
425
|
+
|
|
426
|
+
const maxIndexByPhase = () => {
|
|
427
|
+
if (state.installEndpointsPhase === 'providers') return Math.max(0, providerChoices.length - 1)
|
|
428
|
+
if (state.installEndpointsPhase === 'tools') return Math.max(0, toolChoices.length - 1)
|
|
429
|
+
if (state.installEndpointsPhase === 'scope') return 1
|
|
430
|
+
if (state.installEndpointsPhase === 'models') return Math.max(0, modelChoices.length - 1)
|
|
431
|
+
return 0
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (key.name === 'up') {
|
|
435
|
+
state.installEndpointsCursor = Math.max(0, state.installEndpointsCursor - 1)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
if (key.name === 'down') {
|
|
439
|
+
state.installEndpointsCursor = Math.min(maxIndexByPhase(), state.installEndpointsCursor + 1)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
if (key.name === 'pageup') {
|
|
443
|
+
state.installEndpointsCursor = Math.max(0, state.installEndpointsCursor - pageStep)
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
if (key.name === 'pagedown') {
|
|
447
|
+
state.installEndpointsCursor = Math.min(maxIndexByPhase(), state.installEndpointsCursor + pageStep)
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (key.name === 'home') {
|
|
451
|
+
state.installEndpointsCursor = 0
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
if (key.name === 'end') {
|
|
455
|
+
state.installEndpointsCursor = maxIndexByPhase()
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (key.name === 'escape') {
|
|
460
|
+
state.installEndpointsErrorMsg = null
|
|
461
|
+
if (state.installEndpointsPhase === 'providers' || state.installEndpointsPhase === 'result') {
|
|
462
|
+
resetInstallEndpointsOverlay()
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if (state.installEndpointsPhase === 'tools') {
|
|
466
|
+
state.installEndpointsPhase = 'providers'
|
|
467
|
+
state.installEndpointsCursor = 0
|
|
468
|
+
state.installEndpointsScrollOffset = 0
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
if (state.installEndpointsPhase === 'scope') {
|
|
472
|
+
state.installEndpointsPhase = 'tools'
|
|
473
|
+
state.installEndpointsCursor = 0
|
|
474
|
+
state.installEndpointsScrollOffset = 0
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
if (state.installEndpointsPhase === 'models') {
|
|
478
|
+
state.installEndpointsPhase = 'scope'
|
|
479
|
+
state.installEndpointsCursor = state.installEndpointsScope === 'selected' ? 1 : 0
|
|
480
|
+
state.installEndpointsScrollOffset = 0
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (state.installEndpointsPhase === 'providers') {
|
|
486
|
+
if (key.name === 'return') {
|
|
487
|
+
const selectedProvider = providerChoices[state.installEndpointsCursor]
|
|
488
|
+
if (!selectedProvider) {
|
|
489
|
+
state.installEndpointsErrorMsg = '⚠ No installable configured provider is available yet.'
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
state.installEndpointsProviderKey = selectedProvider.providerKey
|
|
493
|
+
state.installEndpointsToolMode = null
|
|
494
|
+
state.installEndpointsScope = null
|
|
495
|
+
state.installEndpointsSelectedModelIds = new Set()
|
|
496
|
+
state.installEndpointsPhase = 'tools'
|
|
497
|
+
state.installEndpointsCursor = 0
|
|
498
|
+
state.installEndpointsScrollOffset = 0
|
|
499
|
+
state.installEndpointsErrorMsg = null
|
|
500
|
+
}
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (state.installEndpointsPhase === 'tools') {
|
|
505
|
+
if (key.name === 'return') {
|
|
506
|
+
const selectedToolMode = toolChoices[state.installEndpointsCursor]
|
|
507
|
+
if (!selectedToolMode) return
|
|
508
|
+
state.installEndpointsToolMode = selectedToolMode
|
|
509
|
+
state.installEndpointsPhase = 'scope'
|
|
510
|
+
state.installEndpointsCursor = 0
|
|
511
|
+
state.installEndpointsScrollOffset = 0
|
|
512
|
+
state.installEndpointsErrorMsg = null
|
|
513
|
+
}
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (state.installEndpointsPhase === 'scope') {
|
|
518
|
+
if (key.name === 'return') {
|
|
519
|
+
state.installEndpointsScope = state.installEndpointsCursor === 1 ? 'selected' : 'all'
|
|
520
|
+
state.installEndpointsScrollOffset = 0
|
|
521
|
+
state.installEndpointsErrorMsg = null
|
|
522
|
+
if (state.installEndpointsScope === 'all') {
|
|
523
|
+
try {
|
|
524
|
+
await runInstallEndpointsFlow()
|
|
525
|
+
} catch (error) {
|
|
526
|
+
state.installEndpointsResult = {
|
|
527
|
+
type: 'error',
|
|
528
|
+
title: 'Install failed',
|
|
529
|
+
lines: [error instanceof Error ? error.message : String(error)],
|
|
530
|
+
}
|
|
531
|
+
state.installEndpointsPhase = 'result'
|
|
532
|
+
}
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
state.installEndpointsSelectedModelIds = new Set()
|
|
537
|
+
state.installEndpointsPhase = 'models'
|
|
538
|
+
state.installEndpointsCursor = 0
|
|
539
|
+
}
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (state.installEndpointsPhase === 'models') {
|
|
544
|
+
if (key.name === 'a') {
|
|
545
|
+
if (state.installEndpointsSelectedModelIds.size === modelChoices.length) {
|
|
546
|
+
state.installEndpointsSelectedModelIds = new Set()
|
|
547
|
+
} else {
|
|
548
|
+
state.installEndpointsSelectedModelIds = new Set(modelChoices.map((model) => model.modelId))
|
|
549
|
+
}
|
|
550
|
+
state.installEndpointsErrorMsg = null
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (key.name === 'space') {
|
|
555
|
+
const selectedModel = modelChoices[state.installEndpointsCursor]
|
|
556
|
+
if (!selectedModel) return
|
|
557
|
+
const next = new Set(state.installEndpointsSelectedModelIds)
|
|
558
|
+
if (next.has(selectedModel.modelId)) next.delete(selectedModel.modelId)
|
|
559
|
+
else next.add(selectedModel.modelId)
|
|
560
|
+
state.installEndpointsSelectedModelIds = next
|
|
561
|
+
state.installEndpointsErrorMsg = null
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (key.name === 'return') {
|
|
566
|
+
if (state.installEndpointsSelectedModelIds.size === 0) {
|
|
567
|
+
state.installEndpointsErrorMsg = '⚠ Select at least one model before installing.'
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
await runInstallEndpointsFlow()
|
|
573
|
+
} catch (error) {
|
|
574
|
+
state.installEndpointsResult = {
|
|
575
|
+
type: 'error',
|
|
576
|
+
title: 'Install failed',
|
|
577
|
+
lines: [error instanceof Error ? error.message : String(error)],
|
|
578
|
+
}
|
|
579
|
+
state.installEndpointsPhase = 'result'
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (state.installEndpointsPhase === 'result') {
|
|
586
|
+
if (key.name === 'return' || key.name === 'y') {
|
|
587
|
+
resetInstallEndpointsOverlay()
|
|
588
|
+
}
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
|
|
290
595
|
// 📖 Feature Request overlay: intercept ALL keys while overlay is active.
|
|
291
596
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
292
597
|
if (state.featureRequestOpen) {
|
|
@@ -945,6 +1250,21 @@ export function createKeyHandler(ctx) {
|
|
|
945
1250
|
return
|
|
946
1251
|
}
|
|
947
1252
|
|
|
1253
|
+
// 📖 Y key: open Install Endpoints flow for configured providers.
|
|
1254
|
+
if (key.name === 'y') {
|
|
1255
|
+
state.installEndpointsOpen = true
|
|
1256
|
+
state.installEndpointsPhase = 'providers'
|
|
1257
|
+
state.installEndpointsCursor = 0
|
|
1258
|
+
state.installEndpointsScrollOffset = 0
|
|
1259
|
+
state.installEndpointsProviderKey = null
|
|
1260
|
+
state.installEndpointsToolMode = null
|
|
1261
|
+
state.installEndpointsScope = null
|
|
1262
|
+
state.installEndpointsSelectedModelIds = new Set()
|
|
1263
|
+
state.installEndpointsErrorMsg = null
|
|
1264
|
+
state.installEndpointsResult = null
|
|
1265
|
+
return
|
|
1266
|
+
}
|
|
1267
|
+
|
|
948
1268
|
// 📖 Shift+P: cycle through profiles (or show profile picker)
|
|
949
1269
|
if (key.name === 'p' && key.shift) {
|
|
950
1270
|
const profiles = listProfiles(state.config)
|
|
@@ -1007,11 +1327,11 @@ export function createKeyHandler(ctx) {
|
|
|
1007
1327
|
return
|
|
1008
1328
|
}
|
|
1009
1329
|
|
|
1010
|
-
// 📖 Sorting keys: R=rank,
|
|
1011
|
-
// 📖 T is reserved for tier filter cycling
|
|
1330
|
+
// 📖 Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime, G=usage
|
|
1331
|
+
// 📖 T is reserved for tier filter cycling. Y now opens the install-endpoints flow.
|
|
1012
1332
|
// 📖 D is now reserved for provider filter cycling
|
|
1013
1333
|
const sortKeys = {
|
|
1014
|
-
'r': 'rank', '
|
|
1334
|
+
'r': 'rank', 'o': 'origin', 'm': 'model',
|
|
1015
1335
|
'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime', 'g': 'usage'
|
|
1016
1336
|
}
|
|
1017
1337
|
|
package/src/opencode.js
CHANGED
|
@@ -425,7 +425,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
425
425
|
config.provider.codestral = {
|
|
426
426
|
npm: '@ai-sdk/openai-compatible',
|
|
427
427
|
name: 'Mistral Codestral',
|
|
428
|
-
options: { baseURL: 'https://
|
|
428
|
+
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
|
|
429
429
|
models: {}
|
|
430
430
|
}
|
|
431
431
|
} else if (providerKey === 'hyperbolic') {
|
|
@@ -889,7 +889,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
889
889
|
config.provider.codestral = {
|
|
890
890
|
npm: '@ai-sdk/openai-compatible',
|
|
891
891
|
name: 'Mistral Codestral',
|
|
892
|
-
options: { baseURL: 'https://
|
|
892
|
+
options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
|
|
893
893
|
models: {}
|
|
894
894
|
}
|
|
895
895
|
} else if (providerKey === 'hyperbolic') {
|