@heventure/model-provider-x 0.2.0 → 0.2.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/README.md +32 -4
- package/dist/cli/args.js +18 -1
- package/dist/cli/commands.js +1 -1
- package/dist/cli/index.js +148 -16
- package/dist/core/provider.js +55 -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,21 @@ 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
|
+
|
|
59
|
+
When targeting OpenCode, choose the direct API type interactively or pass `--opencode-api`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node dist/cli/index.js setup --target opencode --provider lmstudio --direct --opencode-api responses
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Supported values are:
|
|
66
|
+
|
|
67
|
+
- `chat`: writes `npm: "@ai-sdk/openai-compatible"` for `/v1/chat/completions`.
|
|
68
|
+
- `responses`: writes `npm: "@ai-sdk/openai"` for `/v1/responses`.
|
|
69
|
+
- `messages`: writes `npm: "@ai-sdk/anthropic"` for Anthropic-compatible `/v1/messages`.
|
|
70
|
+
|
|
46
71
|
## Claude Code Setup
|
|
47
72
|
|
|
48
73
|
Create or update a provider profile and write Claude Code user settings:
|
|
@@ -61,6 +86,9 @@ node dist/cli/index.js proxy up --profile unsloth
|
|
|
61
86
|
```
|
|
62
87
|
|
|
63
88
|
The Claude Code setup writes gateway environment values to `~/.claude/settings.json`.
|
|
89
|
+
Claude Code appends `/v1/messages` itself, so direct Anthropic-compatible provider URLs are written without a trailing `/v1`.
|
|
90
|
+
For example, entering `http://localhost:1234/v1` for LM Studio writes `ANTHROPIC_BASE_URL=http://localhost:1234`.
|
|
91
|
+
The setup uses `ANTHROPIC_API_KEY` and removes stale `ANTHROPIC_AUTH_TOKEN` values to avoid Claude Code auth conflicts.
|
|
64
92
|
Upstream provider keys are stored in `~/.config/model-provider-x/config.jsonc`, not in Claude Code settings.
|
|
65
93
|
|
|
66
94
|
## Codex Setup
|
|
@@ -82,9 +110,9 @@ The Codex setup writes a Responses-compatible provider to `~/.codex/config.toml`
|
|
|
82
110
|
It also configures command-backed authentication so Codex can fetch the local proxy token automatically.
|
|
83
111
|
The proxy currently supports non-streaming `/v1/responses` requests and forwards them to upstream OpenAI-compatible `/v1/chat/completions`.
|
|
84
112
|
|
|
85
|
-
##
|
|
113
|
+
## Setup Modes
|
|
86
114
|
|
|
87
|
-
|
|
115
|
+
You can also run the unified setup wizard through the explicit setup command:
|
|
88
116
|
|
|
89
117
|
```bash
|
|
90
118
|
node dist/cli/index.js setup --provider lmstudio
|
package/dist/cli/args.js
CHANGED
|
@@ -35,6 +35,9 @@ export function parseCliArgs(argv) {
|
|
|
35
35
|
case "--provider":
|
|
36
36
|
options.providerPreset = next();
|
|
37
37
|
break;
|
|
38
|
+
case "--opencode-api":
|
|
39
|
+
options.opencodeApiType = parseOpenCodeApiType(next());
|
|
40
|
+
break;
|
|
38
41
|
case "--proxy":
|
|
39
42
|
options.proxy = true;
|
|
40
43
|
break;
|
|
@@ -92,6 +95,7 @@ export function usage() {
|
|
|
92
95
|
|
|
93
96
|
Usage:
|
|
94
97
|
model-provider-x [options]
|
|
98
|
+
model-provider-x setup [options]
|
|
95
99
|
|
|
96
100
|
Options:
|
|
97
101
|
--base-url <url> OpenAI-compatible API base URL, for example http://localhost:8888/v1
|
|
@@ -99,10 +103,11 @@ Options:
|
|
|
99
103
|
--name <name> Provider display name.
|
|
100
104
|
--id <id> Provider id used under provider.<id>.
|
|
101
105
|
--provider <id> Use a built-in provider preset, for example lmstudio or openai.
|
|
106
|
+
--opencode-api <api> OpenCode API type: chat, responses, or messages.
|
|
102
107
|
--proxy Write agent config through the local compatibility proxy.
|
|
103
108
|
--direct Write agent config directly to the upstream provider.
|
|
104
109
|
--models <list> Comma-separated model ids. Skips interactive model selection.
|
|
105
|
-
--config <path> OpenCode config path to write.
|
|
110
|
+
--config <path> OpenCode config path to write when targeting OpenCode.
|
|
106
111
|
--print Print generated JSON and do not write config.
|
|
107
112
|
--yes, -y Accept defaults in non-interactive prompts.
|
|
108
113
|
--help, -h Show this help.
|
|
@@ -110,6 +115,18 @@ Options:
|
|
|
110
115
|
}
|
|
111
116
|
export class HelpRequested extends Error {
|
|
112
117
|
}
|
|
118
|
+
function parseOpenCodeApiType(value) {
|
|
119
|
+
if (value === "chat" || value === "chat-completions" || value === "openai-compatible") {
|
|
120
|
+
return "chat";
|
|
121
|
+
}
|
|
122
|
+
if (value === "responses" || value === "openai-responses") {
|
|
123
|
+
return "responses";
|
|
124
|
+
}
|
|
125
|
+
if (value === "messages" || value === "anthropic-messages") {
|
|
126
|
+
return "messages";
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Unknown OpenCode API type: ${value}`);
|
|
129
|
+
}
|
|
113
130
|
function addModelByOneBasedIndex(selected, models, index) {
|
|
114
131
|
const model = models[index - 1];
|
|
115
132
|
if (!model) {
|
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
|
}
|
|
@@ -77,7 +81,8 @@ export async function runCli(options) {
|
|
|
77
81
|
providerName,
|
|
78
82
|
baseURL: fetched.baseURL,
|
|
79
83
|
apiKey,
|
|
80
|
-
models: selectedModels
|
|
84
|
+
models: selectedModels,
|
|
85
|
+
opencodeApiType: options.opencodeApiType ?? "chat"
|
|
81
86
|
});
|
|
82
87
|
const provider = fragment.provider[providerId];
|
|
83
88
|
const json = JSON.stringify(fragment, null, 2);
|
|
@@ -110,18 +115,18 @@ async function runSetup(command) {
|
|
|
110
115
|
const providerInput = await collectProviderInput(rl, command);
|
|
111
116
|
output.write("Detecting provider capabilities...\n");
|
|
112
117
|
const capabilities = await detectProviderCapabilities({ baseURL: providerInput.baseURL, apiKey: providerInput.apiKey });
|
|
118
|
+
const selection = await collectProviderSelection(rl, command, providerInput);
|
|
113
119
|
const target = await resolveSetupTarget(rl, command.target);
|
|
114
120
|
const useProxy = await resolveProxyMode(rl, command.options, capabilities, target);
|
|
115
|
-
const selection = await collectProviderSelection(rl, command, target, providerInput);
|
|
116
121
|
if (target === "opencode") {
|
|
117
|
-
await writeOpenCodeSetup(rl, command, selection, useProxy);
|
|
122
|
+
await writeOpenCodeSetup(rl, command, selection, useProxy, capabilities);
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
125
|
if (target === "codex") {
|
|
121
126
|
await writeCodexSetup(command, selection, useProxy);
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
|
-
await writeClaudeCodeSetup(command, selection, useProxy);
|
|
129
|
+
await writeClaudeCodeSetup(rl, command, selection, useProxy);
|
|
125
130
|
}
|
|
126
131
|
finally {
|
|
127
132
|
rl.close();
|
|
@@ -135,18 +140,14 @@ async function collectProviderInput(rl, command) {
|
|
|
135
140
|
const apiKey = await resolveApiKey(rl, command.options.apiKey, providerDefaults.preset, "Upstream API key");
|
|
136
141
|
return { providerId, providerName, baseURL, apiKey };
|
|
137
142
|
}
|
|
138
|
-
async function collectProviderSelection(rl, command,
|
|
143
|
+
async function collectProviderSelection(rl, command, providerInput) {
|
|
139
144
|
output.write("Fetching models...\n");
|
|
140
145
|
const fetched = await validateAndFetchModels({ baseURL: providerInput.baseURL, apiKey: providerInput.apiKey });
|
|
141
146
|
const selectedModels = command.options.models ??
|
|
142
147
|
(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))
|
|
148
|
+
? await multiSelectChoices("Select models", createModelChoices(fetched.models))
|
|
148
149
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
149
|
-
const defaultModel = command
|
|
150
|
+
const defaultModel = await resolveDefaultModel(rl, command, selectedModels);
|
|
150
151
|
if (!defaultModel) {
|
|
151
152
|
throw new Error("Select at least one model");
|
|
152
153
|
}
|
|
@@ -172,15 +173,17 @@ async function collectProviderSelection(rl, command, target, providerInput) {
|
|
|
172
173
|
toolConfigPath
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
|
-
async function writeOpenCodeSetup(rl, command, selection, useProxy) {
|
|
176
|
+
async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities) {
|
|
176
177
|
const baseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}/v1` : selection.upstreamBaseURL;
|
|
177
178
|
const apiKey = useProxy ? selection.config.proxy.authToken : selection.apiKey;
|
|
179
|
+
const opencodeApiType = await resolveOpenCodeApiType(rl, command, capabilities, useProxy);
|
|
178
180
|
const fragment = buildProviderConfig({
|
|
179
181
|
providerId: selection.providerId,
|
|
180
182
|
providerName: selection.providerName,
|
|
181
183
|
baseURL,
|
|
182
184
|
apiKey,
|
|
183
|
-
models: selection.selectedModels
|
|
185
|
+
models: selection.selectedModels,
|
|
186
|
+
opencodeApiType
|
|
184
187
|
});
|
|
185
188
|
const provider = fragment.provider[selection.providerId];
|
|
186
189
|
const targetPath = command.options.configPath ?? (await chooseConfigPath(rl, selection.providerId, command.options.yes));
|
|
@@ -204,15 +207,18 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy) {
|
|
|
204
207
|
output.write(`Started proxy: ${proxy.baseURL}\n`);
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
|
-
async function writeClaudeCodeSetup(command, selection, useProxy) {
|
|
210
|
+
async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
|
|
208
211
|
const proxyBaseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}` : selection.upstreamBaseURL;
|
|
212
|
+
const modelMapping = await resolveClaudeModelMapping(rl, command, selection);
|
|
209
213
|
const result = await writeClaudeCodeSettings({
|
|
210
214
|
targetPath: getDefaultClaudeSettingsPath(),
|
|
211
215
|
proxy: {
|
|
212
216
|
baseURL: proxyBaseURL,
|
|
213
217
|
authToken: useProxy ? selection.config.proxy.authToken : selection.apiKey,
|
|
214
218
|
enableModelDiscovery: useProxy,
|
|
215
|
-
defaultModel:
|
|
219
|
+
defaultModel: selection.defaultModel,
|
|
220
|
+
models: selection.selectedModels,
|
|
221
|
+
modelMapping
|
|
216
222
|
}
|
|
217
223
|
});
|
|
218
224
|
output.write(`Saved profile ${selection.providerId} to ${selection.toolConfigPath}\n`);
|
|
@@ -359,6 +365,132 @@ async function resolveProxyMode(rl, options, capabilities, target) {
|
|
|
359
365
|
const accepted = !answer.trim() || answer.trim().toLowerCase() === "y";
|
|
360
366
|
return recommendedProxy ? accepted : !accepted;
|
|
361
367
|
}
|
|
368
|
+
async function resolveOpenCodeApiType(rl, command, capabilities, useProxy) {
|
|
369
|
+
if (command.options.opencodeApiType) {
|
|
370
|
+
return command.options.opencodeApiType;
|
|
371
|
+
}
|
|
372
|
+
const availableApis = useProxy ? new Set(["openai-compatible", "openai-responses", "anthropic-messages"]) : new Set(capabilities.apis);
|
|
373
|
+
const recommended = recommendedOpenCodeApiType(availableApis);
|
|
374
|
+
if (command.options.yes) {
|
|
375
|
+
return recommended;
|
|
376
|
+
}
|
|
377
|
+
const choices = [
|
|
378
|
+
{
|
|
379
|
+
label: "Responses API",
|
|
380
|
+
value: "responses",
|
|
381
|
+
hint: choiceHint("openai-responses", recommended, "responses", availableApis)
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
label: "Chat Completions API",
|
|
385
|
+
value: "chat",
|
|
386
|
+
hint: choiceHint("openai-compatible", recommended, "chat", availableApis)
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
label: "Anthropic Messages API",
|
|
390
|
+
value: "messages",
|
|
391
|
+
hint: choiceHint("anthropic-messages", recommended, "messages", availableApis)
|
|
392
|
+
}
|
|
393
|
+
];
|
|
394
|
+
if (canUseTui()) {
|
|
395
|
+
return selectChoice("OpenCode API type", choices);
|
|
396
|
+
}
|
|
397
|
+
output.write("OpenCode API types:\n");
|
|
398
|
+
choices.forEach((choice, index) => {
|
|
399
|
+
output.write(`${index + 1}. ${choice.label}${choice.hint ? ` - ${choice.hint}` : ""}\n`);
|
|
400
|
+
});
|
|
401
|
+
const answer = await rl.question(`OpenCode API type [${recommended}]: `);
|
|
402
|
+
const value = answer.trim();
|
|
403
|
+
if (!value) {
|
|
404
|
+
return recommended;
|
|
405
|
+
}
|
|
406
|
+
if (value === "1" || value === "responses") {
|
|
407
|
+
return "responses";
|
|
408
|
+
}
|
|
409
|
+
if (value === "2" || value === "chat" || value === "chat-completions") {
|
|
410
|
+
return "chat";
|
|
411
|
+
}
|
|
412
|
+
if (value === "3" || value === "messages") {
|
|
413
|
+
return "messages";
|
|
414
|
+
}
|
|
415
|
+
throw new Error(`Unknown OpenCode API type: ${value}`);
|
|
416
|
+
}
|
|
417
|
+
function recommendedOpenCodeApiType(apis) {
|
|
418
|
+
if (apis.has("openai-responses")) {
|
|
419
|
+
return "responses";
|
|
420
|
+
}
|
|
421
|
+
if (apis.has("openai-compatible")) {
|
|
422
|
+
return "chat";
|
|
423
|
+
}
|
|
424
|
+
if (apis.has("anthropic-messages")) {
|
|
425
|
+
return "messages";
|
|
426
|
+
}
|
|
427
|
+
return "chat";
|
|
428
|
+
}
|
|
429
|
+
function choiceHint(api, recommended, value, availableApis) {
|
|
430
|
+
const hints = [];
|
|
431
|
+
if (value === recommended) {
|
|
432
|
+
hints.push("recommended");
|
|
433
|
+
}
|
|
434
|
+
if (!availableApis.has(api)) {
|
|
435
|
+
hints.push("not detected");
|
|
436
|
+
}
|
|
437
|
+
return hints.join(", ");
|
|
438
|
+
}
|
|
439
|
+
async function resolveDefaultModel(rl, command, selectedModels) {
|
|
440
|
+
if (command.defaultModel) {
|
|
441
|
+
if (!selectedModels.includes(command.defaultModel)) {
|
|
442
|
+
throw new Error(`Default model is not in the selected model list: ${command.defaultModel}`);
|
|
443
|
+
}
|
|
444
|
+
return command.defaultModel;
|
|
445
|
+
}
|
|
446
|
+
if (selectedModels.length === 0) {
|
|
447
|
+
throw new Error("Select at least one model");
|
|
448
|
+
}
|
|
449
|
+
if (selectedModels.length === 1 || command.options.yes) {
|
|
450
|
+
return selectedModels[0];
|
|
451
|
+
}
|
|
452
|
+
if (canUseTui()) {
|
|
453
|
+
return selectChoice("Choose default model", selectedModels.map((model) => ({ label: model, value: model })));
|
|
454
|
+
}
|
|
455
|
+
const answer = await rl.question(`Default model [${selectedModels[0]}]: `);
|
|
456
|
+
const model = answer.trim() || selectedModels[0];
|
|
457
|
+
if (!selectedModels.includes(model)) {
|
|
458
|
+
throw new Error(`Default model is not in the selected model list: ${model}`);
|
|
459
|
+
}
|
|
460
|
+
return model;
|
|
461
|
+
}
|
|
462
|
+
async function resolveClaudeModelMapping(rl, command, selection) {
|
|
463
|
+
const defaultMapping = defaultClaudeModelMapping(selection.defaultModel);
|
|
464
|
+
if (selection.selectedModels.length <= 1 || command.options.yes) {
|
|
465
|
+
return defaultMapping;
|
|
466
|
+
}
|
|
467
|
+
const customize = canUseTui()
|
|
468
|
+
? await selectChoice("Claude Code model mapping", [
|
|
469
|
+
{ label: "Use default model for Opus, Sonnet, Haiku", value: false, hint: "recommended" },
|
|
470
|
+
{ label: "Customize Opus, Sonnet, Haiku models", value: true }
|
|
471
|
+
])
|
|
472
|
+
: (await rl.question("Use default model for Opus, Sonnet, Haiku? [Y/n] ")).trim().toLowerCase() === "n";
|
|
473
|
+
if (!customize) {
|
|
474
|
+
return defaultMapping;
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
opus: await selectModelForRole(rl, "Opus model", selection.selectedModels, selection.defaultModel),
|
|
478
|
+
sonnet: await selectModelForRole(rl, "Sonnet model", selection.selectedModels, selection.defaultModel),
|
|
479
|
+
haiku: await selectModelForRole(rl, "Haiku model", selection.selectedModels, selection.defaultModel),
|
|
480
|
+
subagent: await selectModelForRole(rl, "Subagent model", selection.selectedModels, selection.defaultModel)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async function selectModelForRole(rl, label, models, defaultModel) {
|
|
484
|
+
if (canUseTui()) {
|
|
485
|
+
return selectChoice(label, models.map((model) => ({ label: model, value: model, hint: model === defaultModel ? "default" : undefined })));
|
|
486
|
+
}
|
|
487
|
+
const answer = await rl.question(`${label} [${defaultModel}]: `);
|
|
488
|
+
const model = answer.trim() || defaultModel;
|
|
489
|
+
if (!models.includes(model)) {
|
|
490
|
+
throw new Error(`${label} is not in the selected model list: ${model}`);
|
|
491
|
+
}
|
|
492
|
+
return model;
|
|
493
|
+
}
|
|
362
494
|
async function resolveSetupTarget(rl, target) {
|
|
363
495
|
if (target) {
|
|
364
496
|
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] };
|
|
@@ -71,14 +71,18 @@ export function recommendProxyMode(capabilities, target) {
|
|
|
71
71
|
export function buildProviderConfig(input) {
|
|
72
72
|
const baseURL = normalizeBaseUrl(input.baseURL);
|
|
73
73
|
const apiKey = input.apiKey?.trim();
|
|
74
|
+
const opencodeApiType = input.opencodeApiType ?? "chat";
|
|
74
75
|
const provider = {
|
|
75
|
-
npm:
|
|
76
|
+
npm: npmPackageForOpenCodeApiType(opencodeApiType),
|
|
76
77
|
name: input.providerName.trim(),
|
|
77
78
|
options: {
|
|
78
79
|
baseURL
|
|
79
80
|
},
|
|
80
81
|
models: Object.fromEntries(input.models.map((model) => [model, { name: model }]))
|
|
81
82
|
};
|
|
83
|
+
if (opencodeApiType !== "messages") {
|
|
84
|
+
provider.options.setCacheKey = true;
|
|
85
|
+
}
|
|
82
86
|
if (apiKey) {
|
|
83
87
|
provider.options.apiKey = apiKey;
|
|
84
88
|
}
|
|
@@ -89,6 +93,15 @@ export function buildProviderConfig(input) {
|
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
95
|
}
|
|
96
|
+
export function npmPackageForOpenCodeApiType(apiType) {
|
|
97
|
+
if (apiType === "responses") {
|
|
98
|
+
return "@ai-sdk/openai";
|
|
99
|
+
}
|
|
100
|
+
if (apiType === "messages") {
|
|
101
|
+
return "@ai-sdk/anthropic";
|
|
102
|
+
}
|
|
103
|
+
return "@ai-sdk/openai-compatible";
|
|
104
|
+
}
|
|
92
105
|
function isModelListResponse(body) {
|
|
93
106
|
return (typeof body === "object" &&
|
|
94
107
|
body !== null &&
|
|
@@ -104,9 +117,48 @@ function isOpenCodeCompatibleModel(model) {
|
|
|
104
117
|
async function probe(input, init, fetchImpl) {
|
|
105
118
|
try {
|
|
106
119
|
const response = await fetchImpl(input, init);
|
|
107
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
reachable: response.ok || response.status === 400 || response.status === 401 || response.status === 422,
|
|
122
|
+
response
|
|
123
|
+
};
|
|
108
124
|
}
|
|
109
125
|
catch {
|
|
110
126
|
return { reachable: false };
|
|
111
127
|
}
|
|
112
128
|
}
|
|
129
|
+
async function isAnthropicMessagesProbe(response) {
|
|
130
|
+
if (!response) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const status = response.status ?? (response.ok ? 200 : undefined);
|
|
134
|
+
if (status === 404 || status === undefined) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (!response.json) {
|
|
138
|
+
return !response.ok && (status === 400 || status === 401 || status === 422);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const body = await response.json();
|
|
142
|
+
if (isAnthropicMessageResponse(body) || isAnthropicErrorResponse(body)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return !response.ok && (status === 400 || status === 401 || status === 422);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return !response.ok && (status === 400 || status === 401 || status === 422);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function isAnthropicMessageResponse(body) {
|
|
152
|
+
return (typeof body === "object" &&
|
|
153
|
+
body !== null &&
|
|
154
|
+
body.type === "message" &&
|
|
155
|
+
typeof body.role === "string" &&
|
|
156
|
+
Array.isArray(body.content));
|
|
157
|
+
}
|
|
158
|
+
function isAnthropicErrorResponse(body) {
|
|
159
|
+
return (typeof body === "object" &&
|
|
160
|
+
body !== null &&
|
|
161
|
+
body.type === "error" &&
|
|
162
|
+
typeof body.error === "object" &&
|
|
163
|
+
body.error !== null);
|
|
164
|
+
}
|
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