free-coding-models 0.3.1 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 0.3.2
6
+
7
+ ### Fixed
8
+ - **Claude Code model-family routing now mirrors `free-claude-code`**: The proxy remaps Claude's internal model ids like `claude-3-5-sonnet-*`, `claude-3-haiku-*`, `claude-3-opus-*`, `sonnet`, `haiku`, and `default` back to the selected FCM proxy model instead of rejecting them as missing.
9
+ - **Claude Code helper/background requests stay on the selected model**: Launches now pin the Anthropic helper model env vars and encode the selected proxy model inside `ANTHROPIC_AUTH_TOKEN`, so Claude Code has a stable fallback even when it emits internal aliases.
10
+
5
11
  ## 0.3.1
6
12
 
7
13
  ### Added
package/README.md CHANGED
@@ -182,13 +182,12 @@ bunx free-coding-models YOUR_API_KEY
182
182
 
183
183
  ### 🆕 What's New
184
184
 
185
- **Version 0.3.1 tightens the proxy/tooling path and ships the missing diagnostics:**
185
+ **Version 0.3.2 hardens the Claude Code proxy path to match the routing strategy that works in `free-claude-code`:**
186
186
 
187
- - **Claude Code proxy launches are cleaner** — FCM now launches Claude Code with an Anthropic-only proxy contract (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`) instead of mixing auth modes.
188
- - **Codex proxy launches now use the right API path** — Codex is forced into an explicit custom provider config and the proxy now implements `POST /v1/responses`.
189
- - **Gemini proxy launches fail fast when unsupported** — Older Gemini CLI builds and invalid local config are detected up front, with a clear message instead of a misleading broken launch.
190
- - **Proxy auto-sync follows the current tool** — The FCM Proxy V2 overlay no longer relies on a separate active-tool picker, and `Y` now lists only stable persisted-config install targets.
191
- - **Beta messaging is explicit** — The README and runtime launcher diagnostics now call out that proxy-backed external tool support is still stabilizing.
187
+ - **Claude Code family-model routing is now proxy-side** — FCM remaps Claude's internal family ids such as `claude-3-5-sonnet-*`, `claude-3-haiku-*`, `claude-3-opus-*`, `sonnet`, `haiku`, and `default` back to the selected FCM proxy model.
188
+ - **Claude Code helper model slots are pinned** — FCM now exports the selected model into `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`, and `CLAUDE_CODE_SUBAGENT_MODEL` so background/helper requests stop drifting.
189
+ - **Claude proxy auth now carries the selected model hint** — The launcher encodes the chosen proxy model into `ANTHROPIC_AUTH_TOKEN`, giving the proxy a reliable fallback even when Claude Code ignores the visible `/model` selection.
190
+ - **Proxy support remains beta** — External-tool proxy support is still stabilizing, but Claude Code should now behave much closer to the working `free-claude-code` setup.
192
191
 
193
192
  ---
194
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
5
5
  "keywords": [
6
6
  "nvidia",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file lib/proxy-server.js
3
3
  * @description Multi-account rotation proxy server with SSE streaming,
4
- * token stats tracking, and persistent request logging.
4
+ * token stats tracking, Anthropic/OpenAI translation, and persistent request logging.
5
5
  *
6
6
  * Design:
7
7
  * - Binds to 127.0.0.1 only (never 0.0.0.0)
@@ -10,6 +10,8 @@
10
10
  * - x-ratelimit-* headers are stripped from all responses forwarded to clients
11
11
  * - Retry loop: first attempt uses sticky session fingerprint; subsequent
12
12
  * retries use fresh P2C to avoid hitting the same failed account
13
+ * - Claude-family aliases are resolved inside the proxy so Claude Code can
14
+ * keep emitting `claude-*` / `sonnet` / `haiku` style model ids safely
13
15
  *
14
16
  * @exports ProxyServer
15
17
  */
@@ -106,6 +108,49 @@ function sendJson(res, statusCode, body) {
106
108
  res.end(json)
107
109
  }
108
110
 
111
+ function normalizeRequestedModel(modelId) {
112
+ if (typeof modelId !== 'string') return null
113
+ const trimmed = modelId.trim()
114
+ if (!trimmed) return null
115
+ return trimmed.replace(/^fcm-proxy\//, '')
116
+ }
117
+
118
+ function classifyClaudeVirtualModel(modelId) {
119
+ const normalized = normalizeRequestedModel(modelId)
120
+ if (!normalized) return null
121
+
122
+ const lower = normalized.toLowerCase()
123
+
124
+ // 📖 Mirror free-claude-code's family routing approach: classify by Claude
125
+ // 📖 family keywords, not only exact ids. Claude Code regularly emits both
126
+ // 📖 short aliases (`sonnet`) and full versioned ids (`claude-3-5-sonnet-*`).
127
+ if (lower === 'default') return 'default'
128
+ if (/^opus(?:plan)?(?:\[1m\])?$/.test(lower)) return 'opus'
129
+ if (/^sonnet(?:\[1m\])?$/.test(lower)) return 'sonnet'
130
+ if (lower === 'haiku') return 'haiku'
131
+ if (!lower.startsWith('claude-')) return null
132
+ if (lower.includes('opus')) return 'opus'
133
+ if (lower.includes('haiku')) return 'haiku'
134
+ if (lower.includes('sonnet')) return 'sonnet'
135
+ return null
136
+ }
137
+
138
+ function parseProxyAuthorizationHeader(authorization, expectedToken) {
139
+ if (!expectedToken) return { authorized: true, modelHint: null }
140
+ if (typeof authorization !== 'string' || !authorization.startsWith('Bearer ')) {
141
+ return { authorized: false, modelHint: null }
142
+ }
143
+
144
+ const rawToken = authorization.slice('Bearer '.length).trim()
145
+ if (rawToken === expectedToken) return { authorized: true, modelHint: null }
146
+ if (!rawToken.startsWith(`${expectedToken}:`)) return { authorized: false, modelHint: null }
147
+
148
+ const modelHint = normalizeRequestedModel(rawToken.slice(expectedToken.length + 1))
149
+ return modelHint
150
+ ? { authorized: true, modelHint }
151
+ : { authorized: false, modelHint: null }
152
+ }
153
+
109
154
  // ─── ProxyServer ─────────────────────────────────────────────────────────────
110
155
 
111
156
  export class ProxyServer {
@@ -194,11 +239,32 @@ export class ProxyServer {
194
239
  }
195
240
  }
196
241
 
242
+ _getAuthContext(req) {
243
+ return parseProxyAuthorizationHeader(req.headers.authorization, this._proxyApiKey)
244
+ }
245
+
197
246
  _isAuthorized(req) {
198
- if (!this._proxyApiKey) return true
199
- const authorization = req.headers.authorization
200
- if (typeof authorization !== 'string') return false
201
- return authorization === `Bearer ${this._proxyApiKey}`
247
+ return this._getAuthContext(req).authorized
248
+ }
249
+
250
+ _resolveAnthropicRequestedModel(modelId, authModelHint = null) {
251
+ const requestedModel = normalizeRequestedModel(modelId)
252
+ if (requestedModel && this._accountManager.hasAccountsForModel(requestedModel)) {
253
+ return requestedModel
254
+ }
255
+
256
+ // 📖 Claude Code still emits internal aliases / tier model ids for some
257
+ // 📖 background and helper paths. When the launcher encoded the selected
258
+ // 📖 proxy slug into the auth token, remap those virtual Claude ids here.
259
+ // 📖 This intentionally matches Claude families by substring so ids like
260
+ // 📖 `claude-3-5-sonnet-20241022` behave the same as `sonnet`.
261
+ if (authModelHint && this._accountManager.hasAccountsForModel(authModelHint)) {
262
+ if (!requestedModel || classifyClaudeVirtualModel(requestedModel)) {
263
+ return authModelHint
264
+ }
265
+ }
266
+
267
+ return requestedModel
202
268
  }
203
269
 
204
270
  // ── Request routing ────────────────────────────────────────────────────────
@@ -209,7 +275,8 @@ export class ProxyServer {
209
275
  return this._handleHealth(res)
210
276
  }
211
277
 
212
- if (!this._isAuthorized(req)) {
278
+ const authContext = this._getAuthContext(req)
279
+ if (!authContext.authorized) {
213
280
  return sendJson(res, 401, { error: 'Unauthorized' })
214
281
  }
215
282
 
@@ -227,7 +294,7 @@ export class ProxyServer {
227
294
  })
228
295
  } else if (req.method === 'POST' && req.url === '/v1/messages') {
229
296
  // 📖 Anthropic Messages API translation — enables Claude Code compatibility
230
- this._handleAnthropicMessages(req, res).catch(err => {
297
+ this._handleAnthropicMessages(req, res, authContext).catch(err => {
231
298
  console.error('[proxy] Internal error:', err)
232
299
  const status = err.statusCode === 413 ? 413 : 500
233
300
  const msg = err.statusCode === 413 ? 'Request body too large' : 'Internal server error'
@@ -733,7 +800,7 @@ export class ProxyServer {
733
800
  *
734
801
  * 📖 This makes Claude Code work natively through the FCM proxy.
735
802
  */
736
- async _handleAnthropicMessages(clientReq, clientRes) {
803
+ async _handleAnthropicMessages(clientReq, clientRes, authContext = { modelHint: null }) {
737
804
  const rawBody = await readBody(clientReq)
738
805
  let anthropicBody
739
806
  try {
@@ -744,6 +811,8 @@ export class ProxyServer {
744
811
 
745
812
  // 📖 Translate Anthropic → OpenAI
746
813
  const openaiBody = translateAnthropicToOpenAI(anthropicBody)
814
+ const resolvedModel = this._resolveAnthropicRequestedModel(openaiBody.model, authContext.modelHint)
815
+ if (resolvedModel) openaiBody.model = resolvedModel
747
816
  const isStreaming = openaiBody.stream === true
748
817
 
749
818
  if (isStreaming) {
@@ -25,13 +25,16 @@
25
25
  *
26
26
  * @functions
27
27
  * → `resolveLauncherModelId` — choose the provider-specific id or proxy slug for a launch
28
+ * → `applyClaudeCodeModelOverrides` — force Claude Code auxiliary model slots onto the chosen proxy model
29
+ * → `buildClaudeProxyAuthToken` — encode the proxy token + selected model hint for Claude-only fallback routing
28
30
  * → `buildCodexProxyArgs` — force Codex into a proxy-backed custom provider config
29
31
  * → `inspectGeminiCliSupport` — detect whether the installed Gemini CLI can use proxy mode safely
30
32
  * → `writeGooseConfig` — install provider + set GOOSE_PROVIDER/GOOSE_MODEL in config.yaml
31
33
  * → `writeCrushConfig` — write provider + models.large/small to crush.json
32
34
  * → `startExternalTool` — configure and launch the selected external tool mode
33
35
  *
34
- * @exports resolveLauncherModelId, buildCodexProxyArgs, inspectGeminiCliSupport, startExternalTool
36
+ * @exports resolveLauncherModelId, applyClaudeCodeModelOverrides, buildClaudeProxyAuthToken
37
+ * @exports buildCodexProxyArgs, inspectGeminiCliSupport, startExternalTool
35
38
  *
36
39
  * @see src/tool-metadata.js
37
40
  * @see src/provider-metadata.js
@@ -65,6 +68,11 @@ const ANTHROPIC_ENV_KEYS = [
65
68
  'ANTHROPIC_AUTH_TOKEN',
66
69
  'ANTHROPIC_BASE_URL',
67
70
  'ANTHROPIC_MODEL',
71
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
72
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
73
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
74
+ 'ANTHROPIC_SMALL_FAST_MODEL',
75
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
68
76
  ]
69
77
  const GEMINI_ENV_KEYS = [
70
78
  'GEMINI_API_KEY',
@@ -146,6 +154,28 @@ export function resolveLauncherModelId(model, useProxy = false) {
146
154
  return model?.modelId ?? ''
147
155
  }
148
156
 
157
+ export function applyClaudeCodeModelOverrides(env, modelId) {
158
+ const resolvedModelId = typeof modelId === 'string' ? modelId.trim() : ''
159
+ if (!resolvedModelId) return env
160
+
161
+ // 📖 Claude Code still uses auxiliary model slots (opus/sonnet/haiku/subagents)
162
+ // 📖 even when a custom primary model is selected. Pin them all to the same slug.
163
+ env.ANTHROPIC_MODEL = resolvedModelId
164
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = resolvedModelId
165
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = resolvedModelId
166
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolvedModelId
167
+ env.ANTHROPIC_SMALL_FAST_MODEL = resolvedModelId
168
+ env.CLAUDE_CODE_SUBAGENT_MODEL = resolvedModelId
169
+ return env
170
+ }
171
+
172
+ export function buildClaudeProxyAuthToken(proxyToken, modelId) {
173
+ const resolvedProxyToken = typeof proxyToken === 'string' ? proxyToken.trim() : ''
174
+ const resolvedModelId = typeof modelId === 'string' ? modelId.trim() : ''
175
+ if (!resolvedProxyToken) return ''
176
+ return resolvedModelId ? `${resolvedProxyToken}:${resolvedModelId}` : resolvedProxyToken
177
+ }
178
+
149
179
  export function buildToolEnv(mode, model, config, options = {}) {
150
180
  const {
151
181
  sanitize = false,
@@ -610,8 +640,8 @@ export async function startExternalTool(mode, model, config) {
610
640
  const proxyBase = `http://127.0.0.1:${started.port}`
611
641
  const launchModelId = resolveLauncherModelId(model, true)
612
642
  proxyEnv.ANTHROPIC_BASE_URL = proxyBase
613
- proxyEnv.ANTHROPIC_AUTH_TOKEN = started.proxyToken
614
- proxyEnv.ANTHROPIC_MODEL = launchModelId
643
+ proxyEnv.ANTHROPIC_AUTH_TOKEN = buildClaudeProxyAuthToken(started.proxyToken, launchModelId)
644
+ applyClaudeCodeModelOverrides(proxyEnv, launchModelId)
615
645
  console.log(chalk.dim(` 📖 Claude Code routed through FCM proxy on :${started.port} (Anthropic translation enabled)`))
616
646
  return spawnCommand('claude', ['--model', launchModelId], proxyEnv)
617
647
  }