@jacobbd/relay-ai 0.2.7 → 0.2.8
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/.gemini/xai-models.json +165 -0
- package/CHANGELOG.md +35 -0
- package/README.md +8 -0
- package/assets/demo-part1-thumbnail.png +0 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +13654 -0
- package/coverage/coverage-final.json +106 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +221 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/scripts/index.html +116 -0
- package/coverage/scripts/refresh-models-dev-cache.mjs.html +187 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/cli.js +1031 -277
- package/package.json +8 -3
- package/vitest.log +33 -0
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,94 @@ import { wrapLanguageModel, extractReasoningMiddleware } from "ai";
|
|
|
31
31
|
// src/constants.ts
|
|
32
32
|
import { homedir } from "os";
|
|
33
33
|
import { join } from "path";
|
|
34
|
+
|
|
35
|
+
// package.json
|
|
36
|
+
var package_default = {
|
|
37
|
+
name: "@jacobbd/relay-ai",
|
|
38
|
+
version: "0.2.8",
|
|
39
|
+
publishConfig: {
|
|
40
|
+
access: "public"
|
|
41
|
+
},
|
|
42
|
+
description: "Relay any model into any coding agent \u2014 launch Claude Code, Codex, and more with multi-provider gateways",
|
|
43
|
+
author: "jacob-bd",
|
|
44
|
+
license: "MIT",
|
|
45
|
+
repository: {
|
|
46
|
+
type: "git",
|
|
47
|
+
url: "git+https://github.com/jacob-bd/relay-ai.git"
|
|
48
|
+
},
|
|
49
|
+
homepage: "https://github.com/jacob-bd/relay-ai#readme",
|
|
50
|
+
keywords: [
|
|
51
|
+
"claude",
|
|
52
|
+
"claude-code",
|
|
53
|
+
"codex",
|
|
54
|
+
"ai",
|
|
55
|
+
"llm",
|
|
56
|
+
"cli",
|
|
57
|
+
"gateway",
|
|
58
|
+
"relay",
|
|
59
|
+
"vertex"
|
|
60
|
+
],
|
|
61
|
+
type: "module",
|
|
62
|
+
bin: {
|
|
63
|
+
"relay-ai": "dist/cli.js"
|
|
64
|
+
},
|
|
65
|
+
engines: {
|
|
66
|
+
node: ">=18"
|
|
67
|
+
},
|
|
68
|
+
scripts: {
|
|
69
|
+
build: "tsup",
|
|
70
|
+
dev: "tsup --watch",
|
|
71
|
+
test: "vitest run",
|
|
72
|
+
"test:watch": "vitest",
|
|
73
|
+
typecheck: "tsc --noEmit",
|
|
74
|
+
"refresh:models-dev": "node scripts/refresh-models-dev-cache.mjs",
|
|
75
|
+
prepublishOnly: `node -e "if (require('./package.json').version !== require('./package-lock.json').version) { console.error('Error: package.json and package-lock.json versions are out of sync! Run npm install to sync.'); process.exit(1); }" && npm run build`
|
|
76
|
+
},
|
|
77
|
+
dependencies: {
|
|
78
|
+
"@ai-sdk/alibaba": "^1.0.26",
|
|
79
|
+
"@ai-sdk/amazon-bedrock": "^4.0.113",
|
|
80
|
+
"@ai-sdk/azure": "^3.0.70",
|
|
81
|
+
"@ai-sdk/cerebras": "^2.0.54",
|
|
82
|
+
"@ai-sdk/cohere": "^3.0.36",
|
|
83
|
+
"@ai-sdk/deepinfra": "^2.0.52",
|
|
84
|
+
"@ai-sdk/gateway": "^3.0.125",
|
|
85
|
+
"@ai-sdk/google": "^3.0.80",
|
|
86
|
+
"@ai-sdk/google-vertex": "^4.0.142",
|
|
87
|
+
"@ai-sdk/groq": "^3.0.39",
|
|
88
|
+
"@ai-sdk/mistral": "^3.0.37",
|
|
89
|
+
"@ai-sdk/openai": "^3.0.68",
|
|
90
|
+
"@ai-sdk/openai-compatible": "^2.0.48",
|
|
91
|
+
"@ai-sdk/perplexity": "^3.0.33",
|
|
92
|
+
"@ai-sdk/togetherai": "^2.0.53",
|
|
93
|
+
"@ai-sdk/vercel": "^2.0.50",
|
|
94
|
+
"@ai-sdk/xai": "^3.0.93",
|
|
95
|
+
"@clack/prompts": "^0.9.1",
|
|
96
|
+
"@openrouter/ai-sdk-provider": "^2.9.0",
|
|
97
|
+
ai: "^6.0.197",
|
|
98
|
+
"gitlab-ai-provider": "^6.8.0",
|
|
99
|
+
"ipaddr.js": "^2.4.0",
|
|
100
|
+
open: "^11.0.0",
|
|
101
|
+
picocolors: "^1.1.1",
|
|
102
|
+
"smol-toml": "^1.6.1",
|
|
103
|
+
"venice-ai-sdk-provider": "^2.0.2",
|
|
104
|
+
zod: "^3.25.76"
|
|
105
|
+
},
|
|
106
|
+
devDependencies: {
|
|
107
|
+
"@types/node": "^22.0.0",
|
|
108
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
109
|
+
tsup: "^8.0.0",
|
|
110
|
+
typescript: "^5.5.0",
|
|
111
|
+
vitest: "^2.0.0"
|
|
112
|
+
},
|
|
113
|
+
optionalDependencies: {
|
|
114
|
+
"@napi-rs/keyring": "^1.3.0"
|
|
115
|
+
},
|
|
116
|
+
overrides: {
|
|
117
|
+
ws: "^8.21.0"
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/constants.ts
|
|
34
122
|
var BACKENDS = {
|
|
35
123
|
zen: {
|
|
36
124
|
id: "zen",
|
|
@@ -76,7 +164,7 @@ function classifyModelFormat(modelId, providerNpm) {
|
|
|
76
164
|
if (lower.startsWith("gemini-")) return "unsupported";
|
|
77
165
|
return "openai";
|
|
78
166
|
}
|
|
79
|
-
var VERSION =
|
|
167
|
+
var VERSION = package_default.version;
|
|
80
168
|
|
|
81
169
|
// src/oauth/pkce.ts
|
|
82
170
|
function positiveSecondsToMs(value, defaultMs) {
|
|
@@ -889,6 +977,7 @@ function providerTagColor(providerId) {
|
|
|
889
977
|
case "openai":
|
|
890
978
|
return pc.white;
|
|
891
979
|
case "xai":
|
|
980
|
+
case "xai-oauth":
|
|
892
981
|
return pc.white;
|
|
893
982
|
case "groq":
|
|
894
983
|
return pc.red;
|
|
@@ -1050,6 +1139,20 @@ function findClaudeBinary() {
|
|
|
1050
1139
|
}
|
|
1051
1140
|
return null;
|
|
1052
1141
|
}
|
|
1142
|
+
function getInstalledClaudeVersion() {
|
|
1143
|
+
try {
|
|
1144
|
+
const claudePath = findClaudeBinary();
|
|
1145
|
+
if (!claudePath) return "2.1.183";
|
|
1146
|
+
const result = execSync(`${isWindows ? `"${claudePath}"` : claudePath} --version`, {
|
|
1147
|
+
encoding: "utf8",
|
|
1148
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1149
|
+
});
|
|
1150
|
+
const match = result.match(/(\d+\.\d+\.\d+)/);
|
|
1151
|
+
if (match) return match[1];
|
|
1152
|
+
} catch {
|
|
1153
|
+
}
|
|
1154
|
+
return "2.1.183";
|
|
1155
|
+
}
|
|
1053
1156
|
function buildClaudeArgs(model, extraArgs) {
|
|
1054
1157
|
return ["--model", model, ...extraArgs];
|
|
1055
1158
|
}
|
|
@@ -1334,7 +1437,7 @@ function accessTokenIsExpiring(token, skewMs = OAUTH_REFRESH_SKEW_MS) {
|
|
|
1334
1437
|
return false;
|
|
1335
1438
|
}
|
|
1336
1439
|
}
|
|
1337
|
-
var NATIVE_OAUTH_PROVIDER_IDS = ["xai", "openai", "github-copilot"];
|
|
1440
|
+
var NATIVE_OAUTH_PROVIDER_IDS = ["xai", "xai-oauth", "openai", "openai-oauth", "github-copilot"];
|
|
1338
1441
|
function supportsNativeOAuth(providerId) {
|
|
1339
1442
|
return NATIVE_OAUTH_PROVIDER_IDS.includes(providerId);
|
|
1340
1443
|
}
|
|
@@ -1554,8 +1657,7 @@ async function runXaiDeviceCodeFlow(onDeviceCode, opts) {
|
|
|
1554
1657
|
// src/oauth/refresh.ts
|
|
1555
1658
|
function oauthCredentialShouldRefresh(cred, providerId) {
|
|
1556
1659
|
if (oauthCredentialNeedsRefresh(cred)) return true;
|
|
1557
|
-
if (providerId
|
|
1558
|
-
if (providerId === "github-copilot" && accessTokenIsExpiring(cred.access)) return true;
|
|
1660
|
+
if (NATIVE_OAUTH_PROVIDER_IDS.includes(providerId) && accessTokenIsExpiring(cred.access)) return true;
|
|
1559
1661
|
return false;
|
|
1560
1662
|
}
|
|
1561
1663
|
async function refreshStoredOAuthCredential(providerId, cred) {
|
|
@@ -1563,9 +1665,9 @@ async function refreshStoredOAuthCredential(providerId, cred) {
|
|
|
1563
1665
|
throw new Error(`${providerId}: OAuth refresh token missing \u2014 run relay-ai providers auth ${providerId}`);
|
|
1564
1666
|
}
|
|
1565
1667
|
let tokens;
|
|
1566
|
-
if (providerId === "openai") {
|
|
1668
|
+
if (providerId === "openai" || providerId === "openai-oauth") {
|
|
1567
1669
|
tokens = await refreshOpenAiAccessToken(cred.refresh);
|
|
1568
|
-
} else if (providerId === "xai") {
|
|
1670
|
+
} else if (providerId === "xai" || providerId === "xai-oauth") {
|
|
1569
1671
|
tokens = await refreshXaiAccessToken(cred.refresh);
|
|
1570
1672
|
} else if (providerId === "github-copilot") {
|
|
1571
1673
|
tokens = await refreshGithubCopilotToken(cred.refresh);
|
|
@@ -2459,6 +2561,36 @@ function migrateLegacyCloudProviders(registry) {
|
|
|
2459
2561
|
}
|
|
2460
2562
|
return changed;
|
|
2461
2563
|
}
|
|
2564
|
+
function migrateOAuthOpenAiProvider(registry) {
|
|
2565
|
+
if (registry.providers.some((p19) => p19.id === "openai-oauth")) return false;
|
|
2566
|
+
const idx = registry.providers.findIndex(
|
|
2567
|
+
(p19) => p19.id === "openai" && p19.authType === "oauth"
|
|
2568
|
+
);
|
|
2569
|
+
if (idx < 0) return false;
|
|
2570
|
+
const existing = registry.providers[idx];
|
|
2571
|
+
registry.providers[idx] = {
|
|
2572
|
+
...existing,
|
|
2573
|
+
id: "openai-oauth",
|
|
2574
|
+
templateId: existing.templateId || "openai",
|
|
2575
|
+
name: existing.name === "OpenAI" ? "OpenAI (ChatGPT)" : existing.name
|
|
2576
|
+
};
|
|
2577
|
+
return true;
|
|
2578
|
+
}
|
|
2579
|
+
function migrateOAuthXaiProvider(registry) {
|
|
2580
|
+
if (registry.providers.some((p19) => p19.id === "xai-oauth")) return false;
|
|
2581
|
+
const idx = registry.providers.findIndex(
|
|
2582
|
+
(p19) => p19.id === "xai" && p19.authType === "oauth"
|
|
2583
|
+
);
|
|
2584
|
+
if (idx < 0) return false;
|
|
2585
|
+
const existing = registry.providers[idx];
|
|
2586
|
+
registry.providers[idx] = {
|
|
2587
|
+
...existing,
|
|
2588
|
+
id: "xai-oauth",
|
|
2589
|
+
templateId: existing.templateId || "xai",
|
|
2590
|
+
name: existing.name === "xAI" ? "xAI Grok (SuperGrok)" : existing.name
|
|
2591
|
+
};
|
|
2592
|
+
return true;
|
|
2593
|
+
}
|
|
2462
2594
|
|
|
2463
2595
|
// src/registry/validate.ts
|
|
2464
2596
|
var PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -2566,7 +2698,10 @@ function loadRegistry(path = getProvidersPath()) {
|
|
|
2566
2698
|
try {
|
|
2567
2699
|
const raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
2568
2700
|
const registry = parseRegistry(raw);
|
|
2569
|
-
|
|
2701
|
+
let migrated = migrateLegacyCloudProviders(registry);
|
|
2702
|
+
if (migrateOAuthOpenAiProvider(registry)) migrated = true;
|
|
2703
|
+
if (migrateOAuthXaiProvider(registry)) migrated = true;
|
|
2704
|
+
if (migrated) {
|
|
2570
2705
|
try {
|
|
2571
2706
|
saveRegistry(registry, path);
|
|
2572
2707
|
} catch {
|
|
@@ -2603,6 +2738,7 @@ var TEMPLATE_TO_PRICING_PLATFORM = {
|
|
|
2603
2738
|
cerebras: "cerebras",
|
|
2604
2739
|
deepinfra: "deepinfra",
|
|
2605
2740
|
xai: "xai",
|
|
2741
|
+
"xai-oauth": "xai",
|
|
2606
2742
|
perplexity: "perplexity",
|
|
2607
2743
|
cohere: "cohere",
|
|
2608
2744
|
openai: "openai",
|
|
@@ -2782,6 +2918,7 @@ var REGISTRY_TO_MODELS_DEV = {
|
|
|
2782
2918
|
cerebras: "cerebras",
|
|
2783
2919
|
deepinfra: "deepinfra",
|
|
2784
2920
|
xai: "xai",
|
|
2921
|
+
"xai-oauth": "xai",
|
|
2785
2922
|
perplexity: "perplexity",
|
|
2786
2923
|
cohere: "cohere",
|
|
2787
2924
|
alibaba: "alibaba",
|
|
@@ -3307,6 +3444,11 @@ async function resolveRefreshCredential(provider, resolveKey) {
|
|
|
3307
3444
|
function oauthAuthRef(providerId) {
|
|
3308
3445
|
return `keyring:oauth:provider:${providerId}`;
|
|
3309
3446
|
}
|
|
3447
|
+
function toOAuthRegistryId(id) {
|
|
3448
|
+
if (id === "openai") return "openai-oauth";
|
|
3449
|
+
if (id === "xai") return "xai-oauth";
|
|
3450
|
+
return id;
|
|
3451
|
+
}
|
|
3310
3452
|
function normalizeImportProviderIdentity(provider) {
|
|
3311
3453
|
if (provider.id === "opencode") {
|
|
3312
3454
|
return { ...provider, id: "zen", name: "OpenCode Zen" };
|
|
@@ -3336,8 +3478,10 @@ function buildImportProviderList(raw, authEntries) {
|
|
|
3336
3478
|
{ includeOAuthPlaceholders: true }
|
|
3337
3479
|
);
|
|
3338
3480
|
if (oauthProviders.length === 0) continue;
|
|
3339
|
-
|
|
3340
|
-
|
|
3481
|
+
const registryId = toOAuthRegistryId(provider.id);
|
|
3482
|
+
oauthByProviderId.set(registryId, authEntry);
|
|
3483
|
+
merged.push({ ...oauthProviders[0], id: registryId, apiKey: "" });
|
|
3484
|
+
covered.add(registryId);
|
|
3341
3485
|
covered.add(provider.id);
|
|
3342
3486
|
}
|
|
3343
3487
|
return { providers: merged, oauth: { oauthByProviderId } };
|
|
@@ -4772,13 +4916,13 @@ function upstreamModelId(model) {
|
|
|
4772
4916
|
const id = model.upstreamModelId ?? model.id;
|
|
4773
4917
|
return id.replace(/\[1m\]$/i, "");
|
|
4774
4918
|
}
|
|
4775
|
-
function
|
|
4919
|
+
function supportsDirectOpenAIChatCompletions(model) {
|
|
4776
4920
|
return model.modelFormat === "openai" && (!!model.completionsUrl || model.sourceBackend === "zen" || model.sourceBackend === "go");
|
|
4777
4921
|
}
|
|
4778
4922
|
function formatOpenAIModels(models) {
|
|
4779
4923
|
return {
|
|
4780
4924
|
object: "list",
|
|
4781
|
-
data: models.
|
|
4925
|
+
data: models.map((model) => ({
|
|
4782
4926
|
id: model.id,
|
|
4783
4927
|
object: "model",
|
|
4784
4928
|
created: CREATED_AT_UNIX,
|
|
@@ -4893,6 +5037,9 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
|
|
|
4893
5037
|
res.end(text5);
|
|
4894
5038
|
}
|
|
4895
5039
|
|
|
5040
|
+
// src/proxy.ts
|
|
5041
|
+
import { randomUUID } from "crypto";
|
|
5042
|
+
|
|
4896
5043
|
// src/sdk-adapter.ts
|
|
4897
5044
|
import { streamText, generateText, tool, jsonSchema } from "ai";
|
|
4898
5045
|
|
|
@@ -4957,9 +5104,9 @@ function serializeToolResultContent(content) {
|
|
|
4957
5104
|
|
|
4958
5105
|
// src/tool-search.ts
|
|
4959
5106
|
var TOOL_SEARCH_TYPE_PREFIX = "tool_search_tool";
|
|
4960
|
-
function isToolSearchTool(
|
|
4961
|
-
if (typeof
|
|
4962
|
-
const name =
|
|
5107
|
+
function isToolSearchTool(tool4) {
|
|
5108
|
+
if (typeof tool4.type === "string" && tool4.type.startsWith(TOOL_SEARCH_TYPE_PREFIX)) return true;
|
|
5109
|
+
const name = tool4.name ?? "";
|
|
4963
5110
|
return name.includes("tool_search") || name === "ToolSearch";
|
|
4964
5111
|
}
|
|
4965
5112
|
function extractReferencedToolNames(messages) {
|
|
@@ -4998,16 +5145,16 @@ function resolveUpstreamTools(tools, messages) {
|
|
|
4998
5145
|
if (!tools?.length) return [];
|
|
4999
5146
|
const referenced = extractReferencedToolNames(messages);
|
|
5000
5147
|
const upstream = [];
|
|
5001
|
-
for (const
|
|
5002
|
-
if (isToolSearchTool(
|
|
5003
|
-
upstream.push(
|
|
5148
|
+
for (const tool4 of tools) {
|
|
5149
|
+
if (isToolSearchTool(tool4)) {
|
|
5150
|
+
upstream.push(tool4);
|
|
5004
5151
|
continue;
|
|
5005
5152
|
}
|
|
5006
|
-
if (
|
|
5007
|
-
if (referenced.has(
|
|
5153
|
+
if (tool4.defer_loading === true) {
|
|
5154
|
+
if (referenced.has(tool4.name)) upstream.push(tool4);
|
|
5008
5155
|
continue;
|
|
5009
5156
|
}
|
|
5010
|
-
upstream.push(
|
|
5157
|
+
upstream.push(tool4);
|
|
5011
5158
|
}
|
|
5012
5159
|
return upstream;
|
|
5013
5160
|
}
|
|
@@ -5149,13 +5296,15 @@ function translateRequest(body, npm, options) {
|
|
|
5149
5296
|
messages
|
|
5150
5297
|
);
|
|
5151
5298
|
const effort = anthropicEffortFromRequest(body) ?? options?.defaultEffort;
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
effortProviderOptions(npm, effort, body.model, options?.reasoningMetadata)
|
|
5156
|
-
),
|
|
5157
|
-
options?.openAiOAuth && systemText ? { openai: { instructions: systemText } } : void 0
|
|
5299
|
+
let providerOptions = deepMergeProviderOptions(
|
|
5300
|
+
thinkingProviderOptions(npm),
|
|
5301
|
+
effortProviderOptions(npm, effort, body.model, options?.reasoningMetadata)
|
|
5158
5302
|
);
|
|
5303
|
+
if (options?.openAiOAuth && systemText) {
|
|
5304
|
+
providerOptions = deepMergeProviderOptions(providerOptions, {
|
|
5305
|
+
openai: { instructions: systemText }
|
|
5306
|
+
});
|
|
5307
|
+
}
|
|
5159
5308
|
return {
|
|
5160
5309
|
system: options?.openAiOAuth ? void 0 : systemText,
|
|
5161
5310
|
messages: translateMessages(messages, npm),
|
|
@@ -5395,6 +5544,7 @@ function lookupRoute(byAlias, id) {
|
|
|
5395
5544
|
return void 0;
|
|
5396
5545
|
}
|
|
5397
5546
|
function startProxyCatalog(routes, defaultAliasId, debug = false) {
|
|
5547
|
+
const proxyToken = randomUUID();
|
|
5398
5548
|
silenceSdkWarnings();
|
|
5399
5549
|
if (routes.length === 0) {
|
|
5400
5550
|
return Promise.reject(new Error("Proxy catalog requires at least one route"));
|
|
@@ -5442,6 +5592,10 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
|
|
|
5442
5592
|
}
|
|
5443
5593
|
if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
|
|
5444
5594
|
const inboundKey = extractApiKey(req);
|
|
5595
|
+
if (inboundKey !== proxyToken) {
|
|
5596
|
+
anthropicError(res, 401, "Invalid proxy token");
|
|
5597
|
+
return;
|
|
5598
|
+
}
|
|
5445
5599
|
let anthropicBody;
|
|
5446
5600
|
try {
|
|
5447
5601
|
const raw = await readBody(req);
|
|
@@ -5453,7 +5607,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
|
|
|
5453
5607
|
const originalModel = anthropicBody.model;
|
|
5454
5608
|
const clientWantsStream = Boolean(anthropicBody.stream);
|
|
5455
5609
|
const route = lookupRoute(byAlias, originalModel) ?? defaultRoute;
|
|
5456
|
-
const apiKey = route.apiKey
|
|
5610
|
+
const apiKey = route.apiKey;
|
|
5457
5611
|
const upstreamUrl = route.upstreamUrl;
|
|
5458
5612
|
plog(
|
|
5459
5613
|
() => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${apiKey ? `len:${apiKey.length}` : "MISSING"}`
|
|
@@ -5515,7 +5669,8 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
|
|
|
5515
5669
|
}
|
|
5516
5670
|
} catch (err) {
|
|
5517
5671
|
const message = err instanceof Error ? err.message : String(err);
|
|
5518
|
-
|
|
5672
|
+
const body = err && typeof err === "object" && "responseBody" in err ? err.responseBody : void 0;
|
|
5673
|
+
plog(() => `sdk error: ${message}${body ? ` \u2014 body: ${body}` : ""}`);
|
|
5519
5674
|
if (!res.headersSent) {
|
|
5520
5675
|
anthropicError(res, 502, message);
|
|
5521
5676
|
} else {
|
|
@@ -5544,6 +5699,7 @@ data: ${JSON.stringify({ type: "error", error: { type: "api_error", message } })
|
|
|
5544
5699
|
plog(() => `started on port ${addr.port}, catalog=${routes.length} model(s), default=${defaultRoute.aliasId}`);
|
|
5545
5700
|
resolve({
|
|
5546
5701
|
port: addr.port,
|
|
5702
|
+
token: proxyToken,
|
|
5547
5703
|
close: () => {
|
|
5548
5704
|
process.off("unhandledRejection", onRejection);
|
|
5549
5705
|
process.off("uncaughtException", onException);
|
|
@@ -5553,7 +5709,7 @@ data: ${JSON.stringify({ type: "error", error: { type: "api_error", message } })
|
|
|
5553
5709
|
});
|
|
5554
5710
|
});
|
|
5555
5711
|
}
|
|
5556
|
-
function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk) {
|
|
5712
|
+
function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk, apiKey) {
|
|
5557
5713
|
const bareModelId = stripOneMContextSuffix(modelId);
|
|
5558
5714
|
const clientModelId = claudeCodeClientModelId(modelId, contextWindow);
|
|
5559
5715
|
return startProxyCatalog([{
|
|
@@ -5561,8 +5717,7 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk)
|
|
|
5561
5717
|
realModelId: sdk?.upstreamModelId ?? bareModelId,
|
|
5562
5718
|
displayName: bareModelId,
|
|
5563
5719
|
upstreamUrl: completionsUrl,
|
|
5564
|
-
apiKey: "",
|
|
5565
|
-
// '' → use inbound bearer from Claude Code (single-model compat)
|
|
5720
|
+
apiKey: apiKey ?? "",
|
|
5566
5721
|
modelFormat: "openai",
|
|
5567
5722
|
contextWindow,
|
|
5568
5723
|
npm: sdk?.npm,
|
|
@@ -5664,7 +5819,7 @@ function ensureAppHomeMigrated() {
|
|
|
5664
5819
|
if (existsSync9(configPath)) return;
|
|
5665
5820
|
const legacyConfig = join9(getLegacyAppHome(), "config.json");
|
|
5666
5821
|
if (!existsSync9(legacyConfig)) return;
|
|
5667
|
-
mkdirSync5(getAppHome(), { recursive: true });
|
|
5822
|
+
mkdirSync5(getAppHome(), { recursive: true, mode: 448 });
|
|
5668
5823
|
copyFileSync2(legacyConfig, configPath);
|
|
5669
5824
|
const legacyVertex = join9(getLegacyAppHome(), "vertex-models.json");
|
|
5670
5825
|
const vertexPath = join9(getAppHome(), "vertex-models.json");
|
|
@@ -5680,9 +5835,9 @@ function ensureConfigMigrated() {
|
|
|
5680
5835
|
if (!existsSync9(legacyPath)) return;
|
|
5681
5836
|
const legacy = readJsonFile(legacyPath);
|
|
5682
5837
|
if (!legacy) return;
|
|
5683
|
-
mkdirSync5(dirname4(configPath), { recursive: true });
|
|
5838
|
+
mkdirSync5(dirname4(configPath), { recursive: true, mode: 448 });
|
|
5684
5839
|
writeFileSync4(configPath, `${JSON.stringify(legacy, null, 2)}
|
|
5685
|
-
`, "utf8");
|
|
5840
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5686
5841
|
try {
|
|
5687
5842
|
renameSync2(legacyPath, `${legacyPath}.migrated`);
|
|
5688
5843
|
} catch {
|
|
@@ -5694,9 +5849,9 @@ function readConfig() {
|
|
|
5694
5849
|
}
|
|
5695
5850
|
function writeConfig(config) {
|
|
5696
5851
|
const configPath = getConfigPath();
|
|
5697
|
-
mkdirSync5(dirname4(configPath), { recursive: true });
|
|
5852
|
+
mkdirSync5(dirname4(configPath), { recursive: true, mode: 448 });
|
|
5698
5853
|
writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
|
|
5699
|
-
`, "utf8");
|
|
5854
|
+
`, { encoding: "utf8", mode: 384 });
|
|
5700
5855
|
}
|
|
5701
5856
|
function loadPreferences() {
|
|
5702
5857
|
const config = readConfig();
|
|
@@ -5732,10 +5887,51 @@ function recordLaunchSelection(agent, providerId, modelId, prefs) {
|
|
|
5732
5887
|
recentModelsByProvider: { ...prefs.recentModelsByProvider, [providerId]: updatedRecent }
|
|
5733
5888
|
});
|
|
5734
5889
|
}
|
|
5735
|
-
|
|
5736
|
-
|
|
5890
|
+
var SERVER_PASSWORD_SERVICE = "relay-ai-server-password";
|
|
5891
|
+
var SERVER_PASSWORD_ACCOUNT = "server-password";
|
|
5892
|
+
async function getServerPasswordKeyring() {
|
|
5893
|
+
try {
|
|
5894
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
5895
|
+
return new Entry(SERVER_PASSWORD_SERVICE, SERVER_PASSWORD_ACCOUNT);
|
|
5896
|
+
} catch {
|
|
5897
|
+
return null;
|
|
5898
|
+
}
|
|
5737
5899
|
}
|
|
5738
|
-
function
|
|
5900
|
+
async function getSavedServerPassword() {
|
|
5901
|
+
const config = readConfig();
|
|
5902
|
+
if (config.server?.savedPassword) {
|
|
5903
|
+
const pwd = config.server.savedPassword;
|
|
5904
|
+
const keyring2 = await getServerPasswordKeyring();
|
|
5905
|
+
if (keyring2) {
|
|
5906
|
+
try {
|
|
5907
|
+
await keyring2.setPassword(pwd);
|
|
5908
|
+
delete config.server.savedPassword;
|
|
5909
|
+
if (Object.keys(config.server).length === 0) delete config.server;
|
|
5910
|
+
writeConfig(config);
|
|
5911
|
+
} catch {
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
return pwd;
|
|
5915
|
+
}
|
|
5916
|
+
const keyring = await getServerPasswordKeyring();
|
|
5917
|
+
if (keyring) {
|
|
5918
|
+
try {
|
|
5919
|
+
return await keyring.getPassword();
|
|
5920
|
+
} catch {
|
|
5921
|
+
return null;
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
return null;
|
|
5925
|
+
}
|
|
5926
|
+
async function setSavedServerPassword(password3) {
|
|
5927
|
+
const keyring = await getServerPasswordKeyring();
|
|
5928
|
+
if (keyring) {
|
|
5929
|
+
try {
|
|
5930
|
+
await keyring.setPassword(password3);
|
|
5931
|
+
return;
|
|
5932
|
+
} catch {
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5739
5935
|
const config = readConfig();
|
|
5740
5936
|
config.server = {
|
|
5741
5937
|
...config.server ?? {},
|
|
@@ -5911,11 +6107,19 @@ function zenGoAsLocalProvider(backendId, models) {
|
|
|
5911
6107
|
}
|
|
5912
6108
|
function providersForPicker(catalog) {
|
|
5913
6109
|
const registryIds = new Set(catalog.localProviders.map((p19) => p19.id));
|
|
5914
|
-
|
|
6110
|
+
const providers = [
|
|
5915
6111
|
...catalog.zenModels.length > 0 && !registryIds.has("zen") ? [zenGoAsLocalProvider("zen", catalog.zenModels)] : [],
|
|
5916
6112
|
...catalog.goModels.length > 0 && !registryIds.has("go") ? [zenGoAsLocalProvider("go", catalog.goModels)] : [],
|
|
5917
6113
|
...catalog.localProviders
|
|
5918
6114
|
];
|
|
6115
|
+
for (const p19 of providers) {
|
|
6116
|
+
p19.models.sort((a, b) => {
|
|
6117
|
+
const nameA = a.name || a.id;
|
|
6118
|
+
const nameB = b.name || b.id;
|
|
6119
|
+
return nameA.localeCompare(nameB, void 0, { sensitivity: "base", numeric: true });
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
6122
|
+
return providers.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true }));
|
|
5919
6123
|
}
|
|
5920
6124
|
async function resolveLocalProviderApiKey(provider) {
|
|
5921
6125
|
const direct = provider.apiKey?.trim();
|
|
@@ -6131,6 +6335,147 @@ async function askSaveServerPassword() {
|
|
|
6131
6335
|
|
|
6132
6336
|
// src/server/router.ts
|
|
6133
6337
|
import { createServer as createServer2 } from "http";
|
|
6338
|
+
|
|
6339
|
+
// src/openai-adapter.ts
|
|
6340
|
+
import { tool as tool2, jsonSchema as jsonSchema2, streamText as streamText2, generateText as generateText2 } from "ai";
|
|
6341
|
+
function translateOpenAiRequest(body) {
|
|
6342
|
+
const toolNameById = /* @__PURE__ */ new Map();
|
|
6343
|
+
for (const msg of body.messages) {
|
|
6344
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
6345
|
+
for (const tc of msg.tool_calls) toolNameById.set(tc.id, tc.function.name);
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
let system;
|
|
6349
|
+
const messages = [];
|
|
6350
|
+
for (const msg of body.messages) {
|
|
6351
|
+
switch (msg.role) {
|
|
6352
|
+
case "system":
|
|
6353
|
+
system = typeof msg.content === "string" ? msg.content : void 0;
|
|
6354
|
+
break;
|
|
6355
|
+
case "user":
|
|
6356
|
+
messages.push({ role: "user", content: msg.content });
|
|
6357
|
+
break;
|
|
6358
|
+
case "assistant": {
|
|
6359
|
+
const parts = [];
|
|
6360
|
+
if (typeof msg.content === "string" && msg.content) {
|
|
6361
|
+
parts.push({ type: "text", text: msg.content });
|
|
6362
|
+
}
|
|
6363
|
+
for (const tc of msg.tool_calls ?? []) {
|
|
6364
|
+
parts.push({
|
|
6365
|
+
type: "tool-call",
|
|
6366
|
+
toolCallId: tc.id,
|
|
6367
|
+
toolName: tc.function.name,
|
|
6368
|
+
input: parseToolArguments(tc.function.arguments)
|
|
6369
|
+
});
|
|
6370
|
+
}
|
|
6371
|
+
messages.push({ role: "assistant", content: parts.length > 0 ? parts : "" });
|
|
6372
|
+
break;
|
|
6373
|
+
}
|
|
6374
|
+
case "tool": {
|
|
6375
|
+
const resultPart = {
|
|
6376
|
+
type: "tool-result",
|
|
6377
|
+
toolCallId: msg.tool_call_id ?? "",
|
|
6378
|
+
toolName: toolNameById.get(msg.tool_call_id ?? "") ?? "unknown",
|
|
6379
|
+
output: {
|
|
6380
|
+
type: "text",
|
|
6381
|
+
value: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "")
|
|
6382
|
+
}
|
|
6383
|
+
};
|
|
6384
|
+
const lastMsg = messages[messages.length - 1];
|
|
6385
|
+
if (lastMsg?.role === "tool" && Array.isArray(lastMsg.content)) {
|
|
6386
|
+
lastMsg.content.push(resultPart);
|
|
6387
|
+
} else {
|
|
6388
|
+
messages.push({ role: "tool", content: [resultPart] });
|
|
6389
|
+
}
|
|
6390
|
+
break;
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
let sdkToolChoice;
|
|
6395
|
+
if (body.tool_choice === "auto" || body.tool_choice === "required") {
|
|
6396
|
+
sdkToolChoice = body.tool_choice;
|
|
6397
|
+
} else if (typeof body.tool_choice === "object" && body.tool_choice?.type === "function") {
|
|
6398
|
+
sdkToolChoice = { type: "tool", toolName: body.tool_choice.function.name };
|
|
6399
|
+
}
|
|
6400
|
+
let tools;
|
|
6401
|
+
if (body.tools?.length) {
|
|
6402
|
+
tools = {};
|
|
6403
|
+
for (const t of body.tools) {
|
|
6404
|
+
if (t.type === "function" && t.function.name) {
|
|
6405
|
+
const schema = t.function.parameters ? jsonSchema2(t.function.parameters) : void 0;
|
|
6406
|
+
tools[t.function.name] = tool2({
|
|
6407
|
+
description: t.function.description ?? "",
|
|
6408
|
+
inputSchema: schema ?? jsonSchema2({ type: "object", properties: {} })
|
|
6409
|
+
});
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
return {
|
|
6414
|
+
system,
|
|
6415
|
+
messages,
|
|
6416
|
+
tools,
|
|
6417
|
+
toolChoice: sdkToolChoice,
|
|
6418
|
+
temperature: body.temperature,
|
|
6419
|
+
maxOutputTokens: body.max_completion_tokens ?? body.max_tokens
|
|
6420
|
+
};
|
|
6421
|
+
}
|
|
6422
|
+
async function generateOpenAiResponse(model, params, responseModelId) {
|
|
6423
|
+
const result = await generateText2({ model, ...params });
|
|
6424
|
+
const message = { role: "assistant", content: result.text || null };
|
|
6425
|
+
if (result.toolCalls?.length) {
|
|
6426
|
+
message.tool_calls = result.toolCalls.map((tc) => ({
|
|
6427
|
+
id: tc.toolCallId,
|
|
6428
|
+
type: "function",
|
|
6429
|
+
function: { name: tc.toolName, arguments: JSON.stringify(tc.args) }
|
|
6430
|
+
}));
|
|
6431
|
+
}
|
|
6432
|
+
return {
|
|
6433
|
+
id: `chatcmpl-${Date.now()}`,
|
|
6434
|
+
object: "chat.completion",
|
|
6435
|
+
created: Math.floor(Date.now() / 1e3),
|
|
6436
|
+
model: responseModelId,
|
|
6437
|
+
choices: [{ index: 0, message, finish_reason: result.finishReason || "stop" }],
|
|
6438
|
+
usage: {
|
|
6439
|
+
prompt_tokens: result.usage?.promptTokens ?? 0,
|
|
6440
|
+
completion_tokens: result.usage?.completionTokens ?? 0,
|
|
6441
|
+
total_tokens: result.usage?.totalTokens ?? 0
|
|
6442
|
+
}
|
|
6443
|
+
};
|
|
6444
|
+
}
|
|
6445
|
+
async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
|
|
6446
|
+
const { fullStream } = streamText2({ model, ...params });
|
|
6447
|
+
const baseData = {
|
|
6448
|
+
id: `chatcmpl-${Date.now()}`,
|
|
6449
|
+
object: "chat.completion.chunk",
|
|
6450
|
+
created: Math.floor(Date.now() / 1e3),
|
|
6451
|
+
model: responseModelId
|
|
6452
|
+
};
|
|
6453
|
+
const send = (delta, finish_reason = null) => onChunk(`data: ${JSON.stringify({ ...baseData, choices: [{ index: 0, delta, finish_reason }] })}
|
|
6454
|
+
|
|
6455
|
+
`);
|
|
6456
|
+
for await (const part of fullStream) {
|
|
6457
|
+
const p19 = part;
|
|
6458
|
+
switch (p19.type) {
|
|
6459
|
+
case "text-delta":
|
|
6460
|
+
send({ role: "assistant", content: p19.textDelta ?? p19.text ?? "" });
|
|
6461
|
+
break;
|
|
6462
|
+
case "tool-input-start":
|
|
6463
|
+
case "tool-call-streaming-start":
|
|
6464
|
+
send({ role: "assistant", tool_calls: [{ index: 0, id: p19.id ?? p19.toolCallId, type: "function", function: { name: p19.toolName, arguments: "" } }] });
|
|
6465
|
+
break;
|
|
6466
|
+
case "tool-input-delta":
|
|
6467
|
+
case "tool-call-delta":
|
|
6468
|
+
send({ tool_calls: [{ index: 0, function: { arguments: p19.delta ?? p19.text ?? p19.argsTextDelta ?? "" } }] });
|
|
6469
|
+
break;
|
|
6470
|
+
case "finish":
|
|
6471
|
+
send({}, p19.finishReason || "stop");
|
|
6472
|
+
break;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
onChunk("data: [DONE]\n\n");
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6478
|
+
// src/server/router.ts
|
|
6134
6479
|
function makeServerLog(debugLogPath) {
|
|
6135
6480
|
if (!debugLogPath) return () => {
|
|
6136
6481
|
};
|
|
@@ -6194,7 +6539,7 @@ async function routeRequest(req, res, options, modelCache, plog) {
|
|
|
6194
6539
|
return;
|
|
6195
6540
|
}
|
|
6196
6541
|
if (req.method === "POST" && pathname === "/openai/v1/chat/completions") {
|
|
6197
|
-
await handleOpenAIChatCompletions(req, res, options);
|
|
6542
|
+
await handleOpenAIChatCompletions(req, res, options, modelCache, plog);
|
|
6198
6543
|
return;
|
|
6199
6544
|
}
|
|
6200
6545
|
sendJson(res, 404, { error: { message: "Not found" } });
|
|
@@ -6233,21 +6578,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
6233
6578
|
return;
|
|
6234
6579
|
}
|
|
6235
6580
|
const apiKey = model.apiKey ?? options.apiKey;
|
|
6236
|
-
const
|
|
6237
|
-
let languageModel = modelCache.get(cacheKey);
|
|
6238
|
-
if (!languageModel) {
|
|
6239
|
-
languageModel = await createLanguageModel({
|
|
6240
|
-
npm: model.npm,
|
|
6241
|
-
modelId: upstreamModelId(model),
|
|
6242
|
-
apiKey,
|
|
6243
|
-
baseURL: model.apiBaseUrl,
|
|
6244
|
-
providerId: model.providerId ?? model.sourceBackend,
|
|
6245
|
-
authType: model.authType,
|
|
6246
|
-
oauthAccountId: model.oauthAccountId,
|
|
6247
|
-
vertex: options.vertex
|
|
6248
|
-
});
|
|
6249
|
-
modelCache.set(cacheKey, languageModel);
|
|
6250
|
-
}
|
|
6581
|
+
const languageModel = await getOrInitLanguageModel(modelCache, model, model.npm, model.apiBaseUrl, apiKey, options.vertex);
|
|
6251
6582
|
const params = translateRequest(body, model.npm, {
|
|
6252
6583
|
defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
|
|
6253
6584
|
openAiOAuth: model.npm === "@ai-sdk/openai" && model.authType === "oauth",
|
|
@@ -6260,7 +6591,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
6260
6591
|
}
|
|
6261
6592
|
});
|
|
6262
6593
|
const clientWantsStream = Boolean(body.stream);
|
|
6263
|
-
const responseModelId =
|
|
6594
|
+
const responseModelId = getResponseModelId(body.model, model, options);
|
|
6264
6595
|
plog(() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
|
|
6265
6596
|
try {
|
|
6266
6597
|
if (clientWantsStream) {
|
|
@@ -6284,7 +6615,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
6284
6615
|
}
|
|
6285
6616
|
sendJson(res, 400, { error: { message: `Unsupported model format: ${model.modelFormat}` } });
|
|
6286
6617
|
}
|
|
6287
|
-
async function handleOpenAIChatCompletions(req, res, options) {
|
|
6618
|
+
async function handleOpenAIChatCompletions(req, res, options, modelCache, plog) {
|
|
6288
6619
|
const body = await readJson(req);
|
|
6289
6620
|
if (!body) {
|
|
6290
6621
|
sendJson(res, 400, { error: { message: "Invalid JSON body" } });
|
|
@@ -6292,29 +6623,46 @@ async function handleOpenAIChatCompletions(req, res, options) {
|
|
|
6292
6623
|
}
|
|
6293
6624
|
const model = lookupModel(res, options.catalog, body.model);
|
|
6294
6625
|
if (!model) return;
|
|
6295
|
-
if (model
|
|
6296
|
-
if (!isOpenAIChatCompletionsModel(model)) {
|
|
6297
|
-
sendJson(res, 400, {
|
|
6298
|
-
error: {
|
|
6299
|
-
message: `OpenAI chat completions are not available for model: ${model.id}. Use /anthropic/v1/messages.`
|
|
6300
|
-
}
|
|
6301
|
-
});
|
|
6302
|
-
return;
|
|
6303
|
-
}
|
|
6626
|
+
if (supportsDirectOpenAIChatCompletions(model)) {
|
|
6304
6627
|
if (model.completionsUrl && !/^https?:\/\//i.test(model.completionsUrl)) {
|
|
6305
6628
|
sendJson(res, 400, { error: { message: `Invalid provider completionsUrl: must be http:// or https://` } });
|
|
6306
6629
|
return;
|
|
6307
6630
|
}
|
|
6308
6631
|
const completionsUrl = model.completionsUrl ? model.completionsUrl : `${backendFor(options, model).baseUrl}/v1/chat/completions`;
|
|
6309
|
-
const
|
|
6310
|
-
await
|
|
6632
|
+
const apiKey2 = model.apiKey ?? options.apiKey;
|
|
6633
|
+
await relayAnthropicMessages(res, completionsUrl, body, apiKey2, Boolean(body.stream));
|
|
6311
6634
|
return;
|
|
6312
6635
|
}
|
|
6313
|
-
|
|
6314
|
-
|
|
6636
|
+
const npm = model.npm || (model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : void 0);
|
|
6637
|
+
if (!npm) {
|
|
6638
|
+
sendJson(res, 400, { error: { message: `No SDK provider for model: ${model.id}` } });
|
|
6315
6639
|
return;
|
|
6316
6640
|
}
|
|
6317
|
-
|
|
6641
|
+
const apiKey = model.apiKey ?? options.apiKey;
|
|
6642
|
+
const baseURL = model.modelFormat === "anthropic" ? model.baseUrl : model.apiBaseUrl;
|
|
6643
|
+
const languageModel = await getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, options.vertex);
|
|
6644
|
+
const params = translateOpenAiRequest(body);
|
|
6645
|
+
const clientWantsStream = Boolean(body.stream);
|
|
6646
|
+
const responseModelId = getResponseModelId(body.model, model, options);
|
|
6647
|
+
plog(() => `sdk-openai npm=${npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
|
|
6648
|
+
try {
|
|
6649
|
+
if (clientWantsStream) {
|
|
6650
|
+
res.writeHead(200, {
|
|
6651
|
+
"Content-Type": "text/event-stream",
|
|
6652
|
+
"Cache-Control": "no-cache",
|
|
6653
|
+
"Connection": "keep-alive"
|
|
6654
|
+
});
|
|
6655
|
+
await streamOpenAiResponse(languageModel, params, responseModelId, (chunk) => res.write(chunk));
|
|
6656
|
+
res.end();
|
|
6657
|
+
} else {
|
|
6658
|
+
const response = await generateOpenAiResponse(languageModel, params, responseModelId);
|
|
6659
|
+
sendJson(res, 200, response);
|
|
6660
|
+
}
|
|
6661
|
+
} catch (err) {
|
|
6662
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6663
|
+
if (!res.headersSent) sendJson(res, 502, { error: { message } });
|
|
6664
|
+
else res.end();
|
|
6665
|
+
}
|
|
6318
6666
|
}
|
|
6319
6667
|
function lookupModel(res, catalog, modelId) {
|
|
6320
6668
|
if (typeof modelId !== "string") {
|
|
@@ -6336,14 +6684,32 @@ function backendFor(options, model) {
|
|
|
6336
6684
|
if (model.sourceBackend === "go") return options.backends.go;
|
|
6337
6685
|
throw new Error(`Provider ${model.sourceBackend} is not a cloud backend \u2014 model must set baseUrl/completionsUrl`);
|
|
6338
6686
|
}
|
|
6339
|
-
function
|
|
6340
|
-
|
|
6687
|
+
async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, vertex) {
|
|
6688
|
+
const cacheKey = [
|
|
6341
6689
|
model.providerId ?? model.sourceBackend,
|
|
6342
6690
|
model.id,
|
|
6343
6691
|
upstreamModelId(model),
|
|
6344
|
-
|
|
6345
|
-
|
|
6692
|
+
npm,
|
|
6693
|
+
baseURL ?? ""
|
|
6346
6694
|
].join("");
|
|
6695
|
+
let languageModel = modelCache.get(cacheKey);
|
|
6696
|
+
if (!languageModel) {
|
|
6697
|
+
languageModel = await createLanguageModel({
|
|
6698
|
+
npm,
|
|
6699
|
+
modelId: upstreamModelId(model),
|
|
6700
|
+
apiKey,
|
|
6701
|
+
baseURL,
|
|
6702
|
+
providerId: model.providerId ?? model.sourceBackend,
|
|
6703
|
+
authType: model.authType,
|
|
6704
|
+
oauthAccountId: model.oauthAccountId,
|
|
6705
|
+
vertex
|
|
6706
|
+
});
|
|
6707
|
+
modelCache.set(cacheKey, languageModel);
|
|
6708
|
+
}
|
|
6709
|
+
return languageModel;
|
|
6710
|
+
}
|
|
6711
|
+
function getResponseModelId(bodyModel, model, options) {
|
|
6712
|
+
return options.gateway?.maskGatewayIds ? gatewayDisplayName(model, options.gateway) : typeof bodyModel === "string" ? bodyModel : model.id;
|
|
6347
6713
|
}
|
|
6348
6714
|
async function forwardJson(res, url, body, apiKey, inboundBeta) {
|
|
6349
6715
|
const upstream = await postJsonUpstream(url, body, apiKey, inboundBeta);
|
|
@@ -6607,16 +6973,44 @@ function createVertexModelCatalog(models) {
|
|
|
6607
6973
|
}
|
|
6608
6974
|
|
|
6609
6975
|
// src/server/index.ts
|
|
6610
|
-
function
|
|
6976
|
+
function getLocalIps() {
|
|
6611
6977
|
const ifaces = networkInterfaces();
|
|
6612
|
-
|
|
6978
|
+
const result = [];
|
|
6979
|
+
for (const [name, iface] of Object.entries(ifaces)) {
|
|
6613
6980
|
for (const addr of iface ?? []) {
|
|
6614
6981
|
if (addr.family === "IPv4" && !addr.internal) {
|
|
6615
|
-
|
|
6982
|
+
result.push({ name, address: addr.address });
|
|
6616
6983
|
}
|
|
6617
6984
|
}
|
|
6618
6985
|
}
|
|
6619
|
-
return
|
|
6986
|
+
return result;
|
|
6987
|
+
}
|
|
6988
|
+
function printModelCatalog(models, gateway) {
|
|
6989
|
+
if (models.length === 0) return;
|
|
6990
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6991
|
+
for (const model of models) {
|
|
6992
|
+
const label = gatewayProviderLabel(model);
|
|
6993
|
+
let list = groups.get(label);
|
|
6994
|
+
if (!list) {
|
|
6995
|
+
list = [];
|
|
6996
|
+
groups.set(label, list);
|
|
6997
|
+
}
|
|
6998
|
+
list.push(model);
|
|
6999
|
+
}
|
|
7000
|
+
const sortedGroups = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7001
|
+
console.log(pc6.bold("Model catalog:"));
|
|
7002
|
+
console.log("");
|
|
7003
|
+
for (const [label, groupModels] of sortedGroups) {
|
|
7004
|
+
console.log(` ${pc6.bold(label)}`);
|
|
7005
|
+
const sorted = [...groupModels].sort((a, b) => a.name.localeCompare(b.name));
|
|
7006
|
+
for (const model of sorted) {
|
|
7007
|
+
const anthropicId = exposedGatewayAliasId(model, gateway);
|
|
7008
|
+
console.log(` ${model.name}`);
|
|
7009
|
+
console.log(` ${pc6.dim("anthropic:")} ${pc6.cyan(anthropicId)}`);
|
|
7010
|
+
console.log(` ${pc6.dim("openai: ")} ${pc6.cyan(model.id)}`);
|
|
7011
|
+
}
|
|
7012
|
+
console.log("");
|
|
7013
|
+
}
|
|
6620
7014
|
}
|
|
6621
7015
|
function filterZenModelsForServer(models) {
|
|
6622
7016
|
const zenProvider = loadRegistry().providers.find((entry) => entry.id === "zen" && entry.enabled);
|
|
@@ -6698,13 +7092,19 @@ function waitForShutdown() {
|
|
|
6698
7092
|
});
|
|
6699
7093
|
}
|
|
6700
7094
|
async function getServerPasswordForMode(mode) {
|
|
6701
|
-
if (mode === "local") return null;
|
|
6702
|
-
const savedPassword = getSavedServerPassword();
|
|
7095
|
+
if (mode === "local") return { password: null, wasSaved: false };
|
|
7096
|
+
const savedPassword = await getSavedServerPassword();
|
|
6703
7097
|
let serverPassword = null;
|
|
7098
|
+
let wasSaved = false;
|
|
6704
7099
|
if (savedPassword) {
|
|
6705
7100
|
const savedChoice = await askUseSavedServerPassword();
|
|
6706
7101
|
if (!savedChoice) return void 0;
|
|
6707
|
-
|
|
7102
|
+
if (savedChoice === "use-saved") {
|
|
7103
|
+
serverPassword = savedPassword;
|
|
7104
|
+
wasSaved = true;
|
|
7105
|
+
} else {
|
|
7106
|
+
serverPassword = await askServerPassword();
|
|
7107
|
+
}
|
|
6708
7108
|
} else {
|
|
6709
7109
|
serverPassword = await askServerPassword();
|
|
6710
7110
|
}
|
|
@@ -6712,9 +7112,12 @@ async function getServerPasswordForMode(mode) {
|
|
|
6712
7112
|
if (serverPassword !== savedPassword) {
|
|
6713
7113
|
const savePassword = await askSaveServerPassword();
|
|
6714
7114
|
if (savePassword === null) return void 0;
|
|
6715
|
-
if (savePassword)
|
|
7115
|
+
if (savePassword) {
|
|
7116
|
+
await setSavedServerPassword(serverPassword);
|
|
7117
|
+
wasSaved = true;
|
|
7118
|
+
}
|
|
6716
7119
|
}
|
|
6717
|
-
return serverPassword;
|
|
7120
|
+
return { password: serverPassword, wasSaved };
|
|
6718
7121
|
}
|
|
6719
7122
|
async function configureExposedProviders() {
|
|
6720
7123
|
p6.log.info("Add providers to expose. Listed providers are removed when selected \u2014 like favorites.");
|
|
@@ -6770,8 +7173,9 @@ async function runVertexServerCommand() {
|
|
|
6770
7173
|
}
|
|
6771
7174
|
const mode = await askListenMode();
|
|
6772
7175
|
if (!mode) return 0;
|
|
6773
|
-
const
|
|
6774
|
-
if (
|
|
7176
|
+
const pwResult = await getServerPasswordForMode(mode);
|
|
7177
|
+
if (pwResult === void 0) return 0;
|
|
7178
|
+
const { password: serverPassword, wasSaved: passwordWasSaved } = pwResult;
|
|
6775
7179
|
const host = mode === "network" ? "0.0.0.0" : "127.0.0.1";
|
|
6776
7180
|
const models = vertexModelsToServerModels(vertexConfig);
|
|
6777
7181
|
const server = await startServer({
|
|
@@ -6791,13 +7195,20 @@ async function runVertexServerCommand() {
|
|
|
6791
7195
|
console.log(` Anthropic: http://127.0.0.1:${server.port}/anthropic`);
|
|
6792
7196
|
console.log(` Models: ${models.map((model) => model.id).join(", ")}`);
|
|
6793
7197
|
if (mode === "network") {
|
|
6794
|
-
|
|
6795
|
-
|
|
7198
|
+
for (const { name, address } of getLocalIps()) {
|
|
7199
|
+
console.log(` Network (${name}): http://${address}:${server.port}/anthropic`);
|
|
7200
|
+
}
|
|
7201
|
+
if (passwordWasSaved) {
|
|
7202
|
+
console.log(" API key: saved, rotate with `relay-ai server --setup`");
|
|
7203
|
+
} else {
|
|
7204
|
+
console.log(` API key: ${serverPassword}`);
|
|
7205
|
+
}
|
|
6796
7206
|
} else {
|
|
6797
7207
|
console.log(" API key: any non-empty value");
|
|
6798
7208
|
}
|
|
6799
7209
|
console.log(pc6.dim(" Auth: gcloud Application Default Credentials"));
|
|
6800
7210
|
console.log("");
|
|
7211
|
+
printModelCatalog(models);
|
|
6801
7212
|
console.log(pc6.dim("Press Ctrl+C to stop."));
|
|
6802
7213
|
await waitForShutdown();
|
|
6803
7214
|
await server.close();
|
|
@@ -6835,8 +7246,9 @@ async function runServerCommand(options = {}) {
|
|
|
6835
7246
|
if (!runConfig) return 0;
|
|
6836
7247
|
const mode = await askListenMode();
|
|
6837
7248
|
if (!mode) return 0;
|
|
6838
|
-
const
|
|
6839
|
-
if (
|
|
7249
|
+
const pwResult = await getServerPasswordForMode(mode);
|
|
7250
|
+
if (pwResult === void 0) return 0;
|
|
7251
|
+
const { password: serverPassword, wasSaved: passwordWasSaved } = pwResult;
|
|
6840
7252
|
const host = mode === "network" ? "0.0.0.0" : "127.0.0.1";
|
|
6841
7253
|
const spinner9 = p6.spinner();
|
|
6842
7254
|
spinner9.start("Fetching available models...");
|
|
@@ -6896,10 +7308,18 @@ async function runServerCommand(options = {}) {
|
|
|
6896
7308
|
console.log("");
|
|
6897
7309
|
console.log(pc6.bold(pc6.green("Relay AI server running")));
|
|
6898
7310
|
console.log(` Anthropic: http://127.0.0.1:${server.port}/anthropic`);
|
|
6899
|
-
console.log(` OpenAI: http://127.0.0.1:${server.port}/openai`);
|
|
7311
|
+
console.log(` OpenAI: http://127.0.0.1:${server.port}/openai/v1`);
|
|
6900
7312
|
if (mode === "network") {
|
|
6901
|
-
|
|
6902
|
-
|
|
7313
|
+
for (const { name, address } of getLocalIps()) {
|
|
7314
|
+
console.log(` Network (${name}):`);
|
|
7315
|
+
console.log(` Anthropic: http://${address}:${server.port}/anthropic`);
|
|
7316
|
+
console.log(` OpenAI: http://${address}:${server.port}/openai/v1`);
|
|
7317
|
+
}
|
|
7318
|
+
if (passwordWasSaved) {
|
|
7319
|
+
console.log(" API key: saved, rotate with `relay-ai server --setup`");
|
|
7320
|
+
} else {
|
|
7321
|
+
console.log(` API key: ${serverPassword}`);
|
|
7322
|
+
}
|
|
6903
7323
|
} else {
|
|
6904
7324
|
console.log(" API key: any non-empty value");
|
|
6905
7325
|
}
|
|
@@ -6913,6 +7333,7 @@ async function runServerCommand(options = {}) {
|
|
|
6913
7333
|
console.log(pc6.dim(" Discovery: gateway ids masked for Claude Desktop / Cowork"));
|
|
6914
7334
|
}
|
|
6915
7335
|
console.log("");
|
|
7336
|
+
printModelCatalog(models, gateway);
|
|
6916
7337
|
console.log(pc6.dim("Press Ctrl+C to stop."));
|
|
6917
7338
|
await waitForShutdown();
|
|
6918
7339
|
await server.close();
|
|
@@ -6934,16 +7355,24 @@ var MODE_SEARCH = "search";
|
|
|
6934
7355
|
var MODE_BROWSE = "browse";
|
|
6935
7356
|
function sortModelsByBrand(models) {
|
|
6936
7357
|
return [...models].sort((a, b) => {
|
|
6937
|
-
const brandCmp = a.brand.localeCompare(b.brand);
|
|
6938
|
-
|
|
7358
|
+
const brandCmp = a.brand.localeCompare(b.brand, void 0, { sensitivity: "base" });
|
|
7359
|
+
if (brandCmp !== 0) return brandCmp;
|
|
7360
|
+
const nameA = a.name || a.id;
|
|
7361
|
+
const nameB = b.name || b.id;
|
|
7362
|
+
return nameA.localeCompare(nameB, void 0, { sensitivity: "base", numeric: true });
|
|
6939
7363
|
});
|
|
6940
7364
|
}
|
|
7365
|
+
function normalizeForSearch(s) {
|
|
7366
|
+
return s.toLowerCase().replace(/[\s\-._/]+/g, " ").trim();
|
|
7367
|
+
}
|
|
6941
7368
|
function filterModelsBySearch(models, query) {
|
|
6942
|
-
const q = query.trim()
|
|
7369
|
+
const q = query.trim();
|
|
6943
7370
|
if (!q) return [];
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
7371
|
+
const tokens = normalizeForSearch(q).split(" ").filter(Boolean);
|
|
7372
|
+
return models.filter((m) => {
|
|
7373
|
+
const fields = [normalizeForSearch(m.id), normalizeForSearch(m.name), normalizeForSearch(m.brand)];
|
|
7374
|
+
return tokens.every((token) => fields.some((f) => f.includes(token)));
|
|
7375
|
+
});
|
|
6947
7376
|
}
|
|
6948
7377
|
function sliceModelPage(items, page, pageSize = MODEL_PAGE_SIZE) {
|
|
6949
7378
|
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
@@ -7018,12 +7447,12 @@ async function selectLargeCatalog(models, browseList, toOption, message, initial
|
|
|
7018
7447
|
value: MODE_BROWSE,
|
|
7019
7448
|
label: pc7.cyan("Browse all models"),
|
|
7020
7449
|
hint: `${MODEL_PAGE_SIZE} per page \xB7 ${Math.ceil(browseList.length / MODEL_PAGE_SIZE)} pages`
|
|
7021
|
-
}
|
|
7450
|
+
},
|
|
7451
|
+
navOption("__back__", "\u2190 Go back", "Select a different provider")
|
|
7022
7452
|
]
|
|
7023
7453
|
});
|
|
7024
|
-
if (p7.isCancel(method)) {
|
|
7025
|
-
|
|
7026
|
-
return null;
|
|
7454
|
+
if (p7.isCancel(method) || String(method) === "__back__") {
|
|
7455
|
+
return "back";
|
|
7027
7456
|
}
|
|
7028
7457
|
mode = method === MODE_BROWSE ? "browse" : "search";
|
|
7029
7458
|
continue;
|
|
@@ -7083,16 +7512,18 @@ async function selectModelWithSearch(models, toOption, message, initialModelId,
|
|
|
7083
7512
|
if (models.length === 0) return null;
|
|
7084
7513
|
const orderedBrowse = browseList ?? sortModelsByBrand(models);
|
|
7085
7514
|
if (models.length <= MODEL_SEARCH_THRESHOLD) {
|
|
7086
|
-
const options =
|
|
7515
|
+
const options = [
|
|
7516
|
+
...models.map(toOption),
|
|
7517
|
+
navOption("__back__", "\u2190 Go back", "")
|
|
7518
|
+
];
|
|
7087
7519
|
const initialValue = initialModelId && options.some((o) => o.value === initialModelId) ? initialModelId : options[0]?.value;
|
|
7088
7520
|
const picked = await p7.select({
|
|
7089
7521
|
message,
|
|
7090
7522
|
options,
|
|
7091
7523
|
initialValue
|
|
7092
7524
|
});
|
|
7093
|
-
if (p7.isCancel(picked)) {
|
|
7094
|
-
|
|
7095
|
-
return null;
|
|
7525
|
+
if (p7.isCancel(picked) || String(picked) === "__back__") {
|
|
7526
|
+
return "back";
|
|
7096
7527
|
}
|
|
7097
7528
|
const selected = models.find((m) => m.id === String(picked));
|
|
7098
7529
|
if (!selected) return null;
|
|
@@ -7117,32 +7548,43 @@ async function browseAllModels(provider, prefs) {
|
|
|
7117
7548
|
async function pickLocalModel(provider, conflicts, prefs) {
|
|
7118
7549
|
const recentIds = (prefs.recentModelsByProvider?.[provider.id] ?? []).slice(0, MAX_RECENT);
|
|
7119
7550
|
const recentModels = recentIds.map((id) => provider.models.find((m) => m.id === id)).filter((m) => m !== void 0);
|
|
7120
|
-
let selectedModel;
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7551
|
+
let selectedModel = null;
|
|
7552
|
+
while (true) {
|
|
7553
|
+
if (recentModels.length > 0) {
|
|
7554
|
+
const options = [
|
|
7555
|
+
...recentModels.map((m) => modelToOption(m, "recent")),
|
|
7556
|
+
navOption(BROWSE_ALL, "Browse all models \u2192", `${provider.models.length} available`),
|
|
7557
|
+
navOption("__back__", "\u2190 Go back", "Select a different provider")
|
|
7558
|
+
];
|
|
7559
|
+
const picked = await p7.select({
|
|
7560
|
+
message: "Which model?",
|
|
7561
|
+
options,
|
|
7562
|
+
initialValue: recentModels[0].id
|
|
7563
|
+
});
|
|
7564
|
+
if (p7.isCancel(picked) || String(picked) === "__back__") {
|
|
7565
|
+
return "back";
|
|
7566
|
+
}
|
|
7567
|
+
if (String(picked) === BROWSE_ALL) {
|
|
7568
|
+
const browsed = await browseAllModels(provider, prefs);
|
|
7569
|
+
if (browsed === "back") {
|
|
7570
|
+
continue;
|
|
7571
|
+
}
|
|
7572
|
+
if (!browsed) return null;
|
|
7573
|
+
selectedModel = browsed;
|
|
7574
|
+
break;
|
|
7575
|
+
} else {
|
|
7576
|
+
selectedModel = recentModels.find((m) => m.id === String(picked));
|
|
7577
|
+
break;
|
|
7578
|
+
}
|
|
7579
|
+
} else {
|
|
7136
7580
|
const browsed = await browseAllModels(provider, prefs);
|
|
7581
|
+
if (browsed === "back") {
|
|
7582
|
+
return "back";
|
|
7583
|
+
}
|
|
7137
7584
|
if (!browsed) return null;
|
|
7138
7585
|
selectedModel = browsed;
|
|
7139
|
-
|
|
7140
|
-
selectedModel = recentModels.find((m) => m.id === String(picked));
|
|
7586
|
+
break;
|
|
7141
7587
|
}
|
|
7142
|
-
} else {
|
|
7143
|
-
const browsed = await browseAllModels(provider, prefs);
|
|
7144
|
-
if (!browsed) return null;
|
|
7145
|
-
selectedModel = browsed;
|
|
7146
7588
|
}
|
|
7147
7589
|
noteEnvConflicts(conflicts);
|
|
7148
7590
|
const modelLabel = formatCodexModelLabel(selectedModel);
|
|
@@ -7473,6 +7915,71 @@ function toggleProviderEnabled(id) {
|
|
|
7473
7915
|
return { toggled: true, enabled: provider.enabled };
|
|
7474
7916
|
}
|
|
7475
7917
|
|
|
7918
|
+
// src/data/openai-oauth-models.ts
|
|
7919
|
+
var CHATGPT_CODEX_UNSUPPORTED_MODELS = /* @__PURE__ */ new Set([
|
|
7920
|
+
"gpt-5.5-fast"
|
|
7921
|
+
// confirmed: rejected by chatgpt.com/backend-api/codex
|
|
7922
|
+
]);
|
|
7923
|
+
var OPENAI_OAUTH_MODEL_SEEDS = [
|
|
7924
|
+
// GPT-5.5 family (Pro)
|
|
7925
|
+
{ id: "gpt-5.5", name: "GPT-5.5", reasoning: true },
|
|
7926
|
+
// GPT-5.4 family
|
|
7927
|
+
{ id: "gpt-5.4", name: "GPT-5.4" },
|
|
7928
|
+
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
|
|
7929
|
+
// GPT-5 base (Pro / Plus)
|
|
7930
|
+
{ id: "gpt-5", name: "GPT-5", reasoning: true },
|
|
7931
|
+
// o-series reasoning (Plus+)
|
|
7932
|
+
{ id: "o4-mini", name: "o4 Mini", reasoning: true },
|
|
7933
|
+
{ id: "o3", name: "o3", reasoning: true },
|
|
7934
|
+
{ id: "o3-mini", name: "o3 Mini", reasoning: true },
|
|
7935
|
+
{ id: "o1", name: "o1", reasoning: true },
|
|
7936
|
+
{ id: "o1-mini", name: "o1 Mini", reasoning: true }
|
|
7937
|
+
];
|
|
7938
|
+
function buildOpenAiOAuthModels() {
|
|
7939
|
+
return OPENAI_OAUTH_MODEL_SEEDS.map((seed) => {
|
|
7940
|
+
const prefix = seed.id.split("-")[0] ?? seed.id;
|
|
7941
|
+
return {
|
|
7942
|
+
id: seed.id,
|
|
7943
|
+
name: seed.name,
|
|
7944
|
+
upstreamModelId: seed.id,
|
|
7945
|
+
family: prefix,
|
|
7946
|
+
brand: deriveBrand(prefix),
|
|
7947
|
+
contextWindow: resolveContextWindow(seed.id),
|
|
7948
|
+
modelFormat: "openai",
|
|
7949
|
+
npm: "@ai-sdk/openai",
|
|
7950
|
+
reasoning: seed.reasoning
|
|
7951
|
+
};
|
|
7952
|
+
});
|
|
7953
|
+
}
|
|
7954
|
+
|
|
7955
|
+
// src/data/xai-oauth-models.ts
|
|
7956
|
+
var XAI_OAUTH_MODEL_SEEDS = [
|
|
7957
|
+
// Grok 4 family
|
|
7958
|
+
{ id: "grok-4", name: "Grok 4", reasoning: true },
|
|
7959
|
+
{ id: "grok-4-fast", name: "Grok 4 Fast", reasoning: true },
|
|
7960
|
+
// Grok 3 family
|
|
7961
|
+
{ id: "grok-3", name: "Grok 3", reasoning: true },
|
|
7962
|
+
{ id: "grok-3-fast", name: "Grok 3 Fast" },
|
|
7963
|
+
{ id: "grok-3-mini", name: "Grok 3 Mini", reasoning: true },
|
|
7964
|
+
{ id: "grok-3-mini-fast", name: "Grok 3 Mini Fast", reasoning: true }
|
|
7965
|
+
];
|
|
7966
|
+
function buildXaiOAuthModels() {
|
|
7967
|
+
return XAI_OAUTH_MODEL_SEEDS.map((seed) => {
|
|
7968
|
+
const prefix = seed.id.split("-")[0] ?? seed.id;
|
|
7969
|
+
return {
|
|
7970
|
+
id: seed.id,
|
|
7971
|
+
name: seed.name,
|
|
7972
|
+
upstreamModelId: seed.id,
|
|
7973
|
+
family: prefix,
|
|
7974
|
+
brand: deriveBrand(prefix),
|
|
7975
|
+
contextWindow: resolveContextWindow(seed.id),
|
|
7976
|
+
modelFormat: "openai",
|
|
7977
|
+
npm: "@ai-sdk/xai",
|
|
7978
|
+
reasoning: seed.reasoning
|
|
7979
|
+
};
|
|
7980
|
+
});
|
|
7981
|
+
}
|
|
7982
|
+
|
|
7476
7983
|
// src/registry/refresh-models.ts
|
|
7477
7984
|
function modelInfoToCached(m, npm, apiUrl) {
|
|
7478
7985
|
return {
|
|
@@ -7499,6 +8006,99 @@ async function refreshZenGoProvider(provider) {
|
|
|
7499
8006
|
return modelInfoToCached(m, npm, apiUrl);
|
|
7500
8007
|
});
|
|
7501
8008
|
}
|
|
8009
|
+
async function refreshOAuthProvider(provider, accessToken) {
|
|
8010
|
+
const tpl = provider.templateId ?? provider.id;
|
|
8011
|
+
if (tpl === "openai") return refreshOpenAiOAuthModels(accessToken);
|
|
8012
|
+
if (tpl === "xai") return refreshXaiOAuthModels(accessToken);
|
|
8013
|
+
throw new Error(`refreshOAuthProvider: unsupported template "${tpl}"`);
|
|
8014
|
+
}
|
|
8015
|
+
function parseOpenAiModelEntries(body) {
|
|
8016
|
+
if (!body || typeof body !== "object") return [];
|
|
8017
|
+
const b = body;
|
|
8018
|
+
if (Array.isArray(b.models)) {
|
|
8019
|
+
return b.models.map((m) => ({ id: m.slug ?? "", name: m.title ?? m.name ?? m.slug ?? "", context_window: m.context_window })).filter((m) => m.id.length > 0);
|
|
8020
|
+
}
|
|
8021
|
+
if (Array.isArray(b.data)) {
|
|
8022
|
+
return b.data.map((m) => ({ id: m.id ?? "", name: m.name ?? m.id ?? "", context_window: m.context_window })).filter((m) => m.id.length > 0);
|
|
8023
|
+
}
|
|
8024
|
+
return [];
|
|
8025
|
+
}
|
|
8026
|
+
function buildDynamicOAuthModel(id, name, contextWindow, seedById) {
|
|
8027
|
+
if (seedById.has(id)) return seedById.get(id);
|
|
8028
|
+
const prefix = id.split("-")[0] ?? id;
|
|
8029
|
+
return {
|
|
8030
|
+
id,
|
|
8031
|
+
name,
|
|
8032
|
+
upstreamModelId: id,
|
|
8033
|
+
family: prefix,
|
|
8034
|
+
brand: deriveBrand(prefix),
|
|
8035
|
+
contextWindow: contextWindow ?? resolveContextWindow(id),
|
|
8036
|
+
modelFormat: "openai",
|
|
8037
|
+
npm: "@ai-sdk/openai",
|
|
8038
|
+
reasoning: modelPrefersResponsesApi(id)
|
|
8039
|
+
};
|
|
8040
|
+
}
|
|
8041
|
+
async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
|
|
8042
|
+
try {
|
|
8043
|
+
const controller = new AbortController();
|
|
8044
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
8045
|
+
const response = await fetch(url, {
|
|
8046
|
+
headers: {
|
|
8047
|
+
Accept: "application/json",
|
|
8048
|
+
Authorization: `Bearer ${accessToken}`,
|
|
8049
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
8050
|
+
},
|
|
8051
|
+
signal: controller.signal
|
|
8052
|
+
}).finally(() => clearTimeout(timer));
|
|
8053
|
+
if (!response.ok) return null;
|
|
8054
|
+
return await response.json();
|
|
8055
|
+
} catch {
|
|
8056
|
+
return null;
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
async function refreshOpenAiOAuthModels(accessToken) {
|
|
8060
|
+
const TIMEOUT_MS = 1e4;
|
|
8061
|
+
const seedById = new Map(buildOpenAiOAuthModels().map((m) => [m.id, m]));
|
|
8062
|
+
const toModels = (entries) => entries.map(({ id, name, context_window }) => buildDynamicOAuthModel(id, name, context_window, seedById));
|
|
8063
|
+
const claudeVersion = getInstalledClaudeVersion();
|
|
8064
|
+
const codexBody = await fetchJsonWithAuth(
|
|
8065
|
+
`https://chatgpt.com/backend-api/codex/models?client_version=${claudeVersion}`,
|
|
8066
|
+
accessToken,
|
|
8067
|
+
TIMEOUT_MS
|
|
8068
|
+
);
|
|
8069
|
+
const codexEntries = parseOpenAiModelEntries(codexBody);
|
|
8070
|
+
if (codexEntries.length > 0) {
|
|
8071
|
+
return { models: toModels(codexEntries), source: "live" };
|
|
8072
|
+
}
|
|
8073
|
+
const chatGptBody = await fetchJsonWithAuth(
|
|
8074
|
+
"https://chatgpt.com/backend-api/models",
|
|
8075
|
+
accessToken,
|
|
8076
|
+
TIMEOUT_MS
|
|
8077
|
+
);
|
|
8078
|
+
const chatGptEntries = parseOpenAiModelEntries(chatGptBody).filter(({ id }) => !CHATGPT_CODEX_UNSUPPORTED_MODELS.has(id));
|
|
8079
|
+
if (chatGptEntries.length > 0) {
|
|
8080
|
+
return { models: toModels(chatGptEntries), source: "live" };
|
|
8081
|
+
}
|
|
8082
|
+
return { models: [...seedById.values()], source: "seed" };
|
|
8083
|
+
}
|
|
8084
|
+
async function refreshXaiOAuthModels(accessToken) {
|
|
8085
|
+
const seed = buildXaiOAuthModels();
|
|
8086
|
+
const seedById = new Map(seed.map((m) => [m.id, m]));
|
|
8087
|
+
const body = await fetchJsonWithAuth("https://api.x.ai/v1/models", accessToken, 8e3);
|
|
8088
|
+
if (body) {
|
|
8089
|
+
const ids = (body.data ?? []).map((m) => m.id).filter((id) => !!id);
|
|
8090
|
+
if (ids.length > 0) {
|
|
8091
|
+
const live = ids.map((id) => {
|
|
8092
|
+
const cached = seedById.get(id);
|
|
8093
|
+
if (cached) return cached;
|
|
8094
|
+
const prefix = id.split("-")[0] ?? id;
|
|
8095
|
+
return { id, name: id, upstreamModelId: id, family: prefix, brand: deriveBrand(prefix), contextWindow: resolveContextWindow(id), modelFormat: "openai", npm: "@ai-sdk/xai", reasoning: modelPrefersResponsesApi(id) };
|
|
8096
|
+
});
|
|
8097
|
+
return { models: live, source: "live" };
|
|
8098
|
+
}
|
|
8099
|
+
}
|
|
8100
|
+
return { models: seed, source: "seed" };
|
|
8101
|
+
}
|
|
7502
8102
|
async function refreshApiListProvider(provider, apiKey) {
|
|
7503
8103
|
const npm = provider.api.npm ?? "@ai-sdk/openai-compatible";
|
|
7504
8104
|
const catalogTemplate = resolveProviderTemplate(provider);
|
|
@@ -7578,6 +8178,25 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
|
|
|
7578
8178
|
let baseUrl;
|
|
7579
8179
|
if (source === "zen-go-api") {
|
|
7580
8180
|
models = await refreshZenGoProvider(provider);
|
|
8181
|
+
} else if (provider.authType === "oauth" && (["openai", "xai", "xai-oauth"].includes(provider.templateId ?? provider.id) || provider.id === "openai-oauth" || provider.id === "xai-oauth")) {
|
|
8182
|
+
if (!apiKey) {
|
|
8183
|
+
return {
|
|
8184
|
+
id: provider.id,
|
|
8185
|
+
name: provider.name,
|
|
8186
|
+
ok: false,
|
|
8187
|
+
reason: "OAuth token not available \u2014 try signing in again with relay-ai providers auth."
|
|
8188
|
+
};
|
|
8189
|
+
}
|
|
8190
|
+
const oauthResult = await refreshOAuthProvider(provider, apiKey);
|
|
8191
|
+
models = oauthResult.models;
|
|
8192
|
+
if (models.length === 0) {
|
|
8193
|
+
return {
|
|
8194
|
+
id: provider.id,
|
|
8195
|
+
name: provider.name,
|
|
8196
|
+
ok: false,
|
|
8197
|
+
reason: "No models available for this OAuth provider \u2014 try signing in again."
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
7581
8200
|
} else {
|
|
7582
8201
|
const template = resolveProviderTemplate(provider);
|
|
7583
8202
|
const keyOptional = template?.apiKeyOptional === true;
|
|
@@ -7688,6 +8307,7 @@ async function refreshAllProviderModels(resolveKey) {
|
|
|
7688
8307
|
// src/registry/provider-auth.ts
|
|
7689
8308
|
import pc9 from "picocolors";
|
|
7690
8309
|
import * as p9 from "@clack/prompts";
|
|
8310
|
+
import open from "open";
|
|
7691
8311
|
|
|
7692
8312
|
// src/registry/auth-broker.ts
|
|
7693
8313
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -7715,22 +8335,30 @@ async function runOpencodeAuthBroker(providerId, options = {}) {
|
|
|
7715
8335
|
}
|
|
7716
8336
|
|
|
7717
8337
|
// src/registry/provider-auth.ts
|
|
8338
|
+
var OPENAI_DISPLAY = "OpenAI ChatGPT Plus/Pro";
|
|
7718
8339
|
var PROVIDER_DISPLAY = {
|
|
7719
8340
|
xai: "xAI Grok (SuperGrok)",
|
|
7720
|
-
|
|
8341
|
+
"xai-oauth": "xAI Grok (SuperGrok)",
|
|
8342
|
+
openai: OPENAI_DISPLAY,
|
|
8343
|
+
"openai-oauth": OPENAI_DISPLAY,
|
|
7721
8344
|
"github-copilot": "GitHub Copilot (Individual / Business)"
|
|
7722
8345
|
};
|
|
8346
|
+
function openBrowser(url) {
|
|
8347
|
+
open(url).catch(() => {
|
|
8348
|
+
});
|
|
8349
|
+
}
|
|
7723
8350
|
async function runNativeDeviceCode(providerId) {
|
|
7724
8351
|
const label = PROVIDER_DISPLAY[providerId];
|
|
7725
8352
|
printOAuthStepsPanel(`${label} \u2014 Sign in`, label);
|
|
7726
8353
|
const spinner9 = p9.spinner();
|
|
7727
8354
|
spinner9.start("Waiting for authorization...");
|
|
7728
8355
|
try {
|
|
7729
|
-
if (providerId === "xai") {
|
|
8356
|
+
if (providerId === "xai" || providerId === "xai-oauth") {
|
|
7730
8357
|
const tokens2 = await runXaiDeviceCodeFlow(({ url, userCode }) => {
|
|
7731
8358
|
spinner9.stop("");
|
|
7732
8359
|
p9.log.info(`Visit: ${pc9.cyan(url)}`);
|
|
7733
8360
|
p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
|
|
8361
|
+
openBrowser(url);
|
|
7734
8362
|
spinner9.start("Waiting for authorization...");
|
|
7735
8363
|
});
|
|
7736
8364
|
spinner9.stop(pc9.green("Signed in to xAI"));
|
|
@@ -7741,6 +8369,7 @@ async function runNativeDeviceCode(providerId) {
|
|
|
7741
8369
|
spinner9.stop("");
|
|
7742
8370
|
p9.log.info(`Visit: ${pc9.cyan(url)}`);
|
|
7743
8371
|
p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
|
|
8372
|
+
openBrowser(url);
|
|
7744
8373
|
spinner9.start("Waiting for authorization...");
|
|
7745
8374
|
});
|
|
7746
8375
|
spinner9.stop(pc9.green("Signed in to GitHub Copilot"));
|
|
@@ -7750,6 +8379,7 @@ async function runNativeDeviceCode(providerId) {
|
|
|
7750
8379
|
spinner9.stop("");
|
|
7751
8380
|
p9.log.info(`Visit: ${pc9.cyan(url)}`);
|
|
7752
8381
|
p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
|
|
8382
|
+
openBrowser(url);
|
|
7753
8383
|
spinner9.start("Waiting for authorization...");
|
|
7754
8384
|
});
|
|
7755
8385
|
spinner9.stop(pc9.green("Signed in to OpenAI ChatGPT"));
|
|
@@ -7760,28 +8390,32 @@ async function runNativeDeviceCode(providerId) {
|
|
|
7760
8390
|
}
|
|
7761
8391
|
}
|
|
7762
8392
|
async function upsertOAuthProvider(providerId, cred) {
|
|
8393
|
+
const registryId = toOAuthRegistryId(providerId);
|
|
8394
|
+
const templateId = providerId.replace(/-oauth$/, "") || providerId;
|
|
7763
8395
|
const registry = loadRegistry();
|
|
7764
|
-
const authRef = oauthAuthRef(
|
|
7765
|
-
let entry = registry.providers.find((pr) => pr.id ===
|
|
8396
|
+
const authRef = oauthAuthRef(registryId);
|
|
8397
|
+
let entry = registry.providers.find((pr) => pr.id === registryId);
|
|
7766
8398
|
if (!entry) {
|
|
7767
8399
|
const raw = await fetchRawOpencodeProviders();
|
|
7768
8400
|
if (raw) {
|
|
7769
8401
|
const { providers } = buildImportProviderList(raw, { [providerId]: cred });
|
|
7770
|
-
const lp = providers.find((pr) => pr.id === providerId);
|
|
8402
|
+
const lp = providers.find((pr) => pr.id === registryId || pr.id === providerId);
|
|
7771
8403
|
if (lp) {
|
|
7772
|
-
|
|
8404
|
+
const converted = localProviderToRegistry(lp, { authType: "oauth", authRef });
|
|
8405
|
+
if (converted) entry = { ...converted, id: registryId, templateId };
|
|
7773
8406
|
}
|
|
7774
8407
|
}
|
|
7775
8408
|
}
|
|
7776
8409
|
if (!entry) {
|
|
7777
|
-
const template = getTemplateById(
|
|
8410
|
+
const template = getTemplateById(templateId);
|
|
7778
8411
|
if (!template) {
|
|
7779
8412
|
throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
|
|
7780
8413
|
}
|
|
8414
|
+
const displayName = registryId === "openai-oauth" ? "OpenAI (ChatGPT)" : registryId === "xai-oauth" ? "xAI (SuperGrok)" : template.name;
|
|
7781
8415
|
entry = {
|
|
7782
|
-
id:
|
|
7783
|
-
templateId
|
|
7784
|
-
name:
|
|
8416
|
+
id: registryId,
|
|
8417
|
+
templateId,
|
|
8418
|
+
name: displayName,
|
|
7785
8419
|
enabled: true,
|
|
7786
8420
|
authRef,
|
|
7787
8421
|
authType: "oauth",
|
|
@@ -7789,24 +8423,25 @@ async function upsertOAuthProvider(providerId, cred) {
|
|
|
7789
8423
|
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7790
8424
|
};
|
|
7791
8425
|
} else {
|
|
7792
|
-
entry = { ...entry, authType: "oauth", authRef };
|
|
8426
|
+
entry = { ...entry, authType: "oauth", authRef, templateId };
|
|
7793
8427
|
}
|
|
7794
|
-
const idx = registry.providers.findIndex((pr) => pr.id ===
|
|
8428
|
+
const idx = registry.providers.findIndex((pr) => pr.id === registryId);
|
|
7795
8429
|
if (idx >= 0) registry.providers[idx] = entry;
|
|
7796
8430
|
else registry.providers.push(entry);
|
|
7797
8431
|
saveRegistry(registry);
|
|
7798
8432
|
return entry;
|
|
7799
8433
|
}
|
|
7800
8434
|
async function authenticateProvider(providerId, options = {}) {
|
|
8435
|
+
const registryId = toOAuthRegistryId(providerId);
|
|
7801
8436
|
if (!supportsNativeOAuth(providerId)) {
|
|
7802
8437
|
if (findOpencodeBinary()) {
|
|
7803
8438
|
const cred2 = await runOpencodeAuthBroker(providerId, { method: options.brokerMethod });
|
|
7804
|
-
const saved2 = await saveProviderCredential(oauthAuthRef(
|
|
8439
|
+
const saved2 = await saveProviderCredential(oauthAuthRef(registryId), oauthCredentialToKeychainJson(cred2));
|
|
7805
8440
|
if (!saved2) {
|
|
7806
8441
|
p9.log.warn("Could not save OAuth tokens to Keychain \u2014 session may not persist.");
|
|
7807
8442
|
}
|
|
7808
8443
|
const registryProvider2 = await upsertOAuthProvider(providerId, cred2);
|
|
7809
|
-
return { providerId, credential: cred2, registryProvider: registryProvider2 };
|
|
8444
|
+
return { providerId: registryId, credential: cred2, registryProvider: registryProvider2 };
|
|
7810
8445
|
}
|
|
7811
8446
|
throw new Error(
|
|
7812
8447
|
`Native OAuth is only built in for xai and openai. Install OpenCode for other OAuth providers.`
|
|
@@ -7830,7 +8465,7 @@ async function authenticateProvider(providerId, options = {}) {
|
|
|
7830
8465
|
}
|
|
7831
8466
|
}
|
|
7832
8467
|
const cred = method === "broker" ? await runOpencodeAuthBroker(providerId, { method: options.brokerMethod }) : await runNativeDeviceCode(providerId);
|
|
7833
|
-
const saved = await saveProviderCredential(oauthAuthRef(
|
|
8468
|
+
const saved = await saveProviderCredential(oauthAuthRef(registryId), oauthCredentialToKeychainJson(cred));
|
|
7834
8469
|
if (!saved) {
|
|
7835
8470
|
p9.log.warn("Could not save OAuth tokens to Keychain \u2014 session may not persist.");
|
|
7836
8471
|
}
|
|
@@ -7838,12 +8473,12 @@ async function authenticateProvider(providerId, options = {}) {
|
|
|
7838
8473
|
const refreshSpinner = p9.spinner();
|
|
7839
8474
|
refreshSpinner.start("Refreshing model list...");
|
|
7840
8475
|
try {
|
|
7841
|
-
await refreshProviderModels(
|
|
8476
|
+
await refreshProviderModels(registryId, cred.access);
|
|
7842
8477
|
refreshSpinner.stop("Models refreshed");
|
|
7843
8478
|
} catch {
|
|
7844
8479
|
refreshSpinner.stop("Could not refresh models \u2014 run relay-ai providers refresh-models later");
|
|
7845
8480
|
}
|
|
7846
|
-
return { providerId, credential: cred, registryProvider };
|
|
8481
|
+
return { providerId: registryId, credential: cred, registryProvider };
|
|
7847
8482
|
}
|
|
7848
8483
|
function providerAuthHelpText() {
|
|
7849
8484
|
return `${pc9.bold("relay-ai providers auth")} \u2014 sign in with OAuth
|
|
@@ -8536,7 +9171,7 @@ import * as p13 from "@clack/prompts";
|
|
|
8536
9171
|
import { createServer as createServer3 } from "http";
|
|
8537
9172
|
|
|
8538
9173
|
// src/codex-responses-adapter.ts
|
|
8539
|
-
import { streamText as
|
|
9174
|
+
import { streamText as streamText3, generateText as generateText3, tool as tool3, jsonSchema as jsonSchema3 } from "ai";
|
|
8540
9175
|
function messageText(content) {
|
|
8541
9176
|
if (typeof content === "string") return content;
|
|
8542
9177
|
return (content ?? []).map((p19) => p19.type === "output_text" || p19.type === "input_text" || p19.type === "text" ? p19.text ?? "" : "").join("");
|
|
@@ -8660,9 +9295,9 @@ function translateResponsesTools(tools) {
|
|
|
8660
9295
|
const out = {};
|
|
8661
9296
|
for (const t of tools) {
|
|
8662
9297
|
if (t.type !== "function" || !t.name) continue;
|
|
8663
|
-
out[t.name] =
|
|
9298
|
+
out[t.name] = tool3({
|
|
8664
9299
|
description: t.description ?? "",
|
|
8665
|
-
inputSchema:
|
|
9300
|
+
inputSchema: jsonSchema3(t.parameters ?? { type: "object", properties: {} })
|
|
8666
9301
|
});
|
|
8667
9302
|
}
|
|
8668
9303
|
return Object.keys(out).length ? out : void 0;
|
|
@@ -8905,24 +9540,24 @@ async function writeResponsesStream(fullStream, modelId, write) {
|
|
|
8905
9540
|
});
|
|
8906
9541
|
outputItems.unshift(reasoningItem);
|
|
8907
9542
|
}
|
|
8908
|
-
for (const
|
|
9543
|
+
for (const tool4 of toolStates) {
|
|
8909
9544
|
emit("response.function_call_arguments.done", {
|
|
8910
9545
|
type: "response.function_call_arguments.done",
|
|
8911
|
-
item_id:
|
|
8912
|
-
output_index:
|
|
8913
|
-
arguments:
|
|
9546
|
+
item_id: tool4.itemId,
|
|
9547
|
+
output_index: tool4.outputIndex,
|
|
9548
|
+
arguments: tool4.args
|
|
8914
9549
|
});
|
|
8915
9550
|
const fcItem = {
|
|
8916
9551
|
type: "function_call",
|
|
8917
|
-
id:
|
|
8918
|
-
call_id:
|
|
8919
|
-
name:
|
|
8920
|
-
arguments:
|
|
9552
|
+
id: tool4.itemId,
|
|
9553
|
+
call_id: tool4.callId,
|
|
9554
|
+
name: tool4.name,
|
|
9555
|
+
arguments: tool4.args,
|
|
8921
9556
|
status: "completed"
|
|
8922
9557
|
};
|
|
8923
9558
|
emit("response.output_item.done", {
|
|
8924
9559
|
type: "response.output_item.done",
|
|
8925
|
-
output_index:
|
|
9560
|
+
output_index: tool4.outputIndex,
|
|
8926
9561
|
item: fcItem
|
|
8927
9562
|
});
|
|
8928
9563
|
outputItems.push(fcItem);
|
|
@@ -8941,7 +9576,7 @@ async function writeResponsesStream(fullStream, modelId, write) {
|
|
|
8941
9576
|
});
|
|
8942
9577
|
}
|
|
8943
9578
|
async function streamResponsesResponse(model, params, modelId, write) {
|
|
8944
|
-
const result =
|
|
9579
|
+
const result = streamText3({ model, ...params });
|
|
8945
9580
|
Promise.resolve(result.text).catch(() => {
|
|
8946
9581
|
});
|
|
8947
9582
|
Promise.resolve(result.toolCalls).catch(() => {
|
|
@@ -8955,7 +9590,7 @@ async function streamResponsesResponse(model, params, modelId, write) {
|
|
|
8955
9590
|
await writeResponsesStream(result.fullStream, modelId, write);
|
|
8956
9591
|
}
|
|
8957
9592
|
async function generateResponsesResponse(model, params, modelId) {
|
|
8958
|
-
const r = await
|
|
9593
|
+
const r = await generateText3({ model, ...params });
|
|
8959
9594
|
const createdAt = Math.floor(Date.now() / 1e3);
|
|
8960
9595
|
const responseId = newResponseId();
|
|
8961
9596
|
const output = [];
|
|
@@ -9143,6 +9778,24 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9143
9778
|
if (debug) {
|
|
9144
9779
|
log17(`-> ${req.method} ${url} content-type=${req.headers["content-type"] ?? "(none)"} content-encoding=${req.headers["content-encoding"] ?? "(none)"} content-length=${req.headers["content-length"] ?? "(none)"}`);
|
|
9145
9780
|
}
|
|
9781
|
+
if (!requireAuth && req.method === "POST") {
|
|
9782
|
+
const origin = req.headers.origin;
|
|
9783
|
+
const referer = req.headers.referer;
|
|
9784
|
+
const isValidLoopback = (uStr) => {
|
|
9785
|
+
if (!uStr) return true;
|
|
9786
|
+
try {
|
|
9787
|
+
const parsed = new URL(Array.isArray(uStr) ? uStr[0] : uStr);
|
|
9788
|
+
const h = parsed.hostname;
|
|
9789
|
+
return h === "127.0.0.1" || h === "localhost" || h === "::1";
|
|
9790
|
+
} catch {
|
|
9791
|
+
return false;
|
|
9792
|
+
}
|
|
9793
|
+
};
|
|
9794
|
+
if (!isValidLoopback(origin) || !isValidLoopback(referer)) {
|
|
9795
|
+
sendJson(res, 403, { error: { message: "Forbidden origin", type: "invalid_request_error" } });
|
|
9796
|
+
return;
|
|
9797
|
+
}
|
|
9798
|
+
}
|
|
9146
9799
|
if (req.method === "GET" && url === "/health") {
|
|
9147
9800
|
sendJson(res, 200, { ok: true });
|
|
9148
9801
|
return;
|
|
@@ -9220,6 +9873,9 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9220
9873
|
const modelId = String(body.model ?? "");
|
|
9221
9874
|
const resolved = resolveModel(routes, models, modelId);
|
|
9222
9875
|
if (!resolved) {
|
|
9876
|
+
if (debug) {
|
|
9877
|
+
log17(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
|
|
9878
|
+
}
|
|
9223
9879
|
sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
|
|
9224
9880
|
return;
|
|
9225
9881
|
}
|
|
@@ -9337,7 +9993,7 @@ function resolveCodexRoute(provider, model, apiKey) {
|
|
|
9337
9993
|
reasoning: model.reasoning,
|
|
9338
9994
|
interleavedReasoningField: model.interleavedReasoningField
|
|
9339
9995
|
};
|
|
9340
|
-
if (
|
|
9996
|
+
if (model.npm === "@ai-sdk/openai" && provider.authType !== "oauth" && model.modelFormat === "openai") {
|
|
9341
9997
|
return { tier: "direct", ...base };
|
|
9342
9998
|
}
|
|
9343
9999
|
return { tier: "proxy", ...base };
|
|
@@ -9372,6 +10028,7 @@ function codexProviderEnvKey(providerId) {
|
|
|
9372
10028
|
const known = {
|
|
9373
10029
|
openai: "OPENAI_API_KEY",
|
|
9374
10030
|
xai: "XAI_API_KEY",
|
|
10031
|
+
"xai-oauth": "XAI_API_KEY",
|
|
9375
10032
|
anthropic: "ANTHROPIC_API_KEY",
|
|
9376
10033
|
google: "GEMINI_API_KEY"
|
|
9377
10034
|
};
|
|
@@ -9672,7 +10329,7 @@ function launchCodex(modelId, env, extraArgs) {
|
|
|
9672
10329
|
// src/codex/prompts.ts
|
|
9673
10330
|
import pc11 from "picocolors";
|
|
9674
10331
|
import * as p11 from "@clack/prompts";
|
|
9675
|
-
async function pickCodexProvider(providers, prefs, hasFavorites = false) {
|
|
10332
|
+
async function pickCodexProvider(providers, prefs, hasFavorites = false, initialProviderId) {
|
|
9676
10333
|
if (providers.length === 0 && !hasFavorites) return null;
|
|
9677
10334
|
const options = providers.map((lp) => providerSelectOption(lp));
|
|
9678
10335
|
if (hasFavorites) {
|
|
@@ -9682,7 +10339,7 @@ async function pickCodexProvider(providers, prefs, hasFavorites = false) {
|
|
|
9682
10339
|
hint: `${prefs.favoriteModels?.length ?? 0} saved favorites`
|
|
9683
10340
|
});
|
|
9684
10341
|
}
|
|
9685
|
-
const initial = prefs.lastCodexProvider && options.some((o) => o.value === prefs.lastCodexProvider) ? prefs.lastCodexProvider : options[0].value;
|
|
10342
|
+
const initial = initialProviderId && options.some((o) => o.value === initialProviderId) ? initialProviderId : prefs.lastCodexProvider && options.some((o) => o.value === prefs.lastCodexProvider) ? prefs.lastCodexProvider : options[0].value;
|
|
9686
10343
|
const chosen = await p11.select({
|
|
9687
10344
|
message: "Which provider for Codex?",
|
|
9688
10345
|
options,
|
|
@@ -9698,32 +10355,43 @@ async function pickCodexProvider(providers, prefs, hasFavorites = false) {
|
|
|
9698
10355
|
async function pickCodexModel(provider, prefs) {
|
|
9699
10356
|
const recentIds = (prefs.recentModelsByProvider?.[provider.id] ?? []).slice(0, 3);
|
|
9700
10357
|
const recentModels = recentIds.map((id) => provider.models.find((m) => m.id === id)).filter((m) => m !== void 0);
|
|
9701
|
-
let selectedModel;
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
10358
|
+
let selectedModel = null;
|
|
10359
|
+
while (true) {
|
|
10360
|
+
if (recentModels.length > 0) {
|
|
10361
|
+
const options = [
|
|
10362
|
+
...recentModels.map((m) => modelSelectOption(m, "recent")),
|
|
10363
|
+
navOption("__browse_all__", "Browse all models \u2192", `${provider.models.length} available`),
|
|
10364
|
+
navOption("__back__", "\u2190 Go back", "Select a different provider")
|
|
10365
|
+
];
|
|
10366
|
+
const picked = await p11.select({
|
|
10367
|
+
message: `Model for ${provider.name}?`,
|
|
10368
|
+
options,
|
|
10369
|
+
initialValue: recentModels[0].id
|
|
10370
|
+
});
|
|
10371
|
+
if (p11.isCancel(picked) || String(picked) === "__back__") {
|
|
10372
|
+
return "back";
|
|
10373
|
+
}
|
|
10374
|
+
if (String(picked) === "__browse_all__") {
|
|
10375
|
+
const browsed = await browseAllModels(provider, prefs);
|
|
10376
|
+
if (browsed === "back") {
|
|
10377
|
+
continue;
|
|
10378
|
+
}
|
|
10379
|
+
if (!browsed) return null;
|
|
10380
|
+
selectedModel = browsed;
|
|
10381
|
+
break;
|
|
10382
|
+
} else {
|
|
10383
|
+
selectedModel = recentModels.find((m) => m.id === String(picked));
|
|
10384
|
+
break;
|
|
10385
|
+
}
|
|
10386
|
+
} else {
|
|
9717
10387
|
const browsed = await browseAllModels(provider, prefs);
|
|
10388
|
+
if (browsed === "back") {
|
|
10389
|
+
return "back";
|
|
10390
|
+
}
|
|
9718
10391
|
if (!browsed) return null;
|
|
9719
10392
|
selectedModel = browsed;
|
|
9720
|
-
|
|
9721
|
-
selectedModel = recentModels.find((m) => m.id === String(picked));
|
|
10393
|
+
break;
|
|
9722
10394
|
}
|
|
9723
|
-
} else {
|
|
9724
|
-
const browsed = await browseAllModels(provider, prefs);
|
|
9725
|
-
if (!browsed) return null;
|
|
9726
|
-
selectedModel = browsed;
|
|
9727
10395
|
}
|
|
9728
10396
|
return selectedModel;
|
|
9729
10397
|
}
|
|
@@ -9846,13 +10514,18 @@ function buildFavoritesAppCatalog(resolved) {
|
|
|
9846
10514
|
// src/codex/favorites-launch.ts
|
|
9847
10515
|
import * as p12 from "@clack/prompts";
|
|
9848
10516
|
function buildCodexProxyRoutesFromResolved(resolved, providersById) {
|
|
9849
|
-
|
|
10517
|
+
const skippedOAuth = [];
|
|
10518
|
+
const routes = resolved.map((r) => {
|
|
9850
10519
|
const provider = providersById.get(r.providerId);
|
|
9851
10520
|
if (!provider) return void 0;
|
|
9852
10521
|
const model = r.model;
|
|
10522
|
+
if (!r.apiKey && provider.authType === "oauth") {
|
|
10523
|
+
skippedOAuth.push(`${r.providerId}/${model.id}`);
|
|
10524
|
+
return void 0;
|
|
10525
|
+
}
|
|
9853
10526
|
const route = resolveCodexRoute(provider, model, r.apiKey);
|
|
9854
10527
|
return {
|
|
9855
|
-
modelId:
|
|
10528
|
+
modelId: codexCliFavoritesSlug(r.providerId, model.id),
|
|
9856
10529
|
npm: route.npm,
|
|
9857
10530
|
apiKey: route.apiKey,
|
|
9858
10531
|
baseURL: route.baseURL,
|
|
@@ -9862,6 +10535,12 @@ function buildCodexProxyRoutesFromResolved(resolved, providersById) {
|
|
|
9862
10535
|
oauthAccountId: route.oauthAccountId
|
|
9863
10536
|
};
|
|
9864
10537
|
}).filter((r) => r !== void 0);
|
|
10538
|
+
if (skippedOAuth.length > 0) {
|
|
10539
|
+
p12.log.warn(
|
|
10540
|
+
`Skipped ${skippedOAuth.length} OAuth favorite(s) (OAuth auth not supported in favorites catalog): ${skippedOAuth.join(", ")}`
|
|
10541
|
+
);
|
|
10542
|
+
}
|
|
10543
|
+
return routes;
|
|
9865
10544
|
}
|
|
9866
10545
|
function resolveCodexFavorites(activeProvider, selectedModel, compatible, favorites, agent, zenGoApiKey) {
|
|
9867
10546
|
const ctx = {
|
|
@@ -10381,26 +11060,35 @@ Error: ${launchPlan.error}
|
|
|
10381
11060
|
p13.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
10382
11061
|
}
|
|
10383
11062
|
} else if (!configOnly) {
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
11063
|
+
let currentInitialProvider = prefs.lastCodexProvider && compatible.some((o) => o.id === prefs.lastCodexProvider) ? prefs.lastCodexProvider : compatible[0].id;
|
|
11064
|
+
while (true) {
|
|
11065
|
+
const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive, currentInitialProvider);
|
|
11066
|
+
if (!pickedProvider) return 0;
|
|
11067
|
+
if (pickedProvider === "__favorites__") {
|
|
11068
|
+
const favoriteProviders = compatible.map((provider) => ({
|
|
11069
|
+
...provider,
|
|
11070
|
+
models: routableModelsForProvider(provider, "codex")
|
|
11071
|
+
}));
|
|
11072
|
+
const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
|
|
11073
|
+
if (!favoriteStart) {
|
|
11074
|
+
p13.log.warn("No saved Codex favorites are currently available.");
|
|
11075
|
+
return 0;
|
|
11076
|
+
}
|
|
11077
|
+
activeProvider = favoriteStart.provider;
|
|
11078
|
+
selectedModel = favoriteStart.model;
|
|
11079
|
+
p13.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
11080
|
+
break;
|
|
11081
|
+
} else {
|
|
11082
|
+
activeProvider = pickedProvider;
|
|
11083
|
+
const pickedModelResult = await pickCodexModel(activeProvider, prefs);
|
|
11084
|
+
if (pickedModelResult === "back") {
|
|
11085
|
+
currentInitialProvider = activeProvider.id;
|
|
11086
|
+
continue;
|
|
11087
|
+
}
|
|
11088
|
+
if (!pickedModelResult) return 0;
|
|
11089
|
+
selectedModel = pickedModelResult;
|
|
11090
|
+
break;
|
|
10395
11091
|
}
|
|
10396
|
-
activeProvider = favoriteStart.provider;
|
|
10397
|
-
selectedModel = favoriteStart.model;
|
|
10398
|
-
p13.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
10399
|
-
} else {
|
|
10400
|
-
activeProvider = pickedProvider;
|
|
10401
|
-
const pickedModel = await pickCodexModel(activeProvider, prefs);
|
|
10402
|
-
if (!pickedModel) return 0;
|
|
10403
|
-
selectedModel = pickedModel;
|
|
10404
11092
|
}
|
|
10405
11093
|
}
|
|
10406
11094
|
let resolvedFavorites = [];
|
|
@@ -11045,9 +11733,9 @@ function openCodexAppAt(path) {
|
|
|
11045
11733
|
}
|
|
11046
11734
|
if (process.platform === "win32") {
|
|
11047
11735
|
if (path.startsWith("shell:AppsFolder\\")) {
|
|
11048
|
-
runPowerShell(`Start-Process ${
|
|
11736
|
+
runPowerShell(`Start-Process '${path.replace(/'/g, "''")}'`);
|
|
11049
11737
|
} else {
|
|
11050
|
-
runPowerShell(`Start-Process -FilePath ${
|
|
11738
|
+
runPowerShell(`Start-Process -FilePath '${path.replace(/'/g, "''")}'`);
|
|
11051
11739
|
}
|
|
11052
11740
|
}
|
|
11053
11741
|
}
|
|
@@ -11380,23 +12068,32 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
11380
12068
|
);
|
|
11381
12069
|
let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastCodexModel) ?? activeProvider.models[0];
|
|
11382
12070
|
if (!configOnly) {
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
12071
|
+
let currentInitialProvider = prefs.lastCodexProvider && compatible.some((o) => o.id === prefs.lastCodexProvider) ? prefs.lastCodexProvider : compatible[0].id;
|
|
12072
|
+
while (true) {
|
|
12073
|
+
const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive, currentInitialProvider);
|
|
12074
|
+
if (!pickedProvider) return 0;
|
|
12075
|
+
if (pickedProvider === "__favorites__") {
|
|
12076
|
+
const favoriteProviders = compatible.map(providerForCodexPicker);
|
|
12077
|
+
const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
|
|
12078
|
+
if (!favoriteStart) {
|
|
12079
|
+
p15.log.warn("No saved Codex App favorites are currently available.");
|
|
12080
|
+
return 0;
|
|
12081
|
+
}
|
|
12082
|
+
activeProvider = favoriteStart.provider;
|
|
12083
|
+
selectedModel = favoriteStart.model;
|
|
12084
|
+
p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
12085
|
+
break;
|
|
12086
|
+
} else {
|
|
12087
|
+
activeProvider = providerForCodexPicker(pickedProvider);
|
|
12088
|
+
const pickedModelResult = await pickCodexModel(activeProvider, prefs);
|
|
12089
|
+
if (pickedModelResult === "back") {
|
|
12090
|
+
currentInitialProvider = activeProvider.id;
|
|
12091
|
+
continue;
|
|
12092
|
+
}
|
|
12093
|
+
if (!pickedModelResult) return 0;
|
|
12094
|
+
selectedModel = pickedModelResult;
|
|
12095
|
+
break;
|
|
11391
12096
|
}
|
|
11392
|
-
activeProvider = favoriteStart.provider;
|
|
11393
|
-
selectedModel = favoriteStart.model;
|
|
11394
|
-
p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
11395
|
-
} else {
|
|
11396
|
-
activeProvider = providerForCodexPicker(pickedProvider);
|
|
11397
|
-
const pickedModel = await pickCodexModel(activeProvider, prefs);
|
|
11398
|
-
if (!pickedModel) return 0;
|
|
11399
|
-
selectedModel = pickedModel;
|
|
11400
12097
|
}
|
|
11401
12098
|
}
|
|
11402
12099
|
const regEntry = loadRegistry().providers.find((pr) => pr.id === activeProvider.id);
|
|
@@ -11575,7 +12272,7 @@ import * as p17 from "@clack/prompts";
|
|
|
11575
12272
|
import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
|
|
11576
12273
|
import { homedir as homedir11 } from "os";
|
|
11577
12274
|
import { join as join17, dirname as dirname7 } from "path";
|
|
11578
|
-
import { randomUUID } from "crypto";
|
|
12275
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
11579
12276
|
function getClaudeDesktopHome() {
|
|
11580
12277
|
if (process.platform === "win32") {
|
|
11581
12278
|
return join17(process.env.APPDATA || join17(homedir11(), "AppData", "Roaming"), "Claude-3p");
|
|
@@ -11613,7 +12310,7 @@ function buildRelayAiConfig(proxyPort) {
|
|
|
11613
12310
|
};
|
|
11614
12311
|
}
|
|
11615
12312
|
function writeRelayAiConfig(proxyPort) {
|
|
11616
|
-
const uuid =
|
|
12313
|
+
const uuid = randomUUID2();
|
|
11617
12314
|
const configPath = join17(getConfigLibraryPath(), `${uuid}.json`);
|
|
11618
12315
|
const config = buildRelayAiConfig(proxyPort);
|
|
11619
12316
|
mkdirSync9(dirname7(configPath), { recursive: true });
|
|
@@ -11878,9 +12575,9 @@ function openClaudeAppAt(path) {
|
|
|
11878
12575
|
}
|
|
11879
12576
|
if (process.platform === "win32") {
|
|
11880
12577
|
if (path.startsWith("shell:AppsFolder\\")) {
|
|
11881
|
-
runPowerShell2(`Start-Process ${
|
|
12578
|
+
runPowerShell2(`Start-Process '${path.replace(/'/g, "''")}'`);
|
|
11882
12579
|
} else {
|
|
11883
|
-
runPowerShell2(`Start-Process -FilePath ${
|
|
12580
|
+
runPowerShell2(`Start-Process -FilePath '${path.replace(/'/g, "''")}'`);
|
|
11884
12581
|
}
|
|
11885
12582
|
}
|
|
11886
12583
|
}
|
|
@@ -13002,7 +13699,7 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13002
13699
|
const childEnv = buildChildEnv(
|
|
13003
13700
|
`http://127.0.0.1:${proxyHandle.port}`,
|
|
13004
13701
|
startingRoute.aliasId,
|
|
13005
|
-
|
|
13702
|
+
proxyHandle.token,
|
|
13006
13703
|
proxyHandle.port,
|
|
13007
13704
|
contextWindow,
|
|
13008
13705
|
true
|
|
@@ -13086,39 +13783,86 @@ async function runModelsCommand() {
|
|
|
13086
13783
|
});
|
|
13087
13784
|
if (p18.isCancel(addPath)) continue;
|
|
13088
13785
|
let provider;
|
|
13089
|
-
let
|
|
13786
|
+
let browsedMultiple = [];
|
|
13090
13787
|
if (addPath === "global") {
|
|
13091
13788
|
const globalPick = await pickGlobalFavoriteModel(allProviders, favorites);
|
|
13092
13789
|
if (globalPick === null) continue;
|
|
13093
13790
|
if (globalPick !== ADD_BY_PROVIDER) {
|
|
13094
13791
|
provider = allProviders.find((ap) => ap.id === globalPick.providerId);
|
|
13095
|
-
|
|
13792
|
+
browsedMultiple = [globalPick.model];
|
|
13096
13793
|
}
|
|
13097
13794
|
}
|
|
13098
|
-
if (
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13795
|
+
if (browsedMultiple.length === 0) {
|
|
13796
|
+
let currentInitialProvider = void 0;
|
|
13797
|
+
while (true) {
|
|
13798
|
+
const providerOptions = allProviders.map((ap) => providerSelectOption(ap));
|
|
13799
|
+
const pickedProviderId = await p18.select({
|
|
13800
|
+
message: "Which provider?",
|
|
13801
|
+
options: providerOptions,
|
|
13802
|
+
initialValue: currentInitialProvider
|
|
13803
|
+
});
|
|
13804
|
+
if (p18.isCancel(pickedProviderId)) break;
|
|
13805
|
+
provider = allProviders.find((ap) => ap.id === pickedProviderId);
|
|
13806
|
+
const options2 = provider.models.map((m) => {
|
|
13807
|
+
const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
|
|
13808
|
+
const label = formatCodexModelLabel(m);
|
|
13809
|
+
return {
|
|
13810
|
+
value: m.id,
|
|
13811
|
+
label: fmtModel(label, m.id),
|
|
13812
|
+
hint: favorited ? pc16.yellow("\u2605 already favorite") : ""
|
|
13813
|
+
};
|
|
13814
|
+
});
|
|
13815
|
+
const pickedModelIds = await p18.multiselect({
|
|
13816
|
+
message: `Select models to add from ${provider.name} ${pc16.dim("(Space to select, Enter to confirm)")}`,
|
|
13817
|
+
options: options2,
|
|
13818
|
+
required: false
|
|
13819
|
+
});
|
|
13820
|
+
if (p18.isCancel(pickedModelIds)) {
|
|
13821
|
+
currentInitialProvider = provider.id;
|
|
13822
|
+
continue;
|
|
13823
|
+
}
|
|
13824
|
+
if (pickedModelIds.length === 0) {
|
|
13825
|
+
currentInitialProvider = provider.id;
|
|
13826
|
+
continue;
|
|
13827
|
+
}
|
|
13828
|
+
browsedMultiple = provider.models.filter((m) => pickedModelIds.includes(m.id));
|
|
13829
|
+
break;
|
|
13830
|
+
}
|
|
13831
|
+
if (browsedMultiple.length === 0) continue;
|
|
13108
13832
|
}
|
|
13109
|
-
const
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
13833
|
+
const addedModels = [];
|
|
13834
|
+
let duplicateCount = 0;
|
|
13835
|
+
let limitReached = false;
|
|
13836
|
+
for (const model of browsedMultiple) {
|
|
13837
|
+
const fav = { providerId: provider.id, modelId: model.id };
|
|
13838
|
+
const result = addFavorite(favorites, fav);
|
|
13839
|
+
if (!result.ok) {
|
|
13840
|
+
if (result.reason === "duplicate") {
|
|
13841
|
+
duplicateCount++;
|
|
13842
|
+
} else {
|
|
13843
|
+
limitReached = true;
|
|
13844
|
+
break;
|
|
13845
|
+
}
|
|
13114
13846
|
} else {
|
|
13115
|
-
|
|
13847
|
+
favorites = result.list;
|
|
13848
|
+
favoritesDirty = true;
|
|
13849
|
+
addedModels.push(model);
|
|
13116
13850
|
}
|
|
13117
|
-
continue;
|
|
13118
13851
|
}
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13852
|
+
if (addedModels.length > 0) {
|
|
13853
|
+
if (addedModels.length === 1) {
|
|
13854
|
+
const modelName = addedModels[0].name || addedModels[0].id;
|
|
13855
|
+
p18.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
|
|
13856
|
+
} else {
|
|
13857
|
+
p18.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
|
|
13858
|
+
}
|
|
13859
|
+
}
|
|
13860
|
+
if (duplicateCount > 0) {
|
|
13861
|
+
p18.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
|
|
13862
|
+
}
|
|
13863
|
+
if (limitReached) {
|
|
13864
|
+
p18.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
|
|
13865
|
+
}
|
|
13122
13866
|
} else if (choice.startsWith("fav-")) {
|
|
13123
13867
|
const idx = parseInt(choice.slice(4), 10);
|
|
13124
13868
|
const fav = favorites[idx];
|
|
@@ -13226,31 +13970,40 @@ Error: ${launchPlan.error}
|
|
|
13226
13970
|
}
|
|
13227
13971
|
if (!dryRun) recordLaunchSelection("claude", activeProvider.id, selectedModel.id, prefs);
|
|
13228
13972
|
} else {
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
const providerChoice = chosen;
|
|
13239
|
-
if (providerChoice === "__favorites__") {
|
|
13240
|
-
const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
|
|
13241
|
-
if (!favoriteStart) {
|
|
13242
|
-
p18.log.warn("No saved favorites are currently available.");
|
|
13973
|
+
let currentInitialProvider = initialProvider;
|
|
13974
|
+
while (true) {
|
|
13975
|
+
const chosen = await p18.select({
|
|
13976
|
+
message: "Which provider?",
|
|
13977
|
+
options: providerOptions,
|
|
13978
|
+
initialValue: currentInitialProvider
|
|
13979
|
+
});
|
|
13980
|
+
if (p18.isCancel(chosen)) {
|
|
13981
|
+
p18.cancel("Cancelled.");
|
|
13243
13982
|
return 0;
|
|
13244
13983
|
}
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13984
|
+
const providerChoice = chosen;
|
|
13985
|
+
if (providerChoice === "__favorites__") {
|
|
13986
|
+
const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
|
|
13987
|
+
if (!favoriteStart) {
|
|
13988
|
+
p18.log.warn("No saved favorites are currently available.");
|
|
13989
|
+
return 0;
|
|
13990
|
+
}
|
|
13991
|
+
activeProvider = favoriteStart.provider;
|
|
13992
|
+
selectedModel = favoriteStart.model;
|
|
13993
|
+
p18.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
13994
|
+
break;
|
|
13995
|
+
} else {
|
|
13996
|
+
activeProvider = allProviders.find((lp) => lp.id === providerChoice);
|
|
13997
|
+
const pickedModelResult = await pickLocalModel(activeProvider, conflicts, prefs);
|
|
13998
|
+
if (pickedModelResult === "back") {
|
|
13999
|
+
currentInitialProvider = activeProvider.id;
|
|
14000
|
+
continue;
|
|
14001
|
+
}
|
|
14002
|
+
if (!pickedModelResult) return 0;
|
|
14003
|
+
selectedModel = pickedModelResult;
|
|
14004
|
+
if (!dryRun) recordLaunchSelection("claude", activeProvider.id, selectedModel.id, prefs);
|
|
14005
|
+
break;
|
|
14006
|
+
}
|
|
13254
14007
|
}
|
|
13255
14008
|
}
|
|
13256
14009
|
const localProviders = catalog.localProviders.length > 0 ? catalog.localProviders : null;
|
|
@@ -13346,7 +14099,8 @@ Error: ${launchPlan.error}
|
|
|
13346
14099
|
supportedParameters: selectedModel.supportedParameters,
|
|
13347
14100
|
reasoning: selectedModel.reasoning,
|
|
13348
14101
|
interleavedReasoningField: selectedModel.interleavedReasoningField
|
|
13349
|
-
}
|
|
14102
|
+
},
|
|
14103
|
+
launchApiKey
|
|
13350
14104
|
);
|
|
13351
14105
|
if (!isAgentStdoutMode()) {
|
|
13352
14106
|
p18.log.info(
|
|
@@ -13360,7 +14114,7 @@ Error: ${launchPlan.error}
|
|
|
13360
14114
|
childEnv = buildChildEnv(
|
|
13361
14115
|
`http://127.0.0.1:${proxyHandle.port}`,
|
|
13362
14116
|
selectedModel.id,
|
|
13363
|
-
|
|
14117
|
+
proxyHandle.token,
|
|
13364
14118
|
proxyHandle.port,
|
|
13365
14119
|
selectedModel.contextWindow
|
|
13366
14120
|
);
|