free-coding-models 0.1.83 → 0.1.84
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 +6 -17
- package/bin/free-coding-models.js +297 -4754
- package/package.json +2 -2
- package/src/analysis.js +197 -0
- package/src/constants.js +116 -0
- package/src/favorites.js +98 -0
- package/src/key-handler.js +1005 -0
- package/src/openclaw.js +131 -0
- package/src/opencode.js +952 -0
- package/src/overlays.js +840 -0
- package/src/ping.js +186 -0
- package/src/provider-metadata.js +218 -0
- package/src/quota-capabilities.js +112 -0
- package/src/render-helpers.js +239 -0
- package/src/render-table.js +567 -0
- package/src/setup.js +105 -0
- package/src/telemetry.js +382 -0
- package/src/tier-colors.js +37 -0
- package/{lib → src}/token-stats.js +71 -3
- package/src/token-usage-reader.js +63 -0
- package/src/updater.js +237 -0
- package/{lib → src}/usage-reader.js +63 -21
- package/lib/quota-capabilities.js +0 -79
- /package/{lib → src}/account-manager.js +0 -0
- /package/{lib → src}/config.js +0 -0
- /package/{lib → src}/error-classifier.js +0 -0
- /package/{lib → src}/log-reader.js +0 -0
- /package/{lib → src}/model-merger.js +0 -0
- /package/{lib → src}/opencode-sync.js +0 -0
- /package/{lib → src}/provider-quota-fetchers.js +0 -0
- /package/{lib → src}/proxy-server.js +0 -0
- /package/{lib → src}/request-transformer.js +0 -0
- /package/{lib → src}/utils.js +0 -0
package/src/ping.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ping.js
|
|
3
|
+
* @description HTTP ping infrastructure for model availability and latency measurement.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* This module provides functions for sending ping requests to model providers,
|
|
7
|
+
* extracting quota information from response headers, and managing provider
|
|
8
|
+
* endpoint quota polling with caching.
|
|
9
|
+
*
|
|
10
|
+
* 🎯 Key features:
|
|
11
|
+
* - Provider-specific request building (handles Replicate, Cloudflare, OpenRouter)
|
|
12
|
+
* - Async ping with timeout and abort controller
|
|
13
|
+
* - Quota extraction from rate limit headers (multiple variants supported)
|
|
14
|
+
* - Cached provider quota polling with TTL and error backoff
|
|
15
|
+
* - Cloudflare account ID resolution from environment
|
|
16
|
+
*
|
|
17
|
+
* → Functions:
|
|
18
|
+
* - `resolveCloudflareUrl`: Resolve {account_id} placeholder from CLOUDFLARE_ACCOUNT_ID env var
|
|
19
|
+
* - `buildPingRequest`: Build provider-specific HTTP request for pinging
|
|
20
|
+
* - `ping`: Send async ping request with timeout; returns { code, ms, quotaPercent }
|
|
21
|
+
* - `getHeaderValue`: Helper to extract header value from Headers object or plain object
|
|
22
|
+
* - `extractQuotaPercent`: Parse rate limit headers to calculate remaining quota percentage
|
|
23
|
+
* - `fetchProviderQuotaPercent`: Fetch quota for a provider from dedicated endpoint/headers
|
|
24
|
+
* - `getProviderQuotaPercentCached`: Wrapper for cached provider quota fetching
|
|
25
|
+
* - `usagePlaceholderForProvider`: Return display token for Usage column based on provider behavior
|
|
26
|
+
*
|
|
27
|
+
* 📦 Dependencies:
|
|
28
|
+
* - ../src/constants.js: PING_TIMEOUT
|
|
29
|
+
* - ../src/provider-quota-fetchers.js: _fetchProviderQuotaFromModule (quota fetching with cache)
|
|
30
|
+
* - ../src/quota-capabilities.js: supportsUsagePercent
|
|
31
|
+
*
|
|
32
|
+
* ⚙️ Configuration:
|
|
33
|
+
* - PING_TIMEOUT: Timeout in ms for ping requests (default: 15000)
|
|
34
|
+
* - CLOUDFLARE_ACCOUNT_ID: Env var for Cloudflare Workers AI account ID
|
|
35
|
+
*
|
|
36
|
+
* @see {@link ../src/provider-quota-fetchers.js} Quota fetching implementation
|
|
37
|
+
* @see {@link ../src/quota-capabilities.js} Quota telemetry + Usage behavior detection
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { PING_TIMEOUT } from './constants.js'
|
|
41
|
+
import { fetchProviderQuota as _fetchProviderQuotaFromModule } from './provider-quota-fetchers.js'
|
|
42
|
+
import { supportsUsagePercent } from './quota-capabilities.js'
|
|
43
|
+
|
|
44
|
+
// 📖 resolveCloudflareUrl: Cloudflare's OpenAI-compatible endpoint is account-scoped.
|
|
45
|
+
// 📖 We resolve {account_id} from env so provider setup can stay simple in config.
|
|
46
|
+
export function resolveCloudflareUrl(url) {
|
|
47
|
+
const accountId = (process.env.CLOUDFLARE_ACCOUNT_ID || '').trim()
|
|
48
|
+
if (!url.includes('{account_id}')) return url
|
|
49
|
+
if (!accountId) return url.replace('{account_id}', 'missing-account-id')
|
|
50
|
+
return url.replace('{account_id}', encodeURIComponent(accountId))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 📖 buildPingRequest: Build provider-specific ping request.
|
|
54
|
+
// 📖 Handles Replicate's /v1/predictions format, Cloudflare's account_id in URL,
|
|
55
|
+
// 📖 and standard OpenAI-compliant chat completions with provider-specific headers.
|
|
56
|
+
export function buildPingRequest(apiKey, modelId, providerKey, url) {
|
|
57
|
+
// 📖 ZAI models are stored as "zai/glm-..." in sources.js but the API expects just "glm-..."
|
|
58
|
+
const apiModelId = providerKey === 'zai' ? modelId.replace(/^zai\//, '') : modelId
|
|
59
|
+
|
|
60
|
+
if (providerKey === 'replicate') {
|
|
61
|
+
// 📖 Replicate uses /v1/predictions with a different payload than OpenAI chat-completions.
|
|
62
|
+
const replicateHeaders = { 'Content-Type': 'application/json', Prefer: 'wait=4' }
|
|
63
|
+
if (apiKey) replicateHeaders.Authorization = `Token ${apiKey}`
|
|
64
|
+
return {
|
|
65
|
+
url,
|
|
66
|
+
headers: replicateHeaders,
|
|
67
|
+
body: { version: modelId, input: { prompt: 'hi' } },
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (providerKey === 'cloudflare') {
|
|
72
|
+
// 📖 Cloudflare Workers AI uses OpenAI-compatible payload but needs account_id in URL.
|
|
73
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
74
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
75
|
+
return {
|
|
76
|
+
url: resolveCloudflareUrl(url),
|
|
77
|
+
headers,
|
|
78
|
+
body: { model: apiModelId, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
83
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
84
|
+
if (providerKey === 'openrouter') {
|
|
85
|
+
// 📖 OpenRouter recommends optional app identification headers.
|
|
86
|
+
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
87
|
+
headers['X-Title'] = 'free-coding-models'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
url,
|
|
92
|
+
headers,
|
|
93
|
+
body: { model: apiModelId, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 📖 ping: Send a single chat completion request to measure model availability and latency.
|
|
98
|
+
// 📖 providerKey and url determine provider-specific request format.
|
|
99
|
+
// 📖 apiKey can be null — in that case no Authorization header is sent.
|
|
100
|
+
// 📖 A 401 response still tells us the server is UP and gives us real latency.
|
|
101
|
+
// 📖 Returns { code, ms, quotaPercent }
|
|
102
|
+
export async function ping(apiKey, modelId, providerKey, url) {
|
|
103
|
+
const ctrl = new AbortController()
|
|
104
|
+
const timer = setTimeout(() => ctrl.abort(), PING_TIMEOUT)
|
|
105
|
+
const t0 = performance.now()
|
|
106
|
+
try {
|
|
107
|
+
const req = buildPingRequest(apiKey, modelId, providerKey, url)
|
|
108
|
+
const resp = await fetch(req.url, {
|
|
109
|
+
method: 'POST', signal: ctrl.signal,
|
|
110
|
+
headers: req.headers,
|
|
111
|
+
body: JSON.stringify(req.body),
|
|
112
|
+
})
|
|
113
|
+
// 📖 Normalize all HTTP 2xx statuses to "200" so existing verdict/avg logic still works.
|
|
114
|
+
const code = resp.status >= 200 && resp.status < 300 ? '200' : String(resp.status)
|
|
115
|
+
return {
|
|
116
|
+
code,
|
|
117
|
+
ms: Math.round(performance.now() - t0),
|
|
118
|
+
quotaPercent: extractQuotaPercent(resp.headers),
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const isTimeout = err.name === 'AbortError'
|
|
122
|
+
return {
|
|
123
|
+
code: isTimeout ? '000' : 'ERR',
|
|
124
|
+
ms: isTimeout ? 'TIMEOUT' : Math.round(performance.now() - t0),
|
|
125
|
+
quotaPercent: null,
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timer)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 📖 getHeaderValue: Helper to extract header value from Headers object or plain object.
|
|
133
|
+
// 📖 Returns null if headers is null or key is not found.
|
|
134
|
+
function getHeaderValue(headers, key) {
|
|
135
|
+
if (!headers) return null
|
|
136
|
+
if (typeof headers.get === 'function') return headers.get(key)
|
|
137
|
+
return headers[key] ?? headers[key.toLowerCase()] ?? null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 📖 extractQuotaPercent: Parse rate limit headers to calculate remaining quota percentage.
|
|
141
|
+
// 📖 Checks multiple header variants (x-ratelimit-*, ratelimit-*, etc.).
|
|
142
|
+
// 📖 Returns value clamped 0–100, or null if quota headers not present.
|
|
143
|
+
export function extractQuotaPercent(headers) {
|
|
144
|
+
const variants = [
|
|
145
|
+
['x-ratelimit-remaining', 'x-ratelimit-limit'],
|
|
146
|
+
['x-ratelimit-remaining-requests', 'x-ratelimit-limit-requests'],
|
|
147
|
+
['ratelimit-remaining', 'ratelimit-limit'],
|
|
148
|
+
['ratelimit-remaining-requests', 'ratelimit-limit-requests'],
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
for (const [remainingKey, limitKey] of variants) {
|
|
152
|
+
const remainingRaw = getHeaderValue(headers, remainingKey)
|
|
153
|
+
const limitRaw = getHeaderValue(headers, limitKey)
|
|
154
|
+
const remaining = parseFloat(remainingRaw)
|
|
155
|
+
const limit = parseFloat(limitRaw)
|
|
156
|
+
if (Number.isFinite(remaining) && Number.isFinite(limit) && limit > 0) {
|
|
157
|
+
const pct = Math.round((remaining / limit) * 100)
|
|
158
|
+
return Math.max(0, Math.min(100, pct))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Provider endpoint quota polling ─────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
// 📖 fetchProviderQuotaPercent: Fetch quota for a provider from dedicated endpoint/headers.
|
|
168
|
+
// 📖 Delegates to unified module entrypoint (handles openrouter + siliconflow + others).
|
|
169
|
+
// 📖 The module implements TTL cache and error backoff internally.
|
|
170
|
+
export async function fetchProviderQuotaPercent(providerKey, apiKey) {
|
|
171
|
+
return _fetchProviderQuotaFromModule(providerKey, apiKey)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 📖 getProviderQuotaPercentCached: Wrapper for cached provider quota fetching.
|
|
175
|
+
// 📖 The module already implements TTL cache and error backoff internally.
|
|
176
|
+
// 📖 This wrapper preserves the existing call-site API from bin/free-coding-models.js.
|
|
177
|
+
export async function getProviderQuotaPercentCached(providerKey, apiKey) {
|
|
178
|
+
return fetchProviderQuotaPercent(providerKey, apiKey)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 📖 usagePlaceholderForProvider: Return display token for Usage column.
|
|
182
|
+
// 📖 '--' means this provider can expose a real remaining percentage once telemetry arrives.
|
|
183
|
+
// 📖 '🟢' means the provider is usable, but a live remaining % is not applicable/reliable.
|
|
184
|
+
export function usagePlaceholderForProvider(providerKey) {
|
|
185
|
+
return supportsUsagePercent(providerKey) ? '--' : '🟢'
|
|
186
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file provider-metadata.js
|
|
3
|
+
* @description Provider metadata, environment variable names, and OpenCode model ID mapping.
|
|
4
|
+
* Extracted from bin/free-coding-models.js to allow shared access by setup wizard,
|
|
5
|
+
* Settings overlay, and OpenCode integration helpers.
|
|
6
|
+
*
|
|
7
|
+
* @details
|
|
8
|
+
* This module owns three separate concerns that all relate to "knowing about providers":
|
|
9
|
+
*
|
|
10
|
+
* 1. `PROVIDER_METADATA` — human-readable display info (label, colour, signup URL, rate limits)
|
|
11
|
+
* used in the setup wizard (`promptApiKey`) and the Settings overlay.
|
|
12
|
+
*
|
|
13
|
+
* 2. `ENV_VAR_NAMES` — maps providerKey → the environment variable name that carries the API key.
|
|
14
|
+
* Used when spawning OpenCode child processes so that keys stored only in
|
|
15
|
+
* ~/.free-coding-models.json are also visible to the child via `{env:VAR}` references.
|
|
16
|
+
*
|
|
17
|
+
* 3. `OPENCODE_MODEL_MAP` — sparse mapping of source model IDs to OpenCode built-in model IDs
|
|
18
|
+
* (only entries where the IDs differ need to be listed). Groq's API aliases short names
|
|
19
|
+
* to full names but OpenCode does exact ID matching against its built-in model list.
|
|
20
|
+
*
|
|
21
|
+
* Platform booleans (`isWindows`, `isMac`, `isLinux`) are also exported here so that
|
|
22
|
+
* OpenCode Desktop launch logic and auto-update can share them without re-reading `process.platform`.
|
|
23
|
+
*
|
|
24
|
+
* @exports
|
|
25
|
+
* PROVIDER_METADATA, ENV_VAR_NAMES, OPENCODE_MODEL_MAP,
|
|
26
|
+
* isWindows, isMac, isLinux
|
|
27
|
+
*
|
|
28
|
+
* @see bin/free-coding-models.js — consumes all exports from this module
|
|
29
|
+
* @see src/config.js — resolveApiKeys / getApiKey use ENV_VAR_NAMES indirectly
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import chalk from 'chalk'
|
|
33
|
+
|
|
34
|
+
// 📖 Platform detection — used by Desktop launcher and auto-update to pick the right open/start command.
|
|
35
|
+
export const isWindows = process.platform === 'win32'
|
|
36
|
+
export const isMac = process.platform === 'darwin'
|
|
37
|
+
export const isLinux = process.platform === 'linux'
|
|
38
|
+
|
|
39
|
+
// 📖 ENV_VAR_NAMES: maps providerKey → shell env var name for passing resolved keys to child processes.
|
|
40
|
+
// 📖 When a key is stored only in ~/.free-coding-models.json (not in the shell env), we inject it
|
|
41
|
+
// 📖 into the child's env so OpenCode's {env:VAR} references still resolve.
|
|
42
|
+
export const ENV_VAR_NAMES = {
|
|
43
|
+
nvidia: 'NVIDIA_API_KEY',
|
|
44
|
+
groq: 'GROQ_API_KEY',
|
|
45
|
+
cerebras: 'CEREBRAS_API_KEY',
|
|
46
|
+
sambanova: 'SAMBANOVA_API_KEY',
|
|
47
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
48
|
+
huggingface:'HUGGINGFACE_API_KEY',
|
|
49
|
+
replicate: 'REPLICATE_API_TOKEN',
|
|
50
|
+
deepinfra: 'DEEPINFRA_API_KEY',
|
|
51
|
+
fireworks: 'FIREWORKS_API_KEY',
|
|
52
|
+
codestral: 'CODESTRAL_API_KEY',
|
|
53
|
+
hyperbolic: 'HYPERBOLIC_API_KEY',
|
|
54
|
+
scaleway: 'SCALEWAY_API_KEY',
|
|
55
|
+
googleai: 'GOOGLE_API_KEY',
|
|
56
|
+
siliconflow:'SILICONFLOW_API_KEY',
|
|
57
|
+
together: 'TOGETHER_API_KEY',
|
|
58
|
+
cloudflare: 'CLOUDFLARE_API_TOKEN',
|
|
59
|
+
perplexity: 'PERPLEXITY_API_KEY',
|
|
60
|
+
zai: 'ZAI_API_KEY',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 📖 OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
|
|
64
|
+
// 📖 built-in model registry. Only add entries where they DIFFER — unmapped models pass through as-is.
|
|
65
|
+
export const OPENCODE_MODEL_MAP = {
|
|
66
|
+
groq: {
|
|
67
|
+
'moonshotai/kimi-k2-instruct': 'moonshotai/kimi-k2-instruct-0905',
|
|
68
|
+
'meta-llama/llama-4-scout-17b-16e-preview': 'meta-llama/llama-4-scout-17b-16e-instruct',
|
|
69
|
+
'meta-llama/llama-4-maverick-17b-128e-preview': 'meta-llama/llama-4-maverick-17b-128e-instruct',
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 📖 PROVIDER_METADATA: display info for each provider, used in setup wizard and Settings panel.
|
|
74
|
+
// 📖 `color` is a chalk function for visual distinction in the TUI.
|
|
75
|
+
// 📖 `signupUrl` / `signupHint` guide users through first-time key generation.
|
|
76
|
+
// 📖 `rateLimits` gives a quick reminder of the free-tier quota without opening a browser.
|
|
77
|
+
export const PROVIDER_METADATA = {
|
|
78
|
+
nvidia: {
|
|
79
|
+
label: 'NVIDIA NIM',
|
|
80
|
+
color: chalk.rgb(118, 185, 0),
|
|
81
|
+
signupUrl: 'https://build.nvidia.com',
|
|
82
|
+
signupHint: 'Profile → API Keys → Generate',
|
|
83
|
+
rateLimits: 'Free tier (provider quota by model)',
|
|
84
|
+
},
|
|
85
|
+
groq: {
|
|
86
|
+
label: 'Groq',
|
|
87
|
+
color: chalk.rgb(249, 103, 20),
|
|
88
|
+
signupUrl: 'https://console.groq.com/keys',
|
|
89
|
+
signupHint: 'API Keys → Create API Key',
|
|
90
|
+
rateLimits: 'Free dev tier (provider quota)',
|
|
91
|
+
},
|
|
92
|
+
cerebras: {
|
|
93
|
+
label: 'Cerebras',
|
|
94
|
+
color: chalk.rgb(0, 180, 255),
|
|
95
|
+
signupUrl: 'https://cloud.cerebras.ai',
|
|
96
|
+
signupHint: 'API Keys → Create',
|
|
97
|
+
rateLimits: 'Free dev tier (provider quota)',
|
|
98
|
+
},
|
|
99
|
+
sambanova: {
|
|
100
|
+
label: 'SambaNova',
|
|
101
|
+
color: chalk.rgb(255, 165, 0),
|
|
102
|
+
signupUrl: 'https://cloud.sambanova.ai/apis',
|
|
103
|
+
signupHint: 'SambaCloud portal → Create API key',
|
|
104
|
+
rateLimits: 'Dev tier generous quota',
|
|
105
|
+
},
|
|
106
|
+
openrouter: {
|
|
107
|
+
label: 'OpenRouter',
|
|
108
|
+
color: chalk.rgb(120, 80, 255),
|
|
109
|
+
signupUrl: 'https://openrouter.ai/keys',
|
|
110
|
+
signupHint: 'API Keys → Create',
|
|
111
|
+
rateLimits: '50 req/day, 20/min (:free shared quota)',
|
|
112
|
+
},
|
|
113
|
+
huggingface: {
|
|
114
|
+
label: 'Hugging Face Inference',
|
|
115
|
+
color: chalk.rgb(255, 182, 0),
|
|
116
|
+
signupUrl: 'https://huggingface.co/settings/tokens',
|
|
117
|
+
signupHint: 'Settings → Access Tokens',
|
|
118
|
+
rateLimits: 'Free monthly credits (~$0.10)',
|
|
119
|
+
},
|
|
120
|
+
replicate: {
|
|
121
|
+
label: 'Replicate',
|
|
122
|
+
color: chalk.rgb(120, 160, 255),
|
|
123
|
+
signupUrl: 'https://replicate.com/account/api-tokens',
|
|
124
|
+
signupHint: 'Account → API Tokens',
|
|
125
|
+
rateLimits: 'Developer free quota',
|
|
126
|
+
},
|
|
127
|
+
deepinfra: {
|
|
128
|
+
label: 'DeepInfra',
|
|
129
|
+
color: chalk.rgb(0, 180, 140),
|
|
130
|
+
signupUrl: 'https://deepinfra.com/login',
|
|
131
|
+
signupHint: 'Login → API keys',
|
|
132
|
+
rateLimits: 'Free dev tier (low-latency quota)',
|
|
133
|
+
},
|
|
134
|
+
fireworks: {
|
|
135
|
+
label: 'Fireworks AI',
|
|
136
|
+
color: chalk.rgb(255, 80, 50),
|
|
137
|
+
signupUrl: 'https://fireworks.ai',
|
|
138
|
+
signupHint: 'Create account → Generate API key',
|
|
139
|
+
rateLimits: '$1 free credits (new dev accounts)',
|
|
140
|
+
},
|
|
141
|
+
codestral: {
|
|
142
|
+
label: 'Mistral Codestral',
|
|
143
|
+
color: chalk.rgb(255, 100, 100),
|
|
144
|
+
signupUrl: 'https://codestral.mistral.ai',
|
|
145
|
+
signupHint: 'API Keys → Create',
|
|
146
|
+
rateLimits: '30 req/min, 2000/day',
|
|
147
|
+
},
|
|
148
|
+
hyperbolic: {
|
|
149
|
+
label: 'Hyperbolic',
|
|
150
|
+
color: chalk.rgb(0, 200, 150),
|
|
151
|
+
signupUrl: 'https://app.hyperbolic.ai/settings',
|
|
152
|
+
signupHint: 'Settings → API Keys',
|
|
153
|
+
rateLimits: '$1 free trial credits',
|
|
154
|
+
},
|
|
155
|
+
scaleway: {
|
|
156
|
+
label: 'Scaleway',
|
|
157
|
+
color: chalk.rgb(130, 0, 250),
|
|
158
|
+
signupUrl: 'https://console.scaleway.com/iam/api-keys',
|
|
159
|
+
signupHint: 'IAM → API Keys',
|
|
160
|
+
rateLimits: '1M free tokens',
|
|
161
|
+
},
|
|
162
|
+
googleai: {
|
|
163
|
+
label: 'Google AI Studio',
|
|
164
|
+
color: chalk.rgb(66, 133, 244),
|
|
165
|
+
signupUrl: 'https://aistudio.google.com/apikey',
|
|
166
|
+
signupHint: 'Get API key',
|
|
167
|
+
rateLimits: '14.4K req/day, 30/min',
|
|
168
|
+
},
|
|
169
|
+
siliconflow: {
|
|
170
|
+
label: 'SiliconFlow',
|
|
171
|
+
color: chalk.rgb(255, 120, 30),
|
|
172
|
+
signupUrl: 'https://cloud.siliconflow.cn/account/ak',
|
|
173
|
+
signupHint: 'API Keys → Create',
|
|
174
|
+
rateLimits: 'Free models: usually 100 RPM, varies by model',
|
|
175
|
+
},
|
|
176
|
+
together: {
|
|
177
|
+
label: 'Together AI',
|
|
178
|
+
color: chalk.rgb(0, 180, 255),
|
|
179
|
+
signupUrl: 'https://api.together.ai/settings/api-keys',
|
|
180
|
+
signupHint: 'Settings → API keys',
|
|
181
|
+
rateLimits: 'Credits/promos vary by account (check console)',
|
|
182
|
+
},
|
|
183
|
+
cloudflare: {
|
|
184
|
+
label: 'Cloudflare Workers AI',
|
|
185
|
+
color: chalk.rgb(242, 119, 36),
|
|
186
|
+
signupUrl: 'https://dash.cloudflare.com',
|
|
187
|
+
signupHint: 'Create AI API token + set CLOUDFLARE_ACCOUNT_ID',
|
|
188
|
+
rateLimits: 'Free: 10k neurons/day, text-gen 300 RPM',
|
|
189
|
+
},
|
|
190
|
+
perplexity: {
|
|
191
|
+
label: 'Perplexity API',
|
|
192
|
+
color: chalk.rgb(0, 210, 190),
|
|
193
|
+
signupUrl: 'https://www.perplexity.ai/settings/api',
|
|
194
|
+
signupHint: 'Generate API key (billing may be required)',
|
|
195
|
+
rateLimits: 'Tiered limits by spend (default ~50 RPM)',
|
|
196
|
+
},
|
|
197
|
+
qwen: {
|
|
198
|
+
label: 'Alibaba Cloud (DashScope)',
|
|
199
|
+
color: chalk.rgb(255, 140, 0),
|
|
200
|
+
signupUrl: 'https://modelstudio.console.alibabacloud.com',
|
|
201
|
+
signupHint: 'Model Studio → API Key → Create (1M free tokens, 90 days)',
|
|
202
|
+
rateLimits: '1M free tokens per model (Singapore region, 90 days)',
|
|
203
|
+
},
|
|
204
|
+
zai: {
|
|
205
|
+
label: 'ZAI (z.ai)',
|
|
206
|
+
color: chalk.rgb(0, 150, 255),
|
|
207
|
+
signupUrl: 'https://z.ai',
|
|
208
|
+
signupHint: 'Sign up and generate an API key',
|
|
209
|
+
rateLimits: 'Free tier (generous quota)',
|
|
210
|
+
},
|
|
211
|
+
iflow: {
|
|
212
|
+
label: 'iFlow',
|
|
213
|
+
color: chalk.rgb(100, 200, 255),
|
|
214
|
+
signupUrl: 'https://platform.iflow.cn',
|
|
215
|
+
signupHint: 'Register → Personal Information → Generate API Key (7-day expiry)',
|
|
216
|
+
rateLimits: 'Free for individuals (no request limits)',
|
|
217
|
+
},
|
|
218
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lib/quota-capabilities.js
|
|
3
|
+
* @description Provider quota telemetry and Usage-column behavior map.
|
|
4
|
+
*
|
|
5
|
+
* Describes how we can observe quota state for each provider:
|
|
6
|
+
* - header: Provider sends x-ratelimit-remaining / x-ratelimit-limit headers
|
|
7
|
+
* - endpoint: Provider has a dedicated usage/quota REST endpoint we can poll
|
|
8
|
+
* - unknown: No reliable quota signal available
|
|
9
|
+
*
|
|
10
|
+
* The TUI needs an extra distinction beyond telemetry transport:
|
|
11
|
+
* - `usageDisplay: 'percent'` means we can show a trustworthy remaining %.
|
|
12
|
+
* - `usageDisplay: 'ok'` means Usage is not meaningfully measurable as a live %,
|
|
13
|
+
* so the table shows a green status dot instead of a misleading number.
|
|
14
|
+
*
|
|
15
|
+
* `resetCadence` tells the reader when a stored snapshot should be invalidated
|
|
16
|
+
* even if it is still within the generic freshness TTL.
|
|
17
|
+
*
|
|
18
|
+
* supportsEndpoint (optional, for openrouter/siliconflow):
|
|
19
|
+
* true — provider has a known usage endpoint
|
|
20
|
+
* false — no endpoint, header-only or unknown
|
|
21
|
+
*
|
|
22
|
+
* @exports PROVIDER_CAPABILITIES — full map keyed by providerKey (matches sources.js)
|
|
23
|
+
* @exports getQuotaTelemetry(providerKey) — returns capability object (defaults to unknown)
|
|
24
|
+
* @exports isKnownQuotaTelemetry(providerKey) — true when telemetryType !== 'unknown'
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} ProviderCapability
|
|
29
|
+
* @property {'header'|'endpoint'|'unknown'} telemetryType
|
|
30
|
+
* @property {boolean} [supportsEndpoint]
|
|
31
|
+
* @property {'percent'|'ok'} usageDisplay
|
|
32
|
+
* @property {'rolling'|'daily'|'unknown'|'none'} resetCadence
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @type {Record<string, ProviderCapability>} */
|
|
36
|
+
export const PROVIDER_CAPABILITIES = {
|
|
37
|
+
// Providers that return x-ratelimit-remaining / x-ratelimit-limit headers
|
|
38
|
+
nvidia: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
39
|
+
groq: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
|
|
40
|
+
cerebras: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
41
|
+
sambanova: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
42
|
+
deepinfra: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
43
|
+
fireworks: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
44
|
+
together: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
45
|
+
hyperbolic: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
46
|
+
scaleway: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
47
|
+
googleai: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
|
|
48
|
+
codestral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
|
|
49
|
+
perplexity: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
50
|
+
qwen: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
51
|
+
|
|
52
|
+
// Providers that have a dedicated usage/credits endpoint
|
|
53
|
+
openrouter: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
54
|
+
siliconflow: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'ok', resetCadence: 'unknown' },
|
|
55
|
+
|
|
56
|
+
// Providers with no reliable quota signal
|
|
57
|
+
huggingface: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
58
|
+
replicate: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
59
|
+
cloudflare: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
|
|
60
|
+
zai: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
61
|
+
iflow: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Fallback for unrecognized providers */
|
|
65
|
+
const UNKNOWN_CAPABILITY = { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get quota telemetry capability for a provider.
|
|
69
|
+
* Returns `{ telemetryType: 'unknown', supportsEndpoint: false }` for unrecognized providers.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} providerKey - Provider key matching sources.js (e.g. 'groq', 'openrouter')
|
|
72
|
+
* @returns {ProviderCapability}
|
|
73
|
+
*/
|
|
74
|
+
export function getQuotaTelemetry(providerKey) {
|
|
75
|
+
return PROVIDER_CAPABILITIES[providerKey] ?? UNKNOWN_CAPABILITY
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns true when we have a reliable quota telemetry signal for this provider
|
|
80
|
+
* (either via response headers or a dedicated endpoint).
|
|
81
|
+
*
|
|
82
|
+
* Returns false for 'unknown' providers where quota state must be inferred.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} providerKey
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
export function isKnownQuotaTelemetry(providerKey) {
|
|
88
|
+
return getQuotaTelemetry(providerKey).telemetryType !== 'unknown'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns true when the Usage column can show a real remaining percentage for
|
|
93
|
+
* the given provider.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} providerKey
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
export function supportsUsagePercent(providerKey) {
|
|
99
|
+
return getQuotaTelemetry(providerKey).usageDisplay === 'percent'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns true when the provider's quota commonly resets on a daily cadence.
|
|
104
|
+
* This lets the usage reader invalidate yesterday's snapshots immediately
|
|
105
|
+
* after midnight instead of waiting for the generic TTL window to expire.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} providerKey
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
export function usageResetsDaily(providerKey) {
|
|
111
|
+
return getQuotaTelemetry(providerKey).resetCadence === 'daily'
|
|
112
|
+
}
|