@jacobbd/relay-ai 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -3
- package/CHANGELOG.md +21 -0
- package/README.md +23 -5
- package/dist/cli.js +1932 -675
- package/package.json +1 -1
- package/test-proxy.ts +19 -0
- package/test-split.js +1 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import pc17 from "picocolors";
|
|
5
5
|
|
|
6
6
|
// src/ui.ts
|
|
7
7
|
import pc from "picocolors";
|
|
@@ -35,7 +35,7 @@ import { join } from "path";
|
|
|
35
35
|
// package.json
|
|
36
36
|
var package_default = {
|
|
37
37
|
name: "@jacobbd/relay-ai",
|
|
38
|
-
version: "0.
|
|
38
|
+
version: "0.3.1",
|
|
39
39
|
publishConfig: {
|
|
40
40
|
access: "public"
|
|
41
41
|
},
|
|
@@ -455,8 +455,12 @@ function isDeepSeekReasoningModel(modelId) {
|
|
|
455
455
|
const lower = modelId.toLowerCase();
|
|
456
456
|
return lower === "deepseek-v4-flash" || lower === "deepseek-v4-pro" || lower.startsWith("deepseek-v4-flash-") || lower.startsWith("deepseek-v4-pro-") || lower === "deepseek-reasoner" || lower === "deepseek-chat";
|
|
457
457
|
}
|
|
458
|
+
function isKimiReasoningModel(modelId) {
|
|
459
|
+
const lower = modelId.toLowerCase();
|
|
460
|
+
return lower.startsWith("kimi-");
|
|
461
|
+
}
|
|
458
462
|
function hasSupportedParameter(metadata, param) {
|
|
459
|
-
return (metadata?.supportedParameters ?? []).some((
|
|
463
|
+
return (metadata?.supportedParameters ?? []).some((p21) => p21 === param);
|
|
460
464
|
}
|
|
461
465
|
function isOpenRouterRoute(npm, metadata) {
|
|
462
466
|
return npm === "@openrouter/ai-sdk-provider" || metadata?.providerId === "openrouter" || metadata?.apiBaseUrl?.includes("openrouter.ai") === true;
|
|
@@ -678,6 +682,17 @@ function getReasoningCapabilities(npm, modelId, metadata) {
|
|
|
678
682
|
wireFormat: { kind: "deepseek-thinking" }
|
|
679
683
|
};
|
|
680
684
|
}
|
|
685
|
+
if (isKimiReasoningModel(modelId)) {
|
|
686
|
+
return {
|
|
687
|
+
levels: [...OPENAI_EFFORT_LEVELS],
|
|
688
|
+
defaultLevel: "high",
|
|
689
|
+
supportsSummaries: false,
|
|
690
|
+
mode: "controllable",
|
|
691
|
+
source: "provider-rule",
|
|
692
|
+
confidence: "documented",
|
|
693
|
+
wireFormat: { kind: "openai-reasoning-effort" }
|
|
694
|
+
};
|
|
695
|
+
}
|
|
681
696
|
if (hasSupportedParameter(metadata, "reasoning_effort")) {
|
|
682
697
|
return {
|
|
683
698
|
levels: ["low", "medium", "high", "xhigh"],
|
|
@@ -762,6 +777,10 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
|
|
|
762
777
|
if (isDeepSeekReasoningModel(modelId)) {
|
|
763
778
|
return deepSeekEffortProviderOptions(effort);
|
|
764
779
|
}
|
|
780
|
+
if (isKimiReasoningModel(modelId)) {
|
|
781
|
+
const reasoningEffort = mapCodexEffortToOpenAI(effort);
|
|
782
|
+
return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
|
|
783
|
+
}
|
|
765
784
|
if (hasSupportedParameter(metadata, "reasoning_effort")) {
|
|
766
785
|
const reasoningEffort = mapCodexEffortToOpenAI(effort);
|
|
767
786
|
return reasoningEffort ? { openai: { reasoningEffort }, openaiCompatible: { reasoningEffort }, "openai-compatible": { reasoningEffort } } : void 0;
|
|
@@ -1104,7 +1123,7 @@ function printFavoritesOnlyPanel() {
|
|
|
1104
1123
|
}
|
|
1105
1124
|
|
|
1106
1125
|
// src/cli.ts
|
|
1107
|
-
import * as
|
|
1126
|
+
import * as p20 from "@clack/prompts";
|
|
1108
1127
|
import { realpathSync } from "fs";
|
|
1109
1128
|
import { fileURLToPath } from "url";
|
|
1110
1129
|
|
|
@@ -2562,9 +2581,9 @@ function migrateLegacyCloudProviders(registry) {
|
|
|
2562
2581
|
return changed;
|
|
2563
2582
|
}
|
|
2564
2583
|
function migrateOAuthOpenAiProvider(registry) {
|
|
2565
|
-
if (registry.providers.some((
|
|
2584
|
+
if (registry.providers.some((p21) => p21.id === "openai-oauth")) return false;
|
|
2566
2585
|
const idx = registry.providers.findIndex(
|
|
2567
|
-
(
|
|
2586
|
+
(p21) => p21.id === "openai" && p21.authType === "oauth"
|
|
2568
2587
|
);
|
|
2569
2588
|
if (idx < 0) return false;
|
|
2570
2589
|
const existing = registry.providers[idx];
|
|
@@ -2577,9 +2596,9 @@ function migrateOAuthOpenAiProvider(registry) {
|
|
|
2577
2596
|
return true;
|
|
2578
2597
|
}
|
|
2579
2598
|
function migrateOAuthXaiProvider(registry) {
|
|
2580
|
-
if (registry.providers.some((
|
|
2599
|
+
if (registry.providers.some((p21) => p21.id === "xai-oauth")) return false;
|
|
2581
2600
|
const idx = registry.providers.findIndex(
|
|
2582
|
-
(
|
|
2601
|
+
(p21) => p21.id === "xai" && p21.authType === "oauth"
|
|
2583
2602
|
);
|
|
2584
2603
|
if (idx < 0) return false;
|
|
2585
2604
|
const existing = registry.providers[idx];
|
|
@@ -2636,33 +2655,33 @@ function writeSecureFile(path, content) {
|
|
|
2636
2655
|
}
|
|
2637
2656
|
function parseProvider(raw) {
|
|
2638
2657
|
if (!raw || typeof raw !== "object") return null;
|
|
2639
|
-
const
|
|
2640
|
-
if (typeof
|
|
2641
|
-
if (typeof
|
|
2642
|
-
if (typeof
|
|
2643
|
-
if (typeof
|
|
2644
|
-
if (typeof
|
|
2645
|
-
if (typeof
|
|
2646
|
-
const api =
|
|
2658
|
+
const p21 = raw;
|
|
2659
|
+
if (typeof p21.id !== "string" || !isValidProviderId(p21.id)) return null;
|
|
2660
|
+
if (typeof p21.templateId !== "string" || !p21.templateId) return null;
|
|
2661
|
+
if (typeof p21.name !== "string" || !p21.name) return null;
|
|
2662
|
+
if (typeof p21.enabled !== "boolean") return null;
|
|
2663
|
+
if (typeof p21.authRef !== "string" || !p21.authRef) return null;
|
|
2664
|
+
if (typeof p21.addedAt !== "string" || !p21.addedAt) return null;
|
|
2665
|
+
const api = p21.api;
|
|
2647
2666
|
if (!api || typeof api !== "object") return null;
|
|
2648
2667
|
const provider = {
|
|
2649
|
-
id:
|
|
2650
|
-
templateId:
|
|
2651
|
-
name:
|
|
2652
|
-
enabled:
|
|
2653
|
-
authRef:
|
|
2668
|
+
id: p21.id,
|
|
2669
|
+
templateId: p21.templateId,
|
|
2670
|
+
name: p21.name,
|
|
2671
|
+
enabled: p21.enabled,
|
|
2672
|
+
authRef: p21.authRef,
|
|
2654
2673
|
api,
|
|
2655
|
-
addedAt:
|
|
2674
|
+
addedAt: p21.addedAt
|
|
2656
2675
|
};
|
|
2657
|
-
if (
|
|
2658
|
-
provider.subscriptionFilter =
|
|
2676
|
+
if (p21.subscriptionFilter === "free" || p21.subscriptionFilter === "zen" || p21.subscriptionFilter === "go") {
|
|
2677
|
+
provider.subscriptionFilter = p21.subscriptionFilter;
|
|
2659
2678
|
}
|
|
2660
|
-
if (
|
|
2661
|
-
provider.authType =
|
|
2679
|
+
if (p21.authType === "api" || p21.authType === "oauth" || p21.authType === "none") {
|
|
2680
|
+
provider.authType = p21.authType;
|
|
2662
2681
|
}
|
|
2663
|
-
if (typeof
|
|
2664
|
-
if (
|
|
2665
|
-
const cache =
|
|
2682
|
+
if (typeof p21.refreshedAt === "string") provider.refreshedAt = p21.refreshedAt;
|
|
2683
|
+
if (p21.modelsCache && typeof p21.modelsCache === "object") {
|
|
2684
|
+
const cache = p21.modelsCache;
|
|
2666
2685
|
if (typeof cache.fetchedAt === "string" && Array.isArray(cache.models)) {
|
|
2667
2686
|
provider.modelsCache = {
|
|
2668
2687
|
fetchedAt: cache.fetchedAt,
|
|
@@ -3525,6 +3544,113 @@ function listCredentialSkippedProviders(raw, authEntries, importedIds, alreadyRe
|
|
|
3525
3544
|
return skipped;
|
|
3526
3545
|
}
|
|
3527
3546
|
|
|
3547
|
+
// src/trace-log.ts
|
|
3548
|
+
import {
|
|
3549
|
+
chmodSync as chmodSync4,
|
|
3550
|
+
existsSync as existsSync7,
|
|
3551
|
+
mkdirSync as mkdirSync4,
|
|
3552
|
+
readFileSync as readFileSync6,
|
|
3553
|
+
unlinkSync,
|
|
3554
|
+
writeFileSync as writeFileSync3
|
|
3555
|
+
} from "fs";
|
|
3556
|
+
import { join as join8 } from "path";
|
|
3557
|
+
import pc2 from "picocolors";
|
|
3558
|
+
var DIR_MODE2 = 448;
|
|
3559
|
+
var FILE_MODE4 = 384;
|
|
3560
|
+
var CLAUDE_DEBUG_LOG = "claude-debug.log";
|
|
3561
|
+
var PROXY_DEBUG_LOG = "proxy-debug.log";
|
|
3562
|
+
var CODEX_PROXY_DEBUG_LOG = "codex-proxy-debug.log";
|
|
3563
|
+
var GEMINI_PROXY_DEBUG_LOG = "gemini-proxy-debug.log";
|
|
3564
|
+
var PROVIDER_DEBUG_LOG = "provider-debug.log";
|
|
3565
|
+
function ensureLogsDir() {
|
|
3566
|
+
const dir = getLogsPath();
|
|
3567
|
+
mkdirSync4(dir, { recursive: true, mode: DIR_MODE2 });
|
|
3568
|
+
try {
|
|
3569
|
+
chmodSync4(dir, DIR_MODE2);
|
|
3570
|
+
} catch {
|
|
3571
|
+
}
|
|
3572
|
+
return dir;
|
|
3573
|
+
}
|
|
3574
|
+
function getClaudeDebugLogPath() {
|
|
3575
|
+
return join8(ensureLogsDir(), CLAUDE_DEBUG_LOG);
|
|
3576
|
+
}
|
|
3577
|
+
function prepareClaudeTraceLog() {
|
|
3578
|
+
const path = getClaudeDebugLogPath();
|
|
3579
|
+
resetTraceLog(path);
|
|
3580
|
+
return path;
|
|
3581
|
+
}
|
|
3582
|
+
function getProxyDebugLogPath() {
|
|
3583
|
+
return join8(ensureLogsDir(), PROXY_DEBUG_LOG);
|
|
3584
|
+
}
|
|
3585
|
+
function getCodexProxyDebugLogPath() {
|
|
3586
|
+
return join8(ensureLogsDir(), CODEX_PROXY_DEBUG_LOG);
|
|
3587
|
+
}
|
|
3588
|
+
function getGeminiProxyDebugLogPath() {
|
|
3589
|
+
return join8(ensureLogsDir(), GEMINI_PROXY_DEBUG_LOG);
|
|
3590
|
+
}
|
|
3591
|
+
function getProviderDebugLogPath() {
|
|
3592
|
+
return join8(ensureLogsDir(), PROVIDER_DEBUG_LOG);
|
|
3593
|
+
}
|
|
3594
|
+
function makeTraceLogger(logPath) {
|
|
3595
|
+
resetTraceLog(logPath);
|
|
3596
|
+
return (message) => writeSecureLogLine(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
|
|
3597
|
+
}
|
|
3598
|
+
function resetTraceLog(path) {
|
|
3599
|
+
ensureLogsDir();
|
|
3600
|
+
if (existsSync7(path)) {
|
|
3601
|
+
try {
|
|
3602
|
+
unlinkSync(path);
|
|
3603
|
+
} catch {
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
var REDACTION_PATTERNS = [
|
|
3608
|
+
// Bearer / Authorization headers
|
|
3609
|
+
(line) => line.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [REDACTED]"),
|
|
3610
|
+
(line) => line.replace(/("authorization"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
|
|
3611
|
+
(line) => line.replace(/(x-api-key"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
|
|
3612
|
+
// Common API key prefixes
|
|
3613
|
+
(line) => line.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"),
|
|
3614
|
+
(line) => line.replace(/\bsk-ant-[A-Za-z0-9_-]{8,}\b/g, "sk-ant-[REDACTED]"),
|
|
3615
|
+
(line) => line.replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, "AIza[REDACTED]"),
|
|
3616
|
+
(line) => line.replace(/\bgsk_[A-Za-z0-9]{20,}\b/g, "gsk_[REDACTED]")
|
|
3617
|
+
];
|
|
3618
|
+
function redactTraceLine(line) {
|
|
3619
|
+
let out = line;
|
|
3620
|
+
for (const apply of REDACTION_PATTERNS) {
|
|
3621
|
+
out = apply(out);
|
|
3622
|
+
}
|
|
3623
|
+
return out;
|
|
3624
|
+
}
|
|
3625
|
+
function redactTraceLog(content) {
|
|
3626
|
+
return content.split("\n").map(redactTraceLine).join("\n");
|
|
3627
|
+
}
|
|
3628
|
+
function writeSecureLogLine(path, line) {
|
|
3629
|
+
ensureLogsDir();
|
|
3630
|
+
const redacted = redactTraceLine(line);
|
|
3631
|
+
try {
|
|
3632
|
+
writeFileSync3(path, `${redacted}
|
|
3633
|
+
`, { flag: "a", mode: FILE_MODE4 });
|
|
3634
|
+
chmodSync4(path, FILE_MODE4);
|
|
3635
|
+
} catch {
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
function printTraceLog(debugLogPath) {
|
|
3639
|
+
if (!existsSync7(debugLogPath)) return;
|
|
3640
|
+
const raw = readFileSync6(debugLogPath, "utf8");
|
|
3641
|
+
const log19 = redactTraceLog(raw);
|
|
3642
|
+
const errorLines = log19.split("\n").filter(
|
|
3643
|
+
(l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status") || l.includes("resolveModel failed") || l.includes("resolveModel fallback")
|
|
3644
|
+
);
|
|
3645
|
+
console.log("\n" + pc2.bold(pc2.cyan("\u2500\u2500 Debug trace \u2500\u2500")));
|
|
3646
|
+
if (errorLines.length > 0) {
|
|
3647
|
+
errorLines.slice(0, 30).forEach((l) => console.log(pc2.dim(l)));
|
|
3648
|
+
} else {
|
|
3649
|
+
console.log(pc2.dim("(no errors found in debug log)"));
|
|
3650
|
+
}
|
|
3651
|
+
console.log(pc2.dim(`Full log: ${debugLogPath}`));
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3528
3654
|
// src/registry/fetch-template-models.ts
|
|
3529
3655
|
var TEST_TIMEOUT_MS = 1e4;
|
|
3530
3656
|
function modelFormatForNpm(npm) {
|
|
@@ -3532,7 +3658,9 @@ function modelFormatForNpm(npm) {
|
|
|
3532
3658
|
}
|
|
3533
3659
|
function modelsUrl(baseUrl) {
|
|
3534
3660
|
const trimmed = baseUrl.replace(/\/$/, "");
|
|
3535
|
-
if (
|
|
3661
|
+
if (/\/(v\d+[a-z]*|openai|beta)$/.test(trimmed)) {
|
|
3662
|
+
return `${trimmed}/models`;
|
|
3663
|
+
}
|
|
3536
3664
|
return `${trimmed}/v1/models`;
|
|
3537
3665
|
}
|
|
3538
3666
|
function parseModelList(body, npm) {
|
|
@@ -3569,6 +3697,22 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride) {
|
|
|
3569
3697
|
hint: "Use relay-ai providers import from OpenCode for advanced setups."
|
|
3570
3698
|
};
|
|
3571
3699
|
}
|
|
3700
|
+
if (template.modelSource === "static-seed") {
|
|
3701
|
+
const models = (template.staticModels || []).map((sm) => {
|
|
3702
|
+
const family = sm.id.split(/[-/:]/)[0] ?? sm.id;
|
|
3703
|
+
return {
|
|
3704
|
+
id: sm.id,
|
|
3705
|
+
name: sm.name,
|
|
3706
|
+
upstreamModelId: sm.id,
|
|
3707
|
+
family,
|
|
3708
|
+
brand: deriveBrand(family),
|
|
3709
|
+
contextWindow: resolveContextWindow(sm.id),
|
|
3710
|
+
modelFormat: modelFormatForNpm(template.npm),
|
|
3711
|
+
npm: template.npm
|
|
3712
|
+
};
|
|
3713
|
+
});
|
|
3714
|
+
return { models, baseUrl };
|
|
3715
|
+
}
|
|
3572
3716
|
const url = modelsUrl(baseUrl);
|
|
3573
3717
|
const controller = new AbortController();
|
|
3574
3718
|
const timer = setTimeout(() => controller.abort(), TEST_TIMEOUT_MS);
|
|
@@ -3594,8 +3738,16 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride) {
|
|
|
3594
3738
|
hint: "Check the base URL \u2014 redirects are blocked for security."
|
|
3595
3739
|
};
|
|
3596
3740
|
}
|
|
3741
|
+
let logTrace;
|
|
3742
|
+
if (process.env.RELAY_AI_TRACE === "1") {
|
|
3743
|
+
logTrace = makeTraceLogger(getProviderDebugLogPath());
|
|
3744
|
+
}
|
|
3597
3745
|
if (!response.ok) {
|
|
3598
3746
|
const body = await response.text().catch(() => "");
|
|
3747
|
+
if (logTrace) {
|
|
3748
|
+
logTrace(`[fetchTemplateModels] HTTP ${response.status} from ${url}`);
|
|
3749
|
+
logTrace(`[fetchTemplateModels] Body: ${body}`);
|
|
3750
|
+
}
|
|
3599
3751
|
const detail = body.slice(0, 200).trim();
|
|
3600
3752
|
if (response.status === 401 || response.status === 403) {
|
|
3601
3753
|
return {
|
|
@@ -3612,7 +3764,18 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride) {
|
|
|
3612
3764
|
hint: detail || "Check your API key and try again."
|
|
3613
3765
|
};
|
|
3614
3766
|
}
|
|
3615
|
-
const
|
|
3767
|
+
const rawBodyText = await response.text().catch(() => "");
|
|
3768
|
+
if (logTrace) {
|
|
3769
|
+
logTrace(`[fetchTemplateModels] HTTP ${response.status} from ${url}`);
|
|
3770
|
+
logTrace(`[fetchTemplateModels] Body: ${rawBodyText}`);
|
|
3771
|
+
}
|
|
3772
|
+
let json = {};
|
|
3773
|
+
try {
|
|
3774
|
+
if (rawBodyText.trim()) {
|
|
3775
|
+
json = JSON.parse(rawBodyText);
|
|
3776
|
+
}
|
|
3777
|
+
} catch {
|
|
3778
|
+
}
|
|
3616
3779
|
const models = parseModelList(json, template.npm);
|
|
3617
3780
|
if (models.length === 0) {
|
|
3618
3781
|
return {
|
|
@@ -3768,8 +3931,23 @@ async function fetchAnthropicModels(baseUrl, apiKey) {
|
|
|
3768
3931
|
redirect: "manual",
|
|
3769
3932
|
signal: controller.signal
|
|
3770
3933
|
});
|
|
3934
|
+
let logTrace;
|
|
3935
|
+
if (process.env.RELAY_AI_TRACE === "1") {
|
|
3936
|
+
logTrace = makeTraceLogger(getProviderDebugLogPath());
|
|
3937
|
+
}
|
|
3938
|
+
const rawBodyText = await response.text().catch(() => "");
|
|
3939
|
+
if (logTrace) {
|
|
3940
|
+
logTrace(`[fetchAnthropicModels] HTTP ${response.status} from ${modelsUrl2}`);
|
|
3941
|
+
logTrace(`[fetchAnthropicModels] Body: ${rawBodyText}`);
|
|
3942
|
+
}
|
|
3771
3943
|
if (response.ok) {
|
|
3772
|
-
|
|
3944
|
+
let json = {};
|
|
3945
|
+
try {
|
|
3946
|
+
if (rawBodyText.trim()) {
|
|
3947
|
+
json = JSON.parse(rawBodyText);
|
|
3948
|
+
}
|
|
3949
|
+
} catch {
|
|
3950
|
+
}
|
|
3773
3951
|
const models = [];
|
|
3774
3952
|
for (const row of json.data ?? []) {
|
|
3775
3953
|
const id = row.id?.trim();
|
|
@@ -3812,10 +3990,10 @@ function uniqueProviderId(displayName, registry) {
|
|
|
3812
3990
|
let base = customProviderId(displayName);
|
|
3813
3991
|
if (!base.startsWith("custom-")) base = `custom-${slugifyProviderId(displayName)}`;
|
|
3814
3992
|
if (!isValidProviderId(base)) base = "custom-provider";
|
|
3815
|
-
if (!registry.providers.some((
|
|
3993
|
+
if (!registry.providers.some((p21) => p21.id === base)) return base;
|
|
3816
3994
|
for (let i = 2; i < 100; i++) {
|
|
3817
3995
|
const candidate = `${base}-${i}`;
|
|
3818
|
-
if (isValidProviderId(candidate) && !registry.providers.some((
|
|
3996
|
+
if (isValidProviderId(candidate) && !registry.providers.some((p21) => p21.id === candidate)) {
|
|
3819
3997
|
return candidate;
|
|
3820
3998
|
}
|
|
3821
3999
|
}
|
|
@@ -3939,6 +4117,58 @@ var PROVIDER_TEMPLATES = [
|
|
|
3939
4117
|
modelSource: "api-list",
|
|
3940
4118
|
supported: true
|
|
3941
4119
|
},
|
|
4120
|
+
{
|
|
4121
|
+
id: "deepseek",
|
|
4122
|
+
name: "DeepSeek",
|
|
4123
|
+
authType: "api",
|
|
4124
|
+
npm: "@ai-sdk/openai-compatible",
|
|
4125
|
+
defaultBaseUrl: "https://api.deepseek.com/v1",
|
|
4126
|
+
signupUrl: "https://platform.deepseek.com",
|
|
4127
|
+
modelSource: "api-list",
|
|
4128
|
+
supported: true
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
id: "zhipu",
|
|
4132
|
+
name: "Zhipu AI (GLM)",
|
|
4133
|
+
authType: "api",
|
|
4134
|
+
npm: "@ai-sdk/openai-compatible",
|
|
4135
|
+
defaultBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
4136
|
+
signupUrl: "https://open.bigmodel.cn",
|
|
4137
|
+
modelSource: "api-list",
|
|
4138
|
+
supported: true
|
|
4139
|
+
},
|
|
4140
|
+
{
|
|
4141
|
+
id: "moonshot",
|
|
4142
|
+
name: "Moonshot (Kimi)",
|
|
4143
|
+
authType: "api",
|
|
4144
|
+
npm: "@ai-sdk/openai-compatible",
|
|
4145
|
+
defaultBaseUrl: "https://api.moonshot.cn/v1",
|
|
4146
|
+
signupUrl: "https://platform.moonshot.cn",
|
|
4147
|
+
modelSource: "api-list",
|
|
4148
|
+
supported: true
|
|
4149
|
+
},
|
|
4150
|
+
{
|
|
4151
|
+
id: "moonshot-global",
|
|
4152
|
+
name: "Moonshot Global (kimi.ai)",
|
|
4153
|
+
authType: "api",
|
|
4154
|
+
npm: "@ai-sdk/openai-compatible",
|
|
4155
|
+
defaultBaseUrl: "https://api.moonshot.ai/v1",
|
|
4156
|
+
signupUrl: "https://platform.kimi.ai",
|
|
4157
|
+
modelSource: "api-list",
|
|
4158
|
+
supported: true
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
id: "kimi-code",
|
|
4162
|
+
name: "Kimi Code (Subscription Required)",
|
|
4163
|
+
authType: "api",
|
|
4164
|
+
npm: "@ai-sdk/openai-compatible",
|
|
4165
|
+
defaultBaseUrl: "https://api.kimi.com/coding/v1",
|
|
4166
|
+
modelSource: "static-seed",
|
|
4167
|
+
staticModels: [
|
|
4168
|
+
{ id: "kimi-for-coding", name: "Kimi Code K2.7 (Unified)" }
|
|
4169
|
+
],
|
|
4170
|
+
supported: true
|
|
4171
|
+
},
|
|
3942
4172
|
{
|
|
3943
4173
|
id: "xai",
|
|
3944
4174
|
name: "xAI",
|
|
@@ -4353,7 +4583,7 @@ async function importFromOpencode(options = {}) {
|
|
|
4353
4583
|
}
|
|
4354
4584
|
continue;
|
|
4355
4585
|
}
|
|
4356
|
-
const existingIdx = registry.providers.findIndex((
|
|
4586
|
+
const existingIdx = registry.providers.findIndex((p21) => p21.id === entry.id);
|
|
4357
4587
|
const existing = existingIdx >= 0 ? registry.providers[existingIdx] : void 0;
|
|
4358
4588
|
if (existing && options.resolveConflict) {
|
|
4359
4589
|
const choice = await options.resolveConflict({
|
|
@@ -4391,7 +4621,7 @@ async function importFromOpencode(options = {}) {
|
|
|
4391
4621
|
if (isOAuth) oauthImported += 1;
|
|
4392
4622
|
}
|
|
4393
4623
|
const alreadyReportedIds = new Set(skipped.map((s) => s.id));
|
|
4394
|
-
const registryProviderIds = new Set(registry.providers.map((
|
|
4624
|
+
const registryProviderIds = new Set(registry.providers.map((p21) => p21.id));
|
|
4395
4625
|
for (const provider of listCredentialSkippedProviders(
|
|
4396
4626
|
raw,
|
|
4397
4627
|
authEntries,
|
|
@@ -4418,129 +4648,28 @@ import * as p2 from "@clack/prompts";
|
|
|
4418
4648
|
import { appendFileSync as appendFileSync2, readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
|
|
4419
4649
|
import { homedir as homedir6 } from "os";
|
|
4420
4650
|
import { spawnSync } from "child_process";
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
readFileSync as readFileSync6,
|
|
4428
|
-
unlinkSync,
|
|
4429
|
-
writeFileSync as writeFileSync3
|
|
4430
|
-
} from "fs";
|
|
4431
|
-
import { join as join8 } from "path";
|
|
4432
|
-
import pc2 from "picocolors";
|
|
4433
|
-
var DIR_MODE2 = 448;
|
|
4434
|
-
var FILE_MODE4 = 384;
|
|
4435
|
-
var CLAUDE_DEBUG_LOG = "claude-debug.log";
|
|
4436
|
-
var PROXY_DEBUG_LOG = "proxy-debug.log";
|
|
4437
|
-
var CODEX_PROXY_DEBUG_LOG = "codex-proxy-debug.log";
|
|
4438
|
-
function ensureLogsDir() {
|
|
4439
|
-
const dir = getLogsPath();
|
|
4440
|
-
mkdirSync4(dir, { recursive: true, mode: DIR_MODE2 });
|
|
4441
|
-
try {
|
|
4442
|
-
chmodSync4(dir, DIR_MODE2);
|
|
4443
|
-
} catch {
|
|
4651
|
+
function detectShellProfile() {
|
|
4652
|
+
const shell = process.env["SHELL"] ?? "";
|
|
4653
|
+
if (process.platform === "darwin") {
|
|
4654
|
+
if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
|
|
4655
|
+
if (shell.includes("bash")) return { display: "~/.bash_profile", path: `${homedir6()}/.bash_profile` };
|
|
4656
|
+
return { display: "~/.profile", path: `${homedir6()}/.profile` };
|
|
4444
4657
|
}
|
|
4445
|
-
|
|
4446
|
-
}
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
}
|
|
4450
|
-
function prepareClaudeTraceLog() {
|
|
4451
|
-
const path = getClaudeDebugLogPath();
|
|
4452
|
-
resetTraceLog(path);
|
|
4453
|
-
return path;
|
|
4454
|
-
}
|
|
4455
|
-
function getProxyDebugLogPath() {
|
|
4456
|
-
return join8(ensureLogsDir(), PROXY_DEBUG_LOG);
|
|
4457
|
-
}
|
|
4458
|
-
function getCodexProxyDebugLogPath() {
|
|
4459
|
-
return join8(ensureLogsDir(), CODEX_PROXY_DEBUG_LOG);
|
|
4460
|
-
}
|
|
4461
|
-
function makeTraceLogger(logPath) {
|
|
4462
|
-
resetTraceLog(logPath);
|
|
4463
|
-
return (message) => writeSecureLogLine(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
|
|
4464
|
-
}
|
|
4465
|
-
function resetTraceLog(path) {
|
|
4466
|
-
ensureLogsDir();
|
|
4467
|
-
if (existsSync7(path)) {
|
|
4468
|
-
try {
|
|
4469
|
-
unlinkSync(path);
|
|
4470
|
-
} catch {
|
|
4471
|
-
}
|
|
4658
|
+
if (process.platform === "linux") {
|
|
4659
|
+
if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
|
|
4660
|
+
if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
|
|
4661
|
+
return { display: "~/.profile", path: `${homedir6()}/.profile` };
|
|
4472
4662
|
}
|
|
4663
|
+
if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
|
|
4664
|
+
return { display: "~/.profile", path: `${homedir6()}/.profile` };
|
|
4473
4665
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
(line) => line.replace(/(x-api-key"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
|
|
4479
|
-
// Common API key prefixes
|
|
4480
|
-
(line) => line.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"),
|
|
4481
|
-
(line) => line.replace(/\bsk-ant-[A-Za-z0-9_-]{8,}\b/g, "sk-ant-[REDACTED]"),
|
|
4482
|
-
(line) => line.replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, "AIza[REDACTED]"),
|
|
4483
|
-
(line) => line.replace(/\bgsk_[A-Za-z0-9]{20,}\b/g, "gsk_[REDACTED]")
|
|
4484
|
-
];
|
|
4485
|
-
function redactTraceLine(line) {
|
|
4486
|
-
let out = line;
|
|
4487
|
-
for (const apply of REDACTION_PATTERNS) {
|
|
4488
|
-
out = apply(out);
|
|
4489
|
-
}
|
|
4490
|
-
return out;
|
|
4491
|
-
}
|
|
4492
|
-
function redactTraceLog(content) {
|
|
4493
|
-
return content.split("\n").map(redactTraceLine).join("\n");
|
|
4494
|
-
}
|
|
4495
|
-
function writeSecureLogLine(path, line) {
|
|
4496
|
-
ensureLogsDir();
|
|
4497
|
-
const redacted = redactTraceLine(line);
|
|
4498
|
-
try {
|
|
4499
|
-
writeFileSync3(path, `${redacted}
|
|
4500
|
-
`, { flag: "a", mode: FILE_MODE4 });
|
|
4501
|
-
chmodSync4(path, FILE_MODE4);
|
|
4502
|
-
} catch {
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
|
-
function printTraceLog(debugLogPath) {
|
|
4506
|
-
if (!existsSync7(debugLogPath)) return;
|
|
4507
|
-
const raw = readFileSync6(debugLogPath, "utf8");
|
|
4508
|
-
const log17 = redactTraceLog(raw);
|
|
4509
|
-
const errorLines = log17.split("\n").filter(
|
|
4510
|
-
(l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status")
|
|
4511
|
-
);
|
|
4512
|
-
console.log("\n" + pc2.bold(pc2.cyan("\u2500\u2500 Debug trace \u2500\u2500")));
|
|
4513
|
-
if (errorLines.length > 0) {
|
|
4514
|
-
errorLines.slice(0, 30).forEach((l) => console.log(pc2.dim(l)));
|
|
4515
|
-
} else {
|
|
4516
|
-
console.log(pc2.dim("(no errors found in debug log)"));
|
|
4517
|
-
}
|
|
4518
|
-
console.log(pc2.dim(`Full log: ${debugLogPath}`));
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
// src/key-setup.ts
|
|
4522
|
-
function detectShellProfile() {
|
|
4523
|
-
const shell = process.env["SHELL"] ?? "";
|
|
4524
|
-
if (process.platform === "darwin") {
|
|
4525
|
-
if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
|
|
4526
|
-
if (shell.includes("bash")) return { display: "~/.bash_profile", path: `${homedir6()}/.bash_profile` };
|
|
4527
|
-
return { display: "~/.profile", path: `${homedir6()}/.profile` };
|
|
4528
|
-
}
|
|
4529
|
-
if (process.platform === "linux") {
|
|
4530
|
-
if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
|
|
4531
|
-
if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
|
|
4532
|
-
return { display: "~/.profile", path: `${homedir6()}/.profile` };
|
|
4533
|
-
}
|
|
4534
|
-
if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
|
|
4535
|
-
return { display: "~/.profile", path: `${homedir6()}/.profile` };
|
|
4536
|
-
}
|
|
4537
|
-
async function resolveOrCollectApiKey(simulate = false, trace = false) {
|
|
4538
|
-
if (!simulate) {
|
|
4539
|
-
const existing = resolveApiKey();
|
|
4540
|
-
if (existing) return existing;
|
|
4666
|
+
async function resolveOrCollectApiKey(simulate = false, trace = false) {
|
|
4667
|
+
if (!simulate) {
|
|
4668
|
+
const existing = resolveApiKey();
|
|
4669
|
+
if (existing) return existing;
|
|
4541
4670
|
}
|
|
4542
4671
|
const isMac = process.platform === "darwin";
|
|
4543
|
-
const
|
|
4672
|
+
const isWindows5 = process.platform === "win32";
|
|
4544
4673
|
const isLinux = process.platform === "linux";
|
|
4545
4674
|
if (simulate) {
|
|
4546
4675
|
printDryRunPanel();
|
|
@@ -4554,7 +4683,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
|
|
|
4554
4683
|
};
|
|
4555
4684
|
const storedKey = await readFromCredentialStore(keyDiag);
|
|
4556
4685
|
if (storedKey) {
|
|
4557
|
-
const storeName = isMac ? "macOS Keychain" :
|
|
4686
|
+
const storeName = isMac ? "macOS Keychain" : isWindows5 ? "Windows Credential Manager" : "Secret Service";
|
|
4558
4687
|
p2.log.success(`Found key in ${storeName}`);
|
|
4559
4688
|
process.env["OPENCODE_API_KEY"] = storedKey;
|
|
4560
4689
|
return storedKey;
|
|
@@ -4584,7 +4713,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
|
|
|
4584
4713
|
{ value: "session", label: "This session only", hint: "Not saved anywhere \u2014 you'll be asked again next time" }
|
|
4585
4714
|
];
|
|
4586
4715
|
}
|
|
4587
|
-
if (
|
|
4716
|
+
if (isWindows5) {
|
|
4588
4717
|
return [
|
|
4589
4718
|
{ value: "credential-manager", label: "Windows Credential Manager", hint: "Key stored securely; relay-ai reads it automatically next time" },
|
|
4590
4719
|
{ value: "setx", label: "Persistent environment variable (plaintext)", hint: "Runs setx \u2014 key visible in System Properties \u2192 Environment Variables" },
|
|
@@ -4606,7 +4735,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
|
|
|
4606
4735
|
const saveChoice = await p2.select({
|
|
4607
4736
|
message: "Where should we save the key?",
|
|
4608
4737
|
options: saveOptions,
|
|
4609
|
-
initialValue: isMac ? "keychain" :
|
|
4738
|
+
initialValue: isMac ? "keychain" : isWindows5 ? "credential-manager" : secretServiceAvailable ? "secret-service" : "profile"
|
|
4610
4739
|
});
|
|
4611
4740
|
if (p2.isCancel(saveChoice)) {
|
|
4612
4741
|
p2.cancel("Cancelled.");
|
|
@@ -4753,10 +4882,10 @@ async function runFirstRunWizard(trace = false) {
|
|
|
4753
4882
|
if (p3.isCancel(retry) || retry === "cancel") return "cancel";
|
|
4754
4883
|
return runFirstRunWizard(trace);
|
|
4755
4884
|
}
|
|
4756
|
-
const
|
|
4757
|
-
|
|
4885
|
+
const spinner10 = p3.spinner();
|
|
4886
|
+
spinner10.start("Importing from OpenCode...");
|
|
4758
4887
|
const result = await importFromOpencode();
|
|
4759
|
-
|
|
4888
|
+
spinner10.stop("");
|
|
4760
4889
|
if (result.error) {
|
|
4761
4890
|
p3.log.error(result.error);
|
|
4762
4891
|
return runFirstRunWizard(trace);
|
|
@@ -5104,9 +5233,9 @@ function serializeToolResultContent(content) {
|
|
|
5104
5233
|
|
|
5105
5234
|
// src/tool-search.ts
|
|
5106
5235
|
var TOOL_SEARCH_TYPE_PREFIX = "tool_search_tool";
|
|
5107
|
-
function isToolSearchTool(
|
|
5108
|
-
if (typeof
|
|
5109
|
-
const name =
|
|
5236
|
+
function isToolSearchTool(tool5) {
|
|
5237
|
+
if (typeof tool5.type === "string" && tool5.type.startsWith(TOOL_SEARCH_TYPE_PREFIX)) return true;
|
|
5238
|
+
const name = tool5.name ?? "";
|
|
5110
5239
|
return name.includes("tool_search") || name === "ToolSearch";
|
|
5111
5240
|
}
|
|
5112
5241
|
function extractReferencedToolNames(messages) {
|
|
@@ -5145,16 +5274,16 @@ function resolveUpstreamTools(tools, messages) {
|
|
|
5145
5274
|
if (!tools?.length) return [];
|
|
5146
5275
|
const referenced = extractReferencedToolNames(messages);
|
|
5147
5276
|
const upstream = [];
|
|
5148
|
-
for (const
|
|
5149
|
-
if (isToolSearchTool(
|
|
5150
|
-
upstream.push(
|
|
5277
|
+
for (const tool5 of tools) {
|
|
5278
|
+
if (isToolSearchTool(tool5)) {
|
|
5279
|
+
upstream.push(tool5);
|
|
5151
5280
|
continue;
|
|
5152
5281
|
}
|
|
5153
|
-
if (
|
|
5154
|
-
if (referenced.has(
|
|
5282
|
+
if (tool5.defer_loading === true) {
|
|
5283
|
+
if (referenced.has(tool5.name)) upstream.push(tool5);
|
|
5155
5284
|
continue;
|
|
5156
5285
|
}
|
|
5157
|
-
upstream.push(
|
|
5286
|
+
upstream.push(tool5);
|
|
5158
5287
|
}
|
|
5159
5288
|
return upstream;
|
|
5160
5289
|
}
|
|
@@ -5208,12 +5337,15 @@ function annotateToolNames(messages) {
|
|
|
5208
5337
|
}
|
|
5209
5338
|
}
|
|
5210
5339
|
function thinkingToSdkPart(block, npm) {
|
|
5211
|
-
if (npm !== "@ai-sdk/google" && npm !== "@ai-sdk/openai") return null;
|
|
5212
5340
|
const text5 = block.thinking ?? "";
|
|
5213
5341
|
if (npm === "@ai-sdk/openai" && !block.signature && !text5.trim()) return null;
|
|
5214
5342
|
const part = { type: "reasoning", text: text5 };
|
|
5215
5343
|
if (block.signature) {
|
|
5216
|
-
|
|
5344
|
+
if (npm === "@ai-sdk/google") {
|
|
5345
|
+
part.providerOptions = { google: { thoughtSignature: block.signature } };
|
|
5346
|
+
} else if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/openai-compatible") {
|
|
5347
|
+
part.providerOptions = { openai: { reasoningEncryptedContent: block.signature } };
|
|
5348
|
+
}
|
|
5217
5349
|
}
|
|
5218
5350
|
return part;
|
|
5219
5351
|
}
|
|
@@ -5228,8 +5360,8 @@ function translateMessages(messages, npm) {
|
|
|
5228
5360
|
for (const b of blocks) {
|
|
5229
5361
|
if (b.type === "text") parts.push({ type: "text", text: b.text ?? "" });
|
|
5230
5362
|
else if (b.type === "image") {
|
|
5231
|
-
const
|
|
5232
|
-
if (
|
|
5363
|
+
const p21 = imagePart(b);
|
|
5364
|
+
if (p21) parts.push(p21);
|
|
5233
5365
|
}
|
|
5234
5366
|
}
|
|
5235
5367
|
if (toolResults.length) {
|
|
@@ -5315,7 +5447,7 @@ function translateRequest(body, npm, options) {
|
|
|
5315
5447
|
providerOptions
|
|
5316
5448
|
};
|
|
5317
5449
|
}
|
|
5318
|
-
async function writeAnthropicStream(fullStream, modelId, write,
|
|
5450
|
+
async function writeAnthropicStream(fullStream, modelId, write, log19) {
|
|
5319
5451
|
const messageId = "msg_" + Date.now();
|
|
5320
5452
|
let blockIndex = -1;
|
|
5321
5453
|
let started = false;
|
|
@@ -5447,7 +5579,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log17) {
|
|
|
5447
5579
|
case "error": {
|
|
5448
5580
|
const e = part.error;
|
|
5449
5581
|
const errMsg = e?.message || (typeof part.error === "string" ? part.error : JSON.stringify(e?.data ?? part.error));
|
|
5450
|
-
|
|
5582
|
+
log19?.(() => `sdk stream error: ${errMsg}`);
|
|
5451
5583
|
closeOpen();
|
|
5452
5584
|
emit("error", { type: "error", error: { type: "api_error", message: errMsg } });
|
|
5453
5585
|
return;
|
|
@@ -5461,7 +5593,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log17) {
|
|
|
5461
5593
|
emit("message_delta", { type: "message_delta", delta: { stop_reason: finishReason, stop_sequence: null }, usage });
|
|
5462
5594
|
emit("message_stop", { type: "message_stop" });
|
|
5463
5595
|
}
|
|
5464
|
-
async function streamAnthropicResponse(model, params, modelId, write,
|
|
5596
|
+
async function streamAnthropicResponse(model, params, modelId, write, log19) {
|
|
5465
5597
|
const result = streamText({ model, ...params });
|
|
5466
5598
|
Promise.resolve(result.text).catch(() => {
|
|
5467
5599
|
});
|
|
@@ -5473,7 +5605,7 @@ async function streamAnthropicResponse(model, params, modelId, write, log17) {
|
|
|
5473
5605
|
});
|
|
5474
5606
|
Promise.resolve(result.usage).catch(() => {
|
|
5475
5607
|
});
|
|
5476
|
-
await writeAnthropicStream(result.fullStream, modelId, write,
|
|
5608
|
+
await writeAnthropicStream(result.fullStream, modelId, write, log19);
|
|
5477
5609
|
}
|
|
5478
5610
|
async function generateAnthropicResponse(model, params, modelId) {
|
|
5479
5611
|
const r = await generateText({ model, ...params });
|
|
@@ -5862,6 +5994,8 @@ function loadPreferences() {
|
|
|
5862
5994
|
lastProvider,
|
|
5863
5995
|
lastCodexProvider: config.lastCodexProvider,
|
|
5864
5996
|
lastCodexModel: config.lastCodexModel,
|
|
5997
|
+
lastGeminiProvider: config.lastGeminiProvider,
|
|
5998
|
+
lastGeminiModel: config.lastGeminiModel,
|
|
5865
5999
|
recentModelsByProvider: config.recentModelsByProvider,
|
|
5866
6000
|
favoriteModels: config.favoriteModels,
|
|
5867
6001
|
server: config.server
|
|
@@ -5874,6 +6008,8 @@ function savePreferences(prefs) {
|
|
|
5874
6008
|
if (prefs.lastProvider !== void 0) config.lastProvider = prefs.lastProvider;
|
|
5875
6009
|
if (prefs.lastCodexProvider !== void 0) config.lastCodexProvider = prefs.lastCodexProvider;
|
|
5876
6010
|
if (prefs.lastCodexModel !== void 0) config.lastCodexModel = prefs.lastCodexModel;
|
|
6011
|
+
if (prefs.lastGeminiProvider !== void 0) config.lastGeminiProvider = prefs.lastGeminiProvider;
|
|
6012
|
+
if (prefs.lastGeminiModel !== void 0) config.lastGeminiModel = prefs.lastGeminiModel;
|
|
5877
6013
|
if (prefs.recentModelsByProvider !== void 0) config.recentModelsByProvider = prefs.recentModelsByProvider;
|
|
5878
6014
|
if (prefs.favoriteModels !== void 0) config.favoriteModels = prefs.favoriteModels;
|
|
5879
6015
|
writeConfig(config);
|
|
@@ -5883,7 +6019,7 @@ function recordLaunchSelection(agent, providerId, modelId, prefs) {
|
|
|
5883
6019
|
const prevRecent = prefs.recentModelsByProvider?.[providerId] ?? [];
|
|
5884
6020
|
const updatedRecent = [modelId, ...prevRecent.filter((id) => id !== modelId)].slice(0, MAX_RECENT_MODELS);
|
|
5885
6021
|
savePreferences({
|
|
5886
|
-
...agent === "claude" ? { lastProvider: providerId, lastModel: modelId } : { lastCodexProvider: providerId, lastCodexModel: modelId },
|
|
6022
|
+
...agent === "claude" ? { lastProvider: providerId, lastModel: modelId } : agent === "codex" ? { lastCodexProvider: providerId, lastCodexModel: modelId } : { lastGeminiProvider: providerId, lastGeminiModel: modelId },
|
|
5887
6023
|
recentModelsByProvider: { ...prefs.recentModelsByProvider, [providerId]: updatedRecent }
|
|
5888
6024
|
});
|
|
5889
6025
|
}
|
|
@@ -6106,14 +6242,14 @@ function zenGoAsLocalProvider(backendId, models) {
|
|
|
6106
6242
|
};
|
|
6107
6243
|
}
|
|
6108
6244
|
function providersForPicker(catalog) {
|
|
6109
|
-
const registryIds = new Set(catalog.localProviders.map((
|
|
6245
|
+
const registryIds = new Set(catalog.localProviders.map((p21) => p21.id));
|
|
6110
6246
|
const providers = [
|
|
6111
6247
|
...catalog.zenModels.length > 0 && !registryIds.has("zen") ? [zenGoAsLocalProvider("zen", catalog.zenModels)] : [],
|
|
6112
6248
|
...catalog.goModels.length > 0 && !registryIds.has("go") ? [zenGoAsLocalProvider("go", catalog.goModels)] : [],
|
|
6113
6249
|
...catalog.localProviders
|
|
6114
6250
|
];
|
|
6115
|
-
for (const
|
|
6116
|
-
|
|
6251
|
+
for (const p21 of providers) {
|
|
6252
|
+
p21.models.sort((a, b) => {
|
|
6117
6253
|
const nameA = a.name || a.id;
|
|
6118
6254
|
const nameB = b.name || b.id;
|
|
6119
6255
|
return nameA.localeCompare(nameB, void 0, { sensitivity: "base", numeric: true });
|
|
@@ -6124,7 +6260,7 @@ function providersForPicker(catalog) {
|
|
|
6124
6260
|
async function resolveLocalProviderApiKey(provider) {
|
|
6125
6261
|
const direct = provider.apiKey?.trim();
|
|
6126
6262
|
if (direct) return direct;
|
|
6127
|
-
const reg = loadRegistry().providers.find((
|
|
6263
|
+
const reg = loadRegistry().providers.find((p21) => p21.id === provider.id);
|
|
6128
6264
|
const authRef = reg?.authRef ?? (provider.id === "zen" || provider.id === "go" ? "keyring:global:opencode" : null);
|
|
6129
6265
|
if (!authRef) return null;
|
|
6130
6266
|
return resolveProviderCredential(provider.id, authRef);
|
|
@@ -6454,21 +6590,21 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
|
|
|
6454
6590
|
|
|
6455
6591
|
`);
|
|
6456
6592
|
for await (const part of fullStream) {
|
|
6457
|
-
const
|
|
6458
|
-
switch (
|
|
6593
|
+
const p21 = part;
|
|
6594
|
+
switch (p21.type) {
|
|
6459
6595
|
case "text-delta":
|
|
6460
|
-
send({ role: "assistant", content:
|
|
6596
|
+
send({ role: "assistant", content: p21.textDelta ?? p21.text ?? "" });
|
|
6461
6597
|
break;
|
|
6462
6598
|
case "tool-input-start":
|
|
6463
6599
|
case "tool-call-streaming-start":
|
|
6464
|
-
send({ role: "assistant", tool_calls: [{ index: 0, id:
|
|
6600
|
+
send({ role: "assistant", tool_calls: [{ index: 0, id: p21.id ?? p21.toolCallId, type: "function", function: { name: p21.toolName, arguments: "" } }] });
|
|
6465
6601
|
break;
|
|
6466
6602
|
case "tool-input-delta":
|
|
6467
6603
|
case "tool-call-delta":
|
|
6468
|
-
send({ tool_calls: [{ index: 0, function: { arguments:
|
|
6604
|
+
send({ tool_calls: [{ index: 0, function: { arguments: p21.delta ?? p21.text ?? p21.argsTextDelta ?? "" } }] });
|
|
6469
6605
|
break;
|
|
6470
6606
|
case "finish":
|
|
6471
|
-
send({},
|
|
6607
|
+
send({}, p21.finishReason || "stop");
|
|
6472
6608
|
break;
|
|
6473
6609
|
}
|
|
6474
6610
|
}
|
|
@@ -7121,10 +7257,10 @@ async function getServerPasswordForMode(mode) {
|
|
|
7121
7257
|
}
|
|
7122
7258
|
async function configureExposedProviders() {
|
|
7123
7259
|
p6.log.info("Add providers to expose. Listed providers are removed when selected \u2014 like favorites.");
|
|
7124
|
-
const
|
|
7125
|
-
|
|
7260
|
+
const spinner10 = p6.spinner();
|
|
7261
|
+
spinner10.start("Loading providers...");
|
|
7126
7262
|
const catalog = await fetchProviderCatalog({ agent: "server" });
|
|
7127
|
-
|
|
7263
|
+
spinner10.stop("");
|
|
7128
7264
|
const available = providerOptionsFromCatalog(catalog);
|
|
7129
7265
|
const picked = await selectServerProviders(available, getServerExposedProviders() ?? void 0);
|
|
7130
7266
|
if (!picked) return void 0;
|
|
@@ -7222,8 +7358,8 @@ async function resolveServerUpstreamApiKey() {
|
|
|
7222
7358
|
}));
|
|
7223
7359
|
if (apiKey) {
|
|
7224
7360
|
const isMac = process.platform === "darwin";
|
|
7225
|
-
const
|
|
7226
|
-
const storeName = isMac ? "macOS Keychain" :
|
|
7361
|
+
const isWindows5 = process.platform === "win32";
|
|
7362
|
+
const storeName = isMac ? "macOS Keychain" : isWindows5 ? "Windows Credential Manager" : "Secret Service";
|
|
7227
7363
|
p6.log.success(`Found key in ${storeName}`);
|
|
7228
7364
|
return apiKey;
|
|
7229
7365
|
}
|
|
@@ -7250,8 +7386,8 @@ async function runServerCommand(options = {}) {
|
|
|
7250
7386
|
if (pwResult === void 0) return 0;
|
|
7251
7387
|
const { password: serverPassword, wasSaved: passwordWasSaved } = pwResult;
|
|
7252
7388
|
const host = mode === "network" ? "0.0.0.0" : "127.0.0.1";
|
|
7253
|
-
const
|
|
7254
|
-
|
|
7389
|
+
const spinner10 = p6.spinner();
|
|
7390
|
+
spinner10.start("Fetching available models...");
|
|
7255
7391
|
let models;
|
|
7256
7392
|
try {
|
|
7257
7393
|
models = await loadServerModels();
|
|
@@ -7261,13 +7397,13 @@ async function runServerCommand(options = {}) {
|
|
|
7261
7397
|
if (runConfig.favoritesOnly) {
|
|
7262
7398
|
const favorites = loadPreferences().favoriteModels ?? [];
|
|
7263
7399
|
if (favorites.length === 0) {
|
|
7264
|
-
|
|
7400
|
+
spinner10.stop(pc6.red("No favorite models configured"));
|
|
7265
7401
|
p6.log.error("Run `relay-ai models` to add favorites, or turn off favorites-only in the server wizard.");
|
|
7266
7402
|
return 1;
|
|
7267
7403
|
}
|
|
7268
7404
|
models = filterServerModelsByFavorites(models, favorites).slice(0, MAX_MODEL_CATALOG);
|
|
7269
7405
|
if (models.length === 0) {
|
|
7270
|
-
|
|
7406
|
+
spinner10.stop(pc6.red("No favorite models matched the current provider filter"));
|
|
7271
7407
|
p6.log.error("Adjust favorites with `relay-ai models` or change exposed providers in the server wizard.");
|
|
7272
7408
|
return 1;
|
|
7273
7409
|
}
|
|
@@ -7279,7 +7415,7 @@ async function runServerCommand(options = {}) {
|
|
|
7279
7415
|
p6.log.info("Desktop/Cowork picker will only show these. Edit with `relay-ai models`.");
|
|
7280
7416
|
}
|
|
7281
7417
|
if (models.length === 0) {
|
|
7282
|
-
|
|
7418
|
+
spinner10.stop(pc6.red("No models to expose"));
|
|
7283
7419
|
p6.log.error("Add providers with `relay-ai providers add` or configure exposed providers in the server wizard.");
|
|
7284
7420
|
return 1;
|
|
7285
7421
|
}
|
|
@@ -7288,10 +7424,10 @@ async function runServerCommand(options = {}) {
|
|
|
7288
7424
|
const filterNote = runConfig.exposedProviders ? ` \u2014 ${runConfig.exposedProviders.length} provider${runConfig.exposedProviders.length !== 1 ? "s" : ""}` : "";
|
|
7289
7425
|
const favoritesNote = runConfig.favoritesOnly ? " \u2014 favorites only" : "";
|
|
7290
7426
|
const maskNote = runConfig.maskGatewayIds ? " \u2014 discovery ids masked" : "";
|
|
7291
|
-
|
|
7427
|
+
spinner10.stop(`Loaded ${models.length} models (${localCount} from registry providers)${filterNote}${favoritesNote}${maskNote}`);
|
|
7292
7428
|
if (summary) p6.log.info(summary);
|
|
7293
7429
|
} catch (err) {
|
|
7294
|
-
|
|
7430
|
+
spinner10.stop(pc6.red("Failed to load models"));
|
|
7295
7431
|
console.error(pc6.red(String(err instanceof Error ? err.message : err)));
|
|
7296
7432
|
return 1;
|
|
7297
7433
|
}
|
|
@@ -7800,7 +7936,7 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
7800
7936
|
return { added: false, error: "API key cannot be empty." };
|
|
7801
7937
|
}
|
|
7802
7938
|
const registry = loadRegistry();
|
|
7803
|
-
const existing = registry.providers.find((
|
|
7939
|
+
const existing = registry.providers.find((p21) => p21.id === template.id);
|
|
7804
7940
|
if (existing && !opts?.replaceExisting) {
|
|
7805
7941
|
return {
|
|
7806
7942
|
added: false,
|
|
@@ -7850,7 +7986,7 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
7850
7986
|
}
|
|
7851
7987
|
};
|
|
7852
7988
|
if (existing) {
|
|
7853
|
-
const idx = registry.providers.findIndex((
|
|
7989
|
+
const idx = registry.providers.findIndex((p21) => p21.id === template.id);
|
|
7854
7990
|
registry.providers[idx] = entry;
|
|
7855
7991
|
} else {
|
|
7856
7992
|
registry.providers.push(entry);
|
|
@@ -7862,11 +7998,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
|
|
|
7862
7998
|
|
|
7863
7999
|
// src/registry/crud.ts
|
|
7864
8000
|
function credentialStillReferenced(authRef, remaining) {
|
|
7865
|
-
return remaining.some((
|
|
8001
|
+
return remaining.some((p21) => p21.authRef === authRef);
|
|
7866
8002
|
}
|
|
7867
8003
|
async function removeProviderFromRegistry(id, opts) {
|
|
7868
8004
|
const registry = loadRegistry();
|
|
7869
|
-
const index = registry.providers.findIndex((
|
|
8005
|
+
const index = registry.providers.findIndex((p21) => p21.id === id);
|
|
7870
8006
|
if (index < 0) {
|
|
7871
8007
|
return { removed: false, id, credentialDeleted: false, error: `Provider not found: ${id}` };
|
|
7872
8008
|
}
|
|
@@ -7890,7 +8026,7 @@ async function removeProviderFromRegistry(id, opts) {
|
|
|
7890
8026
|
}
|
|
7891
8027
|
function addZenRegistryStub(opts) {
|
|
7892
8028
|
const registry = loadRegistry();
|
|
7893
|
-
if (registry.providers.some((
|
|
8029
|
+
if (registry.providers.some((p21) => p21.id === "zen")) {
|
|
7894
8030
|
return { added: false, reason: "OpenCode Zen is already configured." };
|
|
7895
8031
|
}
|
|
7896
8032
|
registry.providers.push(zenRegistryStub(opts?.subscriptionFilter));
|
|
@@ -7899,7 +8035,7 @@ function addZenRegistryStub(opts) {
|
|
|
7899
8035
|
}
|
|
7900
8036
|
function addGoRegistryStub() {
|
|
7901
8037
|
const registry = loadRegistry();
|
|
7902
|
-
if (registry.providers.some((
|
|
8038
|
+
if (registry.providers.some((p21) => p21.id === "go")) {
|
|
7903
8039
|
return { added: false, reason: "OpenCode Go is already configured." };
|
|
7904
8040
|
}
|
|
7905
8041
|
registry.providers.push(goRegistryStub());
|
|
@@ -7908,7 +8044,7 @@ function addGoRegistryStub() {
|
|
|
7908
8044
|
}
|
|
7909
8045
|
function toggleProviderEnabled(id) {
|
|
7910
8046
|
const registry = loadRegistry();
|
|
7911
|
-
const provider = registry.providers.find((
|
|
8047
|
+
const provider = registry.providers.find((p21) => p21.id === id);
|
|
7912
8048
|
if (!provider) return { toggled: false, error: `Provider not found: ${id}` };
|
|
7913
8049
|
provider.enabled = !provider.enabled;
|
|
7914
8050
|
saveRegistry(registry);
|
|
@@ -8142,7 +8278,7 @@ async function refreshApiListProvider(provider, apiKey) {
|
|
|
8142
8278
|
};
|
|
8143
8279
|
}
|
|
8144
8280
|
function updateProviderCache(registry, providerId, models, baseUrl) {
|
|
8145
|
-
const idx = registry.providers.findIndex((
|
|
8281
|
+
const idx = registry.providers.findIndex((p21) => p21.id === providerId);
|
|
8146
8282
|
if (idx < 0) return;
|
|
8147
8283
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8148
8284
|
const existing = registry.providers[idx];
|
|
@@ -8157,7 +8293,7 @@ function updateProviderCache(registry, providerId, models, baseUrl) {
|
|
|
8157
8293
|
};
|
|
8158
8294
|
}
|
|
8159
8295
|
async function refreshProviderModels(providerId, apiKey, registry = loadRegistry()) {
|
|
8160
|
-
const provider = registry.providers.find((
|
|
8296
|
+
const provider = registry.providers.find((p21) => p21.id === providerId);
|
|
8161
8297
|
if (!provider) {
|
|
8162
8298
|
return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
|
|
8163
8299
|
}
|
|
@@ -8264,7 +8400,7 @@ async function refreshAllProviderModels(resolveKey) {
|
|
|
8264
8400
|
const opencodeKey = await readGlobalOpencodeCredential();
|
|
8265
8401
|
if (opencodeKey) {
|
|
8266
8402
|
let changed = false;
|
|
8267
|
-
if (!registry.providers.some((
|
|
8403
|
+
if (!registry.providers.some((p21) => p21.id === "zen")) {
|
|
8268
8404
|
registry.providers.push({
|
|
8269
8405
|
id: "zen",
|
|
8270
8406
|
templateId: "zen",
|
|
@@ -8278,7 +8414,7 @@ async function refreshAllProviderModels(resolveKey) {
|
|
|
8278
8414
|
});
|
|
8279
8415
|
changed = true;
|
|
8280
8416
|
}
|
|
8281
|
-
if (!registry.providers.some((
|
|
8417
|
+
if (!registry.providers.some((p21) => p21.id === "go")) {
|
|
8282
8418
|
registry.providers.push({
|
|
8283
8419
|
id: "go",
|
|
8284
8420
|
templateId: "go",
|
|
@@ -8296,7 +8432,7 @@ async function refreshAllProviderModels(resolveKey) {
|
|
|
8296
8432
|
saveRegistry(registry);
|
|
8297
8433
|
}
|
|
8298
8434
|
}
|
|
8299
|
-
const enabledProviders = registry.providers.filter((
|
|
8435
|
+
const enabledProviders = registry.providers.filter((p21) => p21.enabled);
|
|
8300
8436
|
for (const provider of enabledProviders) {
|
|
8301
8437
|
const key = await resolveRefreshCredential(provider, resolveKey);
|
|
8302
8438
|
refreshed.push(await refreshProviderModels(provider.id, key, registry));
|
|
@@ -8350,42 +8486,42 @@ function openBrowser(url) {
|
|
|
8350
8486
|
async function runNativeDeviceCode(providerId) {
|
|
8351
8487
|
const label = PROVIDER_DISPLAY[providerId];
|
|
8352
8488
|
printOAuthStepsPanel(`${label} \u2014 Sign in`, label);
|
|
8353
|
-
const
|
|
8354
|
-
|
|
8489
|
+
const spinner10 = p9.spinner();
|
|
8490
|
+
spinner10.start("Waiting for authorization...");
|
|
8355
8491
|
try {
|
|
8356
8492
|
if (providerId === "xai" || providerId === "xai-oauth") {
|
|
8357
8493
|
const tokens2 = await runXaiDeviceCodeFlow(({ url, userCode }) => {
|
|
8358
|
-
|
|
8494
|
+
spinner10.stop("");
|
|
8359
8495
|
p9.log.info(`Visit: ${pc9.cyan(url)}`);
|
|
8360
8496
|
p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
|
|
8361
8497
|
openBrowser(url);
|
|
8362
|
-
|
|
8498
|
+
spinner10.start("Waiting for authorization...");
|
|
8363
8499
|
});
|
|
8364
|
-
|
|
8500
|
+
spinner10.stop(pc9.green("Signed in to xAI"));
|
|
8365
8501
|
return tokensToStoredCredential(tokens2);
|
|
8366
8502
|
}
|
|
8367
8503
|
if (providerId === "github-copilot") {
|
|
8368
8504
|
const tokens2 = await runGithubDeviceCodeFlow(({ url, userCode }) => {
|
|
8369
|
-
|
|
8505
|
+
spinner10.stop("");
|
|
8370
8506
|
p9.log.info(`Visit: ${pc9.cyan(url)}`);
|
|
8371
8507
|
p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
|
|
8372
8508
|
openBrowser(url);
|
|
8373
|
-
|
|
8509
|
+
spinner10.start("Waiting for authorization...");
|
|
8374
8510
|
});
|
|
8375
|
-
|
|
8511
|
+
spinner10.stop(pc9.green("Signed in to GitHub Copilot"));
|
|
8376
8512
|
return tokensToStoredCredential(tokens2);
|
|
8377
8513
|
}
|
|
8378
8514
|
const { tokens, accountId } = await runOpenAiDeviceCodeFlow(({ url, userCode }) => {
|
|
8379
|
-
|
|
8515
|
+
spinner10.stop("");
|
|
8380
8516
|
p9.log.info(`Visit: ${pc9.cyan(url)}`);
|
|
8381
8517
|
p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
|
|
8382
8518
|
openBrowser(url);
|
|
8383
|
-
|
|
8519
|
+
spinner10.start("Waiting for authorization...");
|
|
8384
8520
|
});
|
|
8385
|
-
|
|
8521
|
+
spinner10.stop(pc9.green("Signed in to OpenAI ChatGPT"));
|
|
8386
8522
|
return tokensToStoredCredential(tokens, void 0, accountId);
|
|
8387
8523
|
} catch (err) {
|
|
8388
|
-
|
|
8524
|
+
spinner10.stop("");
|
|
8389
8525
|
throw err;
|
|
8390
8526
|
}
|
|
8391
8527
|
}
|
|
@@ -8586,10 +8722,10 @@ async function runProvidersImport() {
|
|
|
8586
8722
|
if (p10.isCancel(choice)) return "skip";
|
|
8587
8723
|
return choice;
|
|
8588
8724
|
} : void 0;
|
|
8589
|
-
const
|
|
8590
|
-
|
|
8725
|
+
const spinner10 = p10.spinner();
|
|
8726
|
+
spinner10.start("Importing from OpenCode...");
|
|
8591
8727
|
const result = await importFromOpencode({ resolveConflict });
|
|
8592
|
-
|
|
8728
|
+
spinner10.stop("");
|
|
8593
8729
|
if (result.error) {
|
|
8594
8730
|
p10.log.error(result.error);
|
|
8595
8731
|
return 1;
|
|
@@ -8654,19 +8790,19 @@ async function runProvidersRefreshModels(providerId) {
|
|
|
8654
8790
|
const resolveKey = async (provider) => resolveProviderCredential(provider.id, provider.authRef);
|
|
8655
8791
|
if (providerId) {
|
|
8656
8792
|
const registry = loadRegistry();
|
|
8657
|
-
const provider = registry.providers.find((
|
|
8793
|
+
const provider = registry.providers.find((p21) => p21.id === providerId);
|
|
8658
8794
|
if (!provider) {
|
|
8659
8795
|
p10.log.error(`Provider not found: ${providerId}`);
|
|
8660
8796
|
return 1;
|
|
8661
8797
|
}
|
|
8662
|
-
const
|
|
8663
|
-
|
|
8798
|
+
const spinner11 = p10.spinner();
|
|
8799
|
+
spinner11.start(`Refreshing ${provider.name}...`);
|
|
8664
8800
|
const key = await resolveRefreshCredential(
|
|
8665
8801
|
provider,
|
|
8666
|
-
async (
|
|
8802
|
+
async (p21) => resolveProviderCredential(p21.id, p21.authRef)
|
|
8667
8803
|
);
|
|
8668
8804
|
const result = await refreshProviderModels(providerId, key);
|
|
8669
|
-
|
|
8805
|
+
spinner11.stop("");
|
|
8670
8806
|
if (result.skipped) {
|
|
8671
8807
|
const countNote = result.modelCount ? ` (${result.modelCount} cached models kept)` : "";
|
|
8672
8808
|
p10.log.warn(`${result.name}: ${result.reason}${countNote}`);
|
|
@@ -8681,10 +8817,10 @@ async function runProvidersRefreshModels(providerId) {
|
|
|
8681
8817
|
p10.log.success(`${result.name}: ${result.modelCount} model${result.modelCount === 1 ? "" : "s"} updated${diffStr}.`);
|
|
8682
8818
|
return 0;
|
|
8683
8819
|
}
|
|
8684
|
-
const
|
|
8685
|
-
|
|
8820
|
+
const spinner10 = p10.spinner();
|
|
8821
|
+
spinner10.start("Refreshing model lists...");
|
|
8686
8822
|
const { refreshed } = await refreshAllProviderModels(resolveKey);
|
|
8687
|
-
|
|
8823
|
+
spinner10.stop("");
|
|
8688
8824
|
const ok = refreshed.filter((r) => r.ok && !r.skipped);
|
|
8689
8825
|
const skipped = refreshed.filter((r) => r.skipped);
|
|
8690
8826
|
const failed = refreshed.filter((r) => !r.ok);
|
|
@@ -8724,7 +8860,7 @@ async function runProvidersList() {
|
|
|
8724
8860
|
async function pickTemplateFromCatalog() {
|
|
8725
8861
|
while (true) {
|
|
8726
8862
|
const registry = loadRegistry();
|
|
8727
|
-
const configuredIds = new Set(registry.providers.map((
|
|
8863
|
+
const configuredIds = new Set(registry.providers.map((p21) => p21.id));
|
|
8728
8864
|
const templates = listAddableTemplates(configuredIds);
|
|
8729
8865
|
if (templates.length === 0) return null;
|
|
8730
8866
|
const method = await p10.select({
|
|
@@ -8779,7 +8915,7 @@ async function pickTemplateFromCatalog() {
|
|
|
8779
8915
|
}
|
|
8780
8916
|
}
|
|
8781
8917
|
async function runTemplateAddFlow() {
|
|
8782
|
-
if (listAddableTemplates(loadRegistry().providers.map((
|
|
8918
|
+
if (listAddableTemplates(loadRegistry().providers.map((p21) => p21.id)).length === 0) {
|
|
8783
8919
|
p10.log.info("All catalog providers are already configured.");
|
|
8784
8920
|
return 0;
|
|
8785
8921
|
}
|
|
@@ -8801,12 +8937,12 @@ async function runTemplateAddFlow() {
|
|
|
8801
8937
|
apiKey2 = collected;
|
|
8802
8938
|
}
|
|
8803
8939
|
await migrateGlobalOpencodeCredential();
|
|
8804
|
-
const
|
|
8805
|
-
|
|
8940
|
+
const spinner11 = p10.spinner();
|
|
8941
|
+
spinner11.start(`Adding ${template.name}...`);
|
|
8806
8942
|
const zenStub = addZenRegistryStub();
|
|
8807
8943
|
const goStub = addGoRegistryStub();
|
|
8808
8944
|
if (!zenStub.added && !goStub.added) {
|
|
8809
|
-
|
|
8945
|
+
spinner11.stop("");
|
|
8810
8946
|
p10.log.warn("OpenCode Zen / Go is already configured.");
|
|
8811
8947
|
return 0;
|
|
8812
8948
|
}
|
|
@@ -8815,7 +8951,7 @@ async function runTemplateAddFlow() {
|
|
|
8815
8951
|
await refreshProviderModels("zen", apiKey2, registry),
|
|
8816
8952
|
await refreshProviderModels("go", apiKey2, registry)
|
|
8817
8953
|
];
|
|
8818
|
-
|
|
8954
|
+
spinner11.stop("");
|
|
8819
8955
|
const modelCount = refreshResults.reduce((total, result2) => total + (result2.modelCount ?? 0), 0);
|
|
8820
8956
|
const failed = refreshResults.filter((result2) => !result2.ok);
|
|
8821
8957
|
if (failed.length === 0) {
|
|
@@ -8858,10 +8994,10 @@ async function runTemplateAddFlow() {
|
|
|
8858
8994
|
}
|
|
8859
8995
|
const rawKey = String(apiKeyInput).trim();
|
|
8860
8996
|
const apiKey = template.apiKeyOptional && !rawKey ? template.id : rawKey;
|
|
8861
|
-
const
|
|
8862
|
-
|
|
8997
|
+
const spinner10 = p10.spinner();
|
|
8998
|
+
spinner10.start(`Testing connection to ${template.name}...`);
|
|
8863
8999
|
const result = await addProviderFromTemplate(template, apiKey, { baseUrl: baseUrlOverride });
|
|
8864
|
-
|
|
9000
|
+
spinner10.stop("");
|
|
8865
9001
|
if (!result.added) {
|
|
8866
9002
|
p10.log.error(result.error ?? "Could not add provider.");
|
|
8867
9003
|
if (result.hint) p10.log.info(result.hint);
|
|
@@ -8909,8 +9045,8 @@ async function runCustomEndpointAddFlow() {
|
|
|
8909
9045
|
message: "API key (leave empty for local servers without auth):"
|
|
8910
9046
|
});
|
|
8911
9047
|
if (p10.isCancel(apiKey)) return 0;
|
|
8912
|
-
const
|
|
8913
|
-
|
|
9048
|
+
const spinner10 = p10.spinner();
|
|
9049
|
+
spinner10.start("Testing connection...");
|
|
8914
9050
|
const result = await addCustomEndpointProvider({
|
|
8915
9051
|
displayName: String(displayName).trim(),
|
|
8916
9052
|
baseUrl: String(baseUrl).trim(),
|
|
@@ -8918,7 +9054,7 @@ async function runCustomEndpointAddFlow() {
|
|
|
8918
9054
|
kind: kindChoice,
|
|
8919
9055
|
allowInsecureLocal: allowLocal === true
|
|
8920
9056
|
});
|
|
8921
|
-
|
|
9057
|
+
spinner10.stop("");
|
|
8922
9058
|
if (!result.added) {
|
|
8923
9059
|
p10.log.error(result.error ?? "Could not add custom provider.");
|
|
8924
9060
|
if (result.hint) p10.log.info(result.hint);
|
|
@@ -8937,7 +9073,7 @@ async function runProvidersAdd() {
|
|
|
8937
9073
|
hint: hasOpencode ? "Import Groq, OpenAI, etc. from your OpenCode config" : "Requires OpenCode CLI"
|
|
8938
9074
|
}
|
|
8939
9075
|
];
|
|
8940
|
-
const addableTemplates = listAddableTemplates(registry.providers.map((
|
|
9076
|
+
const addableTemplates = listAddableTemplates(registry.providers.map((p21) => p21.id));
|
|
8941
9077
|
if (addableTemplates.length > 0) {
|
|
8942
9078
|
options.push({
|
|
8943
9079
|
value: "templates",
|
|
@@ -8974,11 +9110,11 @@ async function runProvidersRemove(id, interactive = false) {
|
|
|
8974
9110
|
return 1;
|
|
8975
9111
|
}
|
|
8976
9112
|
if (interactive) {
|
|
8977
|
-
const
|
|
9113
|
+
const confirm10 = await p10.confirm({
|
|
8978
9114
|
message: `Remove ${provider.name} (${id})?`,
|
|
8979
9115
|
initialValue: false
|
|
8980
9116
|
});
|
|
8981
|
-
if (p10.isCancel(
|
|
9117
|
+
if (p10.isCancel(confirm10) || !confirm10) {
|
|
8982
9118
|
p10.cancel("Cancelled.");
|
|
8983
9119
|
return 0;
|
|
8984
9120
|
}
|
|
@@ -9174,7 +9310,7 @@ import { createServer as createServer3 } from "http";
|
|
|
9174
9310
|
import { streamText as streamText3, generateText as generateText3, tool as tool3, jsonSchema as jsonSchema3 } from "ai";
|
|
9175
9311
|
function messageText(content) {
|
|
9176
9312
|
if (typeof content === "string") return content;
|
|
9177
|
-
return (content ?? []).map((
|
|
9313
|
+
return (content ?? []).map((p21) => p21.type === "output_text" || p21.type === "input_text" || p21.type === "text" ? p21.text ?? "" : "").join("");
|
|
9178
9314
|
}
|
|
9179
9315
|
function extractDeveloperAndInstructions(items, instructions) {
|
|
9180
9316
|
const developerParts = [];
|
|
@@ -9540,24 +9676,24 @@ async function writeResponsesStream(fullStream, modelId, write) {
|
|
|
9540
9676
|
});
|
|
9541
9677
|
outputItems.unshift(reasoningItem);
|
|
9542
9678
|
}
|
|
9543
|
-
for (const
|
|
9679
|
+
for (const tool5 of toolStates) {
|
|
9544
9680
|
emit("response.function_call_arguments.done", {
|
|
9545
9681
|
type: "response.function_call_arguments.done",
|
|
9546
|
-
item_id:
|
|
9547
|
-
output_index:
|
|
9548
|
-
arguments:
|
|
9682
|
+
item_id: tool5.itemId,
|
|
9683
|
+
output_index: tool5.outputIndex,
|
|
9684
|
+
arguments: tool5.args
|
|
9549
9685
|
});
|
|
9550
9686
|
const fcItem = {
|
|
9551
9687
|
type: "function_call",
|
|
9552
|
-
id:
|
|
9553
|
-
call_id:
|
|
9554
|
-
name:
|
|
9555
|
-
arguments:
|
|
9688
|
+
id: tool5.itemId,
|
|
9689
|
+
call_id: tool5.callId,
|
|
9690
|
+
name: tool5.name,
|
|
9691
|
+
arguments: tool5.args,
|
|
9556
9692
|
status: "completed"
|
|
9557
9693
|
};
|
|
9558
9694
|
emit("response.output_item.done", {
|
|
9559
9695
|
type: "response.output_item.done",
|
|
9560
|
-
output_index:
|
|
9696
|
+
output_index: tool5.outputIndex,
|
|
9561
9697
|
item: fcItem
|
|
9562
9698
|
});
|
|
9563
9699
|
outputItems.push(fcItem);
|
|
@@ -9766,17 +9902,17 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9766
9902
|
}));
|
|
9767
9903
|
}
|
|
9768
9904
|
return new Promise((resolve, reject2) => {
|
|
9769
|
-
const
|
|
9905
|
+
const log19 = debug ? makeTraceLogger(getCodexProxyDebugLogPath()) : () => {
|
|
9770
9906
|
};
|
|
9771
9907
|
const onRejection = (reason) => {
|
|
9772
9908
|
logUpstreamError(reason);
|
|
9773
|
-
if (debug)
|
|
9909
|
+
if (debug) log19(formatUpstreamError(reason));
|
|
9774
9910
|
};
|
|
9775
9911
|
process.on("unhandledRejection", onRejection);
|
|
9776
9912
|
const server = createServer3(async (req, res) => {
|
|
9777
9913
|
const url = req.url ?? "/";
|
|
9778
9914
|
if (debug) {
|
|
9779
|
-
|
|
9915
|
+
log19(`-> ${req.method} ${url} content-type=${req.headers["content-type"] ?? "(none)"} content-encoding=${req.headers["content-encoding"] ?? "(none)"} content-length=${req.headers["content-length"] ?? "(none)"}`);
|
|
9780
9916
|
}
|
|
9781
9917
|
if (!requireAuth && req.method === "POST") {
|
|
9782
9918
|
const origin = req.headers.origin;
|
|
@@ -9854,7 +9990,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9854
9990
|
rawBody = await readBody(req);
|
|
9855
9991
|
} catch (err) {
|
|
9856
9992
|
if (debug) {
|
|
9857
|
-
|
|
9993
|
+
log19(`Error: failed to read/decode request body on POST ${url}: ${formatUpstreamError(err)} content-encoding=${req.headers["content-encoding"] ?? "(none)"}`);
|
|
9858
9994
|
}
|
|
9859
9995
|
sendJson(res, 400, { error: { message: "Invalid request body", type: "invalid_request_error" } });
|
|
9860
9996
|
return;
|
|
@@ -9865,19 +10001,28 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9865
10001
|
} catch (err) {
|
|
9866
10002
|
if (debug) {
|
|
9867
10003
|
const headers = JSON.stringify(req.headers);
|
|
9868
|
-
|
|
10004
|
+
log19(`Error: Invalid JSON body on POST ${url}: ${formatUpstreamError(err)} headers=${headers} rawBody=${JSON.stringify(rawBody.slice(0, 2e3))}`);
|
|
9869
10005
|
}
|
|
9870
10006
|
sendJson(res, 400, { error: { message: "Invalid JSON body", type: "invalid_request_error" } });
|
|
9871
10007
|
return;
|
|
9872
10008
|
}
|
|
9873
10009
|
const modelId = String(body.model ?? "");
|
|
9874
|
-
|
|
10010
|
+
let resolved = resolveModel(routes, models, modelId);
|
|
9875
10011
|
if (!resolved) {
|
|
9876
|
-
|
|
9877
|
-
|
|
10012
|
+
const fallbackRoute = routes[0];
|
|
10013
|
+
const fallbackLm = fallbackRoute ? models.get(fallbackRoute.modelId) : void 0;
|
|
10014
|
+
if (fallbackRoute && fallbackLm) {
|
|
10015
|
+
if (debug) {
|
|
10016
|
+
log19(`resolveModel fallback: requested="${modelId}" \u2192 ${fallbackRoute.modelId}`);
|
|
10017
|
+
}
|
|
10018
|
+
resolved = { route: fallbackRoute, languageModel: fallbackLm };
|
|
10019
|
+
} else {
|
|
10020
|
+
if (debug) {
|
|
10021
|
+
log19(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
|
|
10022
|
+
}
|
|
10023
|
+
sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
|
|
10024
|
+
return;
|
|
9878
10025
|
}
|
|
9879
|
-
sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
|
|
9880
|
-
return;
|
|
9881
10026
|
}
|
|
9882
10027
|
const { route, languageModel } = resolved;
|
|
9883
10028
|
try {
|
|
@@ -9894,7 +10039,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9894
10039
|
);
|
|
9895
10040
|
if (debug) {
|
|
9896
10041
|
const effort = body.reasoning?.effort;
|
|
9897
|
-
|
|
10042
|
+
log19(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
|
|
9898
10043
|
}
|
|
9899
10044
|
if (body.stream) {
|
|
9900
10045
|
res.writeHead(200, {
|
|
@@ -9924,11 +10069,15 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9924
10069
|
}
|
|
9925
10070
|
} catch (err) {
|
|
9926
10071
|
const msg = formatUpstreamError(err);
|
|
9927
|
-
|
|
10072
|
+
log19(`handler error: ${msg}`);
|
|
9928
10073
|
sendJson(res, 500, { error: { message: msg, type: "api_error" } });
|
|
9929
10074
|
}
|
|
9930
10075
|
return;
|
|
9931
10076
|
}
|
|
10077
|
+
if (req.method === "GET" && url === "/v1/responses") {
|
|
10078
|
+
sendJson(res, 200, { object: "list", data: [] });
|
|
10079
|
+
return;
|
|
10080
|
+
}
|
|
9932
10081
|
sendJson(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
|
|
9933
10082
|
});
|
|
9934
10083
|
server.on("error", reject2);
|
|
@@ -10150,7 +10299,7 @@ function restoreCodexOverlay(env = process.env) {
|
|
|
10150
10299
|
return removed;
|
|
10151
10300
|
}
|
|
10152
10301
|
function remainingOverlayPaths(env = process.env) {
|
|
10153
|
-
return ownedOverlayPaths(env).filter((
|
|
10302
|
+
return ownedOverlayPaths(env).filter((p21) => existsSync11(p21));
|
|
10154
10303
|
}
|
|
10155
10304
|
function recoverInterruptedCodexSession(env = process.env) {
|
|
10156
10305
|
const before = remainingOverlayPaths(env);
|
|
@@ -10547,8 +10696,8 @@ function resolveCodexFavorites(activeProvider, selectedModel, compatible, favori
|
|
|
10547
10696
|
agent,
|
|
10548
10697
|
localProviders: compatible,
|
|
10549
10698
|
zenGoApiKey,
|
|
10550
|
-
zenModels: compatible.find((
|
|
10551
|
-
goModels: compatible.find((
|
|
10699
|
+
zenModels: compatible.find((p21) => p21.id === "zen")?.models,
|
|
10700
|
+
goModels: compatible.find((p21) => p21.id === "go")?.models,
|
|
10552
10701
|
findLocalModel: (pid, mid) => {
|
|
10553
10702
|
const provider = compatible.find((lp) => lp.id === pid);
|
|
10554
10703
|
const model = provider?.models.find((m) => m.id === mid);
|
|
@@ -10620,8 +10769,24 @@ function isClaudeMachineReadableOutput(args) {
|
|
|
10620
10769
|
function isCodexMachineReadableOutput(args) {
|
|
10621
10770
|
return args.includes("--json");
|
|
10622
10771
|
}
|
|
10772
|
+
function isGeminiNonInteractive(args) {
|
|
10773
|
+
for (let i = 0; i < args.length; i++) {
|
|
10774
|
+
const arg = args[i];
|
|
10775
|
+
if (arg === "--") return false;
|
|
10776
|
+
if (arg === "-p" || arg === "--prompt" || arg === "-i" || arg === "--prompt-interactive") return true;
|
|
10777
|
+
if (arg.startsWith("-")) {
|
|
10778
|
+
i = skipAttachedFlagValue(args, i);
|
|
10779
|
+
continue;
|
|
10780
|
+
}
|
|
10781
|
+
return true;
|
|
10782
|
+
}
|
|
10783
|
+
return false;
|
|
10784
|
+
}
|
|
10623
10785
|
function wantsCleanAgentStdout(agent, childArgs) {
|
|
10624
|
-
|
|
10786
|
+
if (agent === "claude") return isClaudeMachineReadableOutput(childArgs);
|
|
10787
|
+
if (agent === "codex") return isCodexMachineReadableOutput(childArgs);
|
|
10788
|
+
const outFmt = readFlagValue(childArgs, "-o") || readFlagValue(childArgs, "--output-format");
|
|
10789
|
+
return outFmt === "json" || outFmt === "stream-json";
|
|
10625
10790
|
}
|
|
10626
10791
|
function normalizeClaudeAgentArgs(args) {
|
|
10627
10792
|
const out = [...args];
|
|
@@ -10653,14 +10818,14 @@ function isCodexNonInteractive(args) {
|
|
|
10653
10818
|
}
|
|
10654
10819
|
function resolveLaunchTarget(explicit, prefs, agent) {
|
|
10655
10820
|
const slug = explicit.modelId ? parseModelSlug(explicit.modelId) : null;
|
|
10656
|
-
const providerId = explicit.providerId ?? slug?.providerId ?? (agent === "claude" ? prefs.lastProvider : prefs.lastCodexProvider);
|
|
10657
|
-
const modelId = slug?.modelId ?? explicit.modelId ?? (agent === "claude" ? prefs.lastModel : prefs.lastCodexModel);
|
|
10821
|
+
const providerId = explicit.providerId ?? slug?.providerId ?? (agent === "claude" ? prefs.lastProvider : agent === "codex" ? prefs.lastCodexProvider : prefs.lastGeminiProvider);
|
|
10822
|
+
const modelId = slug?.modelId ?? explicit.modelId ?? (agent === "claude" ? prefs.lastModel : agent === "codex" ? prefs.lastCodexModel : prefs.lastGeminiModel);
|
|
10658
10823
|
if (!providerId || !modelId) return null;
|
|
10659
10824
|
return { providerId, modelId };
|
|
10660
10825
|
}
|
|
10661
10826
|
function findProviderAndModel(providers, target) {
|
|
10662
10827
|
if (!target.providerId || !target.modelId) return null;
|
|
10663
|
-
const provider = providers.find((
|
|
10828
|
+
const provider = providers.find((p21) => p21.id === target.providerId);
|
|
10664
10829
|
if (!provider) return null;
|
|
10665
10830
|
const model = provider.models.find((m) => m.id === target.modelId);
|
|
10666
10831
|
if (!model) return null;
|
|
@@ -10677,7 +10842,7 @@ function hasCompleteExplicitLaunch(explicit) {
|
|
|
10677
10842
|
function planLaunchWizard(opts) {
|
|
10678
10843
|
const { explicit, childArgs, agent, prefs } = opts;
|
|
10679
10844
|
const explicitComplete = hasCompleteExplicitLaunch(explicit);
|
|
10680
|
-
const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : isCodexNonInteractive(childArgs);
|
|
10845
|
+
const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : agent === "codex" ? isCodexNonInteractive(childArgs) : isGeminiNonInteractive(childArgs);
|
|
10681
10846
|
if (explicitComplete) {
|
|
10682
10847
|
const target = resolveLaunchTarget(explicit, prefs, agent);
|
|
10683
10848
|
if (!target) {
|
|
@@ -10710,7 +10875,9 @@ function planLaunchWizard(opts) {
|
|
|
10710
10875
|
return { skip: false, target: null };
|
|
10711
10876
|
}
|
|
10712
10877
|
function nonInteractiveLaunchError(agent) {
|
|
10713
|
-
|
|
10878
|
+
if (agent === "claude") return "Print mode requires --provider and --model, or saved preferences from a prior launch.";
|
|
10879
|
+
if (agent === "codex") return "Non-interactive Codex launch requires --provider and --model, or saved preferences from a prior launch.";
|
|
10880
|
+
return "Non-interactive Gemini launch requires --provider and --model, or saved preferences from a prior launch.";
|
|
10714
10881
|
}
|
|
10715
10882
|
|
|
10716
10883
|
// src/codex.ts
|
|
@@ -11121,7 +11288,7 @@ Error: ${launchPlan.error}
|
|
|
11121
11288
|
resolvedFavorites = res.resolvedFavorites;
|
|
11122
11289
|
providersById = res.providersById;
|
|
11123
11290
|
}
|
|
11124
|
-
const regEntry = loadRegistry().providers.find((
|
|
11291
|
+
const regEntry = loadRegistry().providers.find((p21) => p21.id === activeProvider.id);
|
|
11125
11292
|
const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
|
|
11126
11293
|
const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
|
|
11127
11294
|
if (!apiKey) {
|
|
@@ -11175,7 +11342,7 @@ Error: ${launchPlan.error}
|
|
|
11175
11342
|
});
|
|
11176
11343
|
if (configOnly) {
|
|
11177
11344
|
const home = process.env["HOME"] ?? "";
|
|
11178
|
-
const shortenPath = (
|
|
11345
|
+
const shortenPath = (p21) => home ? p21.replace(home, "~") : p21;
|
|
11179
11346
|
console.log("");
|
|
11180
11347
|
console.log(pc13.bold(pc13.cyan(" CONFIG PREVIEW \u2014 relay-ai codex")));
|
|
11181
11348
|
console.log("");
|
|
@@ -11239,120 +11406,1122 @@ Error: ${launchPlan.error}
|
|
|
11239
11406
|
}
|
|
11240
11407
|
}
|
|
11241
11408
|
|
|
11242
|
-
// src/
|
|
11409
|
+
// src/gemini.ts
|
|
11243
11410
|
import pc14 from "picocolors";
|
|
11244
11411
|
import * as p15 from "@clack/prompts";
|
|
11245
11412
|
|
|
11246
|
-
// src/
|
|
11247
|
-
import {
|
|
11248
|
-
import {
|
|
11249
|
-
import {
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11413
|
+
// src/gemini/launch.ts
|
|
11414
|
+
import { execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
11415
|
+
import { existsSync as existsSync13 } from "fs";
|
|
11416
|
+
import { homedir as homedir10 } from "os";
|
|
11417
|
+
import { join as join14 } from "path";
|
|
11418
|
+
var isWindows4 = process.platform === "win32";
|
|
11419
|
+
var GEMINI_FALLBACK_PATHS = isWindows4 ? [
|
|
11420
|
+
join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini.cmd"),
|
|
11421
|
+
join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini")
|
|
11422
|
+
] : [
|
|
11423
|
+
join14(homedir10(), ".local", "bin", "gemini"),
|
|
11424
|
+
join14(homedir10(), ".npm", "bin", "gemini"),
|
|
11425
|
+
"/usr/local/bin/gemini",
|
|
11426
|
+
"/opt/homebrew/bin/gemini"
|
|
11427
|
+
];
|
|
11428
|
+
function findGeminiBinary() {
|
|
11429
|
+
try {
|
|
11430
|
+
const result = execSync4(isWindows4 ? "where.exe gemini" : "which gemini", {
|
|
11431
|
+
encoding: "utf8",
|
|
11432
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11433
|
+
});
|
|
11434
|
+
const path = result.trim().split("\n")[0]?.trim();
|
|
11435
|
+
if (path) return path;
|
|
11436
|
+
} catch {
|
|
11437
|
+
}
|
|
11438
|
+
for (const path of GEMINI_FALLBACK_PATHS) {
|
|
11439
|
+
if (existsSync13(path)) return path;
|
|
11440
|
+
}
|
|
11441
|
+
return null;
|
|
11258
11442
|
}
|
|
11259
|
-
function
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11443
|
+
function buildGeminiChildEnv(proxyPort, proxyToken) {
|
|
11444
|
+
const env = { ...process.env };
|
|
11445
|
+
delete env["GOOGLE_GEMINI_BASE_URL"];
|
|
11446
|
+
delete env["GEMINI_API_KEY"];
|
|
11447
|
+
delete env["GOOGLE_API_KEY"];
|
|
11448
|
+
delete env["GOOGLE_GENAI_API_KEY"];
|
|
11449
|
+
env["GOOGLE_GEMINI_BASE_URL"] = `http://127.0.0.1:${proxyPort}`;
|
|
11450
|
+
env["GEMINI_API_KEY"] = proxyToken;
|
|
11451
|
+
return env;
|
|
11263
11452
|
}
|
|
11264
|
-
function
|
|
11265
|
-
|
|
11266
|
-
|
|
11453
|
+
function launchGemini(geminiPath, modelId, env, extraArgs) {
|
|
11454
|
+
return new Promise((resolve) => {
|
|
11455
|
+
const args = ["-m", modelId, ...extraArgs];
|
|
11456
|
+
const child = spawn5(geminiPath, args, {
|
|
11457
|
+
stdio: "inherit",
|
|
11458
|
+
env,
|
|
11459
|
+
shell: isWindows4
|
|
11460
|
+
});
|
|
11461
|
+
const onSigInt = () => child.kill("SIGINT");
|
|
11462
|
+
const onSigTerm = () => child.kill("SIGTERM");
|
|
11463
|
+
process.once("SIGINT", onSigInt);
|
|
11464
|
+
process.once("SIGTERM", onSigTerm);
|
|
11465
|
+
const done = (code) => {
|
|
11466
|
+
process.off("SIGINT", onSigInt);
|
|
11467
|
+
process.off("SIGTERM", onSigTerm);
|
|
11468
|
+
resolve(code);
|
|
11469
|
+
};
|
|
11470
|
+
child.on("error", () => done(1));
|
|
11471
|
+
child.on("exit", (code) => done(code ?? 0));
|
|
11472
|
+
});
|
|
11267
11473
|
}
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11474
|
+
|
|
11475
|
+
// src/gemini/prompts.ts
|
|
11476
|
+
import * as p14 from "@clack/prompts";
|
|
11477
|
+
async function pickGeminiProvider(providers, prefs, hasFavorites = false, initialProviderId) {
|
|
11478
|
+
if (providers.length === 0 && !hasFavorites) return null;
|
|
11479
|
+
const options = providers.map((lp) => providerSelectOption(lp));
|
|
11480
|
+
if (hasFavorites) {
|
|
11481
|
+
options.unshift({
|
|
11482
|
+
value: "__favorites__",
|
|
11483
|
+
label: "\u2B50 Favorites Catalog",
|
|
11484
|
+
hint: `${prefs.favoriteModels?.length ?? 0} saved favorites`
|
|
11485
|
+
});
|
|
11486
|
+
}
|
|
11487
|
+
const initial = initialProviderId && options.some((o) => o.value === initialProviderId) ? initialProviderId : prefs.lastGeminiProvider && options.some((o) => o.value === prefs.lastGeminiProvider) ? prefs.lastGeminiProvider : options[0].value;
|
|
11488
|
+
const chosen = await p14.select({
|
|
11489
|
+
message: "Which provider for Gemini CLI?",
|
|
11490
|
+
options,
|
|
11491
|
+
initialValue: initial
|
|
11492
|
+
});
|
|
11493
|
+
if (p14.isCancel(chosen)) {
|
|
11494
|
+
p14.cancel("Cancelled.");
|
|
11495
|
+
return null;
|
|
11496
|
+
}
|
|
11497
|
+
if (chosen === "__favorites__") return "__favorites__";
|
|
11498
|
+
return providers.find((lp) => lp.id === chosen) ?? null;
|
|
11271
11499
|
}
|
|
11272
|
-
function
|
|
11273
|
-
const
|
|
11274
|
-
const
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11500
|
+
async function pickGeminiModel(provider, prefs) {
|
|
11501
|
+
const recentIds = (prefs.recentModelsByProvider?.[provider.id] ?? []).slice(0, 3);
|
|
11502
|
+
const recentModels = recentIds.map((id) => provider.models.find((m) => m.id === id)).filter((m) => m !== void 0);
|
|
11503
|
+
let selectedModel = null;
|
|
11504
|
+
while (true) {
|
|
11505
|
+
if (recentModels.length > 0) {
|
|
11506
|
+
const options = [
|
|
11507
|
+
...recentModels.map((m) => modelSelectOption(m, "recent")),
|
|
11508
|
+
navOption("__browse_all__", "Browse all models \u2192", `${provider.models.length} available`),
|
|
11509
|
+
navOption("__back__", "\u2190 Go back", "Select a different provider")
|
|
11510
|
+
];
|
|
11511
|
+
const picked = await p14.select({
|
|
11512
|
+
message: `Model for ${provider.name}?`,
|
|
11513
|
+
options,
|
|
11514
|
+
initialValue: recentModels[0].id
|
|
11515
|
+
});
|
|
11516
|
+
if (p14.isCancel(picked) || String(picked) === "__back__") {
|
|
11517
|
+
return "back";
|
|
11518
|
+
}
|
|
11519
|
+
if (String(picked) === "__browse_all__") {
|
|
11520
|
+
const browsed = await browseAllModels(provider, prefs);
|
|
11521
|
+
if (browsed === "back") {
|
|
11522
|
+
continue;
|
|
11523
|
+
}
|
|
11524
|
+
if (!browsed) return null;
|
|
11525
|
+
selectedModel = browsed;
|
|
11526
|
+
break;
|
|
11527
|
+
} else {
|
|
11528
|
+
selectedModel = recentModels.find((m) => m.id === String(picked));
|
|
11529
|
+
break;
|
|
11530
|
+
}
|
|
11531
|
+
} else {
|
|
11532
|
+
const browsed = await browseAllModels(provider, prefs);
|
|
11533
|
+
if (browsed === "back") {
|
|
11534
|
+
return "back";
|
|
11535
|
+
}
|
|
11536
|
+
if (!browsed) return null;
|
|
11537
|
+
selectedModel = browsed;
|
|
11538
|
+
break;
|
|
11539
|
+
}
|
|
11540
|
+
}
|
|
11541
|
+
return selectedModel;
|
|
11294
11542
|
}
|
|
11295
|
-
function
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11543
|
+
function confirmGeminiLaunch(providerName, modelLabel, modelId) {
|
|
11544
|
+
return p14.confirm({
|
|
11545
|
+
message: confirmLaunchMessage("Gemini CLI", modelLabel, modelId, providerName),
|
|
11546
|
+
initialValue: true
|
|
11547
|
+
}).then((answer) => {
|
|
11548
|
+
if (p14.isCancel(answer)) {
|
|
11549
|
+
p14.cancel("Cancelled.");
|
|
11550
|
+
return false;
|
|
11551
|
+
}
|
|
11552
|
+
return answer;
|
|
11553
|
+
});
|
|
11302
11554
|
}
|
|
11303
|
-
function
|
|
11304
|
-
const
|
|
11305
|
-
const
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
out.openai_base_url = patch.openai_base_url;
|
|
11310
|
-
out.model_catalog_json = patch.model_catalog_json;
|
|
11311
|
-
const providers = asRecord(out.model_providers);
|
|
11312
|
-
delete providers[CODEX_APP_PROVIDER_ID];
|
|
11313
|
-
const profiles = asRecord(out.profiles);
|
|
11314
|
-
delete profiles[CODEX_APP_PROVIDER_ID];
|
|
11315
|
-
if (Object.keys(profiles).length === 0) {
|
|
11316
|
-
delete out.profiles;
|
|
11317
|
-
} else {
|
|
11318
|
-
out.profiles = profiles;
|
|
11555
|
+
async function pickGeminiFavoriteModel(providers, favorites) {
|
|
11556
|
+
const favList = [];
|
|
11557
|
+
for (const fav of favorites) {
|
|
11558
|
+
const provider2 = providers.find((lp) => lp.id === fav.providerId);
|
|
11559
|
+
const model2 = provider2?.models.find((m) => m.id === fav.modelId);
|
|
11560
|
+
if (provider2 && model2) favList.push({ provider: provider2, model: model2 });
|
|
11319
11561
|
}
|
|
11320
|
-
if (
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
out.model_providers = providers;
|
|
11562
|
+
if (favList.length === 0) {
|
|
11563
|
+
p14.log.warn("None of your saved favorites are available in the current registry.");
|
|
11564
|
+
return null;
|
|
11324
11565
|
}
|
|
11325
|
-
const
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11566
|
+
const options = [
|
|
11567
|
+
...favList.map(({ provider: provider2, model: model2 }) => ({
|
|
11568
|
+
value: `${provider2.id}::${model2.id}`,
|
|
11569
|
+
label: model2.name || model2.id,
|
|
11570
|
+
hint: provider2.name
|
|
11571
|
+
})),
|
|
11572
|
+
{ value: "__back__", label: "\u2190 Go back", hint: "Select a different provider" }
|
|
11573
|
+
];
|
|
11574
|
+
const picked = await p14.select({
|
|
11575
|
+
message: "Pick a favorite model for Gemini CLI:",
|
|
11576
|
+
options,
|
|
11577
|
+
initialValue: options[0].value
|
|
11578
|
+
});
|
|
11579
|
+
if (p14.isCancel(picked) || String(picked) === "__back__") return "back";
|
|
11580
|
+
const [pickedProviderId, pickedModelId] = picked.split("::");
|
|
11581
|
+
const provider = providers.find((lp) => lp.id === pickedProviderId);
|
|
11582
|
+
const model = provider?.models.find((m) => m.id === pickedModelId);
|
|
11583
|
+
if (!provider || !model) return null;
|
|
11584
|
+
return { provider, model };
|
|
11585
|
+
}
|
|
11586
|
+
function rejectGeminiManagedFlags(geminiArgs) {
|
|
11587
|
+
const blocked = /* @__PURE__ */ new Set(["--provider", "--model", "-m", "--trace"]);
|
|
11588
|
+
const takesValue = /* @__PURE__ */ new Set(["--provider", "--model", "-m"]);
|
|
11589
|
+
const out = [];
|
|
11590
|
+
for (let i = 0; i < geminiArgs.length; i++) {
|
|
11591
|
+
const arg = geminiArgs[i];
|
|
11592
|
+
if (blocked.has(arg)) {
|
|
11593
|
+
if (takesValue.has(arg)) i++;
|
|
11594
|
+
continue;
|
|
11340
11595
|
}
|
|
11596
|
+
if (arg.startsWith("--model=") || arg.startsWith("--provider=") || arg.startsWith("-m=")) continue;
|
|
11597
|
+
out.push(arg);
|
|
11341
11598
|
}
|
|
11342
11599
|
return out;
|
|
11343
11600
|
}
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11601
|
+
|
|
11602
|
+
// src/gemini-proxy.ts
|
|
11603
|
+
import { createServer as createServer4 } from "http";
|
|
11604
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
11605
|
+
import { streamText as streamText4, generateText as generateText4, tool as tool4, jsonSchema as jsonSchema4 } from "ai";
|
|
11606
|
+
function mapFinishReason(reason) {
|
|
11607
|
+
if (reason === "stop" || reason === "tool-calls") return "STOP";
|
|
11608
|
+
if (reason === "length") return "MAX_TOKENS";
|
|
11609
|
+
if (reason === "content-filter") return "SAFETY";
|
|
11610
|
+
return "OTHER";
|
|
11611
|
+
}
|
|
11612
|
+
function lookupGeminiRoute(routes, requestedModel) {
|
|
11613
|
+
const ids = [requestedModel, ...routeLookupIds(requestedModel)];
|
|
11614
|
+
const slashIdx = requestedModel.indexOf("/");
|
|
11615
|
+
if (slashIdx >= 0) {
|
|
11616
|
+
const after = requestedModel.slice(slashIdx + 1);
|
|
11617
|
+
ids.push(after, ...routeLookupIds(after));
|
|
11348
11618
|
}
|
|
11349
|
-
const
|
|
11350
|
-
if (
|
|
11351
|
-
|
|
11619
|
+
const doubleUnderscore = requestedModel.indexOf("__");
|
|
11620
|
+
if (doubleUnderscore >= 0) {
|
|
11621
|
+
const after = requestedModel.slice(doubleUnderscore + 2);
|
|
11622
|
+
ids.push(after, ...routeLookupIds(after));
|
|
11352
11623
|
}
|
|
11353
|
-
const
|
|
11354
|
-
|
|
11355
|
-
|
|
11624
|
+
const uniqueIds = [...new Set(ids)];
|
|
11625
|
+
for (const id of uniqueIds) {
|
|
11626
|
+
const route = routes.find((r) => r.aliasId === id || r.realModelId === id);
|
|
11627
|
+
if (route) return route;
|
|
11628
|
+
}
|
|
11629
|
+
return void 0;
|
|
11630
|
+
}
|
|
11631
|
+
function mergeConsecutiveMessages2(messages) {
|
|
11632
|
+
const merged = [];
|
|
11633
|
+
for (const msg of messages) {
|
|
11634
|
+
if (merged.length === 0) {
|
|
11635
|
+
merged.push(msg);
|
|
11636
|
+
continue;
|
|
11637
|
+
}
|
|
11638
|
+
const last = merged[merged.length - 1];
|
|
11639
|
+
if (last.role === msg.role) {
|
|
11640
|
+
const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }];
|
|
11641
|
+
const nextContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
|
11642
|
+
last.content = [...lastContent, ...nextContent];
|
|
11643
|
+
} else {
|
|
11644
|
+
merged.push(msg);
|
|
11645
|
+
}
|
|
11646
|
+
}
|
|
11647
|
+
return merged;
|
|
11648
|
+
}
|
|
11649
|
+
function stripGeminiIdentity(text5) {
|
|
11650
|
+
return text5.replace(/You are Gemini CLI[\s\S]*?(?=\n\n|$)/gi, "").replace(/I'm Gemini CLI[\s\S]*?(?=\n\n|$)/gi, "").replace(/Gemini CLI/gi, "AI CLI");
|
|
11651
|
+
}
|
|
11652
|
+
function translateGeminiRequest(body) {
|
|
11653
|
+
let system;
|
|
11654
|
+
if (body.systemInstruction?.parts) {
|
|
11655
|
+
const rawSystem = body.systemInstruction.parts.map((p21) => p21.text || "").join("\n");
|
|
11656
|
+
system = stripGeminiIdentity(rawSystem).trim();
|
|
11657
|
+
}
|
|
11658
|
+
const messages = [];
|
|
11659
|
+
const nameToIdList = /* @__PURE__ */ new Map();
|
|
11660
|
+
const contents = body.contents || [];
|
|
11661
|
+
for (const turn of contents) {
|
|
11662
|
+
const role = turn.role === "model" ? "assistant" : "user";
|
|
11663
|
+
const parts = [];
|
|
11664
|
+
const toolResults = [];
|
|
11665
|
+
const turnParts = turn.parts || [];
|
|
11666
|
+
for (const p21 of turnParts) {
|
|
11667
|
+
if (p21.text !== void 0) {
|
|
11668
|
+
const text5 = stripGeminiIdentity(p21.text);
|
|
11669
|
+
if (text5.includes("<thinking>")) {
|
|
11670
|
+
const tokens = text5.split(/<thinking>([\s\S]*?)<\/thinking>/);
|
|
11671
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
11672
|
+
const token = tokens[i].trim();
|
|
11673
|
+
if (!token) continue;
|
|
11674
|
+
parts.push({ type: i % 2 === 1 ? "reasoning" : "text", text: token });
|
|
11675
|
+
}
|
|
11676
|
+
} else {
|
|
11677
|
+
parts.push({ type: "text", text: text5 });
|
|
11678
|
+
}
|
|
11679
|
+
} else if (p21.inlineData) {
|
|
11680
|
+
parts.push({
|
|
11681
|
+
type: "image",
|
|
11682
|
+
image: Buffer.from(p21.inlineData.data, "base64"),
|
|
11683
|
+
mediaType: p21.inlineData.mimeType
|
|
11684
|
+
});
|
|
11685
|
+
} else if (p21.functionCall) {
|
|
11686
|
+
const id = "call_" + randomUUID2().replace(/-/g, "");
|
|
11687
|
+
const name = p21.functionCall.name;
|
|
11688
|
+
if (!nameToIdList.has(name)) nameToIdList.set(name, []);
|
|
11689
|
+
nameToIdList.get(name).push(id);
|
|
11690
|
+
parts.push({
|
|
11691
|
+
type: "tool-call",
|
|
11692
|
+
toolCallId: id,
|
|
11693
|
+
toolName: name,
|
|
11694
|
+
input: p21.functionCall.args || {}
|
|
11695
|
+
});
|
|
11696
|
+
} else if (p21.functionResponse) {
|
|
11697
|
+
const name = p21.functionResponse.name;
|
|
11698
|
+
const idList = nameToIdList.get(name) || [];
|
|
11699
|
+
const id = idList.shift() || "call_" + randomUUID2().replace(/-/g, "");
|
|
11700
|
+
toolResults.push({
|
|
11701
|
+
type: "tool-result",
|
|
11702
|
+
toolCallId: id,
|
|
11703
|
+
toolName: name,
|
|
11704
|
+
output: {
|
|
11705
|
+
type: "text",
|
|
11706
|
+
value: typeof p21.functionResponse.response === "string" ? p21.functionResponse.response : JSON.stringify(p21.functionResponse.response || {})
|
|
11707
|
+
}
|
|
11708
|
+
});
|
|
11709
|
+
}
|
|
11710
|
+
}
|
|
11711
|
+
if (toolResults.length > 0) {
|
|
11712
|
+
messages.push({
|
|
11713
|
+
role: "tool",
|
|
11714
|
+
content: toolResults
|
|
11715
|
+
});
|
|
11716
|
+
}
|
|
11717
|
+
if (parts.length > 0) {
|
|
11718
|
+
messages.push({
|
|
11719
|
+
role,
|
|
11720
|
+
content: parts
|
|
11721
|
+
});
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11724
|
+
const mergedMessages = mergeConsecutiveMessages2(messages);
|
|
11725
|
+
let tools;
|
|
11726
|
+
if (body.tools) {
|
|
11727
|
+
tools = {};
|
|
11728
|
+
for (const t of body.tools) {
|
|
11729
|
+
if (t.functionDeclarations) {
|
|
11730
|
+
for (const fd of t.functionDeclarations) {
|
|
11731
|
+
tools[fd.name] = tool4({
|
|
11732
|
+
description: fd.description || "",
|
|
11733
|
+
inputSchema: jsonSchema4(fd.parameters || { type: "object", properties: {} })
|
|
11734
|
+
});
|
|
11735
|
+
}
|
|
11736
|
+
}
|
|
11737
|
+
}
|
|
11738
|
+
}
|
|
11739
|
+
let toolChoice;
|
|
11740
|
+
const mode = body.toolConfig?.functionCallingConfig?.mode;
|
|
11741
|
+
if (mode === "ANY") {
|
|
11742
|
+
toolChoice = "required";
|
|
11743
|
+
} else if (mode === "AUTO") {
|
|
11744
|
+
toolChoice = "auto";
|
|
11745
|
+
}
|
|
11746
|
+
const generationConfig = body.generationConfig || {};
|
|
11747
|
+
let responseFormat;
|
|
11748
|
+
if (generationConfig.responseMimeType === "application/json") {
|
|
11749
|
+
responseFormat = { type: "json" };
|
|
11750
|
+
}
|
|
11751
|
+
return {
|
|
11752
|
+
system,
|
|
11753
|
+
messages: mergedMessages,
|
|
11754
|
+
tools: tools && Object.keys(tools).length > 0 ? tools : void 0,
|
|
11755
|
+
toolChoice,
|
|
11756
|
+
maxOutputTokens: generationConfig.maxOutputTokens,
|
|
11757
|
+
temperature: generationConfig.temperature,
|
|
11758
|
+
responseFormat
|
|
11759
|
+
};
|
|
11760
|
+
}
|
|
11761
|
+
async function startGeminiProxy(routes, debug = false) {
|
|
11762
|
+
const proxyToken = randomUUID2();
|
|
11763
|
+
silenceSdkWarnings();
|
|
11764
|
+
if (routes.length === 0) {
|
|
11765
|
+
return Promise.reject(new Error("Gemini proxy requires at least one route"));
|
|
11766
|
+
}
|
|
11767
|
+
const defaultRoute = routes[0];
|
|
11768
|
+
const models = /* @__PURE__ */ new Map();
|
|
11769
|
+
const plog = debug ? makeTraceLogger(getGeminiProxyDebugLogPath()) : () => {
|
|
11770
|
+
};
|
|
11771
|
+
const onRejection = (reason) => {
|
|
11772
|
+
plog(`Unhandled Rejection: ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
|
|
11773
|
+
};
|
|
11774
|
+
const onException = (error) => {
|
|
11775
|
+
plog(`Uncaught Exception: ${error.stack || error.message}`);
|
|
11776
|
+
};
|
|
11777
|
+
const getOrInitModel = async (route) => {
|
|
11778
|
+
let m = models.get(route.aliasId);
|
|
11779
|
+
if (!m) {
|
|
11780
|
+
m = await createLanguageModel({
|
|
11781
|
+
npm: route.npm || "@ai-sdk/openai-compatible",
|
|
11782
|
+
modelId: route.realModelId,
|
|
11783
|
+
apiKey: route.apiKey,
|
|
11784
|
+
baseURL: route.baseURL,
|
|
11785
|
+
providerId: route.aliasId,
|
|
11786
|
+
authType: route.authType,
|
|
11787
|
+
oauthAccountId: route.oauthAccountId
|
|
11788
|
+
});
|
|
11789
|
+
models.set(route.aliasId, m);
|
|
11790
|
+
}
|
|
11791
|
+
return m;
|
|
11792
|
+
};
|
|
11793
|
+
const formatGeminiModel = (route) => ({
|
|
11794
|
+
name: `models/${route.aliasId}`,
|
|
11795
|
+
version: "1.0",
|
|
11796
|
+
displayName: route.displayName,
|
|
11797
|
+
description: "Registry model routed through relay-ai proxy",
|
|
11798
|
+
inputTokenLimit: route.contextWindow || 1e6,
|
|
11799
|
+
outputTokenLimit: 8192,
|
|
11800
|
+
supportedGenerationMethods: ["generateContent", "streamGenerateContent"]
|
|
11801
|
+
});
|
|
11802
|
+
let sessionRouteOverride = void 0;
|
|
11803
|
+
const server = createServer4(async (req, res) => {
|
|
11804
|
+
try {
|
|
11805
|
+
const url = req.url ?? "";
|
|
11806
|
+
plog(`${req.method} ${url}`);
|
|
11807
|
+
if (req.method === "GET" && (url.endsWith("/models") || url.includes("/models?"))) {
|
|
11808
|
+
plog("GET models list");
|
|
11809
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
11810
|
+
const payload = JSON.stringify({
|
|
11811
|
+
models: routes.map(formatGeminiModel)
|
|
11812
|
+
});
|
|
11813
|
+
plog(`Response: ${payload}`);
|
|
11814
|
+
res.end(payload);
|
|
11815
|
+
return;
|
|
11816
|
+
}
|
|
11817
|
+
if (req.method === "GET" && url.includes("/models/")) {
|
|
11818
|
+
const modelMatch = url.match(/\/models\/([^?]+)/);
|
|
11819
|
+
if (modelMatch) {
|
|
11820
|
+
const modelId = decodeURIComponent(modelMatch[1]);
|
|
11821
|
+
const route = lookupGeminiRoute(routes, modelId) ?? defaultRoute;
|
|
11822
|
+
plog(`GET model details: ${modelId} -> mapped to route ${route.aliasId}`);
|
|
11823
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
11824
|
+
const payload = JSON.stringify(formatGeminiModel(route));
|
|
11825
|
+
plog(`Response: ${payload}`);
|
|
11826
|
+
res.end(payload);
|
|
11827
|
+
return;
|
|
11828
|
+
}
|
|
11829
|
+
}
|
|
11830
|
+
if (req.method === "POST" && url.includes(":")) {
|
|
11831
|
+
const isStream = url.includes("streamGenerateContent");
|
|
11832
|
+
const rawBody = await readBody(req);
|
|
11833
|
+
plog(`Request body:
|
|
11834
|
+
${rawBody}`);
|
|
11835
|
+
let body;
|
|
11836
|
+
try {
|
|
11837
|
+
body = JSON.parse(rawBody);
|
|
11838
|
+
} catch {
|
|
11839
|
+
plog("Error: Invalid JSON body");
|
|
11840
|
+
res.writeHead(400);
|
|
11841
|
+
res.end("Invalid JSON");
|
|
11842
|
+
return;
|
|
11843
|
+
}
|
|
11844
|
+
const modelMatch = url.match(/\/models\/([^:]+)/);
|
|
11845
|
+
const requestedModel = modelMatch ? decodeURIComponent(modelMatch[1]) : defaultRoute.aliasId;
|
|
11846
|
+
const lastUserTurn = findLastUserTurn(body.contents || []);
|
|
11847
|
+
const modelCommand = parseModelCommand(lastUserTurn);
|
|
11848
|
+
if (modelCommand !== null) {
|
|
11849
|
+
if (modelCommand === "") {
|
|
11850
|
+
const current = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
|
|
11851
|
+
const availableList = routes.map((r) => ` - ${r.aliasId} (${r.displayName})`).join("\n");
|
|
11852
|
+
const exampleId = routes.length > 1 ? routes[1].aliasId : routes[0]?.aliasId ?? "deepseek-v4";
|
|
11853
|
+
const text5 = `Current model: ${current.displayName} (${current.aliasId})
|
|
11854
|
+
|
|
11855
|
+
Available models:
|
|
11856
|
+
${availableList}
|
|
11857
|
+
|
|
11858
|
+
\u{1F4A1} To switch models, type: .model <id>
|
|
11859
|
+
Example: .model ${exampleId}`;
|
|
11860
|
+
sendMockGeminiResponse(res, text5, isStream);
|
|
11861
|
+
return;
|
|
11862
|
+
}
|
|
11863
|
+
const targetRoute = lookupGeminiRoute(routes, modelCommand);
|
|
11864
|
+
if (targetRoute) {
|
|
11865
|
+
sessionRouteOverride = targetRoute;
|
|
11866
|
+
plog(`.model switch: ${targetRoute.aliasId} (${targetRoute.realModelId})`);
|
|
11867
|
+
sendMockGeminiResponse(res, `\u2705 Switched model to ${targetRoute.displayName} (${targetRoute.aliasId})`, isStream);
|
|
11868
|
+
} else {
|
|
11869
|
+
const available = routes.map((r) => r.aliasId).join(", ");
|
|
11870
|
+
sendMockGeminiResponse(res, `\u274C Model '${modelCommand}' not found.
|
|
11871
|
+
|
|
11872
|
+
Available: ${available}`, isStream);
|
|
11873
|
+
}
|
|
11874
|
+
return;
|
|
11875
|
+
}
|
|
11876
|
+
const route = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
|
|
11877
|
+
plog(`Route selected: ${route.aliasId} (upstream model: ${route.realModelId})`);
|
|
11878
|
+
body.contents = sanitizeModelSwitchTurns(body.contents || []);
|
|
11879
|
+
const languageModel = await getOrInitModel(route);
|
|
11880
|
+
const params = translateGeminiRequest(body);
|
|
11881
|
+
plog(`Translated SDK params:
|
|
11882
|
+
${JSON.stringify(params, null, 2)}`);
|
|
11883
|
+
if (isStream) {
|
|
11884
|
+
res.writeHead(200, {
|
|
11885
|
+
"Content-Type": "text/event-stream",
|
|
11886
|
+
"Cache-Control": "no-cache",
|
|
11887
|
+
"Connection": "keep-alive"
|
|
11888
|
+
});
|
|
11889
|
+
plog("Starting streamText...");
|
|
11890
|
+
const { fullStream } = streamText4({
|
|
11891
|
+
model: languageModel,
|
|
11892
|
+
...params
|
|
11893
|
+
});
|
|
11894
|
+
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
11895
|
+
let isThinking = false;
|
|
11896
|
+
for await (const part of fullStream) {
|
|
11897
|
+
const p21 = part;
|
|
11898
|
+
plog(`Stream chunk type: ${p21.type}`);
|
|
11899
|
+
if (isThinking && (p21.type === "tool-input-start" || p21.type === "tool-call" || p21.type === "finish")) {
|
|
11900
|
+
isThinking = false;
|
|
11901
|
+
const chunk = {
|
|
11902
|
+
candidates: [{ content: { role: "model", parts: [{ text: `
|
|
11903
|
+
</thinking>
|
|
11904
|
+
|
|
11905
|
+
` }] } }]
|
|
11906
|
+
};
|
|
11907
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11908
|
+
|
|
11909
|
+
`);
|
|
11910
|
+
}
|
|
11911
|
+
if (p21.type === "reasoning") {
|
|
11912
|
+
let text5 = p21.textDelta ?? p21.text ?? "";
|
|
11913
|
+
if (!isThinking) {
|
|
11914
|
+
isThinking = true;
|
|
11915
|
+
text5 = `<thinking>
|
|
11916
|
+
` + text5;
|
|
11917
|
+
}
|
|
11918
|
+
const chunk = {
|
|
11919
|
+
candidates: [{ content: { role: "model", parts: [{ text: text5 }] } }]
|
|
11920
|
+
};
|
|
11921
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11922
|
+
|
|
11923
|
+
`);
|
|
11924
|
+
} else if (p21.type === "text-delta") {
|
|
11925
|
+
let text5 = p21.textDelta ?? p21.text ?? "";
|
|
11926
|
+
if (isThinking) {
|
|
11927
|
+
isThinking = false;
|
|
11928
|
+
text5 = `
|
|
11929
|
+
</thinking>
|
|
11930
|
+
|
|
11931
|
+
` + text5;
|
|
11932
|
+
}
|
|
11933
|
+
const chunk = {
|
|
11934
|
+
candidates: [{
|
|
11935
|
+
content: {
|
|
11936
|
+
role: "model",
|
|
11937
|
+
parts: [{ text: text5 }]
|
|
11938
|
+
}
|
|
11939
|
+
}]
|
|
11940
|
+
};
|
|
11941
|
+
const data = `data: ${JSON.stringify(chunk)}
|
|
11942
|
+
|
|
11943
|
+
`;
|
|
11944
|
+
plog(`Streaming text delta: ${p21.textDelta}`);
|
|
11945
|
+
res.write(data);
|
|
11946
|
+
} else if (p21.type === "tool-input-start") {
|
|
11947
|
+
toolCallBuffers.set(p21.toolCallId, { name: p21.toolName, json: "" });
|
|
11948
|
+
} else if (p21.type === "tool-input-delta") {
|
|
11949
|
+
const buf = toolCallBuffers.get(p21.toolCallId);
|
|
11950
|
+
if (buf) buf.json += p21.delta;
|
|
11951
|
+
} else if (p21.type === "tool-call") {
|
|
11952
|
+
const buf = toolCallBuffers.get(p21.toolCallId);
|
|
11953
|
+
const args = buf ? JSON.parse(buf.json || "{}") : p21.input || {};
|
|
11954
|
+
const name = buf ? buf.name : p21.toolName;
|
|
11955
|
+
plog(`Streaming tool call: ${name} with args: ${JSON.stringify(args)}`);
|
|
11956
|
+
const chunk = {
|
|
11957
|
+
candidates: [{
|
|
11958
|
+
content: {
|
|
11959
|
+
role: "model",
|
|
11960
|
+
parts: [{
|
|
11961
|
+
functionCall: { name, args }
|
|
11962
|
+
}]
|
|
11963
|
+
}
|
|
11964
|
+
}]
|
|
11965
|
+
};
|
|
11966
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11967
|
+
|
|
11968
|
+
`);
|
|
11969
|
+
} else if (p21.type === "finish") {
|
|
11970
|
+
const chunk = {
|
|
11971
|
+
candidates: [{
|
|
11972
|
+
finishReason: mapFinishReason(p21.finishReason ?? "")
|
|
11973
|
+
}],
|
|
11974
|
+
usageMetadata: {
|
|
11975
|
+
promptTokenCount: p21.totalUsage?.inputTokens || 0,
|
|
11976
|
+
candidatesTokenCount: p21.totalUsage?.outputTokens || 0
|
|
11977
|
+
}
|
|
11978
|
+
};
|
|
11979
|
+
plog(`Stream finish. Reason: ${p21.finishReason}`);
|
|
11980
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11981
|
+
|
|
11982
|
+
`);
|
|
11983
|
+
}
|
|
11984
|
+
}
|
|
11985
|
+
res.end();
|
|
11986
|
+
plog("Stream ended.");
|
|
11987
|
+
} else {
|
|
11988
|
+
plog("Starting generateText...");
|
|
11989
|
+
const result = await generateText4({
|
|
11990
|
+
model: languageModel,
|
|
11991
|
+
...params
|
|
11992
|
+
});
|
|
11993
|
+
plog("generateText finished.");
|
|
11994
|
+
const parts = [];
|
|
11995
|
+
if (result.reasoning) {
|
|
11996
|
+
parts.push({ text: `<thinking>
|
|
11997
|
+
${result.reasoning}
|
|
11998
|
+
</thinking>
|
|
11999
|
+
|
|
12000
|
+
` });
|
|
12001
|
+
}
|
|
12002
|
+
if (result.text) {
|
|
12003
|
+
parts.push({ text: result.text });
|
|
12004
|
+
}
|
|
12005
|
+
if (result.toolCalls?.length) {
|
|
12006
|
+
for (const tc of result.toolCalls) {
|
|
12007
|
+
parts.push({
|
|
12008
|
+
functionCall: { name: tc.toolName, args: tc.args }
|
|
12009
|
+
});
|
|
12010
|
+
}
|
|
12011
|
+
}
|
|
12012
|
+
const response = {
|
|
12013
|
+
candidates: [{
|
|
12014
|
+
content: {
|
|
12015
|
+
role: "model",
|
|
12016
|
+
parts
|
|
12017
|
+
},
|
|
12018
|
+
finishReason: mapFinishReason(result.finishReason ?? "")
|
|
12019
|
+
}],
|
|
12020
|
+
usageMetadata: {
|
|
12021
|
+
promptTokenCount: result.usage?.inputTokens || 0,
|
|
12022
|
+
candidatesTokenCount: result.usage?.outputTokens || 0
|
|
12023
|
+
}
|
|
12024
|
+
};
|
|
12025
|
+
plog(`Response:
|
|
12026
|
+
${JSON.stringify(response, null, 2)}`);
|
|
12027
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
12028
|
+
res.end(JSON.stringify(response));
|
|
12029
|
+
}
|
|
12030
|
+
return;
|
|
12031
|
+
}
|
|
12032
|
+
plog(`404 Not Found: ${url}`);
|
|
12033
|
+
res.writeHead(404);
|
|
12034
|
+
res.end("Not Found");
|
|
12035
|
+
} catch (err) {
|
|
12036
|
+
plog(`Error handling request: ${err instanceof Error ? err.stack || err.message : String(err)}`);
|
|
12037
|
+
if (debug) {
|
|
12038
|
+
console.error("[Gemini Proxy] Critical error in handler:", err);
|
|
12039
|
+
}
|
|
12040
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12041
|
+
if (!res.headersSent) {
|
|
12042
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
12043
|
+
res.end(JSON.stringify({ error: { message: errMsg } }));
|
|
12044
|
+
} else {
|
|
12045
|
+
try {
|
|
12046
|
+
res.write(`data: ${JSON.stringify({ error: { message: errMsg } })}
|
|
12047
|
+
|
|
12048
|
+
`);
|
|
12049
|
+
} catch {
|
|
12050
|
+
}
|
|
12051
|
+
res.end();
|
|
12052
|
+
}
|
|
12053
|
+
}
|
|
12054
|
+
});
|
|
12055
|
+
process.on("unhandledRejection", onRejection);
|
|
12056
|
+
process.on("uncaughtException", onException);
|
|
12057
|
+
const cleanup = () => {
|
|
12058
|
+
process.off("unhandledRejection", onRejection);
|
|
12059
|
+
process.off("uncaughtException", onException);
|
|
12060
|
+
};
|
|
12061
|
+
return new Promise((resolve, reject2) => {
|
|
12062
|
+
server.on("error", (err) => {
|
|
12063
|
+
cleanup();
|
|
12064
|
+
reject2(err);
|
|
12065
|
+
});
|
|
12066
|
+
server.listen(0, "127.0.0.1", () => {
|
|
12067
|
+
const addr = server.address();
|
|
12068
|
+
if (!addr || typeof addr === "string") {
|
|
12069
|
+
cleanup();
|
|
12070
|
+
reject2(new Error("Failed to bind gemini proxy"));
|
|
12071
|
+
return;
|
|
12072
|
+
}
|
|
12073
|
+
resolve({
|
|
12074
|
+
port: addr.port,
|
|
12075
|
+
token: proxyToken,
|
|
12076
|
+
close: () => {
|
|
12077
|
+
cleanup();
|
|
12078
|
+
server.close();
|
|
12079
|
+
}
|
|
12080
|
+
});
|
|
12081
|
+
});
|
|
12082
|
+
});
|
|
12083
|
+
}
|
|
12084
|
+
function sanitizeModelSwitchTurns(contents) {
|
|
12085
|
+
const cleaned = [];
|
|
12086
|
+
let i = 0;
|
|
12087
|
+
while (i < contents.length) {
|
|
12088
|
+
const turn = contents[i];
|
|
12089
|
+
if (isModelSwitchTurn(turn)) {
|
|
12090
|
+
i += 1;
|
|
12091
|
+
if (i < contents.length && contents[i]?.role === "model") {
|
|
12092
|
+
i += 1;
|
|
12093
|
+
}
|
|
12094
|
+
continue;
|
|
12095
|
+
}
|
|
12096
|
+
cleaned.push(turn);
|
|
12097
|
+
i += 1;
|
|
12098
|
+
}
|
|
12099
|
+
return cleaned;
|
|
12100
|
+
}
|
|
12101
|
+
function isModelSwitchTurn(turn) {
|
|
12102
|
+
if (turn?.role !== "user") return false;
|
|
12103
|
+
const parts = turn.parts || [];
|
|
12104
|
+
if (parts.length === 0) return false;
|
|
12105
|
+
const firstText = parts[0]?.text;
|
|
12106
|
+
if (typeof firstText !== "string") return false;
|
|
12107
|
+
return firstText.trim().startsWith(".model");
|
|
12108
|
+
}
|
|
12109
|
+
function findLastUserTurn(contents) {
|
|
12110
|
+
for (let i = contents.length - 1; i >= 0; i--) {
|
|
12111
|
+
if (contents[i]?.role === "user") return contents[i];
|
|
12112
|
+
}
|
|
12113
|
+
return void 0;
|
|
12114
|
+
}
|
|
12115
|
+
function parseModelCommand(turn) {
|
|
12116
|
+
if (!turn || turn.role !== "user") return null;
|
|
12117
|
+
const parts = turn.parts || [];
|
|
12118
|
+
if (parts.length !== 1) return null;
|
|
12119
|
+
const text5 = parts[0]?.text;
|
|
12120
|
+
if (typeof text5 !== "string") return null;
|
|
12121
|
+
const trimmed = text5.trim();
|
|
12122
|
+
if (!trimmed.startsWith(".model")) return null;
|
|
12123
|
+
if (trimmed === ".model") return "";
|
|
12124
|
+
if (trimmed.charAt(6) !== " ") return null;
|
|
12125
|
+
return trimmed.slice(7).trim();
|
|
12126
|
+
}
|
|
12127
|
+
function sendMockGeminiResponse(res, text5, isStream) {
|
|
12128
|
+
if (isStream) {
|
|
12129
|
+
res.writeHead(200, {
|
|
12130
|
+
"Content-Type": "text/event-stream",
|
|
12131
|
+
"Cache-Control": "no-cache",
|
|
12132
|
+
"Connection": "keep-alive"
|
|
12133
|
+
});
|
|
12134
|
+
const chunk = {
|
|
12135
|
+
candidates: [{
|
|
12136
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
12137
|
+
finishReason: "STOP"
|
|
12138
|
+
}],
|
|
12139
|
+
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
|
|
12140
|
+
};
|
|
12141
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
12142
|
+
|
|
12143
|
+
`);
|
|
12144
|
+
const finishChunk = {
|
|
12145
|
+
candidates: [{ finishReason: "STOP" }],
|
|
12146
|
+
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
|
|
12147
|
+
};
|
|
12148
|
+
res.write(`data: ${JSON.stringify(finishChunk)}
|
|
12149
|
+
|
|
12150
|
+
`);
|
|
12151
|
+
res.end();
|
|
12152
|
+
} else {
|
|
12153
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
12154
|
+
res.end(JSON.stringify({
|
|
12155
|
+
candidates: [{
|
|
12156
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
12157
|
+
finishReason: "STOP"
|
|
12158
|
+
}],
|
|
12159
|
+
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
|
|
12160
|
+
}));
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12163
|
+
|
|
12164
|
+
// src/gemini.ts
|
|
12165
|
+
function geminiHelpText() {
|
|
12166
|
+
return `${pc14.bold("relay-ai gemini")} v${VERSION}
|
|
12167
|
+
Launch Google Gemini CLI with OpenCode Zen / Go or local registry providers.
|
|
12168
|
+
|
|
12169
|
+
${pc14.bold("Usage:")}
|
|
12170
|
+
relay-ai gemini [options] [gemini-flags]
|
|
12171
|
+
relay-ai gemini --help
|
|
12172
|
+
relay-ai gemini --version
|
|
12173
|
+
|
|
12174
|
+
${pc14.bold("Options:")}
|
|
12175
|
+
--trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
12176
|
+
--provider Boot provider id (skip wizard when paired with --model or non-interactive)
|
|
12177
|
+
--model Boot model id (skip wizard when paired with --provider or non-interactive)
|
|
12178
|
+
--help Show this command help
|
|
12179
|
+
--version Show version
|
|
12180
|
+
|
|
12181
|
+
${pc14.bold("Description:")}
|
|
12182
|
+
Picks a provider and model from ~/.relay-ai/providers.json, starts a local Gemini-to-SDK translation
|
|
12183
|
+
proxy, and launches the Gemini CLI.
|
|
12184
|
+
All registry models (Anthropic, OpenAI, custom endpoints, etc.) route through the local translation proxy.
|
|
12185
|
+
|
|
12186
|
+
${pc14.bold("Prerequisites:")}
|
|
12187
|
+
npm install -g @google/gemini-cli
|
|
12188
|
+
|
|
12189
|
+
${pc14.bold("Passing flags to Gemini CLI:")}
|
|
12190
|
+
Add Gemini flags directly \u2014 no "--" separator needed.
|
|
12191
|
+
relay-ai manages -m / --model and -p / --prompt; other flags go to Gemini CLI.
|
|
12192
|
+
|
|
12193
|
+
${pc14.bold("Examples:")}
|
|
12194
|
+
relay-ai gemini
|
|
12195
|
+
relay-ai gemini --trace
|
|
12196
|
+
relay-ai gemini --provider zen --model gemini-2.5-flash
|
|
12197
|
+
relay-ai gemini -p "review this file"`;
|
|
12198
|
+
}
|
|
12199
|
+
async function runGeminiCommand(geminiArgs, trace = false, launch = {}) {
|
|
12200
|
+
if (geminiArgs.includes("--help") || geminiArgs.includes("-h")) {
|
|
12201
|
+
console.log(geminiHelpText());
|
|
12202
|
+
return 0;
|
|
12203
|
+
}
|
|
12204
|
+
const geminiPath = findGeminiBinary();
|
|
12205
|
+
if (!geminiPath) {
|
|
12206
|
+
console.error(pc14.red("\nError: gemini binary not found on PATH.\n"));
|
|
12207
|
+
console.error("Install Google Gemini CLI:");
|
|
12208
|
+
console.error(" npm install -g @google/gemini-cli\n");
|
|
12209
|
+
return 1;
|
|
12210
|
+
}
|
|
12211
|
+
const passthroughArgs = rejectGeminiManagedFlags(geminiArgs);
|
|
12212
|
+
const agentStdout = wantsCleanAgentStdout("gemini", passthroughArgs);
|
|
12213
|
+
setAgentStdoutMode(agentStdout);
|
|
12214
|
+
const prefs = loadPreferences();
|
|
12215
|
+
const launchPlan = planLaunchWizard({
|
|
12216
|
+
explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
|
|
12217
|
+
childArgs: passthroughArgs,
|
|
12218
|
+
agent: "gemini",
|
|
12219
|
+
prefs
|
|
12220
|
+
});
|
|
12221
|
+
if (launchPlan.error) {
|
|
12222
|
+
console.error(pc14.red(`
|
|
12223
|
+
Error: ${launchPlan.error}
|
|
12224
|
+
`));
|
|
12225
|
+
return 1;
|
|
12226
|
+
}
|
|
12227
|
+
let catalog;
|
|
12228
|
+
if (agentStdout) {
|
|
12229
|
+
try {
|
|
12230
|
+
catalog = await fetchProviderCatalog({ agent: "gemini" });
|
|
12231
|
+
} catch (err) {
|
|
12232
|
+
console.error(pc14.red(String(err instanceof Error ? err.message : err)));
|
|
12233
|
+
return 1;
|
|
12234
|
+
}
|
|
12235
|
+
} else {
|
|
12236
|
+
const catalogSpinner = p15.spinner();
|
|
12237
|
+
catalogSpinner.start("Loading your providers...");
|
|
12238
|
+
try {
|
|
12239
|
+
catalog = await fetchProviderCatalog({ agent: "gemini" });
|
|
12240
|
+
} catch (err) {
|
|
12241
|
+
catalogSpinner.stop("");
|
|
12242
|
+
console.error(pc14.red(String(err instanceof Error ? err.message : err)));
|
|
12243
|
+
return 1;
|
|
12244
|
+
}
|
|
12245
|
+
catalogSpinner.stop("");
|
|
12246
|
+
}
|
|
12247
|
+
const compatible = providersForPicker(catalog);
|
|
12248
|
+
if (compatible.length === 0) {
|
|
12249
|
+
p15.log.warn("No Gemini-compatible providers in your registry.");
|
|
12250
|
+
p15.log.info("Add a provider with relay-ai providers add, or sign in with relay-ai providers auth openai.");
|
|
12251
|
+
return 0;
|
|
12252
|
+
}
|
|
12253
|
+
let activeProvider = compatible.find((lp) => lp.id === prefs.lastGeminiProvider) ?? compatible[0];
|
|
12254
|
+
let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastGeminiModel) ?? activeProvider.models[0];
|
|
12255
|
+
if (!selectedModel) {
|
|
12256
|
+
p15.log.error(`Provider "${activeProvider.name}" has no models available.`);
|
|
12257
|
+
return 1;
|
|
12258
|
+
}
|
|
12259
|
+
;
|
|
12260
|
+
if (launchPlan.skip && launchPlan.target) {
|
|
12261
|
+
const resolved = findProviderAndModel(compatible, launchPlan.target);
|
|
12262
|
+
if (!resolved) {
|
|
12263
|
+
p15.log.error(
|
|
12264
|
+
`Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
|
|
12265
|
+
);
|
|
12266
|
+
return 1;
|
|
12267
|
+
}
|
|
12268
|
+
activeProvider = resolved.provider;
|
|
12269
|
+
selectedModel = resolved.model;
|
|
12270
|
+
if (!agentStdout) {
|
|
12271
|
+
p15.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
12272
|
+
}
|
|
12273
|
+
} else {
|
|
12274
|
+
if (!agentStdout) {
|
|
12275
|
+
console.log("");
|
|
12276
|
+
p15.log.info(`Launching ${pc14.bold("Gemini CLI")} with relay-ai`);
|
|
12277
|
+
}
|
|
12278
|
+
const chosenProvider = await pickGeminiProvider(
|
|
12279
|
+
compatible,
|
|
12280
|
+
prefs,
|
|
12281
|
+
(prefs.favoriteModels ?? []).length > 0,
|
|
12282
|
+
launch.launchProvider
|
|
12283
|
+
);
|
|
12284
|
+
if (!chosenProvider) return 0;
|
|
12285
|
+
if (chosenProvider === "__favorites__") {
|
|
12286
|
+
const favPick = await pickGeminiFavoriteModel(compatible, prefs.favoriteModels ?? []);
|
|
12287
|
+
if (!favPick || favPick === "back") return 0;
|
|
12288
|
+
activeProvider = favPick.provider;
|
|
12289
|
+
selectedModel = favPick.model;
|
|
12290
|
+
} else {
|
|
12291
|
+
activeProvider = chosenProvider;
|
|
12292
|
+
const chosenModel = await pickGeminiModel(activeProvider, prefs);
|
|
12293
|
+
if (!chosenModel || chosenModel === "back") return 0;
|
|
12294
|
+
selectedModel = chosenModel;
|
|
12295
|
+
}
|
|
12296
|
+
if (!agentStdout) {
|
|
12297
|
+
const ok = await confirmGeminiLaunch(
|
|
12298
|
+
activeProvider.name,
|
|
12299
|
+
selectedModel.name || selectedModel.id,
|
|
12300
|
+
selectedModel.id
|
|
12301
|
+
);
|
|
12302
|
+
if (!ok) return 0;
|
|
12303
|
+
}
|
|
12304
|
+
}
|
|
12305
|
+
recordLaunchSelection("gemini", activeProvider.id, selectedModel.id, prefs);
|
|
12306
|
+
const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
|
|
12307
|
+
if (!launchApiKey?.trim()) {
|
|
12308
|
+
p15.log.error(
|
|
12309
|
+
`No API key found for ${activeProvider.name}. Set it with relay-ai providers add.`
|
|
12310
|
+
);
|
|
12311
|
+
return 1;
|
|
12312
|
+
}
|
|
12313
|
+
const providerRoutes = activeProvider.models.map((m) => ({
|
|
12314
|
+
aliasId: m.id,
|
|
12315
|
+
realModelId: m.upstreamModelId || m.id,
|
|
12316
|
+
displayName: m.name || m.id,
|
|
12317
|
+
upstreamUrl: m.baseUrl || m.apiBaseUrl || "",
|
|
12318
|
+
apiKey: launchApiKey,
|
|
12319
|
+
modelFormat: m.modelFormat,
|
|
12320
|
+
contextWindow: m.contextWindow,
|
|
12321
|
+
npm: m.npm,
|
|
12322
|
+
baseURL: m.apiBaseUrl,
|
|
12323
|
+
providerId: activeProvider.id,
|
|
12324
|
+
authType: activeProvider.authType,
|
|
12325
|
+
oauthAccountId: activeProvider.oauthAccountId,
|
|
12326
|
+
supportedParameters: m.supportedParameters,
|
|
12327
|
+
reasoning: m.reasoning,
|
|
12328
|
+
interleavedReasoningField: m.interleavedReasoningField
|
|
12329
|
+
}));
|
|
12330
|
+
const resolvedFavs = [];
|
|
12331
|
+
const favorites = prefs.favoriteModels ?? [];
|
|
12332
|
+
for (const fav of favorites) {
|
|
12333
|
+
const provider = compatible.find((lp) => lp.id === fav.providerId);
|
|
12334
|
+
const model = provider?.models.find((m) => m.id === fav.modelId);
|
|
12335
|
+
if (provider && model) {
|
|
12336
|
+
const apiKey = await resolveLocalProviderApiKey(provider);
|
|
12337
|
+
if (apiKey) {
|
|
12338
|
+
resolvedFavs.push({
|
|
12339
|
+
aliasId: model.id,
|
|
12340
|
+
realModelId: model.upstreamModelId || model.id,
|
|
12341
|
+
displayName: model.name || model.id,
|
|
12342
|
+
upstreamUrl: model.baseUrl || model.apiBaseUrl || "",
|
|
12343
|
+
apiKey,
|
|
12344
|
+
modelFormat: model.modelFormat,
|
|
12345
|
+
contextWindow: model.contextWindow,
|
|
12346
|
+
npm: model.npm,
|
|
12347
|
+
baseURL: model.apiBaseUrl,
|
|
12348
|
+
providerId: provider.id,
|
|
12349
|
+
authType: provider.authType,
|
|
12350
|
+
oauthAccountId: provider.oauthAccountId,
|
|
12351
|
+
supportedParameters: model.supportedParameters,
|
|
12352
|
+
reasoning: model.reasoning,
|
|
12353
|
+
interleavedReasoningField: model.interleavedReasoningField
|
|
12354
|
+
});
|
|
12355
|
+
}
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
12358
|
+
const routesMap = /* @__PURE__ */ new Map();
|
|
12359
|
+
for (const route of providerRoutes) {
|
|
12360
|
+
routesMap.set(route.aliasId, route);
|
|
12361
|
+
}
|
|
12362
|
+
for (const route of resolvedFavs) {
|
|
12363
|
+
if (!routesMap.has(route.aliasId)) {
|
|
12364
|
+
routesMap.set(route.aliasId, route);
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
12367
|
+
const startingRoute = routesMap.get(selectedModel.id);
|
|
12368
|
+
if (!startingRoute) {
|
|
12369
|
+
routesMap.set(selectedModel.id, {
|
|
12370
|
+
aliasId: selectedModel.id,
|
|
12371
|
+
realModelId: selectedModel.upstreamModelId || selectedModel.id,
|
|
12372
|
+
displayName: selectedModel.name || selectedModel.id,
|
|
12373
|
+
upstreamUrl: selectedModel.baseUrl || selectedModel.apiBaseUrl || "",
|
|
12374
|
+
apiKey: launchApiKey,
|
|
12375
|
+
modelFormat: selectedModel.modelFormat,
|
|
12376
|
+
contextWindow: selectedModel.contextWindow,
|
|
12377
|
+
npm: selectedModel.npm,
|
|
12378
|
+
baseURL: selectedModel.apiBaseUrl,
|
|
12379
|
+
providerId: activeProvider.id,
|
|
12380
|
+
authType: activeProvider.authType,
|
|
12381
|
+
oauthAccountId: activeProvider.oauthAccountId,
|
|
12382
|
+
supportedParameters: selectedModel.supportedParameters,
|
|
12383
|
+
reasoning: selectedModel.reasoning,
|
|
12384
|
+
interleavedReasoningField: selectedModel.interleavedReasoningField
|
|
12385
|
+
});
|
|
12386
|
+
}
|
|
12387
|
+
const finalRoutes = [...routesMap.values()];
|
|
12388
|
+
let proxyHandle = null;
|
|
12389
|
+
try {
|
|
12390
|
+
proxyHandle = await startGeminiProxy(finalRoutes, trace);
|
|
12391
|
+
} catch (err) {
|
|
12392
|
+
p15.log.error(`Failed to start Gemini proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
12393
|
+
return 1;
|
|
12394
|
+
}
|
|
12395
|
+
const childEnv = buildGeminiChildEnv(proxyHandle.port, proxyHandle.token);
|
|
12396
|
+
if (!agentStdout) {
|
|
12397
|
+
p15.log.info(`Gemini proxy started on port ${proxyHandle.port}`);
|
|
12398
|
+
p15.log.info(`\u{1F4A1} Type ${pc14.bold(".model <id>")} in the chat to switch models mid-session.`);
|
|
12399
|
+
}
|
|
12400
|
+
const exitCode = await launchGemini(geminiPath, selectedModel.id, childEnv, passthroughArgs);
|
|
12401
|
+
proxyHandle.close();
|
|
12402
|
+
if (!agentStdout) {
|
|
12403
|
+
p15.log.info("Gemini proxy stopped.");
|
|
12404
|
+
}
|
|
12405
|
+
if (trace) {
|
|
12406
|
+
printTraceLog(getGeminiProxyDebugLogPath());
|
|
12407
|
+
}
|
|
12408
|
+
return exitCode;
|
|
12409
|
+
}
|
|
12410
|
+
|
|
12411
|
+
// src/codex-app.ts
|
|
12412
|
+
import pc15 from "picocolors";
|
|
12413
|
+
import * as p17 from "@clack/prompts";
|
|
12414
|
+
|
|
12415
|
+
// src/codex/app-config.ts
|
|
12416
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
|
|
12417
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
12418
|
+
import { parse, stringify } from "smol-toml";
|
|
12419
|
+
function getCodexConfigPath() {
|
|
12420
|
+
return join15(getCodexHome(), "config.toml");
|
|
12421
|
+
}
|
|
12422
|
+
function getCodexAppSidecarProfilePath() {
|
|
12423
|
+
return join15(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
|
|
12424
|
+
}
|
|
12425
|
+
function asRecord(value) {
|
|
12426
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
12427
|
+
}
|
|
12428
|
+
function rootString(config, key) {
|
|
12429
|
+
if (!(key in config)) return { had: false, value: "" };
|
|
12430
|
+
const v = config[key];
|
|
12431
|
+
return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
|
|
12432
|
+
}
|
|
12433
|
+
function readCodexConfigText(path = getCodexConfigPath()) {
|
|
12434
|
+
if (!existsSync14(path)) return "";
|
|
12435
|
+
return readFileSync11(path, "utf8");
|
|
12436
|
+
}
|
|
12437
|
+
function parseCodexConfig(text5) {
|
|
12438
|
+
if (!text5.trim()) return {};
|
|
12439
|
+
return asRecord(parse(text5));
|
|
12440
|
+
}
|
|
12441
|
+
function captureRestoreState(text5) {
|
|
12442
|
+
const config = parseCodexConfig(text5);
|
|
12443
|
+
const profile = rootString(config, "profile");
|
|
12444
|
+
const model = rootString(config, "model");
|
|
12445
|
+
const modelProvider = rootString(config, "model_provider");
|
|
12446
|
+
const modelCatalog = rootString(config, "model_catalog_json");
|
|
12447
|
+
const openAIBaseUrl = rootString(config, "openai_base_url");
|
|
12448
|
+
const reasoning = rootString(config, "model_reasoning_effort");
|
|
12449
|
+
return {
|
|
12450
|
+
hadProfile: profile.had,
|
|
12451
|
+
profile: profile.value,
|
|
12452
|
+
hadModel: model.had,
|
|
12453
|
+
model: model.value,
|
|
12454
|
+
hadModelProvider: modelProvider.had,
|
|
12455
|
+
modelProvider: modelProvider.value,
|
|
12456
|
+
hadModelCatalogJson: modelCatalog.had,
|
|
12457
|
+
modelCatalogJson: modelCatalog.value,
|
|
12458
|
+
hadOpenAIBaseUrl: openAIBaseUrl.had,
|
|
12459
|
+
openAIBaseUrl: openAIBaseUrl.value,
|
|
12460
|
+
hadModelReasoningEffort: reasoning.had,
|
|
12461
|
+
modelReasoningEffort: reasoning.value
|
|
12462
|
+
};
|
|
12463
|
+
}
|
|
12464
|
+
function isAppManagedConfig(text5) {
|
|
12465
|
+
const config = parseCodexConfig(text5);
|
|
12466
|
+
const mp = rootString(config, "model_provider");
|
|
12467
|
+
if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
|
|
12468
|
+
const baseUrl = rootString(config, "openai_base_url");
|
|
12469
|
+
const catalog = rootString(config, "model_catalog_json");
|
|
12470
|
+
return mp.value === "openai" && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
|
|
12471
|
+
}
|
|
12472
|
+
function mergeAppConfig(existing, spec) {
|
|
12473
|
+
const patch = buildCodexAppRootConfig(spec);
|
|
12474
|
+
const out = { ...existing };
|
|
12475
|
+
delete out.profile;
|
|
12476
|
+
out.model = patch.model;
|
|
12477
|
+
out.model_provider = patch.model_provider;
|
|
12478
|
+
out.openai_base_url = patch.openai_base_url;
|
|
12479
|
+
out.model_catalog_json = patch.model_catalog_json;
|
|
12480
|
+
const providers = asRecord(out.model_providers);
|
|
12481
|
+
delete providers[CODEX_APP_PROVIDER_ID];
|
|
12482
|
+
const profiles = asRecord(out.profiles);
|
|
12483
|
+
delete profiles[CODEX_APP_PROVIDER_ID];
|
|
12484
|
+
if (Object.keys(profiles).length === 0) {
|
|
12485
|
+
delete out.profiles;
|
|
12486
|
+
} else {
|
|
12487
|
+
out.profiles = profiles;
|
|
12488
|
+
}
|
|
12489
|
+
if (Object.keys(providers).length === 0) {
|
|
12490
|
+
delete out.model_providers;
|
|
12491
|
+
} else {
|
|
12492
|
+
out.model_providers = providers;
|
|
12493
|
+
}
|
|
12494
|
+
const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
|
|
12495
|
+
if (existingEffort !== void 0) {
|
|
12496
|
+
const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
|
|
12497
|
+
providerId: spec.route.providerId,
|
|
12498
|
+
apiBaseUrl: spec.route.baseURL,
|
|
12499
|
+
supportedParameters: spec.route.supportedParameters,
|
|
12500
|
+
reasoning: spec.route.reasoning,
|
|
12501
|
+
interleavedReasoningField: spec.route.interleavedReasoningField
|
|
12502
|
+
});
|
|
12503
|
+
if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
|
|
12504
|
+
if (caps.levels.length > 0 && caps.defaultLevel) {
|
|
12505
|
+
out.model_reasoning_effort = caps.defaultLevel;
|
|
12506
|
+
} else {
|
|
12507
|
+
delete out.model_reasoning_effort;
|
|
12508
|
+
}
|
|
12509
|
+
}
|
|
12510
|
+
}
|
|
12511
|
+
return out;
|
|
12512
|
+
}
|
|
12513
|
+
function validateAppConfigText(text5, spec) {
|
|
12514
|
+
const config = parseCodexConfig(text5);
|
|
12515
|
+
if ("profile" in config) {
|
|
12516
|
+
throw new Error("Generated config still contains legacy root profile key");
|
|
12517
|
+
}
|
|
12518
|
+
const profiles = asRecord(config.profiles);
|
|
12519
|
+
if (profiles[CODEX_APP_PROVIDER_ID]) {
|
|
12520
|
+
throw new Error("Generated config still contains legacy profiles table");
|
|
12521
|
+
}
|
|
12522
|
+
const mp = rootString(config, "model_provider");
|
|
12523
|
+
if (mp.value !== "openai") {
|
|
12524
|
+
throw new Error("Generated config must keep the built-in OpenAI model_provider");
|
|
11356
12525
|
}
|
|
11357
12526
|
const baseUrl = rootString(config, "openai_base_url");
|
|
11358
12527
|
if (baseUrl.value !== `http://127.0.0.1:${spec.proxyPort}/v1`) {
|
|
@@ -11409,13 +12578,13 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
11409
12578
|
}
|
|
11410
12579
|
applyRestoreKey(config, "model_reasoning_effort", state.hadModelReasoningEffort, state.modelReasoningEffort);
|
|
11411
12580
|
const sidecar = getCodexAppSidecarProfilePath();
|
|
11412
|
-
if (
|
|
12581
|
+
if (existsSync14(sidecar)) {
|
|
11413
12582
|
try {
|
|
11414
12583
|
rmSync2(sidecar, { force: true });
|
|
11415
12584
|
} catch {
|
|
11416
12585
|
}
|
|
11417
12586
|
}
|
|
11418
|
-
const hadFile =
|
|
12587
|
+
const hadFile = existsSync14(configPath);
|
|
11419
12588
|
const empty = Object.keys(config).length === 0 || Object.keys(config).length === 1 && "model_providers" in config && Object.keys(asRecord(config.model_providers)).length === 0;
|
|
11420
12589
|
if (!hadFile && empty) return false;
|
|
11421
12590
|
if (empty) {
|
|
@@ -11436,25 +12605,25 @@ function previewAppConfigToml(spec) {
|
|
|
11436
12605
|
// src/codex/app-session.ts
|
|
11437
12606
|
import {
|
|
11438
12607
|
copyFileSync as copyFileSync4,
|
|
11439
|
-
existsSync as
|
|
12608
|
+
existsSync as existsSync15,
|
|
11440
12609
|
mkdirSync as mkdirSync8,
|
|
11441
12610
|
readdirSync as readdirSync2,
|
|
11442
12611
|
readFileSync as readFileSync12,
|
|
11443
12612
|
rmSync as rmSync3
|
|
11444
12613
|
} from "fs";
|
|
11445
|
-
import { basename as basename2, join as
|
|
12614
|
+
import { basename as basename2, join as join16 } from "path";
|
|
11446
12615
|
function getAppSessionLockPath(env = process.env) {
|
|
11447
|
-
return
|
|
12616
|
+
return join16(getRelayAiCodexDir(env), "session-app.json");
|
|
11448
12617
|
}
|
|
11449
12618
|
function getAppRestoreStatePath(env = process.env) {
|
|
11450
|
-
return
|
|
12619
|
+
return join16(getRelayAiCodexDir(env), "app-restore-state.json");
|
|
11451
12620
|
}
|
|
11452
12621
|
function getAppCatalogPath(providerId, env = process.env) {
|
|
11453
|
-
return
|
|
12622
|
+
return join16(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
|
|
11454
12623
|
}
|
|
11455
12624
|
function readAppSessionLock(env = process.env) {
|
|
11456
12625
|
const path = getAppSessionLockPath(env);
|
|
11457
|
-
if (!
|
|
12626
|
+
if (!existsSync15(path)) return null;
|
|
11458
12627
|
try {
|
|
11459
12628
|
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
11460
12629
|
if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
|
|
@@ -11468,11 +12637,11 @@ function writeAppSessionLock(lock, env = process.env) {
|
|
|
11468
12637
|
}
|
|
11469
12638
|
function clearAppSessionLock(env = process.env) {
|
|
11470
12639
|
const path = getAppSessionLockPath(env);
|
|
11471
|
-
if (
|
|
12640
|
+
if (existsSync15(path)) rmSync3(path, { force: true });
|
|
11472
12641
|
}
|
|
11473
12642
|
function readAppRestoreState(env = process.env) {
|
|
11474
12643
|
const path = getAppRestoreStatePath(env);
|
|
11475
|
-
if (!
|
|
12644
|
+
if (!existsSync15(path)) return null;
|
|
11476
12645
|
try {
|
|
11477
12646
|
return JSON.parse(readFileSync12(path, "utf8"));
|
|
11478
12647
|
} catch {
|
|
@@ -11486,16 +12655,16 @@ function writeAppRestoreState(state, env = process.env) {
|
|
|
11486
12655
|
}
|
|
11487
12656
|
function clearAppRestoreState(env = process.env) {
|
|
11488
12657
|
const path = getAppRestoreStatePath(env);
|
|
11489
|
-
if (
|
|
12658
|
+
if (existsSync15(path)) rmSync3(path, { force: true });
|
|
11490
12659
|
}
|
|
11491
12660
|
function backupConfigToml(env = process.env) {
|
|
11492
12661
|
const configPath = getCodexConfigPath();
|
|
11493
|
-
if (!
|
|
12662
|
+
if (!existsSync15(configPath)) return void 0;
|
|
11494
12663
|
rotateBackups(configPath, env);
|
|
11495
12664
|
const backupsDir = getBackupsDir(env);
|
|
11496
12665
|
mkdirSync8(backupsDir, { recursive: true });
|
|
11497
12666
|
const base = basename2(configPath);
|
|
11498
|
-
const backupPath =
|
|
12667
|
+
const backupPath = join16(backupsDir, `${base}.${Date.now()}.bak`);
|
|
11499
12668
|
copyFileSync4(configPath, backupPath);
|
|
11500
12669
|
return backupPath;
|
|
11501
12670
|
}
|
|
@@ -11511,8 +12680,8 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
|
|
|
11511
12680
|
}
|
|
11512
12681
|
function ownedAppCatalogPaths(env = process.env) {
|
|
11513
12682
|
const codexDir = getRelayAiCodexDir(env);
|
|
11514
|
-
if (!
|
|
11515
|
-
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) =>
|
|
12683
|
+
if (!existsSync15(codexDir)) return [];
|
|
12684
|
+
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join16(codexDir, n));
|
|
11516
12685
|
}
|
|
11517
12686
|
function removeAppCatalogs(env = process.env) {
|
|
11518
12687
|
const removed = [];
|
|
@@ -11544,7 +12713,7 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
11544
12713
|
}
|
|
11545
12714
|
if (restoreState) {
|
|
11546
12715
|
restoreConfigFromState(restoreState);
|
|
11547
|
-
} else if (lock?.backupPath &&
|
|
12716
|
+
} else if (lock?.backupPath && existsSync15(lock.backupPath)) {
|
|
11548
12717
|
copyFileSync4(lock.backupPath, getCodexConfigPath());
|
|
11549
12718
|
}
|
|
11550
12719
|
removeAppCatalogs(env);
|
|
@@ -11590,11 +12759,11 @@ function waitForShutdown2() {
|
|
|
11590
12759
|
}
|
|
11591
12760
|
|
|
11592
12761
|
// src/codex/app-launch.ts
|
|
11593
|
-
import { execSync as
|
|
11594
|
-
import { existsSync as
|
|
11595
|
-
import { homedir as
|
|
11596
|
-
import { join as
|
|
11597
|
-
import * as
|
|
12762
|
+
import { execSync as execSync5 } from "child_process";
|
|
12763
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
12764
|
+
import { homedir as homedir11 } from "os";
|
|
12765
|
+
import { join as join17 } from "path";
|
|
12766
|
+
import * as p16 from "@clack/prompts";
|
|
11598
12767
|
var CODEX_BUNDLE_ID = "com.openai.codex";
|
|
11599
12768
|
function codexAppSupported() {
|
|
11600
12769
|
if (process.platform !== "darwin" && process.platform !== "win32") {
|
|
@@ -11602,7 +12771,7 @@ function codexAppSupported() {
|
|
|
11602
12771
|
}
|
|
11603
12772
|
}
|
|
11604
12773
|
function run(cmd, encoding = "utf8") {
|
|
11605
|
-
return
|
|
12774
|
+
return execSync5(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
11606
12775
|
}
|
|
11607
12776
|
function runPowerShell(script) {
|
|
11608
12777
|
return run(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
|
|
@@ -11610,30 +12779,30 @@ function runPowerShell(script) {
|
|
|
11610
12779
|
function darwinAppCandidates() {
|
|
11611
12780
|
return [
|
|
11612
12781
|
"/Applications/Codex.app",
|
|
11613
|
-
|
|
12782
|
+
join17(homedir11(), "Applications", "Codex.app")
|
|
11614
12783
|
];
|
|
11615
12784
|
}
|
|
11616
12785
|
function winLocalAppData() {
|
|
11617
|
-
return process.env.LOCALAPPDATA ??
|
|
12786
|
+
return process.env.LOCALAPPDATA ?? join17(homedir11(), "AppData", "Local");
|
|
11618
12787
|
}
|
|
11619
12788
|
function winCodexExeCandidates() {
|
|
11620
12789
|
const local = winLocalAppData();
|
|
11621
12790
|
const bases = [
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
12791
|
+
join17(local, "Programs", "Codex"),
|
|
12792
|
+
join17(local, "Programs", "OpenAI Codex"),
|
|
12793
|
+
join17(local, "Codex"),
|
|
12794
|
+
join17(local, "OpenAI Codex"),
|
|
12795
|
+
join17(local, "OpenAI", "Codex"),
|
|
12796
|
+
join17(local, "openai-codex-electron")
|
|
11628
12797
|
];
|
|
11629
12798
|
const out = [];
|
|
11630
12799
|
for (const base of bases) {
|
|
11631
|
-
out.push(
|
|
12800
|
+
out.push(join17(base, "Codex.exe"));
|
|
11632
12801
|
try {
|
|
11633
|
-
if (
|
|
12802
|
+
if (existsSync16(base)) {
|
|
11634
12803
|
for (const name of readdirSync3(base)) {
|
|
11635
12804
|
if (name.startsWith("app-")) {
|
|
11636
|
-
out.push(
|
|
12805
|
+
out.push(join17(base, name, "Codex.exe"));
|
|
11637
12806
|
}
|
|
11638
12807
|
}
|
|
11639
12808
|
}
|
|
@@ -11646,7 +12815,7 @@ function mdfindCodexApp() {
|
|
|
11646
12815
|
try {
|
|
11647
12816
|
const out = run(`mdfind "kMDItemCFBundleIdentifier == '${CODEX_BUNDLE_ID}'"`);
|
|
11648
12817
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
11649
|
-
return first &&
|
|
12818
|
+
return first && existsSync16(first) ? first : null;
|
|
11650
12819
|
} catch {
|
|
11651
12820
|
return null;
|
|
11652
12821
|
}
|
|
@@ -11654,14 +12823,14 @@ function mdfindCodexApp() {
|
|
|
11654
12823
|
function findCodexApp() {
|
|
11655
12824
|
if (process.platform === "darwin") {
|
|
11656
12825
|
for (const path of darwinAppCandidates()) {
|
|
11657
|
-
if (
|
|
12826
|
+
if (existsSync16(path)) return path;
|
|
11658
12827
|
}
|
|
11659
12828
|
return mdfindCodexApp();
|
|
11660
12829
|
}
|
|
11661
12830
|
if (process.platform === "win32") {
|
|
11662
12831
|
for (const path of winCodexExeCandidates()) {
|
|
11663
12832
|
try {
|
|
11664
|
-
if (
|
|
12833
|
+
if (existsSync16(path) && statSync4(path).isFile()) return path;
|
|
11665
12834
|
} catch {
|
|
11666
12835
|
}
|
|
11667
12836
|
}
|
|
@@ -11725,9 +12894,9 @@ async function waitForQuit(timeoutMs) {
|
|
|
11725
12894
|
function openCodexAppAt(path) {
|
|
11726
12895
|
if (process.platform === "darwin") {
|
|
11727
12896
|
if (path.endsWith(".app")) {
|
|
11728
|
-
|
|
12897
|
+
execSync5(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
|
|
11729
12898
|
} else {
|
|
11730
|
-
|
|
12899
|
+
execSync5(`open -b ${CODEX_BUNDLE_ID}`, { stdio: "inherit" });
|
|
11731
12900
|
}
|
|
11732
12901
|
return;
|
|
11733
12902
|
}
|
|
@@ -11750,9 +12919,9 @@ function openCodexApp() {
|
|
|
11750
12919
|
}
|
|
11751
12920
|
function darwinQuit() {
|
|
11752
12921
|
try {
|
|
11753
|
-
|
|
12922
|
+
execSync5(`osascript -e 'tell application "Codex" to quit'`, { stdio: "pipe" });
|
|
11754
12923
|
} catch {
|
|
11755
|
-
|
|
12924
|
+
execSync5(`osascript -e 'tell application id "${CODEX_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
|
|
11756
12925
|
}
|
|
11757
12926
|
}
|
|
11758
12927
|
function winQuitGraceful() {
|
|
@@ -11780,9 +12949,9 @@ async function launchOrRestartCodexApp(prompt = "Restart Codex to apply relay-ai
|
|
|
11780
12949
|
openCodexAppAt(appPath);
|
|
11781
12950
|
return;
|
|
11782
12951
|
}
|
|
11783
|
-
const restart = await
|
|
11784
|
-
if (
|
|
11785
|
-
|
|
12952
|
+
const restart = await p16.confirm({ message: prompt, initialValue: true });
|
|
12953
|
+
if (p16.isCancel(restart) || !restart) {
|
|
12954
|
+
p16.log.info("Quit and reopen Codex when you are ready for the new model to take effect.");
|
|
11786
12955
|
return;
|
|
11787
12956
|
}
|
|
11788
12957
|
if (process.platform === "darwin") darwinQuit();
|
|
@@ -11800,9 +12969,9 @@ function codexAppInstallHint() {
|
|
|
11800
12969
|
|
|
11801
12970
|
// src/codex-app.ts
|
|
11802
12971
|
function codexAppHelpText() {
|
|
11803
|
-
return `${
|
|
12972
|
+
return `${pc15.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
|
|
11804
12973
|
|
|
11805
|
-
${
|
|
12974
|
+
${pc15.bold("Usage:")}
|
|
11806
12975
|
relay-ai codex-app [options]
|
|
11807
12976
|
relay-ai codex-app --vertex
|
|
11808
12977
|
relay-ai codex-app --restore
|
|
@@ -11810,7 +12979,7 @@ ${pc14.bold("Usage:")}
|
|
|
11810
12979
|
relay-ai codex-app --help
|
|
11811
12980
|
relay-ai codex-app --version
|
|
11812
12981
|
|
|
11813
|
-
${
|
|
12982
|
+
${pc15.bold("Options:")}
|
|
11814
12983
|
--vertex Use Claude models through Google Vertex AI
|
|
11815
12984
|
--restore Restore Codex config after an interrupted app session
|
|
11816
12985
|
--config Preview the generated Codex app configuration without launching
|
|
@@ -11818,31 +12987,31 @@ ${pc14.bold("Options:")}
|
|
|
11818
12987
|
--help Show this command help
|
|
11819
12988
|
--version Show version
|
|
11820
12989
|
|
|
11821
|
-
${
|
|
12990
|
+
${pc15.bold("Description:")}
|
|
11822
12991
|
Picks a provider and model from ~/.relay-ai/providers.json, patches ~/.codex/config.toml
|
|
11823
12992
|
(with backup + restore on Ctrl+C), starts a local Responses proxy, and opens the
|
|
11824
12993
|
Codex desktop app. Keep this terminal open while using Codex.
|
|
11825
12994
|
|
|
11826
|
-
${
|
|
12995
|
+
${pc15.bold("Platforms:")}
|
|
11827
12996
|
macOS and Windows. Linux is not supported (no Codex desktop app).
|
|
11828
12997
|
|
|
11829
|
-
${
|
|
12998
|
+
${pc15.bold("Cleanup:")}
|
|
11830
12999
|
Ctrl+C stops the proxy and restores your previous Codex config.
|
|
11831
13000
|
After crash: relay-ai codex-app --restore
|
|
11832
13001
|
|
|
11833
|
-
${
|
|
13002
|
+
${pc15.bold("Preview (no writes):")}
|
|
11834
13003
|
relay-ai codex-app --config
|
|
11835
13004
|
|
|
11836
13005
|
See docs/CODEX.md for CLI vs app, files touched, and restore.
|
|
11837
13006
|
|
|
11838
|
-
${
|
|
13007
|
+
${pc15.bold("Examples:")}
|
|
11839
13008
|
relay-ai codex-app
|
|
11840
13009
|
relay-ai codex-app --vertex
|
|
11841
13010
|
relay-ai codex-app --config
|
|
11842
13011
|
relay-ai codex-app --restore
|
|
11843
13012
|
|
|
11844
|
-
${
|
|
11845
|
-
When you have saved favorites via ${
|
|
13013
|
+
${pc15.bold("Favorites:")}
|
|
13014
|
+
When you have saved favorites via ${pc15.cyan("relay-ai models")}, the Codex App
|
|
11846
13015
|
picker will show your starting model + favorites for mid-session switching.
|
|
11847
13016
|
Zen/Go favorites are included when an OpenCode API key is available.`;
|
|
11848
13017
|
}
|
|
@@ -11864,26 +13033,26 @@ function vertexEntryToLocalModel2(entry) {
|
|
|
11864
13033
|
}
|
|
11865
13034
|
async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
11866
13035
|
if (!hasApplicationDefaultCredentials()) {
|
|
11867
|
-
|
|
11868
|
-
|
|
13036
|
+
p17.log.error("Google Application Default Credentials not found.");
|
|
13037
|
+
p17.log.info("Run: gcloud auth application-default login");
|
|
11869
13038
|
return 1;
|
|
11870
13039
|
}
|
|
11871
13040
|
const config = buildVertexRuntimeConfig();
|
|
11872
13041
|
if (!config) {
|
|
11873
|
-
|
|
11874
|
-
|
|
13042
|
+
p17.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
|
|
13043
|
+
p17.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
|
|
11875
13044
|
return 1;
|
|
11876
13045
|
}
|
|
11877
13046
|
let selectedEntry;
|
|
11878
13047
|
if (config.models.length === 1) {
|
|
11879
13048
|
selectedEntry = config.models[0];
|
|
11880
13049
|
} else {
|
|
11881
|
-
const choice = await
|
|
13050
|
+
const choice = await p17.select({
|
|
11882
13051
|
message: "Select a starting Vertex AI model:",
|
|
11883
13052
|
options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
|
|
11884
13053
|
});
|
|
11885
|
-
if (
|
|
11886
|
-
|
|
13054
|
+
if (p17.isCancel(choice)) {
|
|
13055
|
+
p17.cancel("Cancelled.");
|
|
11887
13056
|
return 0;
|
|
11888
13057
|
}
|
|
11889
13058
|
selectedEntry = choice;
|
|
@@ -11905,19 +13074,19 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11905
13074
|
const home = process.env["HOME"] ?? "";
|
|
11906
13075
|
const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
|
|
11907
13076
|
console.log("");
|
|
11908
|
-
console.log(
|
|
13077
|
+
console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
|
|
11909
13078
|
console.log("");
|
|
11910
|
-
console.log(` ${
|
|
11911
|
-
console.log(` ${
|
|
11912
|
-
console.log(` ${
|
|
11913
|
-
console.log(` ${
|
|
11914
|
-
console.log(` ${
|
|
13079
|
+
console.log(` ${pc15.bold("Mode:")} Vertex AI`);
|
|
13080
|
+
console.log(` ${pc15.bold("Project:")} ${config.project}`);
|
|
13081
|
+
console.log(` ${pc15.bold("Location:")} ${config.location}`);
|
|
13082
|
+
console.log(` ${pc15.bold("Model:")} ${selectedEntry.display_name}`);
|
|
13083
|
+
console.log(` ${pc15.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
|
|
11915
13084
|
console.log("");
|
|
11916
|
-
console.log(` ${
|
|
11917
|
-
console.log(` ${
|
|
13085
|
+
console.log(` ${pc15.bold("Catalog file:")}`);
|
|
13086
|
+
console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
|
|
11918
13087
|
console.log("");
|
|
11919
|
-
console.log(
|
|
11920
|
-
console.log(
|
|
13088
|
+
console.log(pc15.dim(" No app was launched."));
|
|
13089
|
+
console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app --vertex") + pc15.dim(" to launch."));
|
|
11921
13090
|
console.log("");
|
|
11922
13091
|
return 0;
|
|
11923
13092
|
}
|
|
@@ -11956,14 +13125,14 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11956
13125
|
proxyPort
|
|
11957
13126
|
});
|
|
11958
13127
|
sessionActive = true;
|
|
11959
|
-
|
|
13128
|
+
p17.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
|
|
11960
13129
|
logProxy(proxyPort);
|
|
11961
13130
|
logActiveModel(selectedEntry.display_name, selectedEntry.id);
|
|
11962
13131
|
try {
|
|
11963
13132
|
await launchOrRestartCodexApp();
|
|
11964
13133
|
} catch (err) {
|
|
11965
|
-
|
|
11966
|
-
|
|
13134
|
+
p17.log.warn(String(err instanceof Error ? err.message : err));
|
|
13135
|
+
p17.log.info(codexAppInstallHint());
|
|
11967
13136
|
}
|
|
11968
13137
|
printCodexAppSessionPanel({
|
|
11969
13138
|
modelLabel: selectedEntry.display_name,
|
|
@@ -11979,8 +13148,8 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11979
13148
|
sessionActive = false;
|
|
11980
13149
|
}
|
|
11981
13150
|
if (isCodexAppRunning()) {
|
|
11982
|
-
const shouldClose = await
|
|
11983
|
-
if (shouldClose && !
|
|
13151
|
+
const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
|
|
13152
|
+
if (shouldClose && !p17.isCancel(shouldClose)) {
|
|
11984
13153
|
quitCodexAppGracefully();
|
|
11985
13154
|
}
|
|
11986
13155
|
}
|
|
@@ -12003,7 +13172,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12003
13172
|
try {
|
|
12004
13173
|
codexAppSupported();
|
|
12005
13174
|
} catch (err) {
|
|
12006
|
-
console.error(
|
|
13175
|
+
console.error(pc15.red(String(err instanceof Error ? err.message : err)));
|
|
12007
13176
|
return 1;
|
|
12008
13177
|
}
|
|
12009
13178
|
const interrupted = recoverInterruptedCodexAppSession();
|
|
@@ -12011,17 +13180,17 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12011
13180
|
const trace = args.includes("--trace");
|
|
12012
13181
|
const debugLogPath = getCodexProxyDebugLogPath();
|
|
12013
13182
|
if (trace && !configOnly) {
|
|
12014
|
-
|
|
13183
|
+
p17.log.info(`Debug log: ${debugLogPath}`);
|
|
12015
13184
|
}
|
|
12016
13185
|
const isTty = Boolean(process.stdin.isTTY);
|
|
12017
13186
|
if (!configOnly) {
|
|
12018
13187
|
const sessionCheck = checkAppSessionLock(isTty);
|
|
12019
13188
|
if (!sessionCheck.ok) {
|
|
12020
13189
|
if (sessionCheck.reason === "non_tty") {
|
|
12021
|
-
console.error(
|
|
13190
|
+
console.error(pc15.red("relay-ai codex-app requires an interactive terminal."));
|
|
12022
13191
|
return 1;
|
|
12023
13192
|
}
|
|
12024
|
-
console.error(
|
|
13193
|
+
console.error(pc15.yellow(`Another relay-ai codex-app session may be running (pid ${sessionCheck.lock.pid}).`));
|
|
12025
13194
|
console.error("Stop it with Ctrl+C in that terminal, or run relay-ai codex-app --restore after it exits.");
|
|
12026
13195
|
return 1;
|
|
12027
13196
|
}
|
|
@@ -12029,28 +13198,28 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12029
13198
|
if (!configOnly) {
|
|
12030
13199
|
codexAppIntro();
|
|
12031
13200
|
if (interrupted.recovered) {
|
|
12032
|
-
|
|
13201
|
+
p17.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
|
|
12033
13202
|
}
|
|
12034
13203
|
}
|
|
12035
13204
|
if (opts.vertex) {
|
|
12036
13205
|
return runCodexAppVertexLaunch(configOnly, trace);
|
|
12037
13206
|
}
|
|
12038
|
-
const catalogSpinner =
|
|
13207
|
+
const catalogSpinner = p17.spinner();
|
|
12039
13208
|
catalogSpinner.start("Loading your providers...");
|
|
12040
13209
|
let catalog;
|
|
12041
13210
|
try {
|
|
12042
13211
|
catalog = await fetchProviderCatalog({ agent: "codex-app" });
|
|
12043
13212
|
} catch (err) {
|
|
12044
13213
|
catalogSpinner.stop("");
|
|
12045
|
-
console.error(
|
|
13214
|
+
console.error(pc15.red(String(err instanceof Error ? err.message : err)));
|
|
12046
13215
|
return 1;
|
|
12047
13216
|
}
|
|
12048
13217
|
catalogSpinner.stop("");
|
|
12049
13218
|
const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
|
|
12050
13219
|
if (compatible.length === 0) {
|
|
12051
13220
|
if (!configOnly) {
|
|
12052
|
-
|
|
12053
|
-
|
|
13221
|
+
p17.log.warn("No Codex-compatible providers in your registry.");
|
|
13222
|
+
p17.log.info("Add a provider with relay-ai providers add.");
|
|
12054
13223
|
}
|
|
12055
13224
|
return 0;
|
|
12056
13225
|
}
|
|
@@ -12058,10 +13227,10 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12058
13227
|
const favorites = prefs.favoriteModels ?? [];
|
|
12059
13228
|
const favoritesActive = favorites.length > 0;
|
|
12060
13229
|
if (favoritesActive && !configOnly) {
|
|
12061
|
-
|
|
13230
|
+
p17.log.info(
|
|
12062
13231
|
`Favorites mode active \u2014 Codex App picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
|
|
12063
13232
|
);
|
|
12064
|
-
|
|
13233
|
+
p17.log.info("Edit with `relay-ai models`.");
|
|
12065
13234
|
}
|
|
12066
13235
|
let activeProvider = providerForCodexPicker(
|
|
12067
13236
|
compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
|
|
@@ -12076,12 +13245,12 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12076
13245
|
const favoriteProviders = compatible.map(providerForCodexPicker);
|
|
12077
13246
|
const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
|
|
12078
13247
|
if (!favoriteStart) {
|
|
12079
|
-
|
|
13248
|
+
p17.log.warn("No saved Codex App favorites are currently available.");
|
|
12080
13249
|
return 0;
|
|
12081
13250
|
}
|
|
12082
13251
|
activeProvider = favoriteStart.provider;
|
|
12083
13252
|
selectedModel = favoriteStart.model;
|
|
12084
|
-
|
|
13253
|
+
p17.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
12085
13254
|
break;
|
|
12086
13255
|
} else {
|
|
12087
13256
|
activeProvider = providerForCodexPicker(pickedProvider);
|
|
@@ -12101,7 +13270,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12101
13270
|
const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
|
|
12102
13271
|
if (!apiKey) {
|
|
12103
13272
|
if (!configOnly) {
|
|
12104
|
-
|
|
13273
|
+
p17.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
|
|
12105
13274
|
}
|
|
12106
13275
|
return 1;
|
|
12107
13276
|
}
|
|
@@ -12161,36 +13330,36 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12161
13330
|
const home = process.env["HOME"] ?? "";
|
|
12162
13331
|
const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
|
|
12163
13332
|
console.log("");
|
|
12164
|
-
console.log(
|
|
13333
|
+
console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
|
|
12165
13334
|
console.log("");
|
|
12166
13335
|
if (favoritesActive) {
|
|
12167
|
-
console.log(` ${
|
|
13336
|
+
console.log(` ${pc15.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
|
|
12168
13337
|
console.log("");
|
|
12169
|
-
console.log(` ${
|
|
13338
|
+
console.log(` ${pc15.bold("Models:")}`);
|
|
12170
13339
|
for (const r of resolvedFavorites) {
|
|
12171
|
-
console.log(` ${
|
|
13340
|
+
console.log(` ${pc15.cyan(r.model.id)} ${pc15.dim(`(${r.providerName})`)}`);
|
|
12172
13341
|
}
|
|
12173
13342
|
} else {
|
|
12174
|
-
console.log(` ${
|
|
12175
|
-
console.log(` ${
|
|
12176
|
-
console.log(` ${
|
|
12177
|
-
console.log(` ${
|
|
13343
|
+
console.log(` ${pc15.bold("Mode:")} Single model`);
|
|
13344
|
+
console.log(` ${pc15.bold("Provider:")} ${activeProvider.name}`);
|
|
13345
|
+
console.log(` ${pc15.bold("Model:")} ${formatCodexModelLabel(selectedModel)}`);
|
|
13346
|
+
console.log(` ${pc15.bold("Catalog:")} ${routable.length} model${routable.length !== 1 ? "s" : ""} available`);
|
|
12178
13347
|
}
|
|
12179
13348
|
console.log("");
|
|
12180
|
-
console.log(` ${
|
|
13349
|
+
console.log(` ${pc15.bold("config.toml patch preview:")}`);
|
|
12181
13350
|
const tomlPreview = previewAppConfigToml({
|
|
12182
13351
|
...specBase,
|
|
12183
13352
|
proxyPort: PREVIEW_PROXY_PORT
|
|
12184
13353
|
});
|
|
12185
13354
|
for (const line of tomlPreview.split("\n")) {
|
|
12186
|
-
console.log(` ${
|
|
13355
|
+
console.log(` ${pc15.dim(line)}`);
|
|
12187
13356
|
}
|
|
12188
13357
|
console.log("");
|
|
12189
|
-
console.log(` ${
|
|
12190
|
-
console.log(` ${
|
|
13358
|
+
console.log(` ${pc15.bold("Catalog file:")}`);
|
|
13359
|
+
console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
|
|
12191
13360
|
console.log("");
|
|
12192
|
-
console.log(
|
|
12193
|
-
console.log(
|
|
13361
|
+
console.log(pc15.dim(" No app was launched."));
|
|
13362
|
+
console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app") + pc15.dim(" to launch."));
|
|
12194
13363
|
console.log("");
|
|
12195
13364
|
return 0;
|
|
12196
13365
|
}
|
|
@@ -12234,8 +13403,8 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12234
13403
|
try {
|
|
12235
13404
|
await launchOrRestartCodexApp();
|
|
12236
13405
|
} catch (err) {
|
|
12237
|
-
|
|
12238
|
-
|
|
13406
|
+
p17.log.warn(String(err instanceof Error ? err.message : err));
|
|
13407
|
+
p17.log.info(codexAppInstallHint());
|
|
12239
13408
|
}
|
|
12240
13409
|
printCodexAppSessionPanel({
|
|
12241
13410
|
modelLabel,
|
|
@@ -12252,8 +13421,8 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12252
13421
|
sessionActive = false;
|
|
12253
13422
|
}
|
|
12254
13423
|
if (isCodexAppRunning()) {
|
|
12255
|
-
const shouldClose = await
|
|
12256
|
-
if (shouldClose && !
|
|
13424
|
+
const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
|
|
13425
|
+
if (shouldClose && !p17.isCancel(shouldClose)) {
|
|
12257
13426
|
quitCodexAppGracefully();
|
|
12258
13427
|
}
|
|
12259
13428
|
}
|
|
@@ -12265,29 +13434,29 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12265
13434
|
}
|
|
12266
13435
|
|
|
12267
13436
|
// src/claude-app.ts
|
|
12268
|
-
import
|
|
12269
|
-
import * as
|
|
13437
|
+
import pc16 from "picocolors";
|
|
13438
|
+
import * as p19 from "@clack/prompts";
|
|
12270
13439
|
|
|
12271
13440
|
// src/claude-desktop/app-config.ts
|
|
12272
|
-
import { existsSync as
|
|
12273
|
-
import { homedir as
|
|
12274
|
-
import { join as
|
|
12275
|
-
import { randomUUID as
|
|
13441
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
|
|
13442
|
+
import { homedir as homedir12 } from "os";
|
|
13443
|
+
import { join as join18, dirname as dirname7 } from "path";
|
|
13444
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
12276
13445
|
function getClaudeDesktopHome() {
|
|
12277
13446
|
if (process.platform === "win32") {
|
|
12278
|
-
return
|
|
13447
|
+
return join18(process.env.APPDATA || join18(homedir12(), "AppData", "Roaming"), "Claude-3p");
|
|
12279
13448
|
}
|
|
12280
|
-
return
|
|
13449
|
+
return join18(homedir12(), "Library", "Application Support", "Claude-3p");
|
|
12281
13450
|
}
|
|
12282
13451
|
function getConfigLibraryPath() {
|
|
12283
|
-
return
|
|
13452
|
+
return join18(getClaudeDesktopHome(), "configLibrary");
|
|
12284
13453
|
}
|
|
12285
13454
|
function getMetaJsonPath() {
|
|
12286
|
-
return
|
|
13455
|
+
return join18(getConfigLibraryPath(), "_meta.json");
|
|
12287
13456
|
}
|
|
12288
13457
|
function readMetaJson() {
|
|
12289
13458
|
const metaPath = getMetaJsonPath();
|
|
12290
|
-
if (!
|
|
13459
|
+
if (!existsSync17(metaPath)) return null;
|
|
12291
13460
|
try {
|
|
12292
13461
|
return JSON.parse(readFileSync13(metaPath, "utf8"));
|
|
12293
13462
|
} catch {
|
|
@@ -12310,8 +13479,8 @@ function buildRelayAiConfig(proxyPort) {
|
|
|
12310
13479
|
};
|
|
12311
13480
|
}
|
|
12312
13481
|
function writeRelayAiConfig(proxyPort) {
|
|
12313
|
-
const uuid =
|
|
12314
|
-
const configPath =
|
|
13482
|
+
const uuid = randomUUID3();
|
|
13483
|
+
const configPath = join18(getConfigLibraryPath(), `${uuid}.json`);
|
|
12315
13484
|
const config = buildRelayAiConfig(proxyPort);
|
|
12316
13485
|
mkdirSync9(dirname7(configPath), { recursive: true });
|
|
12317
13486
|
writeFileSync7(configPath, `${JSON.stringify(config, null, 2)}
|
|
@@ -12326,14 +13495,14 @@ function writeRelayAiConfig(proxyPort) {
|
|
|
12326
13495
|
}
|
|
12327
13496
|
|
|
12328
13497
|
// src/claude-desktop/app-session.ts
|
|
12329
|
-
import { existsSync as
|
|
12330
|
-
import { join as
|
|
13498
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync8, copyFileSync as copyFileSync5, unlinkSync as unlinkSync3 } from "fs";
|
|
13499
|
+
import { join as join19 } from "path";
|
|
12331
13500
|
function getSessionLockPath2() {
|
|
12332
|
-
return
|
|
13501
|
+
return join19(getClaudeDesktopHome(), ".relay-ai.lock");
|
|
12333
13502
|
}
|
|
12334
13503
|
function readSessionLock2() {
|
|
12335
13504
|
const path = getSessionLockPath2();
|
|
12336
|
-
if (!
|
|
13505
|
+
if (!existsSync18(path)) return null;
|
|
12337
13506
|
try {
|
|
12338
13507
|
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
12339
13508
|
if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
|
|
@@ -12358,21 +13527,21 @@ function isProcessAlive3(pid) {
|
|
|
12358
13527
|
function backupMetaJson() {
|
|
12359
13528
|
const metaPath = getMetaJsonPath();
|
|
12360
13529
|
const backupPath = `${metaPath}.bak`;
|
|
12361
|
-
if (
|
|
13530
|
+
if (existsSync18(metaPath)) {
|
|
12362
13531
|
copyFileSync5(metaPath, backupPath);
|
|
12363
13532
|
}
|
|
12364
13533
|
}
|
|
12365
13534
|
function restoreMetaJson() {
|
|
12366
13535
|
const metaPath = getMetaJsonPath();
|
|
12367
13536
|
const backupPath = `${metaPath}.bak`;
|
|
12368
|
-
if (
|
|
13537
|
+
if (existsSync18(backupPath)) {
|
|
12369
13538
|
copyFileSync5(backupPath, metaPath);
|
|
12370
13539
|
unlinkSync3(backupPath);
|
|
12371
13540
|
}
|
|
12372
13541
|
}
|
|
12373
13542
|
function removeRelayAiConfig(uuid) {
|
|
12374
|
-
const configPath =
|
|
12375
|
-
if (
|
|
13543
|
+
const configPath = join19(getConfigLibraryPath(), `${uuid}.json`);
|
|
13544
|
+
if (existsSync18(configPath)) {
|
|
12376
13545
|
try {
|
|
12377
13546
|
rmSync4(configPath, { force: true });
|
|
12378
13547
|
} catch {
|
|
@@ -12436,11 +13605,11 @@ function setupExitCleanup(uuid) {
|
|
|
12436
13605
|
}
|
|
12437
13606
|
|
|
12438
13607
|
// src/claude-desktop/app-launch.ts
|
|
12439
|
-
import { execSync as
|
|
12440
|
-
import { existsSync as
|
|
12441
|
-
import { homedir as
|
|
12442
|
-
import { join as
|
|
12443
|
-
import * as
|
|
13608
|
+
import { execSync as execSync6 } from "child_process";
|
|
13609
|
+
import { existsSync as existsSync19, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
13610
|
+
import { homedir as homedir13 } from "os";
|
|
13611
|
+
import { join as join20 } from "path";
|
|
13612
|
+
import * as p18 from "@clack/prompts";
|
|
12444
13613
|
var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
|
|
12445
13614
|
function claudeAppSupported() {
|
|
12446
13615
|
if (process.platform !== "darwin" && process.platform !== "win32") {
|
|
@@ -12448,7 +13617,7 @@ function claudeAppSupported() {
|
|
|
12448
13617
|
}
|
|
12449
13618
|
}
|
|
12450
13619
|
function run2(cmd, encoding = "utf8") {
|
|
12451
|
-
return
|
|
13620
|
+
return execSync6(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
12452
13621
|
}
|
|
12453
13622
|
function runPowerShell2(script) {
|
|
12454
13623
|
return run2(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
|
|
@@ -12456,26 +13625,26 @@ function runPowerShell2(script) {
|
|
|
12456
13625
|
function darwinAppCandidates2() {
|
|
12457
13626
|
return [
|
|
12458
13627
|
"/Applications/Claude.app",
|
|
12459
|
-
|
|
13628
|
+
join20(homedir13(), "Applications", "Claude.app")
|
|
12460
13629
|
];
|
|
12461
13630
|
}
|
|
12462
13631
|
function winLocalAppData2() {
|
|
12463
|
-
return process.env.LOCALAPPDATA ??
|
|
13632
|
+
return process.env.LOCALAPPDATA ?? join20(homedir13(), "AppData", "Local");
|
|
12464
13633
|
}
|
|
12465
13634
|
function winClaudeExeCandidates() {
|
|
12466
13635
|
const local = winLocalAppData2();
|
|
12467
13636
|
const bases = [
|
|
12468
|
-
|
|
12469
|
-
|
|
13637
|
+
join20(local, "Programs", "Claude"),
|
|
13638
|
+
join20(local, "Claude")
|
|
12470
13639
|
];
|
|
12471
13640
|
const out = [];
|
|
12472
13641
|
for (const base of bases) {
|
|
12473
|
-
out.push(
|
|
13642
|
+
out.push(join20(base, "Claude.exe"));
|
|
12474
13643
|
try {
|
|
12475
|
-
if (
|
|
13644
|
+
if (existsSync19(base)) {
|
|
12476
13645
|
for (const name of readdirSync4(base)) {
|
|
12477
13646
|
if (name.startsWith("app-")) {
|
|
12478
|
-
out.push(
|
|
13647
|
+
out.push(join20(base, name, "Claude.exe"));
|
|
12479
13648
|
}
|
|
12480
13649
|
}
|
|
12481
13650
|
}
|
|
@@ -12488,7 +13657,7 @@ function mdfindClaudeApp() {
|
|
|
12488
13657
|
try {
|
|
12489
13658
|
const out = run2(`mdfind "kMDItemCFBundleIdentifier == '${CLAUDE_BUNDLE_ID}'"`);
|
|
12490
13659
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
12491
|
-
return first &&
|
|
13660
|
+
return first && existsSync19(first) ? first : null;
|
|
12492
13661
|
} catch {
|
|
12493
13662
|
return null;
|
|
12494
13663
|
}
|
|
@@ -12496,14 +13665,14 @@ function mdfindClaudeApp() {
|
|
|
12496
13665
|
function findClaudeApp() {
|
|
12497
13666
|
if (process.platform === "darwin") {
|
|
12498
13667
|
for (const path of darwinAppCandidates2()) {
|
|
12499
|
-
if (
|
|
13668
|
+
if (existsSync19(path)) return path;
|
|
12500
13669
|
}
|
|
12501
13670
|
return mdfindClaudeApp();
|
|
12502
13671
|
}
|
|
12503
13672
|
if (process.platform === "win32") {
|
|
12504
13673
|
for (const path of winClaudeExeCandidates()) {
|
|
12505
13674
|
try {
|
|
12506
|
-
if (
|
|
13675
|
+
if (existsSync19(path) && statSync5(path).isFile()) return path;
|
|
12507
13676
|
} catch {
|
|
12508
13677
|
}
|
|
12509
13678
|
}
|
|
@@ -12567,9 +13736,9 @@ async function waitForQuit2(timeoutMs) {
|
|
|
12567
13736
|
function openClaudeAppAt(path) {
|
|
12568
13737
|
if (process.platform === "darwin") {
|
|
12569
13738
|
if (path.endsWith(".app")) {
|
|
12570
|
-
|
|
13739
|
+
execSync6(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
|
|
12571
13740
|
} else {
|
|
12572
|
-
|
|
13741
|
+
execSync6(`open -b ${CLAUDE_BUNDLE_ID}`, { stdio: "inherit" });
|
|
12573
13742
|
}
|
|
12574
13743
|
return;
|
|
12575
13744
|
}
|
|
@@ -12592,9 +13761,9 @@ function openClaudeApp() {
|
|
|
12592
13761
|
}
|
|
12593
13762
|
function darwinQuit2() {
|
|
12594
13763
|
try {
|
|
12595
|
-
|
|
13764
|
+
execSync6(`osascript -e 'tell application "Claude" to quit'`, { stdio: "pipe" });
|
|
12596
13765
|
} catch {
|
|
12597
|
-
|
|
13766
|
+
execSync6(`osascript -e 'tell application id "${CLAUDE_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
|
|
12598
13767
|
}
|
|
12599
13768
|
}
|
|
12600
13769
|
function winQuitGraceful2() {
|
|
@@ -12620,9 +13789,9 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
|
|
|
12620
13789
|
openClaudeAppAt(appPath);
|
|
12621
13790
|
return;
|
|
12622
13791
|
}
|
|
12623
|
-
const restart = await
|
|
12624
|
-
if (
|
|
12625
|
-
|
|
13792
|
+
const restart = await p18.confirm({ message: prompt, initialValue: true });
|
|
13793
|
+
if (p18.isCancel(restart) || !restart) {
|
|
13794
|
+
p18.log.info("Quit and reopen Claude Desktop when you are ready for the new model to take effect.");
|
|
12626
13795
|
return;
|
|
12627
13796
|
}
|
|
12628
13797
|
if (process.platform === "darwin") darwinQuit2();
|
|
@@ -12637,30 +13806,30 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
|
|
|
12637
13806
|
|
|
12638
13807
|
// src/claude-app.ts
|
|
12639
13808
|
function claudeAppHelpText() {
|
|
12640
|
-
return `${
|
|
13809
|
+
return `${pc16.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
|
|
12641
13810
|
|
|
12642
|
-
${
|
|
13811
|
+
${pc16.bold("Usage:")}
|
|
12643
13812
|
relay-ai claude-app [options]
|
|
12644
13813
|
relay-ai claude-app --trace
|
|
12645
13814
|
relay-ai claude-app --restore
|
|
12646
13815
|
relay-ai claude-app --help
|
|
12647
13816
|
relay-ai claude-app --version
|
|
12648
13817
|
|
|
12649
|
-
${
|
|
13818
|
+
${pc16.bold("Options:")}
|
|
12650
13819
|
--trace Write proxy debug logs to ~/.relay-ai/logs/
|
|
12651
13820
|
--restore Restore Claude Desktop config after an interrupted app session
|
|
12652
13821
|
--help Show this command help
|
|
12653
13822
|
--version Show version
|
|
12654
13823
|
|
|
12655
|
-
${
|
|
13824
|
+
${pc16.bold("Description:")}
|
|
12656
13825
|
Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
|
|
12657
13826
|
(with backup + restore on Ctrl+C), starts a local Responses proxy, and opens
|
|
12658
13827
|
the Claude Desktop app. Keep this terminal open while using Claude.
|
|
12659
13828
|
|
|
12660
|
-
${
|
|
13829
|
+
${pc16.bold("Platforms:")}
|
|
12661
13830
|
macOS and Windows. Linux is not supported.
|
|
12662
13831
|
|
|
12663
|
-
${
|
|
13832
|
+
${pc16.bold("Cleanup:")}
|
|
12664
13833
|
Ctrl+C stops the proxy and restores your previous Claude config.
|
|
12665
13834
|
After a crash: relay-ai claude-app --restore
|
|
12666
13835
|
`;
|
|
@@ -12684,37 +13853,37 @@ async function runClaudeAppCommand(args) {
|
|
|
12684
13853
|
try {
|
|
12685
13854
|
claudeAppSupported();
|
|
12686
13855
|
} catch (err) {
|
|
12687
|
-
console.error(
|
|
13856
|
+
console.error(pc16.red(String(err instanceof Error ? err.message : err)));
|
|
12688
13857
|
return 1;
|
|
12689
13858
|
}
|
|
12690
13859
|
const isTty = Boolean(process.stdin.isTTY);
|
|
12691
13860
|
if (!isTty) {
|
|
12692
|
-
console.error(
|
|
13861
|
+
console.error(pc16.red("relay-ai claude-app requires an interactive terminal."));
|
|
12693
13862
|
return 1;
|
|
12694
13863
|
}
|
|
12695
13864
|
if (isConcurrentLiveSession()) {
|
|
12696
|
-
console.error(
|
|
13865
|
+
console.error(pc16.yellow(`Another relay-ai claude-app session may be running.`));
|
|
12697
13866
|
console.error("Stop it with Ctrl+C in that terminal.");
|
|
12698
13867
|
return 1;
|
|
12699
13868
|
}
|
|
12700
13869
|
if (hasStaleSession()) {
|
|
12701
|
-
|
|
13870
|
+
p19.log.warn("Recovered from an interrupted claude-app session.");
|
|
12702
13871
|
recoverSession();
|
|
12703
13872
|
}
|
|
12704
|
-
const catalogSpinner =
|
|
13873
|
+
const catalogSpinner = p19.spinner();
|
|
12705
13874
|
catalogSpinner.start("Loading your providers...");
|
|
12706
13875
|
let catalog;
|
|
12707
13876
|
try {
|
|
12708
13877
|
catalog = await fetchProviderCatalog({ agent: "codex-app" });
|
|
12709
13878
|
} catch (err) {
|
|
12710
13879
|
catalogSpinner.stop("");
|
|
12711
|
-
console.error(
|
|
13880
|
+
console.error(pc16.red(String(err instanceof Error ? err.message : err)));
|
|
12712
13881
|
return 1;
|
|
12713
13882
|
}
|
|
12714
13883
|
catalogSpinner.stop("");
|
|
12715
13884
|
const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
|
|
12716
13885
|
if (compatible.length === 0) {
|
|
12717
|
-
|
|
13886
|
+
p19.log.warn("No compatible providers in your registry.");
|
|
12718
13887
|
return 0;
|
|
12719
13888
|
}
|
|
12720
13889
|
const prefs = loadPreferences();
|
|
@@ -12736,7 +13905,7 @@ async function runClaudeAppCommand(args) {
|
|
|
12736
13905
|
const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
|
|
12737
13906
|
const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
|
|
12738
13907
|
if (!apiKey) {
|
|
12739
|
-
|
|
13908
|
+
p19.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
|
|
12740
13909
|
return 1;
|
|
12741
13910
|
}
|
|
12742
13911
|
activeProvider.apiKey = apiKey;
|
|
@@ -12799,28 +13968,28 @@ async function runClaudeAppCommand(args) {
|
|
|
12799
13968
|
});
|
|
12800
13969
|
}
|
|
12801
13970
|
console.log(`
|
|
12802
|
-
${
|
|
13971
|
+
${pc16.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
|
|
12803
13972
|
try {
|
|
12804
13973
|
await launchOrRestartClaudeApp();
|
|
12805
13974
|
} catch (err) {
|
|
12806
|
-
|
|
13975
|
+
p19.log.warn(String(err instanceof Error ? err.message : err));
|
|
12807
13976
|
}
|
|
12808
13977
|
console.log(`
|
|
12809
|
-
${
|
|
13978
|
+
${pc16.bold("Claude Desktop 3P Mode Active")}`);
|
|
12810
13979
|
if (useFavorites) {
|
|
12811
|
-
console.log(`${
|
|
13980
|
+
console.log(`${pc16.dim("Catalog:")} Favorite models only`);
|
|
12812
13981
|
} else {
|
|
12813
|
-
console.log(`${
|
|
12814
|
-
console.log(`${
|
|
13982
|
+
console.log(`${pc16.dim("Model:")} ${selectedModel.id}`);
|
|
13983
|
+
console.log(`${pc16.dim("Provider:")} ${activeProvider.name}`);
|
|
12815
13984
|
}
|
|
12816
|
-
console.log(`${
|
|
13985
|
+
console.log(`${pc16.cyan("Press Ctrl+C to stop and restore config.")}`);
|
|
12817
13986
|
await waitForShutdown3();
|
|
12818
13987
|
console.log("");
|
|
12819
13988
|
cleanupSession(uuid);
|
|
12820
13989
|
sessionActive = false;
|
|
12821
13990
|
if (isClaudeAppRunning()) {
|
|
12822
|
-
const shouldClose = await
|
|
12823
|
-
if (shouldClose && !
|
|
13991
|
+
const shouldClose = await p19.confirm({ message: "Claude Desktop is still running. Close it?" });
|
|
13992
|
+
if (shouldClose && !p19.isCancel(shouldClose)) {
|
|
12824
13993
|
quitClaudeAppGracefully();
|
|
12825
13994
|
}
|
|
12826
13995
|
}
|
|
@@ -12835,17 +14004,17 @@ ${pc15.bold("Claude Desktop 3P Mode Active")}`);
|
|
|
12835
14004
|
}
|
|
12836
14005
|
|
|
12837
14006
|
// src/ai-doc.ts
|
|
12838
|
-
import { existsSync as
|
|
12839
|
-
import { homedir as
|
|
12840
|
-
import { join as
|
|
14007
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
14008
|
+
import { homedir as homedir14 } from "os";
|
|
14009
|
+
import { join as join21 } from "path";
|
|
12841
14010
|
var SKILL_DIR_NAME = "relay-ai-cli";
|
|
12842
14011
|
var SKILL_INSTALL_DIRS = [
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
14012
|
+
join21(getAppHome(), "skills"),
|
|
14013
|
+
join21(homedir14(), ".claude", "skills"),
|
|
14014
|
+
join21(homedir14(), ".agents", "skills"),
|
|
14015
|
+
join21(homedir14(), ".codex", "skills"),
|
|
14016
|
+
join21(homedir14(), ".cursor", "skills"),
|
|
14017
|
+
join21(homedir14(), ".cursor", "skills-cursor")
|
|
12849
14018
|
];
|
|
12850
14019
|
function parseSkillVersion(content) {
|
|
12851
14020
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -12859,8 +14028,8 @@ function parseSkillVersion(content) {
|
|
|
12859
14028
|
return null;
|
|
12860
14029
|
}
|
|
12861
14030
|
function readInstalledSkillVersion(skillDir) {
|
|
12862
|
-
const skillPath =
|
|
12863
|
-
if (!
|
|
14031
|
+
const skillPath = join21(skillDir, "SKILL.md");
|
|
14032
|
+
if (!existsSync20(skillPath)) return null;
|
|
12864
14033
|
try {
|
|
12865
14034
|
const head = readFileSync15(skillPath, "utf-8").slice(0, 1024);
|
|
12866
14035
|
return parseSkillVersion(head.includes("---", 4) ? head : `${head}
|
|
@@ -12872,8 +14041,8 @@ function readInstalledSkillVersion(skillDir) {
|
|
|
12872
14041
|
}
|
|
12873
14042
|
function skillInstallTargets() {
|
|
12874
14043
|
return SKILL_INSTALL_DIRS.map((dir) => {
|
|
12875
|
-
const skillDir =
|
|
12876
|
-
return { skillDir, skillPath:
|
|
14044
|
+
const skillDir = join21(dir, SKILL_DIR_NAME);
|
|
14045
|
+
return { skillDir, skillPath: join21(skillDir, "SKILL.md") };
|
|
12877
14046
|
});
|
|
12878
14047
|
}
|
|
12879
14048
|
function formatProviderModels(provider) {
|
|
@@ -12886,7 +14055,7 @@ function formatProviderModels(provider) {
|
|
|
12886
14055
|
function buildLiveStateSection() {
|
|
12887
14056
|
const prefs = loadPreferences();
|
|
12888
14057
|
const registry = loadRegistry();
|
|
12889
|
-
const enabled = registry.providers.filter((
|
|
14058
|
+
const enabled = registry.providers.filter((p21) => p21.enabled);
|
|
12890
14059
|
const prefLines = [];
|
|
12891
14060
|
if (prefs.lastProvider || prefs.lastModel) {
|
|
12892
14061
|
prefLines.push(` Claude last launch: provider=${prefs.lastProvider ?? "(none)"} model=${prefs.lastModel ?? "(none)"}`);
|
|
@@ -12894,15 +14063,18 @@ function buildLiveStateSection() {
|
|
|
12894
14063
|
if (prefs.lastCodexProvider || prefs.lastCodexModel) {
|
|
12895
14064
|
prefLines.push(` Codex last launch: provider=${prefs.lastCodexProvider ?? "(none)"} model=${prefs.lastCodexModel ?? "(none)"}`);
|
|
12896
14065
|
}
|
|
14066
|
+
if (prefs.lastGeminiProvider || prefs.lastGeminiModel) {
|
|
14067
|
+
prefLines.push(` Gemini last launch: provider=${prefs.lastGeminiProvider ?? "(none)"} model=${prefs.lastGeminiModel ?? "(none)"}`);
|
|
14068
|
+
}
|
|
12897
14069
|
if (prefs.favoriteModels?.length) {
|
|
12898
14070
|
prefLines.push(` Favorites (${prefs.favoriteModels.length}/${MAX_MODEL_CATALOG}):`);
|
|
12899
14071
|
for (const f of prefs.favoriteModels) {
|
|
12900
14072
|
prefLines.push(` ${f.providerId} / ${f.modelId}`);
|
|
12901
14073
|
}
|
|
12902
14074
|
}
|
|
12903
|
-
const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((
|
|
12904
|
-
` ${
|
|
12905
|
-
formatProviderModels(
|
|
14075
|
+
const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((p21) => [
|
|
14076
|
+
` ${p21.name} (${p21.id}) \u2014 ${p21.modelsCache?.models.length ?? 0} cached model(s)`,
|
|
14077
|
+
formatProviderModels(p21)
|
|
12906
14078
|
].join("\n"));
|
|
12907
14079
|
return `
|
|
12908
14080
|
================================================================================
|
|
@@ -12938,8 +14110,8 @@ function staticAiDocBody() {
|
|
|
12938
14110
|
RELAY-AI \u2014 AI AGENT REFERENCE (v${VERSION})
|
|
12939
14111
|
================================================================================
|
|
12940
14112
|
|
|
12941
|
-
relay-ai launches Claude Code, OpenAI Codex, and desktop apps
|
|
12942
|
-
provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
|
|
14113
|
+
relay-ai launches Claude Code, OpenAI Codex, Google Gemini CLI, and desktop apps
|
|
14114
|
+
against YOUR provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
|
|
12943
14115
|
It handles API translation, local proxies, env isolation, and model routing.
|
|
12944
14116
|
|
|
12945
14117
|
SKILL VERSIONING
|
|
@@ -12991,13 +14163,14 @@ PREVIEW LAUNCH WITHOUT STARTING A SESSION:
|
|
|
12991
14163
|
INTERACTIVE BROWSE (requires TTY \u2014 avoid in agent scripts):
|
|
12992
14164
|
relay-ai claude provider + model wizard
|
|
12993
14165
|
relay-ai codex provider + model wizard
|
|
14166
|
+
relay-ai gemini provider + model wizard
|
|
12994
14167
|
relay-ai providers provider management hub
|
|
12995
14168
|
|
|
12996
14169
|
================================================================================
|
|
12997
14170
|
AGENT PLATFORM PATTERNS \u2014 MULTI-MODEL / ONE-SHOT QUERIES
|
|
12998
14171
|
================================================================================
|
|
12999
14172
|
|
|
13000
|
-
relay-ai is designed so agents can use Claude Code or
|
|
14173
|
+
relay-ai is designed so agents can use Claude Code, Codex, or Gemini CLI as a PLATFORM:
|
|
13001
14174
|
run many models sequentially or in parallel shell jobs, each with a focused
|
|
13002
14175
|
prompt, without interactive wizards.
|
|
13003
14176
|
|
|
@@ -13045,6 +14218,24 @@ OPENAI CODEX \u2014 NON-INTERACTIVE (exec / positional prompt)
|
|
|
13045
14218
|
--provider <id>
|
|
13046
14219
|
--model <id> or provider__model-id slug
|
|
13047
14220
|
|
|
14221
|
+
GOOGLE GEMINI CLI \u2014 NON-INTERACTIVE (-p / --prompt)
|
|
14222
|
+
Skips the provider/model wizard when:
|
|
14223
|
+
\u2022 Both --provider and --model are set, OR
|
|
14224
|
+
\u2022 Non-interactive args (-p / --prompt, -i / --prompt-interactive, or positional query)
|
|
14225
|
+
and saved preferences exist
|
|
14226
|
+
|
|
14227
|
+
Examples:
|
|
14228
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "Review this file"
|
|
14229
|
+
relay-ai gemini -p "What is the capital of France?"
|
|
14230
|
+
|
|
14231
|
+
Machine-readable stdout:
|
|
14232
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o json
|
|
14233
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o stream-json
|
|
14234
|
+
|
|
14235
|
+
Boot flags (relay-ai \u2014 NOT passed to Gemini):
|
|
14236
|
+
--provider <id>
|
|
14237
|
+
--model <id> or provider__model-id slug
|
|
14238
|
+
|
|
13048
14239
|
MULTI-MODEL LOOP (shell pattern):
|
|
13049
14240
|
for model in llama-3.3-70b-versatile mixtral-8x7b-32768; do
|
|
13050
14241
|
relay-ai claude --provider groq --model "$model" -p "Same prompt for all models"
|
|
@@ -13054,10 +14245,14 @@ MULTI-MODEL LOOP (shell pattern):
|
|
|
13054
14245
|
relay-ai codex --provider zen --model "$model" exec "Same task"
|
|
13055
14246
|
done
|
|
13056
14247
|
|
|
14248
|
+
for model in gemini-2.5-flash gemini-2.5-pro; do
|
|
14249
|
+
relay-ai gemini --provider google --model "$model" -p "Same task"
|
|
14250
|
+
done
|
|
14251
|
+
|
|
13057
14252
|
FAVORITES / MID-SESSION SWITCHING:
|
|
13058
14253
|
relay-ai models interactive favorites manager (max ${MAX_MODEL_CATALOG})
|
|
13059
|
-
When favorites exist, interactive claude/codex launches expose /model switching.
|
|
13060
|
-
Boot flags (--provider + --model) or print/exec mode use SINGLE-MODEL launch
|
|
14254
|
+
When favorites exist, interactive claude/codex/gemini launches expose /model switching.
|
|
14255
|
+
Boot flags (--provider + --model) or print/exec/-p mode use SINGLE-MODEL launch
|
|
13061
14256
|
(favorites catalog is skipped \u2014 better for agent one-shots).
|
|
13062
14257
|
|
|
13063
14258
|
================================================================================
|
|
@@ -13092,6 +14287,18 @@ CLAUDE CODE
|
|
|
13092
14287
|
relay-ai claude --provider anthropic --model claude-sonnet-4-6 -p "review file.ts"
|
|
13093
14288
|
relay-ai claude --dry-run --provider groq --model llama-3.3-70b-versatile
|
|
13094
14289
|
|
|
14290
|
+
GOOGLE GEMINI CLI
|
|
14291
|
+
relay-ai gemini [relay-options] [gemini-flags]
|
|
14292
|
+
|
|
14293
|
+
Relay options:
|
|
14294
|
+
--provider <id> Boot provider (skip wizard with --model)
|
|
14295
|
+
--model <id> Boot model id or provider__model slug
|
|
14296
|
+
--trace Debug logs in ~/.relay-ai/logs/
|
|
14297
|
+
|
|
14298
|
+
Examples:
|
|
14299
|
+
relay-ai gemini
|
|
14300
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "What is the capital of France?"
|
|
14301
|
+
|
|
13095
14302
|
OPENAI CODEX CLI
|
|
13096
14303
|
relay-ai codex [relay-options] [codex-flags]
|
|
13097
14304
|
|
|
@@ -13125,7 +14332,7 @@ PROVIDERS REGISTRY
|
|
|
13125
14332
|
|
|
13126
14333
|
MODELS / FAVORITES
|
|
13127
14334
|
relay-ai models manage favoriteModels in config (alias: favorites)
|
|
13128
|
-
Used for mid-session /model switching in interactive Claude/Codex sessions.
|
|
14335
|
+
Used for mid-session /model switching in interactive Claude/Codex/Gemini sessions.
|
|
13129
14336
|
|
|
13130
14337
|
API GATEWAY (for tools that speak Anthropic/OpenAI HTTP)
|
|
13131
14338
|
relay-ai server foreground gateway on port 17645
|
|
@@ -13161,10 +14368,10 @@ DO:
|
|
|
13161
14368
|
|
|
13162
14369
|
DO NOT:
|
|
13163
14370
|
\u2022 Rely on interactive wizards in CI, scripts, or headless agent loops
|
|
13164
|
-
\u2022 Pass --provider / --model to Claude or
|
|
14371
|
+
\u2022 Pass --provider / --model to Claude, Codex, or Gemini directly \u2014 relay-ai consumes them
|
|
13165
14372
|
\u2022 Use Codex -p expecting print mode (it means --profile in Codex)
|
|
13166
|
-
\u2022 Assume favorites catalog in print/exec mode \u2014 use explicit boot flags
|
|
13167
|
-
\u2022 Mutate ~/.claude/settings.json
|
|
14373
|
+
\u2022 Assume favorites catalog in print/exec/-p mode \u2014 use explicit boot flags
|
|
14374
|
+
\u2022 Mutate settings files (e.g. ~/.claude/settings.json, ~/.codex/config.toml, ~/.gemini/config/config.json) \u2014 relay-ai uses env +
|
|
13168
14375
|
temporary overlay profiles only
|
|
13169
14376
|
|
|
13170
14377
|
NON-TTY:
|
|
@@ -13178,6 +14385,7 @@ TROUBLESHOOTING
|
|
|
13178
14385
|
relay-ai codex --restore clean stale overlay after crash
|
|
13179
14386
|
relay-ai claude --trace proxy + Claude debug logs
|
|
13180
14387
|
relay-ai codex --trace proxy debug log on exit
|
|
14388
|
+
relay-ai gemini --trace proxy debug log on exit
|
|
13181
14389
|
relay-ai providers list verify provider ids
|
|
13182
14390
|
relay-ai providers refresh-models repopulate model cache
|
|
13183
14391
|
|
|
@@ -13196,7 +14404,7 @@ Human-readable guide: docs/AI-AGENTS.md in the relay-ai repo.
|
|
|
13196
14404
|
ALEF AGENT INTEGRATION
|
|
13197
14405
|
================================================================================
|
|
13198
14406
|
|
|
13199
|
-
alef-agent shells out to relay-ai to run Claude Code or
|
|
14407
|
+
alef-agent shells out to relay-ai to run Claude Code, Codex, or Gemini CLI against any
|
|
13200
14408
|
provider in ~/.relay-ai/providers.json. relay-ai is a launcher + proxy; the
|
|
13201
14409
|
child CLI owns NDJSON/JSONL on stdout.
|
|
13202
14410
|
|
|
@@ -13253,7 +14461,8 @@ ALEF CHECKLIST
|
|
|
13253
14461
|
\u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
|
|
13254
14462
|
\u25A1 Claude: --output-format stream-json (or json) with -p
|
|
13255
14463
|
\u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
|
|
13256
|
-
\u25A1
|
|
14464
|
+
\u25A1 Gemini: -o json (or stream-json) with -p
|
|
14465
|
+
\u25A1 Parse stdout only; ignore stderr for JSONL/NDJSON stream
|
|
13257
14466
|
\u25A1 Zen/Go: --provider zen explicitly + OPENCODE_API_KEY available
|
|
13258
14467
|
\u25A1 Codex network: default danger-full-access \u2014 no extra -s needed for nlm/curl/npm
|
|
13259
14468
|
\u25A1 MCP (Claude): --allowed-tools mcp__server__tool on claude args after relay-ai flags
|
|
@@ -13442,10 +14651,12 @@ function parseArgs(args) {
|
|
|
13442
14651
|
}
|
|
13443
14652
|
if (first === "providers") {
|
|
13444
14653
|
const parsed2 = emptyParsed("providers");
|
|
13445
|
-
parsed2.claudeArgs =
|
|
14654
|
+
parsed2.claudeArgs = [];
|
|
13446
14655
|
for (const arg of rest) {
|
|
13447
|
-
if (arg === "--
|
|
14656
|
+
if (arg === "--trace") parsed2.trace = true;
|
|
14657
|
+
else if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
|
|
13448
14658
|
else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
|
|
14659
|
+
else parsed2.claudeArgs.push(arg);
|
|
13449
14660
|
}
|
|
13450
14661
|
return parsed2;
|
|
13451
14662
|
}
|
|
@@ -13498,6 +14709,32 @@ function parseArgs(args) {
|
|
|
13498
14709
|
}
|
|
13499
14710
|
return parsed2;
|
|
13500
14711
|
}
|
|
14712
|
+
if (first === "gemini") {
|
|
14713
|
+
const parsed2 = emptyParsed("gemini");
|
|
14714
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
14715
|
+
const arg = rest[i];
|
|
14716
|
+
if (arg === "--trace") {
|
|
14717
|
+
parsed2.trace = true;
|
|
14718
|
+
continue;
|
|
14719
|
+
}
|
|
14720
|
+
if (arg === "--help" || arg === "-h") {
|
|
14721
|
+
parsed2.showHelp = true;
|
|
14722
|
+
continue;
|
|
14723
|
+
}
|
|
14724
|
+
if (arg === "--version" || arg === "-v") {
|
|
14725
|
+
parsed2.showVersion = true;
|
|
14726
|
+
continue;
|
|
14727
|
+
}
|
|
14728
|
+
const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
|
|
14729
|
+
if (consumed !== null) {
|
|
14730
|
+
if ("error" in consumed) return parsed2;
|
|
14731
|
+
i = consumed.next;
|
|
14732
|
+
continue;
|
|
14733
|
+
}
|
|
14734
|
+
parsed2.claudeArgs.push(arg);
|
|
14735
|
+
}
|
|
14736
|
+
return parsed2;
|
|
14737
|
+
}
|
|
13501
14738
|
if (first !== "claude") {
|
|
13502
14739
|
return {
|
|
13503
14740
|
...emptyParsed("root"),
|
|
@@ -13530,15 +14767,16 @@ function parseArgs(args) {
|
|
|
13530
14767
|
return parsed;
|
|
13531
14768
|
}
|
|
13532
14769
|
function rootHelpText() {
|
|
13533
|
-
return `${
|
|
14770
|
+
return `${pc17.bold("relay-ai")} v${VERSION}
|
|
13534
14771
|
Launch AI coding tools with OpenCode Zen / Go or local providers (Groq, Mistral,
|
|
13535
14772
|
OpenAI, Gemini, Ollama, and more).
|
|
13536
14773
|
|
|
13537
|
-
${
|
|
14774
|
+
${pc17.bold("Usage:")}
|
|
13538
14775
|
relay-ai claude [options] [claude-flags]
|
|
13539
14776
|
relay-ai claude-app [options]
|
|
13540
14777
|
relay-ai codex [options] [codex-flags]
|
|
13541
14778
|
relay-ai codex-app [options]
|
|
14779
|
+
relay-ai gemini [options] [gemini-flags]
|
|
13542
14780
|
relay-ai server [options]
|
|
13543
14781
|
relay-ai models
|
|
13544
14782
|
relay-ai favorites
|
|
@@ -13549,32 +14787,34 @@ ${pc16.bold("Usage:")}
|
|
|
13549
14787
|
relay-ai --ai --install Install or upgrade agent skill when version changed
|
|
13550
14788
|
relay-ai --ai --install --force Reinstall skill even if already current
|
|
13551
14789
|
|
|
13552
|
-
${
|
|
14790
|
+
${pc17.bold("Root options:")}
|
|
13553
14791
|
-h, --help Show this help
|
|
13554
14792
|
-v, --version Show version
|
|
13555
14793
|
--ai Print the full reference for AI agents
|
|
13556
14794
|
--ai --install Install or upgrade the relay-ai agent skill
|
|
13557
14795
|
--force Reinstall the agent skill when used with --ai --install
|
|
13558
14796
|
|
|
13559
|
-
${
|
|
14797
|
+
${pc17.bold("Commands:")}
|
|
13560
14798
|
claude Launch Claude Code \u2014 pick a provider from your registry
|
|
13561
14799
|
models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
|
|
13562
14800
|
favorites Alias for models
|
|
13563
14801
|
providers Add, import, and manage your AI providers
|
|
13564
14802
|
server Run a foreground API gateway (OpenCode Zen / Go and local providers)
|
|
13565
14803
|
codex Launch OpenAI Codex CLI with registry providers
|
|
14804
|
+
gemini Launch Google Gemini CLI with registry providers
|
|
13566
14805
|
codex-app Launch Codex desktop app with registry providers (macOS + Windows)
|
|
13567
14806
|
claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
|
|
13568
14807
|
|
|
13569
|
-
${
|
|
14808
|
+
${pc17.bold("Migration:")}
|
|
13570
14809
|
Bare relay-ai prints this help instead of launching Claude Code.
|
|
13571
14810
|
Use relay-ai claude for the wizard and launcher.
|
|
13572
14811
|
|
|
13573
|
-
${
|
|
14812
|
+
${pc17.bold("Examples:")}
|
|
13574
14813
|
relay-ai claude
|
|
13575
14814
|
relay-ai models
|
|
13576
14815
|
relay-ai providers
|
|
13577
14816
|
relay-ai codex
|
|
14817
|
+
relay-ai gemini
|
|
13578
14818
|
relay-ai codex-app
|
|
13579
14819
|
relay-ai claude-app
|
|
13580
14820
|
relay-ai server
|
|
@@ -13583,15 +14823,15 @@ ${pc16.bold("Examples:")}
|
|
|
13583
14823
|
relay-ai claude -- --print "hello"`;
|
|
13584
14824
|
}
|
|
13585
14825
|
function claudeHelpText() {
|
|
13586
|
-
return `${
|
|
14826
|
+
return `${pc17.bold("relay-ai claude")} v${VERSION}
|
|
13587
14827
|
Launch Claude Code with OpenCode Zen, Go, or local providers as the API backend.
|
|
13588
14828
|
|
|
13589
|
-
${
|
|
14829
|
+
${pc17.bold("Usage:")}
|
|
13590
14830
|
relay-ai claude [options] [claude-flags]
|
|
13591
14831
|
relay-ai claude --help
|
|
13592
14832
|
relay-ai claude --version
|
|
13593
14833
|
|
|
13594
|
-
${
|
|
14834
|
+
${pc17.bold("Options:")}
|
|
13595
14835
|
--dry-run Run the wizard but show a preview instead of launching Claude Code
|
|
13596
14836
|
--setup Hint: use relay-ai providers to add or manage providers
|
|
13597
14837
|
--trace Write debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
@@ -13600,22 +14840,22 @@ ${pc16.bold("Options:")}
|
|
|
13600
14840
|
--help Show this command help
|
|
13601
14841
|
--version Show version
|
|
13602
14842
|
|
|
13603
|
-
${
|
|
14843
|
+
${pc17.bold("Providers:")}
|
|
13604
14844
|
Cloud (Zen/Go) Requires OPENCODE_API_KEY \u2014 get one at https://opencode.ai/auth
|
|
13605
14845
|
Registry Configure with relay-ai providers add or import (Groq, Mistral,
|
|
13606
14846
|
Nvidia, DeepSeek, OpenAI, custom endpoints, etc.).
|
|
13607
14847
|
|
|
13608
|
-
${
|
|
14848
|
+
${pc17.bold("Model switching:")}
|
|
13609
14849
|
Run relay-ai models to save favorites (max ${MAX_MODEL_CATALOG}).
|
|
13610
14850
|
When favorites exist, launch starts a multi-route proxy and Claude Code /model
|
|
13611
14851
|
lists your starting model plus favorites for live switching.
|
|
13612
14852
|
With no favorites, launch uses a single model as before.
|
|
13613
14853
|
|
|
13614
|
-
${
|
|
14854
|
+
${pc17.bold("Note:")}
|
|
13615
14855
|
Claude Code may save the launched model to ~/.claude/settings.json.
|
|
13616
14856
|
Bare claude later can still show that model \u2014 reset with claude --model sonnet.
|
|
13617
14857
|
|
|
13618
|
-
${
|
|
14858
|
+
${pc17.bold("Examples:")}
|
|
13619
14859
|
relay-ai claude
|
|
13620
14860
|
relay-ai claude -c
|
|
13621
14861
|
relay-ai claude --resume abc-123
|
|
@@ -13629,54 +14869,54 @@ ${pc16.bold("Examples:")}
|
|
|
13629
14869
|
relay-ai claude -- --dangerously-skip-permissions`;
|
|
13630
14870
|
}
|
|
13631
14871
|
function serverHelpText() {
|
|
13632
|
-
return `${
|
|
14872
|
+
return `${pc17.bold("relay-ai server")} v${VERSION}
|
|
13633
14873
|
Run a foreground API gateway for registry providers, Zen/Go, or Vertex AI.
|
|
13634
14874
|
|
|
13635
|
-
${
|
|
14875
|
+
${pc17.bold("Usage:")}
|
|
13636
14876
|
relay-ai server
|
|
13637
14877
|
relay-ai server --vertex
|
|
13638
14878
|
relay-ai server --help
|
|
13639
14879
|
relay-ai server --version
|
|
13640
14880
|
|
|
13641
|
-
${
|
|
14881
|
+
${pc17.bold("Behavior:")}
|
|
13642
14882
|
Default: interactive wizard for exposed providers, discovery id masking (for
|
|
13643
14883
|
Claude Desktop / Cowork), optional favorites-only catalog, then listen mode.
|
|
13644
14884
|
--vertex: Anthropic-compatible gateway to Claude on Google Vertex AI using
|
|
13645
14885
|
local gcloud Application Default Credentials (no OpenCode API key).
|
|
13646
14886
|
Binds to port 17645. Network mode asks for a server password.
|
|
13647
14887
|
|
|
13648
|
-
${
|
|
14888
|
+
${pc17.bold("Vertex env:")}
|
|
13649
14889
|
ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
|
|
13650
14890
|
GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
|
|
13651
14891
|
Optional catalog: ~/.relay-ai/vertex-models.json (see assets/vertex-models.example.json)
|
|
13652
14892
|
|
|
13653
|
-
${
|
|
14893
|
+
${pc17.bold("Endpoints:")}
|
|
13654
14894
|
Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
|
|
13655
14895
|
OpenAI-compatible: OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
|
|
13656
14896
|
API key: use anything locally; use the server password in network mode.`;
|
|
13657
14897
|
}
|
|
13658
14898
|
function modelsHelpText() {
|
|
13659
|
-
return `${
|
|
14899
|
+
return `${pc17.bold("relay-ai models")} v${VERSION}
|
|
13660
14900
|
Manage favorite models for mid-session switching in Claude Code.
|
|
13661
14901
|
|
|
13662
|
-
${
|
|
14902
|
+
${pc17.bold("Usage:")}
|
|
13663
14903
|
relay-ai models
|
|
13664
14904
|
relay-ai models --help
|
|
13665
14905
|
relay-ai models --version
|
|
13666
14906
|
|
|
13667
|
-
${
|
|
14907
|
+
${pc17.bold("Behavior:")}
|
|
13668
14908
|
Opens an interactive manager to add or remove favorites.
|
|
13669
14909
|
Search all providers at once (paginated results) or browse one provider at a time.
|
|
13670
14910
|
Pick from Zen, Go, or any provider in your registry.
|
|
13671
14911
|
Favorites are saved to ~/.relay-ai/config.json (max ${MAX_MODEL_CATALOG}).
|
|
13672
14912
|
|
|
13673
|
-
${
|
|
14913
|
+
${pc17.bold("How it works:")}
|
|
13674
14914
|
When favorites exist, relay-ai claude starts a multi-route catalog proxy.
|
|
13675
14915
|
Claude Code /model lists your starting model plus favorites \u2014 switch live
|
|
13676
14916
|
without restarting. Mix cloud and local favorites in one session.
|
|
13677
14917
|
With no favorites, launch uses a single model as before.
|
|
13678
14918
|
|
|
13679
|
-
${
|
|
14919
|
+
${pc17.bold("Examples:")}
|
|
13680
14920
|
relay-ai models
|
|
13681
14921
|
relay-ai claude # switch menu active when favorites are set`;
|
|
13682
14922
|
}
|
|
@@ -13689,11 +14929,11 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13689
14929
|
let proxyHandle;
|
|
13690
14930
|
try {
|
|
13691
14931
|
proxyHandle = await startProxyCatalog(catalogRoutes, startingRoute.aliasId, trace);
|
|
13692
|
-
|
|
13693
|
-
`Switch menu active \u2014 proxy on port ${proxyHandle.port} ` +
|
|
14932
|
+
p20.log.info(
|
|
14933
|
+
`Switch menu active \u2014 proxy on port ${proxyHandle.port} ` + pc17.dim(`(${catalogRoutes.length} model${catalogRoutes.length !== 1 ? "s" : ""} in /model)`)
|
|
13694
14934
|
);
|
|
13695
14935
|
} catch (err) {
|
|
13696
|
-
|
|
14936
|
+
p20.log.error(`Failed to start proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
13697
14937
|
return 1;
|
|
13698
14938
|
}
|
|
13699
14939
|
const childEnv = buildChildEnv(
|
|
@@ -13706,7 +14946,7 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13706
14946
|
);
|
|
13707
14947
|
const debugLogPath = prepareClaudeTraceLog();
|
|
13708
14948
|
const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
|
|
13709
|
-
if (trace)
|
|
14949
|
+
if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
|
|
13710
14950
|
const exitCode = await launchClaude(
|
|
13711
14951
|
childEnv,
|
|
13712
14952
|
claudeCodeClientModelId(startingRoute.aliasId, contextWindow),
|
|
@@ -13718,14 +14958,14 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13718
14958
|
}
|
|
13719
14959
|
async function runModelsCommand() {
|
|
13720
14960
|
relayIntro("Favorite Models");
|
|
13721
|
-
const
|
|
13722
|
-
|
|
14961
|
+
const spinner10 = p20.spinner();
|
|
14962
|
+
spinner10.start("Loading providers...");
|
|
13723
14963
|
const catalog = await fetchProviderCatalog();
|
|
13724
|
-
|
|
14964
|
+
spinner10.stop("");
|
|
13725
14965
|
const allProviders = providersForPicker(catalog);
|
|
13726
14966
|
if (allProviders.length === 0) {
|
|
13727
|
-
|
|
13728
|
-
|
|
14967
|
+
p20.log.warn("No providers found.");
|
|
14968
|
+
p20.log.info(`${pc17.dim("OpenCode Zen/Go is always available. Add providers with ")}${pc17.cyan("relay-ai providers")}${pc17.dim(".")}`);
|
|
13729
14969
|
relayOutro("Done");
|
|
13730
14970
|
return 0;
|
|
13731
14971
|
}
|
|
@@ -13743,45 +14983,45 @@ async function runModelsCommand() {
|
|
|
13743
14983
|
for (let i = 0; i < favorites.length; i++) {
|
|
13744
14984
|
const fav = favorites[i];
|
|
13745
14985
|
const entry = modelLookup.get(`${fav.providerId}:${fav.modelId}`);
|
|
13746
|
-
const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${
|
|
14986
|
+
const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${pc17.dim(`(${entry.providerName})`)}` : pc17.dim(`\u2605 ${fav.modelId} \u2014 provider gone`);
|
|
13747
14987
|
options.push({ value: `fav-${i}`, label, hint: "select to remove" });
|
|
13748
14988
|
}
|
|
13749
14989
|
const atCap = favorites.length >= MAX_MODEL_CATALOG;
|
|
13750
14990
|
options.push({
|
|
13751
14991
|
value: "__add__",
|
|
13752
|
-
label: atCap ?
|
|
14992
|
+
label: atCap ? pc17.dim(`+ Add a model \u2192 (limit of ${MAX_MODEL_CATALOG} reached)`) : pc17.cyan("+ Add a model \u2192"),
|
|
13753
14993
|
hint: atCap ? "Remove a favorite first to make room" : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
|
|
13754
14994
|
});
|
|
13755
14995
|
options.push({ value: "__done__", label: "Done", hint: "" });
|
|
13756
14996
|
const header = favorites.length === 0 ? `Favorites (0/${MAX_MODEL_CATALOG})` : `Favorites (${favorites.length}/${MAX_MODEL_CATALOG}) \u2014 select to remove`;
|
|
13757
|
-
const choice = await
|
|
14997
|
+
const choice = await p20.select({
|
|
13758
14998
|
message: header,
|
|
13759
14999
|
options,
|
|
13760
15000
|
initialValue: "__done__"
|
|
13761
15001
|
});
|
|
13762
|
-
if (
|
|
15002
|
+
if (p20.isCancel(choice) || choice === "__done__") break;
|
|
13763
15003
|
if (choice === "__add__") {
|
|
13764
15004
|
if (atCap) {
|
|
13765
|
-
|
|
15005
|
+
p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 remove one first.`);
|
|
13766
15006
|
continue;
|
|
13767
15007
|
}
|
|
13768
15008
|
const globalCount = buildGlobalFavoriteIndex(allProviders).length;
|
|
13769
|
-
const addPath = await
|
|
15009
|
+
const addPath = await p20.select({
|
|
13770
15010
|
message: "Add a favorite",
|
|
13771
15011
|
options: [
|
|
13772
15012
|
{
|
|
13773
15013
|
value: "global",
|
|
13774
|
-
label:
|
|
15014
|
+
label: pc17.cyan("Search all providers"),
|
|
13775
15015
|
hint: `${globalCount} models \xB7 ${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""}`
|
|
13776
15016
|
},
|
|
13777
15017
|
{
|
|
13778
15018
|
value: "provider",
|
|
13779
|
-
label:
|
|
15019
|
+
label: pc17.cyan("Browse by provider \u2192"),
|
|
13780
15020
|
hint: "Pick one provider first"
|
|
13781
15021
|
}
|
|
13782
15022
|
]
|
|
13783
15023
|
});
|
|
13784
|
-
if (
|
|
15024
|
+
if (p20.isCancel(addPath)) continue;
|
|
13785
15025
|
let provider;
|
|
13786
15026
|
let browsedMultiple = [];
|
|
13787
15027
|
if (addPath === "global") {
|
|
@@ -13796,12 +15036,12 @@ async function runModelsCommand() {
|
|
|
13796
15036
|
let currentInitialProvider = void 0;
|
|
13797
15037
|
while (true) {
|
|
13798
15038
|
const providerOptions = allProviders.map((ap) => providerSelectOption(ap));
|
|
13799
|
-
const pickedProviderId = await
|
|
15039
|
+
const pickedProviderId = await p20.select({
|
|
13800
15040
|
message: "Which provider?",
|
|
13801
15041
|
options: providerOptions,
|
|
13802
15042
|
initialValue: currentInitialProvider
|
|
13803
15043
|
});
|
|
13804
|
-
if (
|
|
15044
|
+
if (p20.isCancel(pickedProviderId)) break;
|
|
13805
15045
|
provider = allProviders.find((ap) => ap.id === pickedProviderId);
|
|
13806
15046
|
const options2 = provider.models.map((m) => {
|
|
13807
15047
|
const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
|
|
@@ -13809,15 +15049,15 @@ async function runModelsCommand() {
|
|
|
13809
15049
|
return {
|
|
13810
15050
|
value: m.id,
|
|
13811
15051
|
label: fmtModel(label, m.id),
|
|
13812
|
-
hint: favorited ?
|
|
15052
|
+
hint: favorited ? pc17.yellow("\u2605 already favorite") : ""
|
|
13813
15053
|
};
|
|
13814
15054
|
});
|
|
13815
|
-
const pickedModelIds = await
|
|
13816
|
-
message: `Select models to add from ${provider.name} ${
|
|
15055
|
+
const pickedModelIds = await p20.multiselect({
|
|
15056
|
+
message: `Select models to add from ${provider.name} ${pc17.dim("(Space to select, Enter to confirm)")}`,
|
|
13817
15057
|
options: options2,
|
|
13818
15058
|
required: false
|
|
13819
15059
|
});
|
|
13820
|
-
if (
|
|
15060
|
+
if (p20.isCancel(pickedModelIds)) {
|
|
13821
15061
|
currentInitialProvider = provider.id;
|
|
13822
15062
|
continue;
|
|
13823
15063
|
}
|
|
@@ -13852,16 +15092,16 @@ async function runModelsCommand() {
|
|
|
13852
15092
|
if (addedModels.length > 0) {
|
|
13853
15093
|
if (addedModels.length === 1) {
|
|
13854
15094
|
const modelName = addedModels[0].name || addedModels[0].id;
|
|
13855
|
-
|
|
15095
|
+
p20.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
|
|
13856
15096
|
} else {
|
|
13857
|
-
|
|
15097
|
+
p20.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
|
|
13858
15098
|
}
|
|
13859
15099
|
}
|
|
13860
15100
|
if (duplicateCount > 0) {
|
|
13861
|
-
|
|
15101
|
+
p20.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
|
|
13862
15102
|
}
|
|
13863
15103
|
if (limitReached) {
|
|
13864
|
-
|
|
15104
|
+
p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
|
|
13865
15105
|
}
|
|
13866
15106
|
} else if (choice.startsWith("fav-")) {
|
|
13867
15107
|
const idx = parseInt(choice.slice(4), 10);
|
|
@@ -13870,7 +15110,7 @@ async function runModelsCommand() {
|
|
|
13870
15110
|
const label = entry ? `${entry.modelName} (${entry.providerName})` : fav.modelId;
|
|
13871
15111
|
favorites = removeFavorite(favorites, fav);
|
|
13872
15112
|
favoritesDirty = true;
|
|
13873
|
-
|
|
15113
|
+
p20.log.success(`Removed ${label} from favorites.`);
|
|
13874
15114
|
}
|
|
13875
15115
|
}
|
|
13876
15116
|
if (favoritesDirty) {
|
|
@@ -13878,7 +15118,7 @@ async function runModelsCommand() {
|
|
|
13878
15118
|
}
|
|
13879
15119
|
relayOutro(
|
|
13880
15120
|
favorites.length === 0 ? "No favorites saved" : `${favorites.length} favorite${favorites.length !== 1 ? "s" : ""} saved`,
|
|
13881
|
-
favorites.length === 0 ?
|
|
15121
|
+
favorites.length === 0 ? pc17.dim("Launch uses single-model mode") : pc17.cyan("/model menu ready on next launch")
|
|
13882
15122
|
);
|
|
13883
15123
|
return 0;
|
|
13884
15124
|
}
|
|
@@ -13889,7 +15129,7 @@ async function runClaudeCommand(parsed) {
|
|
|
13889
15129
|
setAgentStdoutMode(agentStdout);
|
|
13890
15130
|
const claudePath = findClaudeBinary();
|
|
13891
15131
|
if (!claudePath) {
|
|
13892
|
-
console.error(
|
|
15132
|
+
console.error(pc17.red("\nError: claude binary not found on PATH.\n"));
|
|
13893
15133
|
console.error("Install Claude Code:");
|
|
13894
15134
|
console.error(" npm install -g @anthropic-ai/claude-code\n");
|
|
13895
15135
|
return 1;
|
|
@@ -13904,7 +15144,7 @@ async function runClaudeCommand(parsed) {
|
|
|
13904
15144
|
prefs
|
|
13905
15145
|
});
|
|
13906
15146
|
if (launchPlan.error) {
|
|
13907
|
-
console.error(
|
|
15147
|
+
console.error(pc17.red(`
|
|
13908
15148
|
Error: ${launchPlan.error}
|
|
13909
15149
|
`));
|
|
13910
15150
|
return 1;
|
|
@@ -13912,7 +15152,7 @@ Error: ${launchPlan.error}
|
|
|
13912
15152
|
const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
|
|
13913
15153
|
if (!agentStdout) relayIntro("Claude Code");
|
|
13914
15154
|
if (setup && !dryRun && !agentStdout) {
|
|
13915
|
-
|
|
15155
|
+
p20.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
|
|
13916
15156
|
}
|
|
13917
15157
|
if (!dryRun && await needsFirstRunSetup()) {
|
|
13918
15158
|
const firstRun = await runFirstRunWizard(trace);
|
|
@@ -13923,25 +15163,25 @@ Error: ${launchPlan.error}
|
|
|
13923
15163
|
try {
|
|
13924
15164
|
catalog = await fetchProviderCatalog();
|
|
13925
15165
|
} catch (err) {
|
|
13926
|
-
console.error(
|
|
15166
|
+
console.error(pc17.red(String(err instanceof Error ? err.message : err)));
|
|
13927
15167
|
return 1;
|
|
13928
15168
|
}
|
|
13929
15169
|
} else {
|
|
13930
|
-
const catalogSpinner =
|
|
15170
|
+
const catalogSpinner = p20.spinner();
|
|
13931
15171
|
catalogSpinner.start("Loading your providers...");
|
|
13932
15172
|
try {
|
|
13933
15173
|
catalog = await fetchProviderCatalog();
|
|
13934
15174
|
} catch (err) {
|
|
13935
15175
|
catalogSpinner.stop("");
|
|
13936
|
-
console.error(
|
|
15176
|
+
console.error(pc17.red(String(err instanceof Error ? err.message : err)));
|
|
13937
15177
|
return 1;
|
|
13938
15178
|
}
|
|
13939
15179
|
catalogSpinner.stop("");
|
|
13940
15180
|
}
|
|
13941
15181
|
const allProviders = providersForPicker(catalog);
|
|
13942
15182
|
if (allProviders.length === 0) {
|
|
13943
|
-
|
|
13944
|
-
|
|
15183
|
+
p20.log.warn("No providers available.");
|
|
15184
|
+
p20.log.info(pc17.dim("Run relay-ai providers add or import to get started."));
|
|
13945
15185
|
return 0;
|
|
13946
15186
|
}
|
|
13947
15187
|
const providerOptions = allProviders.map((lp) => providerSelectOption(lp));
|
|
@@ -13958,7 +15198,7 @@ Error: ${launchPlan.error}
|
|
|
13958
15198
|
if (launchPlan.skip && launchPlan.target) {
|
|
13959
15199
|
const resolved = findProviderAndModel(allProviders, launchPlan.target);
|
|
13960
15200
|
if (!resolved) {
|
|
13961
|
-
|
|
15201
|
+
p20.log.error(
|
|
13962
15202
|
`Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
|
|
13963
15203
|
);
|
|
13964
15204
|
return 1;
|
|
@@ -13966,31 +15206,31 @@ Error: ${launchPlan.error}
|
|
|
13966
15206
|
activeProvider = resolved.provider;
|
|
13967
15207
|
selectedModel = resolved.model;
|
|
13968
15208
|
if (!agentStdout) {
|
|
13969
|
-
|
|
15209
|
+
p20.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
13970
15210
|
}
|
|
13971
15211
|
if (!dryRun) recordLaunchSelection("claude", activeProvider.id, selectedModel.id, prefs);
|
|
13972
15212
|
} else {
|
|
13973
15213
|
let currentInitialProvider = initialProvider;
|
|
13974
15214
|
while (true) {
|
|
13975
|
-
const chosen = await
|
|
15215
|
+
const chosen = await p20.select({
|
|
13976
15216
|
message: "Which provider?",
|
|
13977
15217
|
options: providerOptions,
|
|
13978
15218
|
initialValue: currentInitialProvider
|
|
13979
15219
|
});
|
|
13980
|
-
if (
|
|
13981
|
-
|
|
15220
|
+
if (p20.isCancel(chosen)) {
|
|
15221
|
+
p20.cancel("Cancelled.");
|
|
13982
15222
|
return 0;
|
|
13983
15223
|
}
|
|
13984
15224
|
const providerChoice = chosen;
|
|
13985
15225
|
if (providerChoice === "__favorites__") {
|
|
13986
15226
|
const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
|
|
13987
15227
|
if (!favoriteStart) {
|
|
13988
|
-
|
|
15228
|
+
p20.log.warn("No saved favorites are currently available.");
|
|
13989
15229
|
return 0;
|
|
13990
15230
|
}
|
|
13991
15231
|
activeProvider = favoriteStart.provider;
|
|
13992
15232
|
selectedModel = favoriteStart.model;
|
|
13993
|
-
|
|
15233
|
+
p20.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
13994
15234
|
break;
|
|
13995
15235
|
} else {
|
|
13996
15236
|
activeProvider = allProviders.find((lp) => lp.id === providerChoice);
|
|
@@ -14017,27 +15257,27 @@ Error: ${launchPlan.error}
|
|
|
14017
15257
|
);
|
|
14018
15258
|
const startingRoute = resolveRoute(activeProvider.id, selectedModel.id) ?? null;
|
|
14019
15259
|
if (!startingRoute) {
|
|
14020
|
-
|
|
15260
|
+
p20.log.error("Could not resolve a proxy route for the selected model.");
|
|
14021
15261
|
return 1;
|
|
14022
15262
|
}
|
|
14023
15263
|
const { routes: catalogRoutes, droppedFavorites } = buildCatalogRoutes(startingRoute, favorites, resolveRoute);
|
|
14024
15264
|
if (droppedFavorites.length > 0) {
|
|
14025
|
-
|
|
15265
|
+
p20.log.warn(
|
|
14026
15266
|
`Skipping ${droppedFavorites.length} favorite${droppedFavorites.length === 1 ? "" : "s"} that are no longer available in /model`
|
|
14027
15267
|
);
|
|
14028
15268
|
}
|
|
14029
15269
|
if (dryRun) {
|
|
14030
15270
|
const endpoint = selectedModel.baseUrl ?? selectedModel.completionsUrl ?? "(unknown)";
|
|
14031
15271
|
console.log("");
|
|
14032
|
-
console.log(
|
|
15272
|
+
console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute (switch-menu mode):")));
|
|
14033
15273
|
console.log("");
|
|
14034
|
-
console.log(` ${
|
|
14035
|
-
console.log(` ${
|
|
14036
|
-
console.log(` ${
|
|
14037
|
-
console.log(` ${
|
|
14038
|
-
catalogRoutes.forEach((r) => console.log(` ${
|
|
15274
|
+
console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
|
|
15275
|
+
console.log(` ${pc17.bold("Starting model:")} ${selectedModel.id}`);
|
|
15276
|
+
console.log(` ${pc17.bold("Endpoint:")} ${endpoint}`);
|
|
15277
|
+
console.log(` ${pc17.bold("/model catalog:")} ${catalogRoutes.length} model(s)`);
|
|
15278
|
+
catalogRoutes.forEach((r) => console.log(` ${pc17.dim(r.displayName)}`));
|
|
14039
15279
|
console.log("");
|
|
14040
|
-
console.log(
|
|
15280
|
+
console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
|
|
14041
15281
|
console.log("");
|
|
14042
15282
|
return 0;
|
|
14043
15283
|
}
|
|
@@ -14053,21 +15293,21 @@ Error: ${launchPlan.error}
|
|
|
14053
15293
|
const formatDesc = selectedModel.modelFormat === "anthropic" ? "direct passthrough" : "via SDK adapter proxy";
|
|
14054
15294
|
const endpoint = selectedModel.modelFormat === "anthropic" ? selectedModel.baseUrl ?? "(unknown)" : selectedModel.npm ?? "SDK";
|
|
14055
15295
|
console.log("");
|
|
14056
|
-
console.log(
|
|
15296
|
+
console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute:")));
|
|
14057
15297
|
console.log("");
|
|
14058
|
-
console.log(` ${
|
|
14059
|
-
console.log(` ${
|
|
14060
|
-
console.log(` ${
|
|
14061
|
-
console.log(` ${
|
|
14062
|
-
console.log(` ${
|
|
15298
|
+
console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
|
|
15299
|
+
console.log(` ${pc17.bold("Model:")} ${selectedModel.id}`);
|
|
15300
|
+
console.log(` ${pc17.bold("Format:")} ${selectedModel.modelFormat} (${formatDesc})`);
|
|
15301
|
+
console.log(` ${pc17.bold(selectedModel.modelFormat === "anthropic" ? "Endpoint:" : "SDK npm:")} ${endpoint}`);
|
|
15302
|
+
console.log(` ${pc17.bold("Key:")} ${activeProvider.name} provider key`);
|
|
14063
15303
|
console.log("");
|
|
14064
|
-
console.log(
|
|
15304
|
+
console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
|
|
14065
15305
|
console.log("");
|
|
14066
15306
|
return 0;
|
|
14067
15307
|
}
|
|
14068
15308
|
const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
|
|
14069
15309
|
if (!launchApiKey?.trim()) {
|
|
14070
|
-
|
|
15310
|
+
p20.log.error(
|
|
14071
15311
|
`No credential found for ${activeProvider.name}. Add a key with relay-ai providers or set OPENCODE_API_KEY.`
|
|
14072
15312
|
);
|
|
14073
15313
|
return 1;
|
|
@@ -14103,12 +15343,12 @@ Error: ${launchPlan.error}
|
|
|
14103
15343
|
launchApiKey
|
|
14104
15344
|
);
|
|
14105
15345
|
if (!isAgentStdoutMode()) {
|
|
14106
|
-
|
|
14107
|
-
`SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ?
|
|
15346
|
+
p20.log.info(
|
|
15347
|
+
`SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ? pc17.dim(` (${selectedModel.npm})`) : "")
|
|
14108
15348
|
);
|
|
14109
15349
|
}
|
|
14110
15350
|
} catch (err) {
|
|
14111
|
-
|
|
15351
|
+
p20.log.error(`Failed to start SDK adapter proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
14112
15352
|
return 1;
|
|
14113
15353
|
}
|
|
14114
15354
|
childEnv = buildChildEnv(
|
|
@@ -14124,7 +15364,7 @@ Error: ${launchPlan.error}
|
|
|
14124
15364
|
}
|
|
14125
15365
|
const debugLogPath = prepareClaudeTraceLog();
|
|
14126
15366
|
const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
|
|
14127
|
-
if (trace)
|
|
15367
|
+
if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
|
|
14128
15368
|
const exitCode = await launchClaude(
|
|
14129
15369
|
childEnv,
|
|
14130
15370
|
claudeCodeClientModelId(selectedModel.id, selectedModel.contextWindow),
|
|
@@ -14137,7 +15377,7 @@ Error: ${launchPlan.error}
|
|
|
14137
15377
|
async function main(args = process.argv.slice(2)) {
|
|
14138
15378
|
const parsed = parseArgs(args);
|
|
14139
15379
|
if (parsed.error) {
|
|
14140
|
-
console.error(
|
|
15380
|
+
console.error(pc17.red(`
|
|
14141
15381
|
Error: ${parsed.error}
|
|
14142
15382
|
`));
|
|
14143
15383
|
printHelp(rootHelpText());
|
|
@@ -14192,6 +15432,9 @@ Error: ${parsed.error}
|
|
|
14192
15432
|
printHelp(providersHelpText());
|
|
14193
15433
|
return 0;
|
|
14194
15434
|
}
|
|
15435
|
+
if (parsed.trace) {
|
|
15436
|
+
process.env.RELAY_AI_TRACE = "1";
|
|
15437
|
+
}
|
|
14195
15438
|
return runProvidersCommand(parsed.claudeArgs);
|
|
14196
15439
|
}
|
|
14197
15440
|
if (parsed.command === "codex-app") {
|
|
@@ -14223,6 +15466,20 @@ Error: ${parsed.error}
|
|
|
14223
15466
|
vertex: parsed.vertex
|
|
14224
15467
|
});
|
|
14225
15468
|
}
|
|
15469
|
+
if (parsed.command === "gemini") {
|
|
15470
|
+
if (parsed.showVersion) {
|
|
15471
|
+
console.log(VERSION);
|
|
15472
|
+
return 0;
|
|
15473
|
+
}
|
|
15474
|
+
if (parsed.showHelp) {
|
|
15475
|
+
console.log(geminiHelpText());
|
|
15476
|
+
return 0;
|
|
15477
|
+
}
|
|
15478
|
+
return runGeminiCommand(parsed.claudeArgs, parsed.trace, {
|
|
15479
|
+
launchProvider: parsed.launchProvider,
|
|
15480
|
+
launchModel: parsed.launchModel
|
|
15481
|
+
});
|
|
15482
|
+
}
|
|
14226
15483
|
if (parsed.showVersion) {
|
|
14227
15484
|
console.log(VERSION);
|
|
14228
15485
|
return 0;
|
|
@@ -14248,7 +15505,7 @@ if (isCliEntryPoint()) {
|
|
|
14248
15505
|
if (err === /* @__PURE__ */ Symbol.for("clack:cancel")) {
|
|
14249
15506
|
process.exit(0);
|
|
14250
15507
|
}
|
|
14251
|
-
console.error(
|
|
15508
|
+
console.error(pc17.red("\nUnexpected error:"), err);
|
|
14252
15509
|
process.exit(1);
|
|
14253
15510
|
});
|
|
14254
15511
|
}
|