@heventure/model-provider-x 0.2.0 → 0.2.1
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 +20 -4
- package/dist/cli/args.js +2 -1
- package/dist/cli/commands.js +1 -1
- package/dist/cli/index.js +70 -12
- package/dist/core/provider.js +43 -3
- package/dist/proxy/server.js +13 -1
- package/dist/targets/claude-code.js +73 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Or run the published package directly:
|
|
|
24
24
|
npx @heventure/model-provider-x --help
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Unified Setup
|
|
28
28
|
|
|
29
29
|
Run the TUI wizard:
|
|
30
30
|
|
|
@@ -32,7 +32,17 @@ Run the TUI wizard:
|
|
|
32
32
|
node dist/cli/index.js
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
The wizard asks for:
|
|
36
|
+
|
|
37
|
+
1. Provider.
|
|
38
|
+
2. Models.
|
|
39
|
+
3. Agent platform: OpenCode, Codex, or Claude Code.
|
|
40
|
+
4. Direct or proxy mode, with a recommendation based on provider API support.
|
|
41
|
+
5. Any platform-specific install target.
|
|
42
|
+
|
|
43
|
+
## OpenCode Setup
|
|
44
|
+
|
|
45
|
+
Print a config fragment without writing files:
|
|
36
46
|
|
|
37
47
|
```bash
|
|
38
48
|
node dist/cli/index.js \
|
|
@@ -43,6 +53,9 @@ node dist/cli/index.js \
|
|
|
43
53
|
--print
|
|
44
54
|
```
|
|
45
55
|
|
|
56
|
+
OpenCode provider entries include `options.setCacheKey=true` by default.
|
|
57
|
+
This lets OpenCode pass a stable cache key through the OpenAI-compatible provider path so relays or local providers that support prompt caching can route repeated context to the same cache.
|
|
58
|
+
|
|
46
59
|
## Claude Code Setup
|
|
47
60
|
|
|
48
61
|
Create or update a provider profile and write Claude Code user settings:
|
|
@@ -61,6 +74,9 @@ node dist/cli/index.js proxy up --profile unsloth
|
|
|
61
74
|
```
|
|
62
75
|
|
|
63
76
|
The Claude Code setup writes gateway environment values to `~/.claude/settings.json`.
|
|
77
|
+
Claude Code appends `/v1/messages` itself, so direct Anthropic-compatible provider URLs are written without a trailing `/v1`.
|
|
78
|
+
For example, entering `http://localhost:1234/v1` for LM Studio writes `ANTHROPIC_BASE_URL=http://localhost:1234`.
|
|
79
|
+
The setup uses `ANTHROPIC_API_KEY` and removes stale `ANTHROPIC_AUTH_TOKEN` values to avoid Claude Code auth conflicts.
|
|
64
80
|
Upstream provider keys are stored in `~/.config/model-provider-x/config.jsonc`, not in Claude Code settings.
|
|
65
81
|
|
|
66
82
|
## Codex Setup
|
|
@@ -82,9 +98,9 @@ The Codex setup writes a Responses-compatible provider to `~/.codex/config.toml`
|
|
|
82
98
|
It also configures command-backed authentication so Codex can fetch the local proxy token automatically.
|
|
83
99
|
The proxy currently supports non-streaming `/v1/responses` requests and forwards them to upstream OpenAI-compatible `/v1/chat/completions`.
|
|
84
100
|
|
|
85
|
-
##
|
|
101
|
+
## Setup Modes
|
|
86
102
|
|
|
87
|
-
|
|
103
|
+
You can also run the unified setup wizard through the explicit setup command:
|
|
88
104
|
|
|
89
105
|
```bash
|
|
90
106
|
node dist/cli/index.js setup --provider lmstudio
|
package/dist/cli/args.js
CHANGED
|
@@ -92,6 +92,7 @@ export function usage() {
|
|
|
92
92
|
|
|
93
93
|
Usage:
|
|
94
94
|
model-provider-x [options]
|
|
95
|
+
model-provider-x setup [options]
|
|
95
96
|
|
|
96
97
|
Options:
|
|
97
98
|
--base-url <url> OpenAI-compatible API base URL, for example http://localhost:8888/v1
|
|
@@ -102,7 +103,7 @@ Options:
|
|
|
102
103
|
--proxy Write agent config through the local compatibility proxy.
|
|
103
104
|
--direct Write agent config directly to the upstream provider.
|
|
104
105
|
--models <list> Comma-separated model ids. Skips interactive model selection.
|
|
105
|
-
--config <path> OpenCode config path to write.
|
|
106
|
+
--config <path> OpenCode config path to write when targeting OpenCode.
|
|
106
107
|
--print Print generated JSON and do not write config.
|
|
107
108
|
--yes, -y Accept defaults in non-interactive prompts.
|
|
108
109
|
--help, -h Show this help.
|
package/dist/cli/commands.js
CHANGED
|
@@ -2,7 +2,7 @@ import { HelpRequested, parseCliArgs, usage } from "./args.js";
|
|
|
2
2
|
export function parseCommand(argv) {
|
|
3
3
|
const [command, ...rest] = argv;
|
|
4
4
|
if (!command || command.startsWith("--")) {
|
|
5
|
-
return { command: "
|
|
5
|
+
return { command: "setup", target: undefined, options: parseCliArgs(argv) };
|
|
6
6
|
}
|
|
7
7
|
if (command === "setup") {
|
|
8
8
|
return parseSetupCommand(rest);
|
package/dist/cli/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getDefaultToolConfigPath, readToolConfig, upsertProviderProfile } from
|
|
|
7
7
|
import { discoverOpenCodeConfigs, getDefaultConfigPath, writeProviderToConfig } from "../core/config.js";
|
|
8
8
|
import { buildProviderConfig, detectProviderCapabilities, recommendProxyMode, validateAndFetchModels } from "../core/provider.js";
|
|
9
9
|
import { startProxyServer } from "../proxy/server.js";
|
|
10
|
-
import { getDefaultClaudeSettingsPath, writeClaudeCodeSettings } from "../targets/claude-code.js";
|
|
10
|
+
import { defaultClaudeModelMapping, getDefaultClaudeSettingsPath, writeClaudeCodeSettings } from "../targets/claude-code.js";
|
|
11
11
|
import { getDefaultCodexConfigPath, writeCodexConfig } from "../targets/codex.js";
|
|
12
12
|
import { HelpRequested, parseModelSelection } from "./args.js";
|
|
13
13
|
import { commandUsage, parseCommand } from "./commands.js";
|
|
@@ -34,6 +34,10 @@ export async function runCommand(command) {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
if (command.command === "setup") {
|
|
37
|
+
if (command.options.print) {
|
|
38
|
+
await runCli(command.options);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
37
41
|
await runSetup(command);
|
|
38
42
|
return;
|
|
39
43
|
}
|
|
@@ -110,9 +114,9 @@ async function runSetup(command) {
|
|
|
110
114
|
const providerInput = await collectProviderInput(rl, command);
|
|
111
115
|
output.write("Detecting provider capabilities...\n");
|
|
112
116
|
const capabilities = await detectProviderCapabilities({ baseURL: providerInput.baseURL, apiKey: providerInput.apiKey });
|
|
117
|
+
const selection = await collectProviderSelection(rl, command, providerInput);
|
|
113
118
|
const target = await resolveSetupTarget(rl, command.target);
|
|
114
119
|
const useProxy = await resolveProxyMode(rl, command.options, capabilities, target);
|
|
115
|
-
const selection = await collectProviderSelection(rl, command, target, providerInput);
|
|
116
120
|
if (target === "opencode") {
|
|
117
121
|
await writeOpenCodeSetup(rl, command, selection, useProxy);
|
|
118
122
|
return;
|
|
@@ -121,7 +125,7 @@ async function runSetup(command) {
|
|
|
121
125
|
await writeCodexSetup(command, selection, useProxy);
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
124
|
-
await writeClaudeCodeSetup(command, selection, useProxy);
|
|
128
|
+
await writeClaudeCodeSetup(rl, command, selection, useProxy);
|
|
125
129
|
}
|
|
126
130
|
finally {
|
|
127
131
|
rl.close();
|
|
@@ -135,18 +139,14 @@ async function collectProviderInput(rl, command) {
|
|
|
135
139
|
const apiKey = await resolveApiKey(rl, command.options.apiKey, providerDefaults.preset, "Upstream API key");
|
|
136
140
|
return { providerId, providerName, baseURL, apiKey };
|
|
137
141
|
}
|
|
138
|
-
async function collectProviderSelection(rl, command,
|
|
142
|
+
async function collectProviderSelection(rl, command, providerInput) {
|
|
139
143
|
output.write("Fetching models...\n");
|
|
140
144
|
const fetched = await validateAndFetchModels({ baseURL: providerInput.baseURL, apiKey: providerInput.apiKey });
|
|
141
145
|
const selectedModels = command.options.models ??
|
|
142
146
|
(canUseTui()
|
|
143
|
-
? await multiSelectChoices(
|
|
144
|
-
? "Select Codex Responses gateway models"
|
|
145
|
-
: target === "claude-code"
|
|
146
|
-
? "Select Claude Code gateway models"
|
|
147
|
-
: "Select models", createModelChoices(fetched.models))
|
|
147
|
+
? await multiSelectChoices("Select models", createModelChoices(fetched.models))
|
|
148
148
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
149
|
-
const defaultModel = command
|
|
149
|
+
const defaultModel = await resolveDefaultModel(rl, command, selectedModels);
|
|
150
150
|
if (!defaultModel) {
|
|
151
151
|
throw new Error("Select at least one model");
|
|
152
152
|
}
|
|
@@ -204,15 +204,18 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy) {
|
|
|
204
204
|
output.write(`Started proxy: ${proxy.baseURL}\n`);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
-
async function writeClaudeCodeSetup(command, selection, useProxy) {
|
|
207
|
+
async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
|
|
208
208
|
const proxyBaseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}` : selection.upstreamBaseURL;
|
|
209
|
+
const modelMapping = await resolveClaudeModelMapping(rl, command, selection);
|
|
209
210
|
const result = await writeClaudeCodeSettings({
|
|
210
211
|
targetPath: getDefaultClaudeSettingsPath(),
|
|
211
212
|
proxy: {
|
|
212
213
|
baseURL: proxyBaseURL,
|
|
213
214
|
authToken: useProxy ? selection.config.proxy.authToken : selection.apiKey,
|
|
214
215
|
enableModelDiscovery: useProxy,
|
|
215
|
-
defaultModel:
|
|
216
|
+
defaultModel: selection.defaultModel,
|
|
217
|
+
models: selection.selectedModels,
|
|
218
|
+
modelMapping
|
|
216
219
|
}
|
|
217
220
|
});
|
|
218
221
|
output.write(`Saved profile ${selection.providerId} to ${selection.toolConfigPath}\n`);
|
|
@@ -359,6 +362,61 @@ async function resolveProxyMode(rl, options, capabilities, target) {
|
|
|
359
362
|
const accepted = !answer.trim() || answer.trim().toLowerCase() === "y";
|
|
360
363
|
return recommendedProxy ? accepted : !accepted;
|
|
361
364
|
}
|
|
365
|
+
async function resolveDefaultModel(rl, command, selectedModels) {
|
|
366
|
+
if (command.defaultModel) {
|
|
367
|
+
if (!selectedModels.includes(command.defaultModel)) {
|
|
368
|
+
throw new Error(`Default model is not in the selected model list: ${command.defaultModel}`);
|
|
369
|
+
}
|
|
370
|
+
return command.defaultModel;
|
|
371
|
+
}
|
|
372
|
+
if (selectedModels.length === 0) {
|
|
373
|
+
throw new Error("Select at least one model");
|
|
374
|
+
}
|
|
375
|
+
if (selectedModels.length === 1 || command.options.yes) {
|
|
376
|
+
return selectedModels[0];
|
|
377
|
+
}
|
|
378
|
+
if (canUseTui()) {
|
|
379
|
+
return selectChoice("Choose default model", selectedModels.map((model) => ({ label: model, value: model })));
|
|
380
|
+
}
|
|
381
|
+
const answer = await rl.question(`Default model [${selectedModels[0]}]: `);
|
|
382
|
+
const model = answer.trim() || selectedModels[0];
|
|
383
|
+
if (!selectedModels.includes(model)) {
|
|
384
|
+
throw new Error(`Default model is not in the selected model list: ${model}`);
|
|
385
|
+
}
|
|
386
|
+
return model;
|
|
387
|
+
}
|
|
388
|
+
async function resolveClaudeModelMapping(rl, command, selection) {
|
|
389
|
+
const defaultMapping = defaultClaudeModelMapping(selection.defaultModel);
|
|
390
|
+
if (selection.selectedModels.length <= 1 || command.options.yes) {
|
|
391
|
+
return defaultMapping;
|
|
392
|
+
}
|
|
393
|
+
const customize = canUseTui()
|
|
394
|
+
? await selectChoice("Claude Code model mapping", [
|
|
395
|
+
{ label: "Use default model for Opus, Sonnet, Haiku", value: false, hint: "recommended" },
|
|
396
|
+
{ label: "Customize Opus, Sonnet, Haiku models", value: true }
|
|
397
|
+
])
|
|
398
|
+
: (await rl.question("Use default model for Opus, Sonnet, Haiku? [Y/n] ")).trim().toLowerCase() === "n";
|
|
399
|
+
if (!customize) {
|
|
400
|
+
return defaultMapping;
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
opus: await selectModelForRole(rl, "Opus model", selection.selectedModels, selection.defaultModel),
|
|
404
|
+
sonnet: await selectModelForRole(rl, "Sonnet model", selection.selectedModels, selection.defaultModel),
|
|
405
|
+
haiku: await selectModelForRole(rl, "Haiku model", selection.selectedModels, selection.defaultModel),
|
|
406
|
+
subagent: await selectModelForRole(rl, "Subagent model", selection.selectedModels, selection.defaultModel)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function selectModelForRole(rl, label, models, defaultModel) {
|
|
410
|
+
if (canUseTui()) {
|
|
411
|
+
return selectChoice(label, models.map((model) => ({ label: model, value: model, hint: model === defaultModel ? "default" : undefined })));
|
|
412
|
+
}
|
|
413
|
+
const answer = await rl.question(`${label} [${defaultModel}]: `);
|
|
414
|
+
const model = answer.trim() || defaultModel;
|
|
415
|
+
if (!models.includes(model)) {
|
|
416
|
+
throw new Error(`${label} is not in the selected model list: ${model}`);
|
|
417
|
+
}
|
|
418
|
+
return model;
|
|
419
|
+
}
|
|
362
420
|
async function resolveSetupTarget(rl, target) {
|
|
363
421
|
if (target) {
|
|
364
422
|
return target;
|
package/dist/core/provider.js
CHANGED
|
@@ -51,7 +51,7 @@ export async function detectProviderCapabilities(input, fetchImpl = globalThis.f
|
|
|
51
51
|
apis.add("openai-responses");
|
|
52
52
|
}
|
|
53
53
|
const messages = await probe(`${baseURL}/messages`, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: "{}" }, fetchImpl);
|
|
54
|
-
if (messages.reachable) {
|
|
54
|
+
if (messages.reachable && (await isAnthropicMessagesProbe(messages.response))) {
|
|
55
55
|
apis.add("anthropic-messages");
|
|
56
56
|
}
|
|
57
57
|
return { baseURL, apis: [...apis] };
|
|
@@ -75,7 +75,8 @@ export function buildProviderConfig(input) {
|
|
|
75
75
|
npm: "@ai-sdk/openai-compatible",
|
|
76
76
|
name: input.providerName.trim(),
|
|
77
77
|
options: {
|
|
78
|
-
baseURL
|
|
78
|
+
baseURL,
|
|
79
|
+
setCacheKey: true
|
|
79
80
|
},
|
|
80
81
|
models: Object.fromEntries(input.models.map((model) => [model, { name: model }]))
|
|
81
82
|
};
|
|
@@ -104,9 +105,48 @@ function isOpenCodeCompatibleModel(model) {
|
|
|
104
105
|
async function probe(input, init, fetchImpl) {
|
|
105
106
|
try {
|
|
106
107
|
const response = await fetchImpl(input, init);
|
|
107
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
reachable: response.ok || response.status === 400 || response.status === 401 || response.status === 422,
|
|
110
|
+
response
|
|
111
|
+
};
|
|
108
112
|
}
|
|
109
113
|
catch {
|
|
110
114
|
return { reachable: false };
|
|
111
115
|
}
|
|
112
116
|
}
|
|
117
|
+
async function isAnthropicMessagesProbe(response) {
|
|
118
|
+
if (!response) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const status = response.status ?? (response.ok ? 200 : undefined);
|
|
122
|
+
if (status === 404 || status === undefined) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (!response.json) {
|
|
126
|
+
return !response.ok && (status === 400 || status === 401 || status === 422);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const body = await response.json();
|
|
130
|
+
if (isAnthropicMessageResponse(body) || isAnthropicErrorResponse(body)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return !response.ok && (status === 400 || status === 401 || status === 422);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return !response.ok && (status === 400 || status === 401 || status === 422);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function isAnthropicMessageResponse(body) {
|
|
140
|
+
return (typeof body === "object" &&
|
|
141
|
+
body !== null &&
|
|
142
|
+
body.type === "message" &&
|
|
143
|
+
typeof body.role === "string" &&
|
|
144
|
+
Array.isArray(body.content));
|
|
145
|
+
}
|
|
146
|
+
function isAnthropicErrorResponse(body) {
|
|
147
|
+
return (typeof body === "object" &&
|
|
148
|
+
body !== null &&
|
|
149
|
+
body.type === "error" &&
|
|
150
|
+
typeof body.error === "object" &&
|
|
151
|
+
body.error !== null);
|
|
152
|
+
}
|
package/dist/proxy/server.js
CHANGED
|
@@ -114,7 +114,7 @@ async function handleResponses(request, response, profile, fetchImpl) {
|
|
|
114
114
|
writeJson(response, 200, chatCompletionToResponses((await upstream.json())));
|
|
115
115
|
}
|
|
116
116
|
async function handleMessages(request, response, profile, fetchImpl) {
|
|
117
|
-
const body = (await readJson(request));
|
|
117
|
+
const body = normalizeAnthropicModel((await readJson(request)), profile);
|
|
118
118
|
const chatRequest = anthropicMessageToChatRequest(body);
|
|
119
119
|
const upstream = await fetchImpl(`${profile.baseURL.replace(/\/+$/, "")}/chat/completions`, {
|
|
120
120
|
method: "POST",
|
|
@@ -150,6 +150,18 @@ async function handleMessages(request, response, profile, fetchImpl) {
|
|
|
150
150
|
}
|
|
151
151
|
writeJson(response, 200, chatCompletionToAnthropicMessage((await upstream.json())));
|
|
152
152
|
}
|
|
153
|
+
function normalizeAnthropicModel(body, profile) {
|
|
154
|
+
if (!isClaudeModelAlias(body.model) || profile.models.includes(body.model) || profile.models.length === 0) {
|
|
155
|
+
return body;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
...body,
|
|
159
|
+
model: profile.models[0]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function isClaudeModelAlias(model) {
|
|
163
|
+
return /(^claude-|sonnet|opus|haiku)/i.test(model);
|
|
164
|
+
}
|
|
153
165
|
function isAuthorized(request, authToken) {
|
|
154
166
|
const authorization = request.headers.authorization;
|
|
155
167
|
const apiKey = request.headers["x-api-key"];
|
|
@@ -1,25 +1,85 @@
|
|
|
1
1
|
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
const OPUS_MODEL_OVERRIDE_KEYS = [
|
|
5
|
+
"claude-opus-4-7",
|
|
6
|
+
"claude-opus-4-6",
|
|
7
|
+
"claude-opus-4-5",
|
|
8
|
+
"claude-opus-4-1",
|
|
9
|
+
"claude-opus-4-0",
|
|
10
|
+
"claude-3-opus-latest",
|
|
11
|
+
"claude-3-opus-20240229"
|
|
12
|
+
];
|
|
13
|
+
const SONNET_MODEL_OVERRIDE_KEYS = [
|
|
14
|
+
"claude-sonnet-4-6",
|
|
15
|
+
"claude-sonnet-4-5",
|
|
16
|
+
"claude-sonnet-4-0",
|
|
17
|
+
"claude-3-7-sonnet-latest",
|
|
18
|
+
"claude-3-7-sonnet-20250219",
|
|
19
|
+
"claude-3-5-sonnet-latest",
|
|
20
|
+
"claude-3-5-sonnet-20241022",
|
|
21
|
+
"claude-3-5-sonnet-20240620"
|
|
22
|
+
];
|
|
23
|
+
const HAIKU_MODEL_OVERRIDE_KEYS = [
|
|
24
|
+
"claude-haiku-4-5",
|
|
25
|
+
"claude-3-5-haiku-latest",
|
|
26
|
+
"claude-3-5-haiku-20241022",
|
|
27
|
+
"claude-3-haiku-20240307"
|
|
28
|
+
];
|
|
4
29
|
export function getDefaultClaudeSettingsPath(homeDir = homedir()) {
|
|
5
30
|
return join(homeDir, ".claude", "settings.json");
|
|
6
31
|
}
|
|
7
32
|
export function mergeClaudeCodeSettings(settings, proxy) {
|
|
8
33
|
const env = {
|
|
9
34
|
...(isRecord(settings.env) ? stringifyRecord(settings.env) : {}),
|
|
10
|
-
ANTHROPIC_BASE_URL: proxy.baseURL,
|
|
11
|
-
|
|
35
|
+
ANTHROPIC_BASE_URL: normalizeClaudeCodeBaseURL(proxy.baseURL),
|
|
36
|
+
ANTHROPIC_API_KEY: proxy.authToken
|
|
12
37
|
};
|
|
38
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
13
39
|
if (proxy.enableModelDiscovery) {
|
|
14
40
|
env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
|
|
15
41
|
}
|
|
16
42
|
if (proxy.defaultModel) {
|
|
43
|
+
const mapping = proxy.modelMapping ?? defaultClaudeModelMapping(proxy.defaultModel);
|
|
17
44
|
env.ANTHROPIC_MODEL = proxy.defaultModel;
|
|
45
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = mapping.opus;
|
|
46
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = mapping.sonnet;
|
|
47
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = mapping.haiku;
|
|
48
|
+
env.CLAUDE_CODE_SUBAGENT_MODEL = mapping.subagent;
|
|
18
49
|
}
|
|
19
|
-
|
|
50
|
+
const next = {
|
|
20
51
|
...settings,
|
|
21
52
|
env
|
|
22
53
|
};
|
|
54
|
+
if (proxy.defaultModel) {
|
|
55
|
+
const defaultModel = proxy.defaultModel;
|
|
56
|
+
const mapping = proxy.modelMapping ?? defaultClaudeModelMapping(defaultModel);
|
|
57
|
+
next.model = defaultModel;
|
|
58
|
+
next.modelOverrides = {
|
|
59
|
+
...(isRecord(settings.modelOverrides) ? stringifyRecord(settings.modelOverrides) : {}),
|
|
60
|
+
...modelOverridesForMapping(mapping)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const availableModels = uniqueStrings([
|
|
64
|
+
...(Array.isArray(settings.availableModels) ? settings.availableModels.map(String) : []),
|
|
65
|
+
...(proxy.models ?? []),
|
|
66
|
+
...(proxy.defaultModel ? [proxy.defaultModel] : [])
|
|
67
|
+
]);
|
|
68
|
+
if (availableModels.length > 0) {
|
|
69
|
+
next.availableModels = availableModels;
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
export function defaultClaudeModelMapping(defaultModel) {
|
|
74
|
+
return {
|
|
75
|
+
opus: defaultModel,
|
|
76
|
+
sonnet: defaultModel,
|
|
77
|
+
haiku: defaultModel,
|
|
78
|
+
subagent: defaultModel
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function normalizeClaudeCodeBaseURL(baseURL) {
|
|
82
|
+
return baseURL.trim().replace(/\/+$/, "").replace(/\/v1$/i, "");
|
|
23
83
|
}
|
|
24
84
|
export async function writeClaudeCodeSettings(input) {
|
|
25
85
|
const targetPath = input.targetPath ?? getDefaultClaudeSettingsPath();
|
|
@@ -46,6 +106,16 @@ async function fileExists(path) {
|
|
|
46
106
|
function stringifyRecord(record) {
|
|
47
107
|
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, String(value)]));
|
|
48
108
|
}
|
|
109
|
+
function uniqueStrings(values) {
|
|
110
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
111
|
+
}
|
|
112
|
+
function modelOverridesForMapping(mapping) {
|
|
113
|
+
return {
|
|
114
|
+
...Object.fromEntries(OPUS_MODEL_OVERRIDE_KEYS.map((model) => [model, mapping.opus])),
|
|
115
|
+
...Object.fromEntries(SONNET_MODEL_OVERRIDE_KEYS.map((model) => [model, mapping.sonnet])),
|
|
116
|
+
...Object.fromEntries(HAIKU_MODEL_OVERRIDE_KEYS.map((model) => [model, mapping.haiku]))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
49
119
|
function isRecord(value) {
|
|
50
120
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
121
|
}
|
package/package.json
CHANGED