free-coding-models 0.5.34 → 0.5.36
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 +2 -0
- package/changelog/v0.5.35.md +4 -0
- package/changelog/v0.5.36.md +13 -0
- package/package.json +1 -1
- package/src/core/provider-key-tester.js +369 -0
- package/src/core/router-daemon.js +53 -19
- package/src/tui/key-handler.js +30 -301
- package/web/dist/assets/{index-zzUvtumw.js → index-DH3ml-b4.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/server.js +14 -9
package/src/tui/key-handler.js
CHANGED
|
@@ -10,22 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 📖 Key I opens the changelog overlay.
|
|
12
12
|
*
|
|
13
|
-
* It also owns the "test key" model selection used by the Settings overlay
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* tool mode, provider, and selected model right before the app hands control
|
|
17
|
-
* to an external CLI.
|
|
18
|
-
* Some providers expose models in `/v1/models` that are not actually callable
|
|
19
|
-
* on the chat-completions endpoint. To avoid false negatives when a user
|
|
20
|
-
* presses `T` in Settings, the helpers below discover candidate model IDs,
|
|
21
|
-
* merge them with repo defaults, then probe several until one is accepted.
|
|
13
|
+
* It also owns the "test key" model selection used by the Settings overlay,
|
|
14
|
+
* delegated to the shared `provider-key-tester.js` module for consistency
|
|
15
|
+
* with the Web Dashboard's `/api/key/:provider/test` endpoint.
|
|
22
16
|
*
|
|
23
17
|
* → Functions:
|
|
24
|
-
* - `buildProviderModelsUrl` — derive the matching `/models` endpoint when available
|
|
25
|
-
* - `parseProviderModelIds` — extract model ids from an OpenAI-style `/models` payload
|
|
26
|
-
* - `listProviderTestModels` — build an ordered candidate list for provider key verification
|
|
27
|
-
* - `classifyProviderTestOutcome` — convert attempted HTTP codes into a settings badge state
|
|
28
|
-
* - `buildProviderTestDetail` — turn probe attempts into a readable failure explanation
|
|
29
18
|
* - `createKeyHandler` — returns the async keypress handler
|
|
30
19
|
*
|
|
31
20
|
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, buildProviderTestDetail, createKeyHandler }
|
|
@@ -39,7 +28,6 @@ import { join, dirname } from 'node:path'
|
|
|
39
28
|
import { fileURLToPath } from 'node:url'
|
|
40
29
|
import { spawn } from 'node:child_process'
|
|
41
30
|
import { cleanupLegacyProxyArtifacts } from '../core/legacy-proxy-cleanup.js'
|
|
42
|
-
import { sleep } from '../core/shared-helpers.js'
|
|
43
31
|
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
44
32
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
45
33
|
import { syncShellEnv, ensureShellRcSource, removeShellEnv } from '../core/shell-env.js'
|
|
@@ -64,20 +52,15 @@ import {
|
|
|
64
52
|
} from '../core/playground.js'
|
|
65
53
|
import { benchmarkModel } from '../core/benchmark.js'
|
|
66
54
|
import { isPackageDevMode } from '../core/updater.js'
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 📖 Settings key tests retry retryable failures across several models so a
|
|
78
|
-
// 📖 single stale catalog entry or transient timeout does not mark a valid key as dead.
|
|
79
|
-
const SETTINGS_TEST_MAX_ATTEMPTS = 10
|
|
80
|
-
const SETTINGS_TEST_RETRY_DELAY_MS = 4000
|
|
55
|
+
import {
|
|
56
|
+
runProviderKeyTest,
|
|
57
|
+
testProviderKeyDirect,
|
|
58
|
+
buildProviderModelsUrl,
|
|
59
|
+
parseProviderModelIds,
|
|
60
|
+
listProviderTestModels,
|
|
61
|
+
classifyProviderTestOutcome,
|
|
62
|
+
buildProviderTestDetail,
|
|
63
|
+
} from '../core/provider-key-tester.js'
|
|
81
64
|
|
|
82
65
|
// 📖 spawnDaemonCommand — spawns `--daemon-bg` or `--daemon-stop` and captures
|
|
83
66
|
// 📖 the JSON result from stdout so we can surface startup errors to the user.
|
|
@@ -125,185 +108,6 @@ function spawnDaemonCommand(state, args) {
|
|
|
125
108
|
})
|
|
126
109
|
}
|
|
127
110
|
|
|
128
|
-
// 📖 PROVIDER_AUTH_ENDPOINTS maps provider keys to their auth-check URL + method.
|
|
129
|
-
// 📖 For most providers this is the /models endpoint (returns 200=valid, 401=invalid).
|
|
130
|
-
// 📖 Providers without an auth-check endpoint use null (falls back to chat completion ping).
|
|
131
|
-
// 📖 Special cases:
|
|
132
|
-
// 📖 - replicate: uses /v1/predictions (not /models) but needs a different payload
|
|
133
|
-
// 📖 - cloudflare: no auth endpoint — only has chat completions, always uses ping fallback
|
|
134
|
-
const PROVIDER_AUTH_ENDPOINTS = {
|
|
135
|
-
nvidia: { url: 'https://api.nvidia.com/v1/account', method: 'GET' },
|
|
136
|
-
groq: { url: 'https://api.groq.com/v1/models', method: 'GET' },
|
|
137
|
-
cerebras: { url: 'https://api.cerebras.ai/v1/models', method: 'GET' },
|
|
138
|
-
sambanova: { url: 'https://api.sambanova.ai/v1/models', method: 'GET' },
|
|
139
|
-
openrouter: { url: 'https://openrouter.ai/api/v1/key', method: 'GET' },
|
|
140
|
-
mistral: { url: 'https://api.mistral.ai/v1/models', method: 'GET' },
|
|
141
|
-
huggingface: { url: 'https://router.huggingface.co/v1/models', method: 'GET' },
|
|
142
|
-
deepinfra: { url: 'https://api.deepinfra.com/v1/models', method: 'GET' },
|
|
143
|
-
fireworks: { url: 'https://api.fireworks.ai/v1/models', method: 'GET' },
|
|
144
|
-
hyperbolic: { url: 'https://api.hyperbolic.xyz/v1/models', method: 'GET' },
|
|
145
|
-
scaleway: { url: 'https://api.scaleway.ai/v1/models', method: 'GET' },
|
|
146
|
-
siliconflow: { url: 'https://api.siliconflow.com/v1/models', method: 'GET' },
|
|
147
|
-
together: { url: 'https://api.together.xyz/v1/models', method: 'GET' },
|
|
148
|
-
perplexity: { url: 'https://api.perplexity.ai/v1/models', method: 'GET' },
|
|
149
|
-
chutes: { url: 'https://chutes.ai/v1/models', method: 'GET' },
|
|
150
|
-
ovhcloud: { url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models', method: 'GET' },
|
|
151
|
-
qwen: { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', method: 'GET' },
|
|
152
|
-
iflow: { url: 'https://apis.iflow.cn/v1/models', method: 'GET' },
|
|
153
|
-
'github-models': null, // 📖 GitHub Models catalog is public; use chat ping to validate the token.
|
|
154
|
-
replicate: null, // 📖 Replicate has no /models endpoint; use chat completions ping
|
|
155
|
-
cloudflare: null, // 📖 Workers AI has no auth-check endpoint; use ping only
|
|
156
|
-
zai: null, // 📖 ZAI undocumented; use ping only
|
|
157
|
-
googleai: null, // 📖 Google AI Studio has no OpenAI-compatible /models; use ping
|
|
158
|
-
'opencode-zen': null, // 📖 OpenCode Zen uses OpenCode auth only; use ping
|
|
159
|
-
kilo: { url: 'https://api.kilo.ai/api/gateway/models', method: 'GET' },
|
|
160
|
-
llm7: { url: 'https://api.llm7.io/v1/models', method: 'GET' },
|
|
161
|
-
routeway: { url: 'https://api.routeway.ai/v1/models', method: 'GET' },
|
|
162
|
-
novita: { url: 'https://api.novita.ai/openai/v1/models', method: 'GET' },
|
|
163
|
-
'ollama-cloud': { url: 'https://ollama.com/v1/models', method: 'GET' },
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 📖 Sleep imported from shared-helpers.js
|
|
167
|
-
|
|
168
|
-
// 📖 testProviderKeyDirect: Fast auth-only check using /v1/account or /v1/models.
|
|
169
|
-
// 📖 Fires 3 parallel probes to get a fast decisive result (auth error vs timeout vs 200).
|
|
170
|
-
// 📖 Returns { code, ms } from the first non-timeout response, or the best available.
|
|
171
|
-
async function testProviderKeyDirect(apiKey, providerKey) {
|
|
172
|
-
const authConfig = PROVIDER_AUTH_ENDPOINTS[providerKey]
|
|
173
|
-
if (!authConfig) return null
|
|
174
|
-
|
|
175
|
-
const { url, method } = authConfig
|
|
176
|
-
const headers = { Authorization: `Bearer ${apiKey}` }
|
|
177
|
-
if (providerKey === 'openrouter') {
|
|
178
|
-
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
179
|
-
headers['X-Title'] = 'free-coding-models'
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const parallel = 3
|
|
183
|
-
const promises = Array.from({ length: parallel }, async () => {
|
|
184
|
-
const ctrl = new AbortController()
|
|
185
|
-
const timer = setTimeout(() => ctrl.abort(), 8000)
|
|
186
|
-
const t0 = performance.now()
|
|
187
|
-
try {
|
|
188
|
-
const resp = await fetch(url, { method, headers, signal: ctrl.signal })
|
|
189
|
-
return { code: resp.status, ms: Math.round(performance.now() - t0) }
|
|
190
|
-
} catch (err) {
|
|
191
|
-
const isTimeout = err.name === 'AbortError'
|
|
192
|
-
return { code: isTimeout ? '000' : 'ERR', ms: isTimeout ? 'TIMEOUT' : Math.round(performance.now() - t0) }
|
|
193
|
-
} finally {
|
|
194
|
-
clearTimeout(timer)
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
const results = await Promise.all(promises)
|
|
199
|
-
const success = results.find(r => r.code === 200)
|
|
200
|
-
if (success) return success
|
|
201
|
-
const authFailure = results.find(r => r.code === 401 || r.code === 403)
|
|
202
|
-
if (authFailure) return authFailure
|
|
203
|
-
return results[0]
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 📖 buildProviderModelsUrl derives the matching `/models` endpoint for providers
|
|
208
|
-
* 📖 that expose an OpenAI-compatible model list next to `/chat/completions`.
|
|
209
|
-
* @param {string} url
|
|
210
|
-
* @returns {string|null}
|
|
211
|
-
*/
|
|
212
|
-
export function buildProviderModelsUrl(url) {
|
|
213
|
-
if (typeof url !== 'string' || !url.includes('/chat/completions')) return null
|
|
214
|
-
return url.replace(/\/chat\/completions$/, '/models')
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* 📖 parseProviderModelIds extracts ids from a standard OpenAI-style `/models` response.
|
|
219
|
-
* 📖 Invalid payloads return an empty list so the key-test flow can safely fall back.
|
|
220
|
-
* @param {unknown} data
|
|
221
|
-
* @returns {string[]}
|
|
222
|
-
*/
|
|
223
|
-
export function parseProviderModelIds(data) {
|
|
224
|
-
if (!data || typeof data !== 'object' || !Array.isArray(data.data)) return []
|
|
225
|
-
return data.data
|
|
226
|
-
.map(entry => (entry && typeof entry.id === 'string') ? entry.id.trim() : '')
|
|
227
|
-
.filter(Boolean)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 📖 listProviderTestModels builds the ordered probe list used by the Settings `T` key.
|
|
232
|
-
* 📖 Order matters:
|
|
233
|
-
* 📖 1. provider-specific known-good overrides
|
|
234
|
-
* 📖 2. discovered `/models` ids that also exist in this repo
|
|
235
|
-
* 📖 3. all discovered `/models` ids
|
|
236
|
-
* 📖 4. repo static model ids as final fallback
|
|
237
|
-
* @param {string} providerKey
|
|
238
|
-
* @param {{ models?: Array<[string, string, string, string, string]> } | undefined} src
|
|
239
|
-
* @param {string[]} [discoveredModelIds=[]]
|
|
240
|
-
* @returns {string[]}
|
|
241
|
-
*/
|
|
242
|
-
export function listProviderTestModels(providerKey, src, discoveredModelIds = []) {
|
|
243
|
-
const staticModelIds = Array.isArray(src?.models) ? src.models.map(model => model[0]).filter(Boolean) : []
|
|
244
|
-
const staticModelSet = new Set(staticModelIds)
|
|
245
|
-
const preferredDiscoveredIds = discoveredModelIds.filter(modelId => staticModelSet.has(modelId))
|
|
246
|
-
const orderedCandidates = [
|
|
247
|
-
...(PROVIDER_TEST_MODEL_OVERRIDES[providerKey] ?? []),
|
|
248
|
-
...preferredDiscoveredIds,
|
|
249
|
-
...discoveredModelIds,
|
|
250
|
-
...staticModelIds,
|
|
251
|
-
]
|
|
252
|
-
return [...new Set(orderedCandidates)]
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* 📖 classifyProviderTestOutcome maps attempted probe codes to a user-facing test result.
|
|
257
|
-
* 📖 This keeps Settings more honest than a binary success/fail badge:
|
|
258
|
-
* 📖 - `rate_limited` means the key is valid but the provider is currently throttling
|
|
259
|
-
* 📖 - `no_callable_model` means the provider responded, but none of the attempted models were callable
|
|
260
|
-
* @param {string[]} codes
|
|
261
|
-
* @returns {'ok'|'auth_error'|'rate_limited'|'no_callable_model'|'fail'}
|
|
262
|
-
*/
|
|
263
|
-
export function classifyProviderTestOutcome(codes) {
|
|
264
|
-
if (codes.includes('200')) return 'ok'
|
|
265
|
-
if (codes.includes('401') || codes.includes('403')) return 'auth_error'
|
|
266
|
-
if (codes.length > 0 && codes.every(code => code === '429')) return 'rate_limited'
|
|
267
|
-
if (codes.length > 0 && codes.every(code => code === '404' || code === '410')) return 'no_callable_model'
|
|
268
|
-
return 'fail'
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// 📖 buildProviderTestDetail explains why the Settings `T` probe failed, with
|
|
272
|
-
// 📖 enough context for the user to know whether the key, model list, or provider
|
|
273
|
-
// 📖 quota is the problem.
|
|
274
|
-
export function buildProviderTestDetail(providerLabel, outcome, attempts = [], discoveryNote = '') {
|
|
275
|
-
const introByOutcome = {
|
|
276
|
-
missing_key: `${providerLabel} has no saved API key right now, so no authenticated test could be sent.`,
|
|
277
|
-
ok: `${providerLabel} accepted the key.`,
|
|
278
|
-
auth_error: `${providerLabel} rejected the configured key with an authentication error.`,
|
|
279
|
-
rate_limited: `${providerLabel} throttled every probe, so the key may still be valid but is currently rate-limited.`,
|
|
280
|
-
no_callable_model: `${providerLabel} answered the requests, but none of the probed models were callable on its chat endpoint.`,
|
|
281
|
-
fail: `${providerLabel} never returned a successful probe during the retry window.`,
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const hintsByOutcome = {
|
|
285
|
-
missing_key: 'Save the key with Enter in Settings, then rerun T.',
|
|
286
|
-
ok: attempts.length > 0 ? `Validated on ${attempts[attempts.length - 1].model}.` : 'The provider returned a success response.',
|
|
287
|
-
auth_error: 'This usually means the saved key is invalid, expired, revoked, or truncated before it reached disk.',
|
|
288
|
-
rate_limited: 'Wait for the provider quota window to reset, then rerun T.',
|
|
289
|
-
no_callable_model: 'The provider catalog or repo defaults likely drifted; try another model family or refresh the catalog.',
|
|
290
|
-
fail: 'This can be caused by timeouts, 5xx responses, or a provider-side outage.',
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const attemptSummary = attempts.length > 0
|
|
294
|
-
? `Attempts: ${attempts.map(({ attempt, model, code }) => `#${attempt} ${model} -> ${code}`).join(' | ')}`
|
|
295
|
-
: 'Attempts: none'
|
|
296
|
-
|
|
297
|
-
const segments = [
|
|
298
|
-
introByOutcome[outcome] || introByOutcome.fail,
|
|
299
|
-
hintsByOutcome[outcome] || hintsByOutcome.fail,
|
|
300
|
-
discoveryNote,
|
|
301
|
-
attemptSummary,
|
|
302
|
-
].filter(Boolean)
|
|
303
|
-
|
|
304
|
-
return segments.join(' ')
|
|
305
|
-
}
|
|
306
|
-
|
|
307
111
|
export function createKeyHandler(ctx) {
|
|
308
112
|
const {
|
|
309
113
|
state,
|
|
@@ -634,10 +438,8 @@ export function createKeyHandler(ctx) {
|
|
|
634
438
|
}
|
|
635
439
|
|
|
636
440
|
// ─── Settings key test helper ───────────────────────────────────────────────
|
|
637
|
-
// 📖 Verifies an API key by
|
|
638
|
-
// 📖
|
|
639
|
-
// 📖 Auth-only result is decisive (200=valid, 401/403=invalid); only timeouts or
|
|
640
|
-
// 📖 providers without auth endpoints fall through to the ping-based approach.
|
|
441
|
+
// 📖 Verifies an API key by delegating to the shared runProviderKeyTest()
|
|
442
|
+
// 📖 pipeline. Wraps the result into TUI state for display.
|
|
641
443
|
async function testProviderKey(providerKey) {
|
|
642
444
|
const src = sources[providerKey]
|
|
643
445
|
if (!src) return
|
|
@@ -650,98 +452,17 @@ export function createKeyHandler(ctx) {
|
|
|
650
452
|
return
|
|
651
453
|
}
|
|
652
454
|
|
|
653
|
-
// 📖 Fast path: parallel auth-only probes (3×8s) to /v1/account or /v1/models.
|
|
654
|
-
// 📖 200 = key valid and accepted. 401/403 = key rejected. null = no auth endpoint.
|
|
655
|
-
const authResult = await testProviderKeyDirect(testKey, providerKey)
|
|
656
|
-
if (authResult) {
|
|
657
|
-
if (authResult.code === 200) {
|
|
658
|
-
state.settingsTestResults[providerKey] = 'ok'
|
|
659
|
-
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'ok', [], `Auth-only probe returned HTTP 200.`)
|
|
660
|
-
return
|
|
661
|
-
}
|
|
662
|
-
if (authResult.code === 401 || authResult.code === 403) {
|
|
663
|
-
state.settingsTestResults[providerKey] = 'auth_error'
|
|
664
|
-
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'auth_error', [], `Auth probe returned HTTP ${authResult.code}.`)
|
|
665
|
-
return
|
|
666
|
-
}
|
|
667
|
-
// 📖 Timeout or ERR — fall through to ping-based approach below.
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// 📖 Slow path: ping-based verification (providers without auth endpoint or timeouts).
|
|
671
455
|
state.settingsTestResults[providerKey] = 'pending'
|
|
672
|
-
state.settingsTestDetails[providerKey] = `Testing ${providerLabel}
|
|
673
|
-
const discoveredModelIds = []
|
|
674
|
-
const modelsUrl = buildProviderModelsUrl(src.url)
|
|
675
|
-
let discoveryNote = ''
|
|
676
|
-
|
|
677
|
-
if (modelsUrl) {
|
|
678
|
-
try {
|
|
679
|
-
const headers = { Authorization: `Bearer ${testKey}` }
|
|
680
|
-
if (providerKey === 'openrouter') {
|
|
681
|
-
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
682
|
-
headers['X-Title'] = 'free-coding-models'
|
|
683
|
-
}
|
|
684
|
-
const modelsResp = await fetch(modelsUrl, { headers })
|
|
685
|
-
if (modelsResp.ok) {
|
|
686
|
-
const data = await modelsResp.json()
|
|
687
|
-
discoveredModelIds.push(...parseProviderModelIds(data))
|
|
688
|
-
discoveryNote = discoveredModelIds.length > 0
|
|
689
|
-
? `Live model discovery returned ${discoveredModelIds.length} ids.`
|
|
690
|
-
: 'Live model discovery succeeded but returned no callable ids.'
|
|
691
|
-
} else {
|
|
692
|
-
discoveryNote = `Live model discovery returned HTTP ${modelsResp.status}; falling back to the repo catalog.`
|
|
693
|
-
}
|
|
694
|
-
} catch (err) {
|
|
695
|
-
discoveryNote = `Live model discovery failed (${err?.name || 'error'}); falling back to the repo catalog.`
|
|
696
|
-
}
|
|
697
|
-
}
|
|
456
|
+
state.settingsTestDetails[providerKey] = `Testing ${providerLabel}...`
|
|
698
457
|
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// 📖 Parallel ping burst: fire up to 5 probes simultaneously to get fast feedback.
|
|
707
|
-
const PARALLEL_PROBES = 5
|
|
708
|
-
const attempts = []
|
|
709
|
-
let settled = false
|
|
710
|
-
|
|
711
|
-
while (!settled) {
|
|
712
|
-
const batch = []
|
|
713
|
-
for (let i = 0; i < PARALLEL_PROBES && attempts.length + batch.length < SETTINGS_TEST_MAX_ATTEMPTS; i++) {
|
|
714
|
-
const testModel = candidateModels[(attempts.length + batch.length) % candidateModels.length]
|
|
715
|
-
batch.push(ping(testKey, testModel, providerKey, src.url).then(({ code }) => ({ attempt: attempts.length + batch.length + 1, model: testModel, code })))
|
|
716
|
-
}
|
|
717
|
-
const batchResults = await Promise.all(batch)
|
|
718
|
-
attempts.push(...batchResults)
|
|
719
|
-
|
|
720
|
-
// 📖 Check outcome after each parallel batch.
|
|
721
|
-
const outcome = classifyProviderTestOutcome(attempts.map(({ code }) => code))
|
|
722
|
-
if (outcome === 'ok') {
|
|
723
|
-
state.settingsTestResults[providerKey] = 'ok'
|
|
724
|
-
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'ok', attempts, discoveryNote)
|
|
725
|
-
settled = true
|
|
726
|
-
continue
|
|
727
|
-
}
|
|
728
|
-
if (outcome === 'auth_error') {
|
|
729
|
-
state.settingsTestResults[providerKey] = 'auth_error'
|
|
730
|
-
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'auth_error', attempts, discoveryNote)
|
|
731
|
-
settled = true
|
|
732
|
-
continue
|
|
733
|
-
}
|
|
734
|
-
if (attempts.length >= SETTINGS_TEST_MAX_ATTEMPTS) {
|
|
735
|
-
state.settingsTestResults[providerKey] = outcome
|
|
736
|
-
state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, outcome, attempts, discoveryNote)
|
|
737
|
-
settled = true
|
|
738
|
-
continue
|
|
739
|
-
}
|
|
458
|
+
const result = await runProviderKeyTest(testKey, providerKey, src, {
|
|
459
|
+
onProgress: ({ attempts, maxAttempts }) => {
|
|
460
|
+
state.settingsTestDetails[providerKey] = `Testing ${providerLabel}... ${attempts}/${maxAttempts} probes tried.`
|
|
461
|
+
},
|
|
462
|
+
})
|
|
740
463
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
await sleep(SETTINGS_TEST_RETRY_DELAY_MS)
|
|
744
|
-
}
|
|
464
|
+
state.settingsTestResults[providerKey] = result.outcome
|
|
465
|
+
state.settingsTestDetails[providerKey] = result.detail
|
|
745
466
|
}
|
|
746
467
|
|
|
747
468
|
// 📖 Manual update checker from settings; keeps status visible in maintenance row.
|
|
@@ -3763,3 +3484,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
3763
3484
|
// 📖 Clicks outside any recognized zone are silently ignored.
|
|
3764
3485
|
}
|
|
3765
3486
|
}
|
|
3487
|
+
|
|
3488
|
+
export {
|
|
3489
|
+
buildProviderModelsUrl,
|
|
3490
|
+
parseProviderModelIds,
|
|
3491
|
+
listProviderTestModels,
|
|
3492
|
+
classifyProviderTestOutcome,
|
|
3493
|
+
buildProviderTestDetail,
|
|
3494
|
+
};
|