free-coding-models 0.5.49 → 0.5.51

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.
@@ -48,8 +48,8 @@
48
48
  <link rel="preconnect" href="https://fonts.googleapis.com">
49
49
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
50
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
51
- <script type="module" crossorigin src="/assets/index-Cv5m3BUX.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-r8niexgJ.css">
51
+ <script type="module" crossorigin src="/assets/index-DhqlYW96.js"></script>
52
+ <link rel="stylesheet" crossorigin href="/assets/index-BkK1gdJN.css">
53
53
  </head>
54
54
  <body>
55
55
  <div id="root"></div>
package/web/server.js CHANGED
@@ -37,7 +37,7 @@ import { sources, MODELS } from '../sources.js'
37
37
  import { loadConfig, getApiKey, saveConfig, isProviderEnabled } from '../src/core/config.js'
38
38
  import { getProviderBillingNote, getProviderLabelWithBilling, PROVIDER_METADATA } from '../src/core/provider-metadata.js'
39
39
  import { ensureFavoritesConfig } from '../src/core/favorites.js'
40
- import { ping } from '../src/core/ping.js'
40
+ import { ping, getProviderQuotaPercentCached } from '../src/core/ping.js'
41
41
  import { runProviderKeyTest } from '../src/core/provider-key-tester.js'
42
42
  import { loadChangelog } from '../src/core/changelog-loader.js'
43
43
  import { checkForUpdateDetailed, checkForUpdate, runUpdate, fetchLastReleaseDate } from '../src/core/updater.js'
@@ -231,6 +231,19 @@ async function pingModel(result) {
231
231
  const apiKey = getApiKey(config, result.providerKey) ?? null
232
232
  try {
233
233
  const pingResult = await ping(apiKey, result.modelId, result.providerKey, result.url)
234
+ let quotaPercent = pingResult.quotaPercent
235
+ if ((quotaPercent === null || quotaPercent === undefined) && apiKey) {
236
+ const providerQuota = await getProviderQuotaPercentCached(result.providerKey, apiKey)
237
+ if (typeof providerQuota === 'number' && Number.isFinite(providerQuota)) quotaPercent = providerQuota
238
+ }
239
+ if (typeof quotaPercent === 'number' && Number.isFinite(quotaPercent)) {
240
+ result.usagePercent = quotaPercent
241
+ for (const sibling of results) {
242
+ if (sibling.providerKey === result.providerKey && (sibling.usagePercent === undefined || sibling.usagePercent === null)) {
243
+ sibling.usagePercent = quotaPercent
244
+ }
245
+ }
246
+ }
234
247
  updateHealthFromPing(result, pingResult, !!apiKey)
235
248
  } catch (err) {
236
249
  updateHealthFromPing(result, { code: '000', ms: null, error: err?.message || 'Ping failed' }, !!apiKey)
@@ -295,6 +308,7 @@ function serializeModel(result) {
295
308
  pingHistory: result.pings.slice(-20).map((p) => ({ ms: p.ms, code: p.code })),
296
309
  pingCount: result.pings.length,
297
310
  hasApiKey: !!getApiKey(config, result.providerKey),
311
+ usagePercent: typeof result.usagePercent === 'number' && Number.isFinite(result.usagePercent) ? result.usagePercent : null,
298
312
  inRouterSet,
299
313
  benchmarkKey: key,
300
314
  isBenchmarking: benchmarkRunning.has(key),
@@ -44,6 +44,7 @@ import { sweClass } from '../../utils/ranks.js'
44
44
  // 📖 file the TUI uses; both surfaces share the source of truth for which
45
45
  // 📖 providers are compatible with which tools.
46
46
  import { getCompatibleTools } from '../../../../src/core/tool-metadata.js'
47
+ import { supportsUsagePercent } from '../../../../src/core/quota-capabilities.js'
47
48
  import ExpandedDetailRow from './ExpandedDetailRow.jsx'
48
49
  import styles from './ModelTable.module.css'
49
50
 
@@ -51,7 +52,7 @@ const colHelper = createColumnHelper()
51
52
 
52
53
  const SORTABLE_COLUMN_IDS = new Set([
53
54
  'mood', 'idx', 'tier', 'sweScore', 'ctx', 'label', 'origin', 'latestPing',
54
- 'avg', 'condition', 'verdict', 'stability', 'uptime', 'aiLatency', 'tps', 'trend',
55
+ 'avg', 'condition', 'verdict', 'stability', 'uptime', 'aiLatency', 'tps', 'quota', 'trend',
55
56
  ])
56
57
 
57
58
  // ─── Cell renderers ───────────────────────────────────────────────────────────
@@ -167,6 +168,18 @@ function TPSCellRenderer({ row }) {
167
168
  return <TPSCell result={m.benchmark || null} isRunning={Boolean(m.isBenchmarking)} />
168
169
  }
169
170
 
171
+ function QuotaCellRenderer({ row }) {
172
+ const m = row.original
173
+ if (!m.hasApiKey) return <span className={`${styles.quota} ${styles.quotaNone}`}>-</span>
174
+ if (!supportsUsagePercent(m.providerKey)) return <span className={`${styles.quota} ${styles.quotaNA}`}>N/A</span>
175
+ if (typeof m.usagePercent !== 'number' || !Number.isFinite(m.usagePercent)) {
176
+ return <span className={`${styles.quota} ${styles.quotaNone}`}>…</span>
177
+ }
178
+ const pct = Math.round(m.usagePercent)
179
+ const cls = pct >= 50 ? styles.quotaGood : pct >= 20 ? styles.quotaWarn : styles.quotaBad
180
+ return <span className={`${styles.quota} ${cls}`}>{pct}%</span>
181
+ }
182
+
170
183
  function TrendCellRenderer({ row }) {
171
184
  return <Sparkline history={row.original.pingHistory} />
172
185
  }
@@ -301,6 +314,13 @@ const buildColumns = ({ favorites, onBenchmarkRow, onSelectModel, onLaunch, tool
301
314
  cell: TPSCellRenderer,
302
315
  enableSorting: true,
303
316
  }),
317
+ colHelper.display({
318
+ id: 'quota',
319
+ header: 'Quota',
320
+ size: 64,
321
+ cell: QuotaCellRenderer,
322
+ enableSorting: true,
323
+ }),
304
324
  colHelper.display({
305
325
  id: 'trend',
306
326
  header: 'Trend',
@@ -259,6 +259,17 @@
259
259
 
260
260
  .uptime { font-family: var(--font-mono); font-size: 13px; }
261
261
 
262
+ .quota {
263
+ font-family: var(--font-mono);
264
+ font-size: 13px;
265
+ font-weight: 800;
266
+ }
267
+ .quotaGood { color: #00ff88; }
268
+ .quotaWarn { color: #ffaa00; }
269
+ .quotaBad { color: #ff4444; }
270
+ .quotaNA,
271
+ .quotaNone { color: var(--color-text-dim); }
272
+
262
273
  /* ─── Model cell ─── */
263
274
  .modelCell { display: flex; align-items: center; gap: 6px; overflow: hidden; }
264
275
  .modelMeta { flex: 1; min-width: 0; }
@@ -232,6 +232,8 @@ export function useFilter(models) {
232
232
  cmp = compareNullableNumber(a.benchmark?.ok ? a.benchmark.totalMs : null, b.benchmark?.ok ? b.benchmark.totalMs : null, direction)
233
233
  } else if (sortColumn === 'tps') {
234
234
  cmp = compareNullableNumber(a.benchmark?.ok ? a.benchmark.tokensPerSecond ?? 0 : null, b.benchmark?.ok ? b.benchmark.tokensPerSecond ?? 0 : null, direction)
235
+ } else if (sortColumn === 'quota') {
236
+ cmp = compareNullableNumber(numericOrNull(a.usagePercent), numericOrNull(b.usagePercent), direction)
235
237
  } else if (sortColumn === 'trend') {
236
238
  cmp = compareNullableNumber(trendDelta(a), trendDelta(b), direction)
237
239
  }