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 +6 -0
- package/README.md +5 -6
- package/package.json +1 -1
- package/src/proxy-server.js +77 -8
- package/src/tool-launchers.js +33 -3
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.
|
|
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
|
|
188
|
-
- **
|
|
189
|
-
- **
|
|
190
|
-
- **Proxy
|
|
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.
|
|
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",
|
package/src/proxy-server.js
CHANGED
|
@@ -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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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) {
|
package/src/tool-launchers.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
}
|