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.
- package/README.md +9 -9
- package/changelog/v0.5.97.md +24 -0
- package/package.json +1 -1
- package/sources.js +158 -95
- package/src/core/config.js +12 -3
- package/src/core/provider-metadata.js +4 -4
- package/src/core/router-daemon.js +21 -2
- package/src/core/shell-env.js +6 -0
- package/src/core/utils.js +22 -0
- package/web/dist/assets/index-C2lIYp0O.js +45 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-Cq9X7sN8.js +0 -45
package/src/core/config.js
CHANGED
|
@@ -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
|
|
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
|
|
352
|
-
detailedLimits: 'Zero-markup AI gateway: token prices are passed through at provider rates.\nFree
|
|
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: '
|
|
368
|
-
detailedLimits: 'Public beta: not every feature is guaranteed to work; feedback is welcome at https://onomeo.com/feedback.\
|
|
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 =
|
|
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 {
|
package/src/core/shell-env.js
CHANGED
|
@@ -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
|
+
}
|