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.
Files changed (36) hide show
  1. package/README.md +10 -4
  2. package/bin/free-coding-models.js +33 -2
  3. package/changelog/v0.5.89.md +20 -0
  4. package/changelog/v0.5.90.md +12 -0
  5. package/package.json +1 -1
  6. package/sources.js +5 -2
  7. package/src/core/benchmark.js +9 -0
  8. package/src/core/cloudflare-account.js +311 -0
  9. package/src/core/endpoint-installer.js +8 -4
  10. package/src/core/opencode.js +9 -6
  11. package/src/core/ping.js +52 -16
  12. package/src/core/provider-key-tester.js +10 -3
  13. package/src/core/provider-metadata.js +1 -1
  14. package/src/core/router-daemon.js +1138 -501
  15. package/src/core/router-v2/anthropic-compat.js +473 -0
  16. package/src/core/router-v2/bench.js +171 -0
  17. package/src/core/router-v2/breaker-store.js +265 -0
  18. package/src/core/router-v2/constants.js +108 -0
  19. package/src/core/router-v2/decision-trace.js +134 -0
  20. package/src/core/router-v2/failure-classifier.js +231 -0
  21. package/src/core/router-v2/request-history.js +137 -0
  22. package/src/core/router-v2/response-gate.js +175 -0
  23. package/src/core/router-v2/tui-dashboard.js +632 -0
  24. package/src/core/schema-normalizer.js +23 -6
  25. package/src/core/utils.js +12 -0
  26. package/src/tui/app.js +7 -2
  27. package/src/tui/cli-help.js +4 -0
  28. package/src/tui/key-handler.js +117 -2
  29. package/src/tui/overlays.js +19 -3
  30. package/src/tui/tui-state.js +22 -0
  31. package/web/dist/assets/index-CCaxIOti.css +1 -0
  32. package/web/dist/assets/index-CCkuXrqE.js +48 -0
  33. package/web/dist/index.html +2 -2
  34. package/web/server.js +105 -1
  35. package/web/dist/assets/index-CAzFIt8P.css +0 -1
  36. 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 ID resolution from environment
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
- * - `resolveCloudflareUrl`: Resolve {account_id} placeholder from CLOUDFLARE_ACCOUNT_ID env var
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 ID
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 so provider
60
- // 📖 setup can stay simple in config. Supports both the `{account_id}` placeholder
61
- // 📖 and the explicit `{$CLOUDFLARE_ACCOUNT_ID}` form used in sources.js.
62
- export function resolveCloudflareUrl(url) {
63
- const accountId = (process.env.CLOUDFLARE_ACCOUNT_ID || '').trim()
64
- const hasPlaceholder = url.includes('{$CLOUDFLARE_ACCOUNT_ID}') || url.includes('{account_id}')
65
- if (!hasPlaceholder) return url
66
- const replacement = accountId ? encodeURIComponent(accountId) : 'missing-account-id'
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
- const modelsUrl = buildProviderModelsUrl(source?.url)
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
- const headers = { Authorization: `Bearer ${apiKey}` }
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 + set CLOUDFLARE_ACCOUNT_ID',
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: {