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.
@@ -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 { code } = await ping(testKey, testModel, providerKey, src.url)
88
- state.settingsTestResults[providerKey] = code === '200' ? 'ok' : 'fail'
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
- state.pingInterval = settings.pingInterval || PING_INTERVAL
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
- state.pingInterval = settings.pingInterval || PING_INTERVAL
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
- // 📖 Interval adjustment keys: W=decrease (faster), ==increase (slower)
875
- // 📖 X was previously used for interval increase but is now reserved for the log page overlay.
876
- // 📖 Minimum 1s, maximum 60s
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
- state.pingInterval = Math.max(1000, state.pingInterval - 1000)
879
- } else if (str === '=' || key.name === '=') {
880
- state.pingInterval = Math.min(60000, state.pingInterval + 1000)
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 successful pings (ms) ${chalk.dim('Sort:')} ${chalk.yellow('A')}`)
254
- lines.push(` ${chalk.dim('The long-term truth. Ignore lucky one-off pings, this tells you real everyday speed.')}`)
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')} Decrease ping interval (faster)`)
282
- lines.push(` ${chalk.yellow('=')} Increase ping interval (slower) ${chalk.dim('(was X X is now the log page)')}`)
283
- lines.push(` ${chalk.yellow('X')} Toggle request log page ${chalk.dim('(shows recent requests from request-log.jsonl)')}`)
284
- lines.push(` ${chalk.yellow('Z')} Cycle launch mode ${chalk.dim('(OpenCode CLI → OpenCode Desktop → OpenClaw)')}`)
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`)
@@ -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(118, 185, 0),
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(249, 103, 20),
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(0, 180, 255),
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, 165, 0),
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(120, 80, 255),
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, 182, 0),
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(120, 160, 255),
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(0, 180, 140),
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, 80, 50),
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(255, 100, 100),
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(0, 200, 150),
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(130, 0, 250),
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(66, 133, 244),
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(255, 120, 30),
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(0, 180, 255),
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(242, 119, 36),
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(0, 210, 190),
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, 140, 0),
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(0, 150, 255),
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(100, 200, 255),
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)',