free-coding-models 0.5.88 → 0.5.90
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 +10 -4
- package/bin/free-coding-models.js +33 -2
- package/changelog/v0.5.89.md +20 -0
- package/changelog/v0.5.90.md +12 -0
- package/package.json +1 -1
- package/sources.js +5 -2
- package/src/core/benchmark.js +9 -0
- package/src/core/cloudflare-account.js +311 -0
- package/src/core/endpoint-installer.js +8 -4
- package/src/core/opencode.js +9 -6
- package/src/core/ping.js +52 -16
- package/src/core/provider-key-tester.js +10 -3
- package/src/core/provider-metadata.js +1 -1
- package/src/core/router-daemon.js +1138 -501
- package/src/core/router-v2/anthropic-compat.js +473 -0
- package/src/core/router-v2/bench.js +171 -0
- package/src/core/router-v2/breaker-store.js +265 -0
- package/src/core/router-v2/constants.js +108 -0
- package/src/core/router-v2/decision-trace.js +134 -0
- package/src/core/router-v2/failure-classifier.js +231 -0
- package/src/core/router-v2/request-history.js +137 -0
- package/src/core/router-v2/response-gate.js +175 -0
- package/src/core/router-v2/tui-dashboard.js +632 -0
- package/src/core/schema-normalizer.js +23 -6
- package/src/core/utils.js +12 -0
- package/src/tui/app.js +7 -2
- package/src/tui/cli-help.js +4 -0
- package/src/tui/key-handler.js +117 -2
- package/src/tui/overlays.js +19 -3
- package/src/tui/tui-state.js +22 -0
- package/web/dist/assets/index-CCaxIOti.css +1 -0
- package/web/dist/assets/index-CCkuXrqE.js +48 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +105 -1
- package/web/dist/assets/index-CAzFIt8P.css +0 -1
- package/web/dist/assets/index-DFg1h0Nd.js +0 -44
package/src/core/ping.js
CHANGED
|
@@ -8,17 +8,20 @@
|
|
|
8
8
|
* endpoint quota polling with caching.
|
|
9
9
|
*
|
|
10
10
|
* 🎯 Key features:
|
|
11
|
-
* - Provider-specific request building (handles Replicate, Cloudflare, OpenRouter
|
|
11
|
+
* - Provider-specific request building (handles Replicate, Cloudflare, OpenRouter,
|
|
12
|
+
* OpenCode Zen mandatory session header)
|
|
12
13
|
* - Async ping with timeout and abort controller
|
|
13
14
|
* - Quota extraction from rate limit headers (multiple variants supported)
|
|
14
15
|
* - Cached provider quota polling with TTL and error backoff
|
|
15
|
-
* - Cloudflare account
|
|
16
|
+
* - Cloudflare account-id resolution from env var, stored config or zero-setup
|
|
17
|
+
* auto-discovery via the /accounts endpoint (see ./cloudflare-account.js)
|
|
16
18
|
* - Per-provider circuit-breaker on 429 (issue #146): pauses ALL models of a provider
|
|
17
19
|
* when the provider returns a quota-exhausted response, so the ping loop stops
|
|
18
20
|
* hammering the user's daily quota while the provider's retry window is active.
|
|
19
21
|
*
|
|
20
22
|
* → Functions:
|
|
21
|
-
* - `
|
|
23
|
+
* - `getProviderSessionHeaders`: Extra mandatory headers per provider (e.g. OpenCode Zen session id)
|
|
24
|
+
* - `resolveCloudflareUrl`: Resolve {account_id} placeholders (env > cache > stored config, issue #181)
|
|
22
25
|
* - `buildChatCompletionPingBody`: Build minimal chat-completion probe payloads with thinking disabled
|
|
23
26
|
* - `markDisabledThinkingUnsupported`: Cache strict providers that reject the optional thinking control
|
|
24
27
|
* - `shouldUseDisabledThinkingForProvider`: Decide whether a provider should receive disabled-thinking probes
|
|
@@ -38,13 +41,21 @@
|
|
|
38
41
|
*
|
|
39
42
|
* ⚙️ Configuration:
|
|
40
43
|
* - PING_TIMEOUT: Timeout in ms for ping requests (default: 15000)
|
|
41
|
-
* - CLOUDFLARE_ACCOUNT_ID: Env var for Cloudflare Workers AI account
|
|
44
|
+
* - CLOUDFLARE_ACCOUNT_ID: Env var for the Cloudflare Workers AI account id
|
|
45
|
+
* (fallbacks: stored settings.cloudflareAccountId, then auto-discovery via
|
|
46
|
+
* the /accounts endpoint, handled in ./cloudflare-account.js)
|
|
42
47
|
*
|
|
43
48
|
* @see {@link ../src/provider-quota-fetchers.js} Quota fetching implementation
|
|
44
49
|
* @see {@link ../src/quota-capabilities.js} Quota telemetry + Usage behavior detection
|
|
45
50
|
*/
|
|
46
51
|
|
|
52
|
+
import { randomUUID } from 'node:crypto'
|
|
47
53
|
import { PING_TIMEOUT } from './constants.js'
|
|
54
|
+
import {
|
|
55
|
+
applyCloudflareAccountId,
|
|
56
|
+
getCloudflareAccountIdSync,
|
|
57
|
+
ensureCloudflareAccountId,
|
|
58
|
+
} from './cloudflare-account.js'
|
|
48
59
|
import { fetchProviderQuota as _fetchProviderQuotaFromModule, extractQuota as _extractQuotaFromModule, processResponseHeaders as _processResponseHeadersFromModule } from './provider-quota-fetchers.js'
|
|
49
60
|
import { supportsUsagePercent } from './quota-capabilities.js'
|
|
50
61
|
import {
|
|
@@ -56,17 +67,15 @@ const DISABLED_THINKING_RETRY_STATUSES = new Set([400, 422])
|
|
|
56
67
|
const disabledThinkingUnsupportedProviders = new Set()
|
|
57
68
|
|
|
58
69
|
// 📖 resolveCloudflareUrl: Cloudflare's OpenAI-compatible endpoint is account-scoped.
|
|
59
|
-
// 📖 We resolve the placeholder from the CLOUDFLARE_ACCOUNT_ID env var
|
|
60
|
-
// 📖
|
|
61
|
-
// 📖
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return url
|
|
68
|
-
.replace(/\{\$CLOUDFLARE_ACCOUNT_ID\}/g, replacement)
|
|
69
|
-
.replace(/\{account_id\}/g, replacement)
|
|
70
|
+
// 📖 We resolve the placeholder from the CLOUDFLARE_ACCOUNT_ID env var, the
|
|
71
|
+
// 📖 in-process cache or the stored config settings (issue #181), so provider
|
|
72
|
+
// 📖 setup can stay simple. Supports both the `{account_id}` placeholder and
|
|
73
|
+
// 📖 the explicit `{$CLOUDFLARE_ACCOUNT_ID}` form used in sources.js.
|
|
74
|
+
// 📖 When nothing resolves, the historical 'missing-account-id' segment keeps
|
|
75
|
+
// 📖 verdicts stable. Async paths should call ensureCloudflareAccountId() first
|
|
76
|
+
// 📖 so zero-setup auto-discovery can populate the cache (see ping()).
|
|
77
|
+
export function resolveCloudflareUrl(url, options = {}) {
|
|
78
|
+
return applyCloudflareAccountId(url, getCloudflareAccountIdSync(options))
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
// 📖 buildChatCompletionPingBody: Use the smallest useful chat-completion probe.
|
|
@@ -97,6 +106,25 @@ export function shouldUseDisabledThinkingForProvider(providerKey) {
|
|
|
97
106
|
return !disabledThinkingUnsupportedProviders.has(providerKey)
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
// 📖 getProviderSessionHeaders: extra mandatory headers a provider requires beyond
|
|
110
|
+
// 📖 the standard Content-Type/Authorization pair, centralised so probes (ping.js),
|
|
111
|
+
// 📖 the router daemon and the web dashboard all send identical headers instead of
|
|
112
|
+
// 📖 duplicating per-provider knowledge (issue #181).
|
|
113
|
+
// 📖 OpenCode Zen rejects every request without an `x-opencode-session` header with
|
|
114
|
+
// 📖 HTTP 400 MissingSessionID ("OpenCode's free tier can only be used in OpenCode").
|
|
115
|
+
// 📖 The Zen docs describe a stable per-conversation session id, so we generate ONE
|
|
116
|
+
// 📖 uuid per process at module load: every request this process sends shares the
|
|
117
|
+
// 📖 same identity, mirroring how the real OpenCode client behaves and keeping the
|
|
118
|
+
// 📖 value stable across repeated probes.
|
|
119
|
+
const OPENCODE_ZEN_SESSION_ID = randomUUID()
|
|
120
|
+
|
|
121
|
+
export function getProviderSessionHeaders(providerKey) {
|
|
122
|
+
if (providerKey === 'opencode-zen') {
|
|
123
|
+
return { 'x-opencode-session': OPENCODE_ZEN_SESSION_ID }
|
|
124
|
+
}
|
|
125
|
+
return {}
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
// 📖 buildPingRequest: Build provider-specific ping request.
|
|
101
129
|
// 📖 Handles Replicate's /v1/predictions format, Cloudflare's account_id in URL,
|
|
102
130
|
// 📖 and standard OpenAI-compliant chat completions with provider-specific headers.
|
|
@@ -128,7 +156,7 @@ export function buildPingRequest(apiKey, modelId, providerKey, url, options = {}
|
|
|
128
156
|
}
|
|
129
157
|
}
|
|
130
158
|
|
|
131
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
159
|
+
const headers = { 'Content-Type': 'application/json', ...getProviderSessionHeaders(providerKey) }
|
|
132
160
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
133
161
|
if (providerKey === 'openrouter') {
|
|
134
162
|
// 📖 OpenRouter recommends optional app identification headers.
|
|
@@ -178,6 +206,14 @@ async function isDisabledThinkingRejected(resp, req) {
|
|
|
178
206
|
// 📖 A 401 response still tells us the server is UP and gives us real latency.
|
|
179
207
|
// 📖 Returns { code, ms, quotaPercent }
|
|
180
208
|
export async function ping(apiKey, modelId, providerKey, url) {
|
|
209
|
+
// 📖 Cloudflare zero-setup (issue #181): before building the request, give the
|
|
210
|
+
// 📖 account-id resolver a chance to auto-discover the id from the stored API
|
|
211
|
+
// 📖 key. Done BEFORE the timer starts so a one-time discovery round-trip never
|
|
212
|
+
// 📖 pollutes the measured latency or races PING_TIMEOUT. No-op when the id is
|
|
213
|
+
// 📖 already known from env/cache/config.
|
|
214
|
+
if (providerKey === 'cloudflare') {
|
|
215
|
+
await ensureCloudflareAccountId()
|
|
216
|
+
}
|
|
181
217
|
const ctrl = new AbortController()
|
|
182
218
|
const timer = setTimeout(() => ctrl.abort(), PING_TIMEOUT)
|
|
183
219
|
const t0 = performance.now()
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* runProviderKeyTest, PROVIDER_AUTH_ENDPOINTS
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { ping } from './ping.js'
|
|
30
|
+
import { ping, getProviderSessionHeaders, resolveCloudflareUrl } from './ping.js'
|
|
31
31
|
import { sleep } from './shared-helpers.js'
|
|
32
32
|
|
|
33
33
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -295,12 +295,19 @@ export async function runProviderKeyTest(apiKey, providerKey, source, options =
|
|
|
295
295
|
|
|
296
296
|
// 📖 Slow path: ping-based verification (providers without auth endpoint or timeouts).
|
|
297
297
|
const discoveredModelIds = []
|
|
298
|
-
|
|
298
|
+
// 📖 Cloudflare's models URL is account-scoped (issue #181): resolve the
|
|
299
|
+
// 📖 {$CLOUDFLARE_ACCOUNT_ID} placeholder so discovery reaches the real API
|
|
300
|
+
// 📖 instead of 404ing on the raw catalog URL and silently falling back.
|
|
301
|
+
const modelsUrl = providerKey === 'cloudflare'
|
|
302
|
+
? resolveCloudflareUrl(buildProviderModelsUrl(source?.url) || '')
|
|
303
|
+
: buildProviderModelsUrl(source?.url)
|
|
299
304
|
let discoveryNote = ''
|
|
300
305
|
|
|
301
306
|
if (modelsUrl) {
|
|
302
307
|
try {
|
|
303
|
-
|
|
308
|
+
// 📖 Mandatory per-provider headers (issue #181): OpenCode Zen gates every
|
|
309
|
+
// 📖 request (model discovery included) behind `x-opencode-session`.
|
|
310
|
+
const headers = { Authorization: `Bearer ${apiKey}`, ...getProviderSessionHeaders(providerKey) }
|
|
304
311
|
if (providerKey === 'openrouter') {
|
|
305
312
|
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
306
313
|
headers['X-Title'] = 'free-coding-models'
|
|
@@ -228,7 +228,7 @@ export const PROVIDER_METADATA = {
|
|
|
228
228
|
label: 'Cloudflare Workers AI',
|
|
229
229
|
color: chalk.rgb(255, 204, 128),
|
|
230
230
|
signupUrl: 'https://dash.cloudflare.com',
|
|
231
|
-
signupHint: 'Create AI API token
|
|
231
|
+
signupHint: 'Create AI API token (account id auto-discovered; CLOUDFLARE_ACCOUNT_ID optional)',
|
|
232
232
|
rateLimits: 'Free: 10k neurons/day, text-gen 300 RPM',
|
|
233
233
|
},
|
|
234
234
|
perplexity: {
|