free-coding-models 0.1.84 → 0.1.86
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 -12
- package/bin/free-coding-models.js +167 -73
- package/package.json +1 -1
- package/sources.js +12 -10
- package/src/config.js +14 -3
- package/src/constants.js +3 -1
- package/src/key-handler.js +160 -17
- package/src/overlays.js +9 -7
- package/src/provider-metadata.js +20 -20
- package/src/render-table.js +105 -62
- package/src/utils.js +31 -26
package/src/key-handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file key-handler.js
|
|
3
|
-
* @description Factory for the main TUI keypress handler.
|
|
3
|
+
* @description Factory for the main TUI keypress handler and provider key-test model selection.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module encapsulates the full onKeyPress switch used by the TUI,
|
|
@@ -8,11 +8,93 @@
|
|
|
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
|
*
|
|
11
|
+
* It also owns the "test key" model selection used by the Settings overlay.
|
|
12
|
+
* Some providers expose models in `/v1/models` that are not actually callable
|
|
13
|
+
* on the chat-completions endpoint. To avoid false negatives when a user
|
|
14
|
+
* presses `T` in Settings, the helpers below discover candidate model IDs,
|
|
15
|
+
* merge them with repo defaults, then probe several until one is accepted.
|
|
16
|
+
*
|
|
11
17
|
* → Functions:
|
|
18
|
+
* - `buildProviderModelsUrl` — derive the matching `/models` endpoint when available
|
|
19
|
+
* - `parseProviderModelIds` — extract model ids from an OpenAI-style `/models` payload
|
|
20
|
+
* - `listProviderTestModels` — build an ordered candidate list for provider key verification
|
|
21
|
+
* - `classifyProviderTestOutcome` — convert attempted HTTP codes into a settings badge state
|
|
12
22
|
* - `createKeyHandler` — returns the async keypress handler
|
|
13
23
|
*
|
|
14
|
-
* @exports { createKeyHandler }
|
|
24
|
+
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, createKeyHandler }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
28
|
+
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
29
|
+
const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
30
|
+
sambanova: ['DeepSeek-V3-0324'],
|
|
31
|
+
nvidia: ['deepseek-ai/deepseek-v3.1-terminus', 'openai/gpt-oss-120b'],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 📖 buildProviderModelsUrl derives the matching `/models` endpoint for providers
|
|
36
|
+
* 📖 that expose an OpenAI-compatible model list next to `/chat/completions`.
|
|
37
|
+
* @param {string} url
|
|
38
|
+
* @returns {string|null}
|
|
15
39
|
*/
|
|
40
|
+
export function buildProviderModelsUrl(url) {
|
|
41
|
+
if (typeof url !== 'string' || !url.includes('/chat/completions')) return null
|
|
42
|
+
return url.replace(/\/chat\/completions$/, '/models')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 📖 parseProviderModelIds extracts ids from a standard OpenAI-style `/models` response.
|
|
47
|
+
* 📖 Invalid payloads return an empty list so the key-test flow can safely fall back.
|
|
48
|
+
* @param {unknown} data
|
|
49
|
+
* @returns {string[]}
|
|
50
|
+
*/
|
|
51
|
+
export function parseProviderModelIds(data) {
|
|
52
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.data)) return []
|
|
53
|
+
return data.data
|
|
54
|
+
.map(entry => (entry && typeof entry.id === 'string') ? entry.id.trim() : '')
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 📖 listProviderTestModels builds the ordered probe list used by the Settings `T` key.
|
|
60
|
+
* 📖 Order matters:
|
|
61
|
+
* 📖 1. provider-specific known-good overrides
|
|
62
|
+
* 📖 2. discovered `/models` ids that also exist in this repo
|
|
63
|
+
* 📖 3. all discovered `/models` ids
|
|
64
|
+
* 📖 4. repo static model ids as final fallback
|
|
65
|
+
* @param {string} providerKey
|
|
66
|
+
* @param {{ models?: Array<[string, string, string, string, string]> } | undefined} src
|
|
67
|
+
* @param {string[]} [discoveredModelIds=[]]
|
|
68
|
+
* @returns {string[]}
|
|
69
|
+
*/
|
|
70
|
+
export function listProviderTestModels(providerKey, src, discoveredModelIds = []) {
|
|
71
|
+
const staticModelIds = Array.isArray(src?.models) ? src.models.map(model => model[0]).filter(Boolean) : []
|
|
72
|
+
const staticModelSet = new Set(staticModelIds)
|
|
73
|
+
const preferredDiscoveredIds = discoveredModelIds.filter(modelId => staticModelSet.has(modelId))
|
|
74
|
+
const orderedCandidates = [
|
|
75
|
+
...(PROVIDER_TEST_MODEL_OVERRIDES[providerKey] ?? []),
|
|
76
|
+
...preferredDiscoveredIds,
|
|
77
|
+
...discoveredModelIds,
|
|
78
|
+
...staticModelIds,
|
|
79
|
+
]
|
|
80
|
+
return [...new Set(orderedCandidates)]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 📖 classifyProviderTestOutcome maps attempted probe codes to a user-facing test result.
|
|
85
|
+
* 📖 This keeps Settings more honest than a binary success/fail badge:
|
|
86
|
+
* 📖 - `rate_limited` means the key is valid but the provider is currently throttling
|
|
87
|
+
* 📖 - `no_callable_model` means the provider responded, but none of the attempted models were callable
|
|
88
|
+
* @param {string[]} codes
|
|
89
|
+
* @returns {'ok'|'fail'|'rate_limited'|'no_callable_model'}
|
|
90
|
+
*/
|
|
91
|
+
export function classifyProviderTestOutcome(codes) {
|
|
92
|
+
if (codes.includes('200')) return 'ok'
|
|
93
|
+
if (codes.includes('401') || codes.includes('403')) return 'fail'
|
|
94
|
+
if (codes.length > 0 && codes.every(code => code === '429')) return 'rate_limited'
|
|
95
|
+
if (codes.length > 0 && codes.every(code => code === '404' || code === '410')) return 'no_callable_model'
|
|
96
|
+
return 'fail'
|
|
97
|
+
}
|
|
16
98
|
|
|
17
99
|
export function createKeyHandler(ctx) {
|
|
18
100
|
const {
|
|
@@ -65,6 +147,10 @@ export function createKeyHandler(ctx) {
|
|
|
65
147
|
mergedModels,
|
|
66
148
|
apiKey,
|
|
67
149
|
chalk,
|
|
150
|
+
setPingMode,
|
|
151
|
+
noteUserActivity,
|
|
152
|
+
intervalToPingMode,
|
|
153
|
+
PING_MODE_CYCLE,
|
|
68
154
|
setResults,
|
|
69
155
|
readline,
|
|
70
156
|
} = ctx
|
|
@@ -79,13 +165,45 @@ export function createKeyHandler(ctx) {
|
|
|
79
165
|
const testKey = getApiKey(state.config, providerKey)
|
|
80
166
|
if (!testKey) { state.settingsTestResults[providerKey] = 'fail'; return }
|
|
81
167
|
|
|
82
|
-
// 📖 Use the first model in the provider's list for the test ping
|
|
83
|
-
const testModel = src.models[0]?.[0]
|
|
84
|
-
if (!testModel) { state.settingsTestResults[providerKey] = 'fail'; return }
|
|
85
|
-
|
|
86
168
|
state.settingsTestResults[providerKey] = 'pending'
|
|
87
|
-
const
|
|
88
|
-
|
|
169
|
+
const discoveredModelIds = []
|
|
170
|
+
const modelsUrl = buildProviderModelsUrl(src.url)
|
|
171
|
+
|
|
172
|
+
if (modelsUrl) {
|
|
173
|
+
try {
|
|
174
|
+
const headers = { Authorization: `Bearer ${testKey}` }
|
|
175
|
+
if (providerKey === 'openrouter') {
|
|
176
|
+
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
177
|
+
headers['X-Title'] = 'free-coding-models'
|
|
178
|
+
}
|
|
179
|
+
const modelsResp = await fetch(modelsUrl, { headers })
|
|
180
|
+
if (modelsResp.ok) {
|
|
181
|
+
const data = await modelsResp.json()
|
|
182
|
+
discoveredModelIds.push(...parseProviderModelIds(data))
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// 📖 Discovery failure is non-fatal; we still have repo-defined fallbacks.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const candidateModels = listProviderTestModels(providerKey, src, discoveredModelIds)
|
|
190
|
+
if (candidateModels.length === 0) { state.settingsTestResults[providerKey] = 'fail'; return }
|
|
191
|
+
const attemptedCodes = []
|
|
192
|
+
|
|
193
|
+
for (const testModel of candidateModels.slice(0, 8)) {
|
|
194
|
+
const { code } = await ping(testKey, testModel, providerKey, src.url)
|
|
195
|
+
attemptedCodes.push(code)
|
|
196
|
+
if (code === '200') {
|
|
197
|
+
state.settingsTestResults[providerKey] = 'ok'
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if (code === '401' || code === '403') {
|
|
201
|
+
state.settingsTestResults[providerKey] = 'fail'
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
state.settingsTestResults[providerKey] = classifyProviderTestOutcome(attemptedCodes)
|
|
89
207
|
}
|
|
90
208
|
|
|
91
209
|
// 📖 Manual update checker from settings; keeps status visible in maintenance row.
|
|
@@ -121,6 +239,7 @@ export function createKeyHandler(ctx) {
|
|
|
121
239
|
|
|
122
240
|
return async (str, key) => {
|
|
123
241
|
if (!key) return
|
|
242
|
+
noteUserActivity()
|
|
124
243
|
|
|
125
244
|
// 📖 Profile save mode: intercept ALL keys while inline name input is active.
|
|
126
245
|
// 📖 Enter → save, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
@@ -141,6 +260,7 @@ export function createKeyHandler(ctx) {
|
|
|
141
260
|
sortColumn: state.sortColumn,
|
|
142
261
|
sortAsc: state.sortDirection === 'asc',
|
|
143
262
|
pingInterval: state.pingInterval,
|
|
263
|
+
hideUnconfiguredModels: state.hideUnconfiguredModels,
|
|
144
264
|
})
|
|
145
265
|
setActiveProfile(state.config, name)
|
|
146
266
|
state.activeProfile = name
|
|
@@ -503,7 +623,7 @@ export function createKeyHandler(ctx) {
|
|
|
503
623
|
// 📖 Try to reuse existing result to keep ping history
|
|
504
624
|
const existing = state.results.find(r => r.modelId === modelId && r.providerKey === providerKey)
|
|
505
625
|
if (existing) return existing
|
|
506
|
-
return { idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey, status: 'pending', pings: [], httpCode: null, hidden: false }
|
|
626
|
+
return { idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey, status: 'pending', pings: [], httpCode: null, isPinging: false, hidden: false }
|
|
507
627
|
})
|
|
508
628
|
// 📖 Re-index results
|
|
509
629
|
nextResults.forEach((r, i) => { r.idx = i + 1 })
|
|
@@ -524,6 +644,7 @@ export function createKeyHandler(ctx) {
|
|
|
524
644
|
r.status = 'pending'
|
|
525
645
|
r.pings = []
|
|
526
646
|
r.httpCode = null
|
|
647
|
+
r.isPinging = false
|
|
527
648
|
pingModel(r).catch(() => {})
|
|
528
649
|
}
|
|
529
650
|
})
|
|
@@ -582,7 +703,7 @@ export function createKeyHandler(ctx) {
|
|
|
582
703
|
if (settings) {
|
|
583
704
|
state.sortColumn = settings.sortColumn || 'avg'
|
|
584
705
|
state.sortDirection = settings.sortAsc ? 'asc' : 'desc'
|
|
585
|
-
|
|
706
|
+
setPingMode(intervalToPingMode(settings.pingInterval || PING_INTERVAL), 'manual')
|
|
586
707
|
if (settings.tierFilter) {
|
|
587
708
|
const tierIdx = TIER_CYCLE.indexOf(settings.tierFilter)
|
|
588
709
|
if (tierIdx >= 0) state.tierFilterMode = tierIdx
|
|
@@ -753,6 +874,7 @@ export function createKeyHandler(ctx) {
|
|
|
753
874
|
sortColumn: state.sortColumn,
|
|
754
875
|
sortAsc: state.sortDirection === 'asc',
|
|
755
876
|
pingInterval: state.pingInterval,
|
|
877
|
+
hideUnconfiguredModels: state.hideUnconfiguredModels,
|
|
756
878
|
})
|
|
757
879
|
setActiveProfile(state.config, 'default')
|
|
758
880
|
state.activeProfile = 'default'
|
|
@@ -773,13 +895,14 @@ export function createKeyHandler(ctx) {
|
|
|
773
895
|
// 📖 Apply profile's TUI settings to live state
|
|
774
896
|
state.sortColumn = settings.sortColumn || 'avg'
|
|
775
897
|
state.sortDirection = settings.sortAsc ? 'asc' : 'desc'
|
|
776
|
-
|
|
898
|
+
setPingMode(intervalToPingMode(settings.pingInterval || PING_INTERVAL), 'manual')
|
|
777
899
|
if (settings.tierFilter) {
|
|
778
900
|
const tierIdx = TIER_CYCLE.indexOf(settings.tierFilter)
|
|
779
901
|
if (tierIdx >= 0) state.tierFilterMode = tierIdx
|
|
780
902
|
} else {
|
|
781
903
|
state.tierFilterMode = 0
|
|
782
904
|
}
|
|
905
|
+
state.hideUnconfiguredModels = settings.hideUnconfiguredModels === true
|
|
783
906
|
state.activeProfile = nextProfile
|
|
784
907
|
// 📖 Rebuild favorites from profile data
|
|
785
908
|
syncFavoriteFlags(state.results, state.config)
|
|
@@ -871,13 +994,33 @@ export function createKeyHandler(ctx) {
|
|
|
871
994
|
return
|
|
872
995
|
}
|
|
873
996
|
|
|
874
|
-
// 📖
|
|
875
|
-
// 📖
|
|
876
|
-
// 📖
|
|
997
|
+
// 📖 W cycles the supported ping modes:
|
|
998
|
+
// 📖 speed (2s) → normal (10s) → slow (30s) → forced (4s) → speed.
|
|
999
|
+
// 📖 forced ignores auto speed/slow transitions until the user leaves it manually.
|
|
877
1000
|
if (key.name === 'w') {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1001
|
+
const currentIdx = PING_MODE_CYCLE.indexOf(state.pingMode)
|
|
1002
|
+
const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % PING_MODE_CYCLE.length : 0
|
|
1003
|
+
setPingMode(PING_MODE_CYCLE[nextIdx], 'manual')
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// 📖 E toggles hiding models whose provider has no configured API key.
|
|
1007
|
+
// 📖 The preference is saved globally and mirrored into the active profile.
|
|
1008
|
+
if (key.name === 'e') {
|
|
1009
|
+
state.hideUnconfiguredModels = !state.hideUnconfiguredModels
|
|
1010
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1011
|
+
state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
|
|
1012
|
+
if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
|
|
1013
|
+
const profile = state.config.profiles[state.activeProfile]
|
|
1014
|
+
if (!profile.settings || typeof profile.settings !== 'object') profile.settings = {}
|
|
1015
|
+
profile.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
|
|
1016
|
+
}
|
|
1017
|
+
saveConfig(state.config)
|
|
1018
|
+
applyTierFilter()
|
|
1019
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
1020
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1021
|
+
state.cursor = 0
|
|
1022
|
+
state.scrollOffset = 0
|
|
1023
|
+
return
|
|
881
1024
|
}
|
|
882
1025
|
|
|
883
1026
|
// 📖 Tier toggle key: T = cycle through each individual tier (All → S+ → S → A+ → A → A- → B+ → B → C → All)
|
package/src/overlays.js
CHANGED
|
@@ -105,6 +105,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
105
105
|
let testBadge = chalk.dim('[Test —]')
|
|
106
106
|
if (testResult === 'pending') testBadge = chalk.yellow('[Testing…]')
|
|
107
107
|
else if (testResult === 'ok') testBadge = chalk.greenBright('[Test ✅]')
|
|
108
|
+
else if (testResult === 'rate_limited') testBadge = chalk.yellow('[Rate limit ⏳]')
|
|
109
|
+
else if (testResult === 'no_callable_model') testBadge = chalk.magenta('[No model ⚠]')
|
|
108
110
|
else if (testResult === 'fail') testBadge = chalk.red('[Test ❌]')
|
|
109
111
|
const rateSummary = chalk.dim((meta.rateLimits || 'No limit info').slice(0, 36))
|
|
110
112
|
|
|
@@ -250,8 +252,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
250
252
|
lines.push(` ${chalk.cyan('Latest')} Most recent ping response time (ms) ${chalk.dim('Sort:')} ${chalk.yellow('L')}`)
|
|
251
253
|
lines.push(` ${chalk.dim('Shows how fast the server is responding right now — useful to catch live slowdowns.')}`)
|
|
252
254
|
lines.push('')
|
|
253
|
-
lines.push(` ${chalk.cyan('Avg Ping')} Average response time across all
|
|
254
|
-
lines.push(` ${chalk.dim('The long-term truth.
|
|
255
|
+
lines.push(` ${chalk.cyan('Avg Ping')} Average response time across all measurable pings (200 + 401) (ms) ${chalk.dim('Sort:')} ${chalk.yellow('A')}`)
|
|
256
|
+
lines.push(` ${chalk.dim('The long-term truth. Even without a key, a 401 still gives real latency so the average stays useful.')}`)
|
|
255
257
|
lines.push('')
|
|
256
258
|
lines.push(` ${chalk.cyan('Health')} Live status: ✅ UP / 🔥 429 / ⏳ TIMEOUT / ❌ ERR / 🔑 NO KEY ${chalk.dim('Sort:')} ${chalk.yellow('H')}`)
|
|
257
259
|
lines.push(` ${chalk.dim('Tells you instantly if a model is reachable or down — no guesswork needed.')}`)
|
|
@@ -278,10 +280,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
278
280
|
lines.push(` ${chalk.yellow('Enter')} Select model and launch`)
|
|
279
281
|
lines.push('')
|
|
280
282
|
lines.push(` ${chalk.bold('Controls')}`)
|
|
281
|
-
lines.push(` ${chalk.yellow('W')}
|
|
282
|
-
lines.push(` ${chalk.yellow('
|
|
283
|
-
lines.push(` ${chalk.yellow('X')} Toggle
|
|
284
|
-
lines.push(` ${chalk.yellow('Z')} Cycle
|
|
283
|
+
lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
284
|
+
lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default, persisted globally + in profiles)')}`)
|
|
285
|
+
lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
|
|
286
|
+
lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode CLI → OpenCode Desktop → OpenClaw)')}`)
|
|
285
287
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
286
288
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
287
289
|
lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(📝 send anonymous feedback to the project team)')}`)
|
|
@@ -289,7 +291,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
289
291
|
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, manual update)')}`)
|
|
290
292
|
lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
|
|
291
293
|
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
292
|
-
lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, API keys.')}`)
|
|
294
|
+
lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
|
|
293
295
|
lines.push(` ${chalk.dim('Use --profile <name> to load a profile on startup.')}`)
|
|
294
296
|
lines.push(` ${chalk.yellow('K')} / ${chalk.yellow('Esc')} Show/hide this help`)
|
|
295
297
|
lines.push(` ${chalk.yellow('Ctrl+C')} Exit`)
|
package/src/provider-metadata.js
CHANGED
|
@@ -77,140 +77,140 @@ export const OPENCODE_MODEL_MAP = {
|
|
|
77
77
|
export const PROVIDER_METADATA = {
|
|
78
78
|
nvidia: {
|
|
79
79
|
label: 'NVIDIA NIM',
|
|
80
|
-
color: chalk.rgb(
|
|
80
|
+
color: chalk.rgb(178, 235, 190),
|
|
81
81
|
signupUrl: 'https://build.nvidia.com',
|
|
82
82
|
signupHint: 'Profile → API Keys → Generate',
|
|
83
83
|
rateLimits: 'Free tier (provider quota by model)',
|
|
84
84
|
},
|
|
85
85
|
groq: {
|
|
86
86
|
label: 'Groq',
|
|
87
|
-
color: chalk.rgb(
|
|
87
|
+
color: chalk.rgb(255, 204, 188),
|
|
88
88
|
signupUrl: 'https://console.groq.com/keys',
|
|
89
89
|
signupHint: 'API Keys → Create API Key',
|
|
90
90
|
rateLimits: 'Free dev tier (provider quota)',
|
|
91
91
|
},
|
|
92
92
|
cerebras: {
|
|
93
93
|
label: 'Cerebras',
|
|
94
|
-
color: chalk.rgb(
|
|
94
|
+
color: chalk.rgb(179, 229, 252),
|
|
95
95
|
signupUrl: 'https://cloud.cerebras.ai',
|
|
96
96
|
signupHint: 'API Keys → Create',
|
|
97
97
|
rateLimits: 'Free dev tier (provider quota)',
|
|
98
98
|
},
|
|
99
99
|
sambanova: {
|
|
100
100
|
label: 'SambaNova',
|
|
101
|
-
color: chalk.rgb(255,
|
|
101
|
+
color: chalk.rgb(255, 224, 178),
|
|
102
102
|
signupUrl: 'https://cloud.sambanova.ai/apis',
|
|
103
103
|
signupHint: 'SambaCloud portal → Create API key',
|
|
104
104
|
rateLimits: 'Dev tier generous quota',
|
|
105
105
|
},
|
|
106
106
|
openrouter: {
|
|
107
107
|
label: 'OpenRouter',
|
|
108
|
-
color: chalk.rgb(
|
|
108
|
+
color: chalk.rgb(225, 190, 231),
|
|
109
109
|
signupUrl: 'https://openrouter.ai/keys',
|
|
110
110
|
signupHint: 'API Keys → Create',
|
|
111
111
|
rateLimits: '50 req/day, 20/min (:free shared quota)',
|
|
112
112
|
},
|
|
113
113
|
huggingface: {
|
|
114
114
|
label: 'Hugging Face Inference',
|
|
115
|
-
color: chalk.rgb(255,
|
|
115
|
+
color: chalk.rgb(255, 245, 157),
|
|
116
116
|
signupUrl: 'https://huggingface.co/settings/tokens',
|
|
117
117
|
signupHint: 'Settings → Access Tokens',
|
|
118
118
|
rateLimits: 'Free monthly credits (~$0.10)',
|
|
119
119
|
},
|
|
120
120
|
replicate: {
|
|
121
121
|
label: 'Replicate',
|
|
122
|
-
color: chalk.rgb(
|
|
122
|
+
color: chalk.rgb(187, 222, 251),
|
|
123
123
|
signupUrl: 'https://replicate.com/account/api-tokens',
|
|
124
124
|
signupHint: 'Account → API Tokens',
|
|
125
125
|
rateLimits: 'Developer free quota',
|
|
126
126
|
},
|
|
127
127
|
deepinfra: {
|
|
128
128
|
label: 'DeepInfra',
|
|
129
|
-
color: chalk.rgb(
|
|
129
|
+
color: chalk.rgb(178, 223, 219),
|
|
130
130
|
signupUrl: 'https://deepinfra.com/login',
|
|
131
131
|
signupHint: 'Login → API keys',
|
|
132
132
|
rateLimits: 'Free dev tier (low-latency quota)',
|
|
133
133
|
},
|
|
134
134
|
fireworks: {
|
|
135
135
|
label: 'Fireworks AI',
|
|
136
|
-
color: chalk.rgb(255,
|
|
136
|
+
color: chalk.rgb(255, 205, 210),
|
|
137
137
|
signupUrl: 'https://fireworks.ai',
|
|
138
138
|
signupHint: 'Create account → Generate API key',
|
|
139
139
|
rateLimits: '$1 free credits (new dev accounts)',
|
|
140
140
|
},
|
|
141
141
|
codestral: {
|
|
142
142
|
label: 'Mistral Codestral',
|
|
143
|
-
color: chalk.rgb(
|
|
143
|
+
color: chalk.rgb(248, 187, 208),
|
|
144
144
|
signupUrl: 'https://codestral.mistral.ai',
|
|
145
145
|
signupHint: 'API Keys → Create',
|
|
146
146
|
rateLimits: '30 req/min, 2000/day',
|
|
147
147
|
},
|
|
148
148
|
hyperbolic: {
|
|
149
149
|
label: 'Hyperbolic',
|
|
150
|
-
color: chalk.rgb(
|
|
150
|
+
color: chalk.rgb(200, 230, 201),
|
|
151
151
|
signupUrl: 'https://app.hyperbolic.ai/settings',
|
|
152
152
|
signupHint: 'Settings → API Keys',
|
|
153
153
|
rateLimits: '$1 free trial credits',
|
|
154
154
|
},
|
|
155
155
|
scaleway: {
|
|
156
156
|
label: 'Scaleway',
|
|
157
|
-
color: chalk.rgb(
|
|
157
|
+
color: chalk.rgb(129, 212, 250),
|
|
158
158
|
signupUrl: 'https://console.scaleway.com/iam/api-keys',
|
|
159
159
|
signupHint: 'IAM → API Keys',
|
|
160
160
|
rateLimits: '1M free tokens',
|
|
161
161
|
},
|
|
162
162
|
googleai: {
|
|
163
163
|
label: 'Google AI Studio',
|
|
164
|
-
color: chalk.rgb(
|
|
164
|
+
color: chalk.rgb(187, 222, 251),
|
|
165
165
|
signupUrl: 'https://aistudio.google.com/apikey',
|
|
166
166
|
signupHint: 'Get API key',
|
|
167
167
|
rateLimits: '14.4K req/day, 30/min',
|
|
168
168
|
},
|
|
169
169
|
siliconflow: {
|
|
170
170
|
label: 'SiliconFlow',
|
|
171
|
-
color: chalk.rgb(
|
|
171
|
+
color: chalk.rgb(178, 235, 242),
|
|
172
172
|
signupUrl: 'https://cloud.siliconflow.cn/account/ak',
|
|
173
173
|
signupHint: 'API Keys → Create',
|
|
174
174
|
rateLimits: 'Free models: usually 100 RPM, varies by model',
|
|
175
175
|
},
|
|
176
176
|
together: {
|
|
177
177
|
label: 'Together AI',
|
|
178
|
-
color: chalk.rgb(
|
|
178
|
+
color: chalk.rgb(197, 225, 165),
|
|
179
179
|
signupUrl: 'https://api.together.ai/settings/api-keys',
|
|
180
180
|
signupHint: 'Settings → API keys',
|
|
181
181
|
rateLimits: 'Credits/promos vary by account (check console)',
|
|
182
182
|
},
|
|
183
183
|
cloudflare: {
|
|
184
184
|
label: 'Cloudflare Workers AI',
|
|
185
|
-
color: chalk.rgb(
|
|
185
|
+
color: chalk.rgb(255, 204, 128),
|
|
186
186
|
signupUrl: 'https://dash.cloudflare.com',
|
|
187
187
|
signupHint: 'Create AI API token + set CLOUDFLARE_ACCOUNT_ID',
|
|
188
188
|
rateLimits: 'Free: 10k neurons/day, text-gen 300 RPM',
|
|
189
189
|
},
|
|
190
190
|
perplexity: {
|
|
191
191
|
label: 'Perplexity API',
|
|
192
|
-
color: chalk.rgb(
|
|
192
|
+
color: chalk.rgb(159, 234, 201),
|
|
193
193
|
signupUrl: 'https://www.perplexity.ai/settings/api',
|
|
194
194
|
signupHint: 'Generate API key (billing may be required)',
|
|
195
195
|
rateLimits: 'Tiered limits by spend (default ~50 RPM)',
|
|
196
196
|
},
|
|
197
197
|
qwen: {
|
|
198
198
|
label: 'Alibaba Cloud (DashScope)',
|
|
199
|
-
color: chalk.rgb(255,
|
|
199
|
+
color: chalk.rgb(255, 224, 130),
|
|
200
200
|
signupUrl: 'https://modelstudio.console.alibabacloud.com',
|
|
201
201
|
signupHint: 'Model Studio → API Key → Create (1M free tokens, 90 days)',
|
|
202
202
|
rateLimits: '1M free tokens per model (Singapore region, 90 days)',
|
|
203
203
|
},
|
|
204
204
|
zai: {
|
|
205
205
|
label: 'ZAI (z.ai)',
|
|
206
|
-
color: chalk.rgb(
|
|
206
|
+
color: chalk.rgb(174, 213, 255),
|
|
207
207
|
signupUrl: 'https://z.ai',
|
|
208
208
|
signupHint: 'Sign up and generate an API key',
|
|
209
209
|
rateLimits: 'Free tier (generous quota)',
|
|
210
210
|
},
|
|
211
211
|
iflow: {
|
|
212
212
|
label: 'iFlow',
|
|
213
|
-
color: chalk.rgb(
|
|
213
|
+
color: chalk.rgb(220, 231, 117),
|
|
214
214
|
signupUrl: 'https://platform.iflow.cn',
|
|
215
215
|
signupHint: 'Register → Personal Information → Generate API Key (7-day expiry)',
|
|
216
216
|
rateLimits: 'Free for individuals (no request limits)',
|