free-coding-models 0.5.96 โ†’ 0.5.97

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.
@@ -23,8 +23,14 @@
23
23
  * "scaleway": "scw-xxx",
24
24
  * "googleai": "AIza...",
25
25
  * "cloudflare": "cf-xxx",
26
- * "zai": "zai-xxx"
26
+ * "zai": "zai-xxx",
27
+ * "onomeo": "${ONOMEO_API_KEY}"
27
28
  * },
29
+ *
30
+ * ๐Ÿ“– ${VAR} values (issue #196): an apiKeys entry of the form "${MY_VAR}" is
31
+ * ๐Ÿ“– resolved from the environment at read time. The file on disk keeps the
32
+ * ๐Ÿ“– placeholder so secrets never land on disk; saveConfig never writes the
33
+ * ๐Ÿ“– resolved value back. See getApiKey() and resolveEnvPlaceholderValue().
28
34
  * "providers": {
29
35
  * "nvidia": { "enabled": true },
30
36
  * "groq": { "enabled": true },
@@ -106,6 +112,7 @@ import { homedir } from 'node:os'
106
112
  import { join, resolve, dirname } from 'node:path'
107
113
  import { syncShellEnv } from './shell-env.js'
108
114
  import { DEFAULT_LOCALE, normalizeLocale } from './i18n/index.js'
115
+ import { resolveEnvPlaceholderValue } from './utils.js'
109
116
 
110
117
  // ๐Ÿ“– Expand leading ~/ or bare ~ to homedir so --config-dir ~/.config/... works.
111
118
  // ๐Ÿ“– resolve() alone does NOT expand tilde, it would treat ~ as a literal folder.
@@ -1157,12 +1164,14 @@ export function getApiKey(config, providerKey) {
1157
1164
  // ๐Ÿ“– but every consumer (ping Authorization header, tool env vars, daemon) needs
1158
1165
  // ๐Ÿ“– a single string: an array would stringify to "key1,key2" and get a
1159
1166
  // ๐Ÿ“– guaranteed 401. Return the first non-empty entry instead.
1167
+ // ๐Ÿ“– ${VAR} references (issue #196) are expanded here so the file on disk
1168
+ // ๐Ÿ“– can stay secret-free while every consumer gets the real value.
1160
1169
  const key = config?.apiKeys?.[providerKey]
1161
1170
  if (Array.isArray(key)) {
1162
1171
  const firstUsable = key.find(k => typeof k === 'string' && k.length > 0)
1163
- return firstUsable ?? null
1172
+ return firstUsable !== undefined ? resolveEnvPlaceholderValue(firstUsable) : null
1164
1173
  }
1165
- if (key) return key
1174
+ if (key) return resolveEnvPlaceholderValue(key)
1166
1175
 
1167
1176
  return null
1168
1177
  }
@@ -348,8 +348,8 @@ export const PROVIDER_METADATA = {
348
348
  color: chalk.rgb(255, 138, 64),
349
349
  signupUrl: 'https://www.orcarouter.ai',
350
350
  signupHint: 'Register (GitHub OAuth, no credit card) โ†’ API keys',
351
- rateLimits: 'Free Hacker tier ยท zero token markup ยท 3 API keys',
352
- detailedLimits: 'Zero-markup AI gateway: token prices are passed through at provider rates.\nFree Hacker plan: 3 API keys, adaptive routing + automatic failover + guardrails included.\nOnly the explicit $-0 models are listed in this catalog; the orcarouter/fusion family is pay-as-you-go.',
351
+ rateLimits: 'Free ยท 10 req/min ยท 50 req/day ยท zero token markup',
352
+ detailedLimits: 'Zero-markup AI gateway: token prices are passed through at provider rates.\nFree tier: 10 req/min, 50 req/day, GitHub account required, hidden per-request prompt cap.\nOnly the explicit $-0 models are listed in this catalog (verified against /api/free-package/public, 2026-09-29). The orcarouter/fusion family bills the sum of its paid panel legs, it is not free.',
353
353
  },
354
354
  'vercel-gateway': {
355
355
  label: 'Vercel AI Gateway',
@@ -364,8 +364,8 @@ export const PROVIDER_METADATA = {
364
364
  color: chalk.rgb(240, 120, 60),
365
365
  signupUrl: 'https://onomeo.com/dashboard',
366
366
  signupHint: 'Sign in (email, Google or GitHub, no card) โ†’ check in daily โ†’ Dashboard โ†’ API key',
367
- rateLimits: 'Daily check-in credits (50,000-200,000/day) ยท 12 req/min per key ยท 60 req/5h per account',
368
- detailedLimits: 'Public beta: not every feature is guaranteed to work; feedback is welcome at https://onomeo.com/feedback.\nCredits come from a daily check-in: 50,000 on day 1, rising to 200,000/day from day 7 of a streak. No card needed.\nEvery call spends credits and the cost per reply varies by model (see https://onomeo.com/models), so this is a small allowance. Each account that has not paid can spend up to 50,000 credits/day on premium models. Optional: $5 one-time buys 1,000,000 credits.\nLimits: 12 req/min per key, 60 req per 5 hours per account, 120 per 5 hours per IP, 450 per 5 hours site-wide for accounts that have not bought credits. Health probes count against them.\nSome upstreams may train on prompts; each model page on onomeo.com says which.',
367
+ rateLimits: 'Free models, no credits ยท 12 req/min per key ยท 60 req/5h per account',
368
+ detailedLimits: 'Public beta: not every feature is guaranteed to work; feedback is welcome at https://onomeo.com/feedback.\nThe listed models are free models: they spend no credits and are limited by calls. No card needed.\nLimits: 12 req/min per key, 60 calls per 5 hours per account (a long request counts as several), 120 per 5 hours per IP, 450 per 5 hours shared site-wide by all accounts. Health probes count against them.\nPremium models spend credits from a daily check-in (20,000 on day 1, rising to 50,000/day from day 7 of a streak); each account that has not paid can spend up to 50,000 credits/day on them. Optional: $5/month buys 3,000,000 credits a month.\n31 of the 47 models may train on prompts; each model page on onomeo.com says which.',
369
369
  },
370
370
  'ollama-cloud': {
371
371
  label: 'Ollama Cloud',
@@ -310,7 +310,10 @@ export function isPrivateNetworkHostname(hostname) {
310
310
  }
311
311
 
312
312
  // ๐Ÿ“– Cache the FCM_ALLOWED_ORIGINS env var split into a Set on first access.
313
- // ๐Ÿ“– Format: comma-separated origin URLs, e.g. "http://mybox:19280,http://10.0.0.5:19280"
313
+ // ๐Ÿ“– Format: comma-separated origin URLs, e.g. "http://mybox:19280,http://10.0.0.5:19280".
314
+ // ๐Ÿ“– The special entry "*" allowlists every origin: the user explicitly opted
315
+ // ๐Ÿ“– out of origin checking, which is handy for LAN/Docker setups where the
316
+ // ๐Ÿ“– dashboard hostname changes (mDNS names, DHCP IPs).
314
317
  let _allowedOriginsCache = null
315
318
  function getAllowedOrigins() {
316
319
  if (_allowedOriginsCache === null) {
@@ -323,6 +326,16 @@ function getAllowedOrigins() {
323
326
  return _allowedOriginsCache
324
327
  }
325
328
 
329
+ function isWildcardOriginsEnabled() {
330
+ return getAllowedOrigins().includes('*')
331
+ }
332
+
333
+ // ๐Ÿ“– Test-only: drop the cached FCM_ALLOWED_ORIGINS list so a test that
334
+ // ๐Ÿ“– mutates process.env sees fresh parsing instead of the first-read value.
335
+ export function resetAllowedOriginsCacheForTest() {
336
+ _allowedOriginsCache = null
337
+ }
338
+
326
339
  // ๐Ÿ“– v2: CORS for loopback (and explicitly allowed) origins so a browser
327
340
  // dashboard served from another local port (web dashboard 3333) can call
328
341
  // this daemon directly without a proxy.
@@ -335,7 +348,8 @@ function applyCors(req, res) {
335
348
  } catch {
336
349
  return
337
350
  }
338
- const allowed = isLoopbackHostname(hostname) || getAllowedOrigins().includes(origin)
351
+ const allowed =
352
+ isWildcardOriginsEnabled() || isLoopbackHostname(hostname) || getAllowedOrigins().includes(origin)
339
353
  if (!allowed) return
340
354
  res.setHeader('Access-Control-Allow-Origin', origin)
341
355
  res.setHeader('Vary', 'Origin')
@@ -363,6 +377,11 @@ export function isSameOriginOrLocal(req) {
363
377
  // ๐Ÿ“– No Origin/Referer โ†’ non-browser caller (curl, native app). Allow.
364
378
  if (candidates.length === 0) return true
365
379
 
380
+ // ๐Ÿ“– FCM_ALLOWED_ORIGINS="*" is an explicit operator decision to trust any
381
+ // ๐Ÿ“– origin (LAN/Docker setups with changing hostnames). Host-header
382
+ // ๐Ÿ“– validation still applies to every request.
383
+ if (isWildcardOriginsEnabled()) return true
384
+
366
385
  let hostHeader = typeof req.headers.host === 'string' ? req.headers.host.toLowerCase() : ''
367
386
  for (const c of candidates) {
368
387
  try {
@@ -41,6 +41,7 @@ import { join } from 'node:path'
41
41
  import * as readline from 'node:readline'
42
42
  import chalk from 'chalk'
43
43
  import { ENV_VAR_NAMES } from './provider-metadata.js'
44
+ import { isPureEnvPlaceholder } from './utils.js'
44
45
  import { saveConfig } from './config.js'
45
46
 
46
47
  // ๐Ÿ“– Unique marker used to identify the source line we inject into shell rc files.
@@ -126,6 +127,11 @@ export function buildEnvContent(config, shell) {
126
127
  const value = Array.isArray(rawValue) ? rawValue[0] : rawValue
127
128
  if (!value || typeof value !== 'string') continue
128
129
 
130
+ // ๐Ÿ“– Issue #196: a config value of "${MY_KEY}" means the key comes from the
131
+ // ๐Ÿ“– user's own environment. Never export the literal placeholder into the
132
+ // ๐Ÿ“– rc/env file, it would clobber the real variable on every shell start.
133
+ if (isPureEnvPlaceholder(value)) continue
134
+
129
135
  const safeValue = value.replace(/'/g, "'\\''")
130
136
 
131
137
  if (isFish) {
package/src/core/utils.js CHANGED
@@ -1168,3 +1168,25 @@ export function formatResultsAsJSON(results, sortBy = 'avg', limit = 0) {
1168
1168
 
1169
1169
  return JSON.stringify(formatted, null, 2)
1170
1170
  }
1171
+
1172
+ // ๐Ÿ“– Issue #196: ${VAR} placeholder support inside config apiKeys values.
1173
+ // ๐Ÿ“– The config file on disk keeps the literal placeholder, so saveConfig never
1174
+ // ๐Ÿ“– writes the resolved secret back; expansion happens only in memory, at the
1175
+ // ๐Ÿ“– moment a consumer reads the key (getApiKey, tool env sync). Only the
1176
+ // ๐Ÿ“– explicit ${NAME} form is recognized, so a literal key containing a bare
1177
+ // ๐Ÿ“– "$" is never mangled. An unset variable leaves the placeholder untouched:
1178
+ // ๐Ÿ“– probes will fail auth, which is the honest signal that something is off.
1179
+ export function resolveEnvPlaceholderValue(value) {
1180
+ if (typeof value !== 'string') return value
1181
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (placeholder, name) => {
1182
+ const resolved = process.env[name]
1183
+ return typeof resolved === 'string' ? resolved : placeholder
1184
+ })
1185
+ }
1186
+
1187
+ // ๐Ÿ“– True when the value is nothing but one ${VAR} reference. Used by the
1188
+ // ๐Ÿ“– shell rc/env generator to skip entries the user's own environment already
1189
+ // ๐Ÿ“– provides (exporting the literal placeholder would clobber the real var).
1190
+ export function isPureEnvPlaceholder(value) {
1191
+ return typeof value === 'string' && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value.trim())
1192
+ }