@jacobbd/relay-ai 0.2.8 → 0.3.0
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 +11 -0
- package/README.md +23 -5
- package/dist/cli.js +1920 -676
- 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.0",
|
|
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")
|
|
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,7 +10001,7 @@ 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;
|
|
@@ -9874,7 +10010,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9874
10010
|
const resolved = resolveModel(routes, models, modelId);
|
|
9875
10011
|
if (!resolved) {
|
|
9876
10012
|
if (debug) {
|
|
9877
|
-
|
|
10013
|
+
log19(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
|
|
9878
10014
|
}
|
|
9879
10015
|
sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
|
|
9880
10016
|
return;
|
|
@@ -9894,7 +10030,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9894
10030
|
);
|
|
9895
10031
|
if (debug) {
|
|
9896
10032
|
const effort = body.reasoning?.effort;
|
|
9897
|
-
|
|
10033
|
+
log19(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
|
|
9898
10034
|
}
|
|
9899
10035
|
if (body.stream) {
|
|
9900
10036
|
res.writeHead(200, {
|
|
@@ -9924,7 +10060,7 @@ async function startCodexProxy(routes, options = {}) {
|
|
|
9924
10060
|
}
|
|
9925
10061
|
} catch (err) {
|
|
9926
10062
|
const msg = formatUpstreamError(err);
|
|
9927
|
-
|
|
10063
|
+
log19(`handler error: ${msg}`);
|
|
9928
10064
|
sendJson(res, 500, { error: { message: msg, type: "api_error" } });
|
|
9929
10065
|
}
|
|
9930
10066
|
return;
|
|
@@ -10150,7 +10286,7 @@ function restoreCodexOverlay(env = process.env) {
|
|
|
10150
10286
|
return removed;
|
|
10151
10287
|
}
|
|
10152
10288
|
function remainingOverlayPaths(env = process.env) {
|
|
10153
|
-
return ownedOverlayPaths(env).filter((
|
|
10289
|
+
return ownedOverlayPaths(env).filter((p21) => existsSync11(p21));
|
|
10154
10290
|
}
|
|
10155
10291
|
function recoverInterruptedCodexSession(env = process.env) {
|
|
10156
10292
|
const before = remainingOverlayPaths(env);
|
|
@@ -10547,8 +10683,8 @@ function resolveCodexFavorites(activeProvider, selectedModel, compatible, favori
|
|
|
10547
10683
|
agent,
|
|
10548
10684
|
localProviders: compatible,
|
|
10549
10685
|
zenGoApiKey,
|
|
10550
|
-
zenModels: compatible.find((
|
|
10551
|
-
goModels: compatible.find((
|
|
10686
|
+
zenModels: compatible.find((p21) => p21.id === "zen")?.models,
|
|
10687
|
+
goModels: compatible.find((p21) => p21.id === "go")?.models,
|
|
10552
10688
|
findLocalModel: (pid, mid) => {
|
|
10553
10689
|
const provider = compatible.find((lp) => lp.id === pid);
|
|
10554
10690
|
const model = provider?.models.find((m) => m.id === mid);
|
|
@@ -10620,8 +10756,24 @@ function isClaudeMachineReadableOutput(args) {
|
|
|
10620
10756
|
function isCodexMachineReadableOutput(args) {
|
|
10621
10757
|
return args.includes("--json");
|
|
10622
10758
|
}
|
|
10759
|
+
function isGeminiNonInteractive(args) {
|
|
10760
|
+
for (let i = 0; i < args.length; i++) {
|
|
10761
|
+
const arg = args[i];
|
|
10762
|
+
if (arg === "--") return false;
|
|
10763
|
+
if (arg === "-p" || arg === "--prompt" || arg === "-i" || arg === "--prompt-interactive") return true;
|
|
10764
|
+
if (arg.startsWith("-")) {
|
|
10765
|
+
i = skipAttachedFlagValue(args, i);
|
|
10766
|
+
continue;
|
|
10767
|
+
}
|
|
10768
|
+
return true;
|
|
10769
|
+
}
|
|
10770
|
+
return false;
|
|
10771
|
+
}
|
|
10623
10772
|
function wantsCleanAgentStdout(agent, childArgs) {
|
|
10624
|
-
|
|
10773
|
+
if (agent === "claude") return isClaudeMachineReadableOutput(childArgs);
|
|
10774
|
+
if (agent === "codex") return isCodexMachineReadableOutput(childArgs);
|
|
10775
|
+
const outFmt = readFlagValue(childArgs, "-o") || readFlagValue(childArgs, "--output-format");
|
|
10776
|
+
return outFmt === "json" || outFmt === "stream-json";
|
|
10625
10777
|
}
|
|
10626
10778
|
function normalizeClaudeAgentArgs(args) {
|
|
10627
10779
|
const out = [...args];
|
|
@@ -10653,14 +10805,14 @@ function isCodexNonInteractive(args) {
|
|
|
10653
10805
|
}
|
|
10654
10806
|
function resolveLaunchTarget(explicit, prefs, agent) {
|
|
10655
10807
|
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);
|
|
10808
|
+
const providerId = explicit.providerId ?? slug?.providerId ?? (agent === "claude" ? prefs.lastProvider : agent === "codex" ? prefs.lastCodexProvider : prefs.lastGeminiProvider);
|
|
10809
|
+
const modelId = slug?.modelId ?? explicit.modelId ?? (agent === "claude" ? prefs.lastModel : agent === "codex" ? prefs.lastCodexModel : prefs.lastGeminiModel);
|
|
10658
10810
|
if (!providerId || !modelId) return null;
|
|
10659
10811
|
return { providerId, modelId };
|
|
10660
10812
|
}
|
|
10661
10813
|
function findProviderAndModel(providers, target) {
|
|
10662
10814
|
if (!target.providerId || !target.modelId) return null;
|
|
10663
|
-
const provider = providers.find((
|
|
10815
|
+
const provider = providers.find((p21) => p21.id === target.providerId);
|
|
10664
10816
|
if (!provider) return null;
|
|
10665
10817
|
const model = provider.models.find((m) => m.id === target.modelId);
|
|
10666
10818
|
if (!model) return null;
|
|
@@ -10677,7 +10829,7 @@ function hasCompleteExplicitLaunch(explicit) {
|
|
|
10677
10829
|
function planLaunchWizard(opts) {
|
|
10678
10830
|
const { explicit, childArgs, agent, prefs } = opts;
|
|
10679
10831
|
const explicitComplete = hasCompleteExplicitLaunch(explicit);
|
|
10680
|
-
const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : isCodexNonInteractive(childArgs);
|
|
10832
|
+
const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : agent === "codex" ? isCodexNonInteractive(childArgs) : isGeminiNonInteractive(childArgs);
|
|
10681
10833
|
if (explicitComplete) {
|
|
10682
10834
|
const target = resolveLaunchTarget(explicit, prefs, agent);
|
|
10683
10835
|
if (!target) {
|
|
@@ -10710,7 +10862,9 @@ function planLaunchWizard(opts) {
|
|
|
10710
10862
|
return { skip: false, target: null };
|
|
10711
10863
|
}
|
|
10712
10864
|
function nonInteractiveLaunchError(agent) {
|
|
10713
|
-
|
|
10865
|
+
if (agent === "claude") return "Print mode requires --provider and --model, or saved preferences from a prior launch.";
|
|
10866
|
+
if (agent === "codex") return "Non-interactive Codex launch requires --provider and --model, or saved preferences from a prior launch.";
|
|
10867
|
+
return "Non-interactive Gemini launch requires --provider and --model, or saved preferences from a prior launch.";
|
|
10714
10868
|
}
|
|
10715
10869
|
|
|
10716
10870
|
// src/codex.ts
|
|
@@ -11121,7 +11275,7 @@ Error: ${launchPlan.error}
|
|
|
11121
11275
|
resolvedFavorites = res.resolvedFavorites;
|
|
11122
11276
|
providersById = res.providersById;
|
|
11123
11277
|
}
|
|
11124
|
-
const regEntry = loadRegistry().providers.find((
|
|
11278
|
+
const regEntry = loadRegistry().providers.find((p21) => p21.id === activeProvider.id);
|
|
11125
11279
|
const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
|
|
11126
11280
|
const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
|
|
11127
11281
|
if (!apiKey) {
|
|
@@ -11175,7 +11329,7 @@ Error: ${launchPlan.error}
|
|
|
11175
11329
|
});
|
|
11176
11330
|
if (configOnly) {
|
|
11177
11331
|
const home = process.env["HOME"] ?? "";
|
|
11178
|
-
const shortenPath = (
|
|
11332
|
+
const shortenPath = (p21) => home ? p21.replace(home, "~") : p21;
|
|
11179
11333
|
console.log("");
|
|
11180
11334
|
console.log(pc13.bold(pc13.cyan(" CONFIG PREVIEW \u2014 relay-ai codex")));
|
|
11181
11335
|
console.log("");
|
|
@@ -11239,126 +11393,1128 @@ Error: ${launchPlan.error}
|
|
|
11239
11393
|
}
|
|
11240
11394
|
}
|
|
11241
11395
|
|
|
11242
|
-
// src/
|
|
11396
|
+
// src/gemini.ts
|
|
11243
11397
|
import pc14 from "picocolors";
|
|
11244
11398
|
import * as p15 from "@clack/prompts";
|
|
11245
11399
|
|
|
11246
|
-
// src/
|
|
11247
|
-
import {
|
|
11248
|
-
import {
|
|
11249
|
-
import {
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11400
|
+
// src/gemini/launch.ts
|
|
11401
|
+
import { execSync as execSync4, spawn as spawn5 } from "child_process";
|
|
11402
|
+
import { existsSync as existsSync13 } from "fs";
|
|
11403
|
+
import { homedir as homedir10 } from "os";
|
|
11404
|
+
import { join as join14 } from "path";
|
|
11405
|
+
var isWindows4 = process.platform === "win32";
|
|
11406
|
+
var GEMINI_FALLBACK_PATHS = isWindows4 ? [
|
|
11407
|
+
join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini.cmd"),
|
|
11408
|
+
join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini")
|
|
11409
|
+
] : [
|
|
11410
|
+
join14(homedir10(), ".local", "bin", "gemini"),
|
|
11411
|
+
join14(homedir10(), ".npm", "bin", "gemini"),
|
|
11412
|
+
"/usr/local/bin/gemini",
|
|
11413
|
+
"/opt/homebrew/bin/gemini"
|
|
11414
|
+
];
|
|
11415
|
+
function findGeminiBinary() {
|
|
11416
|
+
try {
|
|
11417
|
+
const result = execSync4(isWindows4 ? "where.exe gemini" : "which gemini", {
|
|
11418
|
+
encoding: "utf8",
|
|
11419
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11420
|
+
});
|
|
11421
|
+
const path = result.trim().split("\n")[0]?.trim();
|
|
11422
|
+
if (path) return path;
|
|
11423
|
+
} catch {
|
|
11424
|
+
}
|
|
11425
|
+
for (const path of GEMINI_FALLBACK_PATHS) {
|
|
11426
|
+
if (existsSync13(path)) return path;
|
|
11427
|
+
}
|
|
11428
|
+
return null;
|
|
11258
11429
|
}
|
|
11259
|
-
function
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11430
|
+
function buildGeminiChildEnv(proxyPort, proxyToken) {
|
|
11431
|
+
const env = { ...process.env };
|
|
11432
|
+
delete env["GOOGLE_GEMINI_BASE_URL"];
|
|
11433
|
+
delete env["GEMINI_API_KEY"];
|
|
11434
|
+
delete env["GOOGLE_API_KEY"];
|
|
11435
|
+
delete env["GOOGLE_GENAI_API_KEY"];
|
|
11436
|
+
env["GOOGLE_GEMINI_BASE_URL"] = `http://127.0.0.1:${proxyPort}`;
|
|
11437
|
+
env["GEMINI_API_KEY"] = proxyToken;
|
|
11438
|
+
return env;
|
|
11263
11439
|
}
|
|
11264
|
-
function
|
|
11265
|
-
|
|
11266
|
-
|
|
11440
|
+
function launchGemini(geminiPath, modelId, env, extraArgs) {
|
|
11441
|
+
return new Promise((resolve) => {
|
|
11442
|
+
const args = ["-m", modelId, ...extraArgs];
|
|
11443
|
+
const child = spawn5(geminiPath, args, {
|
|
11444
|
+
stdio: "inherit",
|
|
11445
|
+
env,
|
|
11446
|
+
shell: isWindows4
|
|
11447
|
+
});
|
|
11448
|
+
const onSigInt = () => child.kill("SIGINT");
|
|
11449
|
+
const onSigTerm = () => child.kill("SIGTERM");
|
|
11450
|
+
process.once("SIGINT", onSigInt);
|
|
11451
|
+
process.once("SIGTERM", onSigTerm);
|
|
11452
|
+
const done = (code) => {
|
|
11453
|
+
process.off("SIGINT", onSigInt);
|
|
11454
|
+
process.off("SIGTERM", onSigTerm);
|
|
11455
|
+
resolve(code);
|
|
11456
|
+
};
|
|
11457
|
+
child.on("error", () => done(1));
|
|
11458
|
+
child.on("exit", (code) => done(code ?? 0));
|
|
11459
|
+
});
|
|
11267
11460
|
}
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11461
|
+
|
|
11462
|
+
// src/gemini/prompts.ts
|
|
11463
|
+
import * as p14 from "@clack/prompts";
|
|
11464
|
+
async function pickGeminiProvider(providers, prefs, hasFavorites = false, initialProviderId) {
|
|
11465
|
+
if (providers.length === 0 && !hasFavorites) return null;
|
|
11466
|
+
const options = providers.map((lp) => providerSelectOption(lp));
|
|
11467
|
+
if (hasFavorites) {
|
|
11468
|
+
options.unshift({
|
|
11469
|
+
value: "__favorites__",
|
|
11470
|
+
label: "\u2B50 Favorites Catalog",
|
|
11471
|
+
hint: `${prefs.favoriteModels?.length ?? 0} saved favorites`
|
|
11472
|
+
});
|
|
11473
|
+
}
|
|
11474
|
+
const initial = initialProviderId && options.some((o) => o.value === initialProviderId) ? initialProviderId : prefs.lastGeminiProvider && options.some((o) => o.value === prefs.lastGeminiProvider) ? prefs.lastGeminiProvider : options[0].value;
|
|
11475
|
+
const chosen = await p14.select({
|
|
11476
|
+
message: "Which provider for Gemini CLI?",
|
|
11477
|
+
options,
|
|
11478
|
+
initialValue: initial
|
|
11479
|
+
});
|
|
11480
|
+
if (p14.isCancel(chosen)) {
|
|
11481
|
+
p14.cancel("Cancelled.");
|
|
11482
|
+
return null;
|
|
11483
|
+
}
|
|
11484
|
+
if (chosen === "__favorites__") return "__favorites__";
|
|
11485
|
+
return providers.find((lp) => lp.id === chosen) ?? null;
|
|
11271
11486
|
}
|
|
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
|
-
|
|
11487
|
+
async function pickGeminiModel(provider, prefs) {
|
|
11488
|
+
const recentIds = (prefs.recentModelsByProvider?.[provider.id] ?? []).slice(0, 3);
|
|
11489
|
+
const recentModels = recentIds.map((id) => provider.models.find((m) => m.id === id)).filter((m) => m !== void 0);
|
|
11490
|
+
let selectedModel = null;
|
|
11491
|
+
while (true) {
|
|
11492
|
+
if (recentModels.length > 0) {
|
|
11493
|
+
const options = [
|
|
11494
|
+
...recentModels.map((m) => modelSelectOption(m, "recent")),
|
|
11495
|
+
navOption("__browse_all__", "Browse all models \u2192", `${provider.models.length} available`),
|
|
11496
|
+
navOption("__back__", "\u2190 Go back", "Select a different provider")
|
|
11497
|
+
];
|
|
11498
|
+
const picked = await p14.select({
|
|
11499
|
+
message: `Model for ${provider.name}?`,
|
|
11500
|
+
options,
|
|
11501
|
+
initialValue: recentModels[0].id
|
|
11502
|
+
});
|
|
11503
|
+
if (p14.isCancel(picked) || String(picked) === "__back__") {
|
|
11504
|
+
return "back";
|
|
11505
|
+
}
|
|
11506
|
+
if (String(picked) === "__browse_all__") {
|
|
11507
|
+
const browsed = await browseAllModels(provider, prefs);
|
|
11508
|
+
if (browsed === "back") {
|
|
11509
|
+
continue;
|
|
11510
|
+
}
|
|
11511
|
+
if (!browsed) return null;
|
|
11512
|
+
selectedModel = browsed;
|
|
11513
|
+
break;
|
|
11514
|
+
} else {
|
|
11515
|
+
selectedModel = recentModels.find((m) => m.id === String(picked));
|
|
11516
|
+
break;
|
|
11517
|
+
}
|
|
11518
|
+
} else {
|
|
11519
|
+
const browsed = await browseAllModels(provider, prefs);
|
|
11520
|
+
if (browsed === "back") {
|
|
11521
|
+
return "back";
|
|
11522
|
+
}
|
|
11523
|
+
if (!browsed) return null;
|
|
11524
|
+
selectedModel = browsed;
|
|
11525
|
+
break;
|
|
11526
|
+
}
|
|
11527
|
+
}
|
|
11528
|
+
return selectedModel;
|
|
11294
11529
|
}
|
|
11295
|
-
function
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11530
|
+
function confirmGeminiLaunch(providerName, modelLabel, modelId) {
|
|
11531
|
+
return p14.confirm({
|
|
11532
|
+
message: confirmLaunchMessage("Gemini CLI", modelLabel, modelId, providerName),
|
|
11533
|
+
initialValue: true
|
|
11534
|
+
}).then((answer) => {
|
|
11535
|
+
if (p14.isCancel(answer)) {
|
|
11536
|
+
p14.cancel("Cancelled.");
|
|
11537
|
+
return false;
|
|
11538
|
+
}
|
|
11539
|
+
return answer;
|
|
11540
|
+
});
|
|
11302
11541
|
}
|
|
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;
|
|
11542
|
+
async function pickGeminiFavoriteModel(providers, favorites) {
|
|
11543
|
+
const favList = [];
|
|
11544
|
+
for (const fav of favorites) {
|
|
11545
|
+
const provider2 = providers.find((lp) => lp.id === fav.providerId);
|
|
11546
|
+
const model2 = provider2?.models.find((m) => m.id === fav.modelId);
|
|
11547
|
+
if (provider2 && model2) favList.push({ provider: provider2, model: model2 });
|
|
11319
11548
|
}
|
|
11320
|
-
if (
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
out.model_providers = providers;
|
|
11549
|
+
if (favList.length === 0) {
|
|
11550
|
+
p14.log.warn("None of your saved favorites are available in the current registry.");
|
|
11551
|
+
return null;
|
|
11324
11552
|
}
|
|
11325
|
-
const
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11553
|
+
const options = [
|
|
11554
|
+
...favList.map(({ provider: provider2, model: model2 }) => ({
|
|
11555
|
+
value: `${provider2.id}::${model2.id}`,
|
|
11556
|
+
label: model2.name || model2.id,
|
|
11557
|
+
hint: provider2.name
|
|
11558
|
+
})),
|
|
11559
|
+
{ value: "__back__", label: "\u2190 Go back", hint: "Select a different provider" }
|
|
11560
|
+
];
|
|
11561
|
+
const picked = await p14.select({
|
|
11562
|
+
message: "Pick a favorite model for Gemini CLI:",
|
|
11563
|
+
options,
|
|
11564
|
+
initialValue: options[0].value
|
|
11565
|
+
});
|
|
11566
|
+
if (p14.isCancel(picked) || String(picked) === "__back__") return "back";
|
|
11567
|
+
const [pickedProviderId, pickedModelId] = picked.split("::");
|
|
11568
|
+
const provider = providers.find((lp) => lp.id === pickedProviderId);
|
|
11569
|
+
const model = provider?.models.find((m) => m.id === pickedModelId);
|
|
11570
|
+
if (!provider || !model) return null;
|
|
11571
|
+
return { provider, model };
|
|
11572
|
+
}
|
|
11573
|
+
function rejectGeminiManagedFlags(geminiArgs) {
|
|
11574
|
+
const blocked = /* @__PURE__ */ new Set(["--provider", "--model", "-m", "--trace"]);
|
|
11575
|
+
const takesValue = /* @__PURE__ */ new Set(["--provider", "--model", "-m"]);
|
|
11576
|
+
const out = [];
|
|
11577
|
+
for (let i = 0; i < geminiArgs.length; i++) {
|
|
11578
|
+
const arg = geminiArgs[i];
|
|
11579
|
+
if (blocked.has(arg)) {
|
|
11580
|
+
if (takesValue.has(arg)) i++;
|
|
11581
|
+
continue;
|
|
11340
11582
|
}
|
|
11583
|
+
if (arg.startsWith("--model=") || arg.startsWith("--provider=") || arg.startsWith("-m=")) continue;
|
|
11584
|
+
out.push(arg);
|
|
11341
11585
|
}
|
|
11342
11586
|
return out;
|
|
11343
11587
|
}
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
if (
|
|
11351
|
-
|
|
11588
|
+
|
|
11589
|
+
// src/gemini-proxy.ts
|
|
11590
|
+
import { createServer as createServer4 } from "http";
|
|
11591
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
11592
|
+
import { streamText as streamText4, generateText as generateText4, tool as tool4, jsonSchema as jsonSchema4 } from "ai";
|
|
11593
|
+
function mapFinishReason(reason) {
|
|
11594
|
+
if (reason === "stop" || reason === "tool-calls") return "STOP";
|
|
11595
|
+
if (reason === "length") return "MAX_TOKENS";
|
|
11596
|
+
if (reason === "content-filter") return "SAFETY";
|
|
11597
|
+
return "OTHER";
|
|
11598
|
+
}
|
|
11599
|
+
function lookupGeminiRoute(routes, requestedModel) {
|
|
11600
|
+
const ids = [requestedModel, ...routeLookupIds(requestedModel)];
|
|
11601
|
+
const slashIdx = requestedModel.indexOf("/");
|
|
11602
|
+
if (slashIdx >= 0) {
|
|
11603
|
+
const after = requestedModel.slice(slashIdx + 1);
|
|
11604
|
+
ids.push(after, ...routeLookupIds(after));
|
|
11352
11605
|
}
|
|
11353
|
-
const
|
|
11354
|
-
if (
|
|
11355
|
-
|
|
11606
|
+
const doubleUnderscore = requestedModel.indexOf("__");
|
|
11607
|
+
if (doubleUnderscore >= 0) {
|
|
11608
|
+
const after = requestedModel.slice(doubleUnderscore + 2);
|
|
11609
|
+
ids.push(after, ...routeLookupIds(after));
|
|
11356
11610
|
}
|
|
11357
|
-
const
|
|
11358
|
-
|
|
11359
|
-
|
|
11611
|
+
const uniqueIds = [...new Set(ids)];
|
|
11612
|
+
for (const id of uniqueIds) {
|
|
11613
|
+
const route = routes.find((r) => r.aliasId === id || r.realModelId === id);
|
|
11614
|
+
if (route) return route;
|
|
11360
11615
|
}
|
|
11361
|
-
|
|
11616
|
+
return void 0;
|
|
11617
|
+
}
|
|
11618
|
+
function mergeConsecutiveMessages2(messages) {
|
|
11619
|
+
const merged = [];
|
|
11620
|
+
for (const msg of messages) {
|
|
11621
|
+
if (merged.length === 0) {
|
|
11622
|
+
merged.push(msg);
|
|
11623
|
+
continue;
|
|
11624
|
+
}
|
|
11625
|
+
const last = merged[merged.length - 1];
|
|
11626
|
+
if (last.role === msg.role) {
|
|
11627
|
+
const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }];
|
|
11628
|
+
const nextContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
|
|
11629
|
+
last.content = [...lastContent, ...nextContent];
|
|
11630
|
+
} else {
|
|
11631
|
+
merged.push(msg);
|
|
11632
|
+
}
|
|
11633
|
+
}
|
|
11634
|
+
return merged;
|
|
11635
|
+
}
|
|
11636
|
+
function stripGeminiIdentity(text5) {
|
|
11637
|
+
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");
|
|
11638
|
+
}
|
|
11639
|
+
function translateGeminiRequest(body) {
|
|
11640
|
+
let system;
|
|
11641
|
+
if (body.systemInstruction?.parts) {
|
|
11642
|
+
const rawSystem = body.systemInstruction.parts.map((p21) => p21.text || "").join("\n");
|
|
11643
|
+
system = stripGeminiIdentity(rawSystem).trim();
|
|
11644
|
+
}
|
|
11645
|
+
const messages = [];
|
|
11646
|
+
const nameToIdList = /* @__PURE__ */ new Map();
|
|
11647
|
+
const contents = body.contents || [];
|
|
11648
|
+
for (const turn of contents) {
|
|
11649
|
+
const role = turn.role === "model" ? "assistant" : "user";
|
|
11650
|
+
const parts = [];
|
|
11651
|
+
const toolResults = [];
|
|
11652
|
+
const turnParts = turn.parts || [];
|
|
11653
|
+
for (const p21 of turnParts) {
|
|
11654
|
+
if (p21.text !== void 0) {
|
|
11655
|
+
const text5 = stripGeminiIdentity(p21.text);
|
|
11656
|
+
if (text5.includes("<thinking>")) {
|
|
11657
|
+
const tokens = text5.split(/<thinking>([\s\S]*?)<\/thinking>/);
|
|
11658
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
11659
|
+
const token = tokens[i].trim();
|
|
11660
|
+
if (!token) continue;
|
|
11661
|
+
parts.push({ type: i % 2 === 1 ? "reasoning" : "text", text: token });
|
|
11662
|
+
}
|
|
11663
|
+
} else {
|
|
11664
|
+
parts.push({ type: "text", text: text5 });
|
|
11665
|
+
}
|
|
11666
|
+
} else if (p21.inlineData) {
|
|
11667
|
+
parts.push({
|
|
11668
|
+
type: "image",
|
|
11669
|
+
image: Buffer.from(p21.inlineData.data, "base64"),
|
|
11670
|
+
mediaType: p21.inlineData.mimeType
|
|
11671
|
+
});
|
|
11672
|
+
} else if (p21.functionCall) {
|
|
11673
|
+
const id = "call_" + randomUUID2().replace(/-/g, "");
|
|
11674
|
+
const name = p21.functionCall.name;
|
|
11675
|
+
if (!nameToIdList.has(name)) nameToIdList.set(name, []);
|
|
11676
|
+
nameToIdList.get(name).push(id);
|
|
11677
|
+
parts.push({
|
|
11678
|
+
type: "tool-call",
|
|
11679
|
+
toolCallId: id,
|
|
11680
|
+
toolName: name,
|
|
11681
|
+
input: p21.functionCall.args || {}
|
|
11682
|
+
});
|
|
11683
|
+
} else if (p21.functionResponse) {
|
|
11684
|
+
const name = p21.functionResponse.name;
|
|
11685
|
+
const idList = nameToIdList.get(name) || [];
|
|
11686
|
+
const id = idList.shift() || "call_" + randomUUID2().replace(/-/g, "");
|
|
11687
|
+
toolResults.push({
|
|
11688
|
+
type: "tool-result",
|
|
11689
|
+
toolCallId: id,
|
|
11690
|
+
toolName: name,
|
|
11691
|
+
output: {
|
|
11692
|
+
type: "text",
|
|
11693
|
+
value: typeof p21.functionResponse.response === "string" ? p21.functionResponse.response : JSON.stringify(p21.functionResponse.response || {})
|
|
11694
|
+
}
|
|
11695
|
+
});
|
|
11696
|
+
}
|
|
11697
|
+
}
|
|
11698
|
+
if (toolResults.length > 0) {
|
|
11699
|
+
messages.push({
|
|
11700
|
+
role: "tool",
|
|
11701
|
+
content: toolResults
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
11704
|
+
if (parts.length > 0) {
|
|
11705
|
+
messages.push({
|
|
11706
|
+
role,
|
|
11707
|
+
content: parts
|
|
11708
|
+
});
|
|
11709
|
+
}
|
|
11710
|
+
}
|
|
11711
|
+
const mergedMessages = mergeConsecutiveMessages2(messages);
|
|
11712
|
+
let tools;
|
|
11713
|
+
if (body.tools) {
|
|
11714
|
+
tools = {};
|
|
11715
|
+
for (const t of body.tools) {
|
|
11716
|
+
if (t.functionDeclarations) {
|
|
11717
|
+
for (const fd of t.functionDeclarations) {
|
|
11718
|
+
tools[fd.name] = tool4({
|
|
11719
|
+
description: fd.description || "",
|
|
11720
|
+
inputSchema: jsonSchema4(fd.parameters || { type: "object", properties: {} })
|
|
11721
|
+
});
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
let toolChoice;
|
|
11727
|
+
const mode = body.toolConfig?.functionCallingConfig?.mode;
|
|
11728
|
+
if (mode === "ANY") {
|
|
11729
|
+
toolChoice = "required";
|
|
11730
|
+
} else if (mode === "AUTO") {
|
|
11731
|
+
toolChoice = "auto";
|
|
11732
|
+
}
|
|
11733
|
+
const generationConfig = body.generationConfig || {};
|
|
11734
|
+
let responseFormat;
|
|
11735
|
+
if (generationConfig.responseMimeType === "application/json") {
|
|
11736
|
+
responseFormat = { type: "json" };
|
|
11737
|
+
}
|
|
11738
|
+
return {
|
|
11739
|
+
system,
|
|
11740
|
+
messages: mergedMessages,
|
|
11741
|
+
tools: tools && Object.keys(tools).length > 0 ? tools : void 0,
|
|
11742
|
+
toolChoice,
|
|
11743
|
+
maxOutputTokens: generationConfig.maxOutputTokens,
|
|
11744
|
+
temperature: generationConfig.temperature,
|
|
11745
|
+
responseFormat
|
|
11746
|
+
};
|
|
11747
|
+
}
|
|
11748
|
+
async function startGeminiProxy(routes, debug = false) {
|
|
11749
|
+
const proxyToken = randomUUID2();
|
|
11750
|
+
silenceSdkWarnings();
|
|
11751
|
+
if (routes.length === 0) {
|
|
11752
|
+
return Promise.reject(new Error("Gemini proxy requires at least one route"));
|
|
11753
|
+
}
|
|
11754
|
+
const defaultRoute = routes[0];
|
|
11755
|
+
const models = /* @__PURE__ */ new Map();
|
|
11756
|
+
const plog = debug ? makeTraceLogger(getGeminiProxyDebugLogPath()) : () => {
|
|
11757
|
+
};
|
|
11758
|
+
const onRejection = (reason) => {
|
|
11759
|
+
plog(`Unhandled Rejection: ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
|
|
11760
|
+
};
|
|
11761
|
+
const onException = (error) => {
|
|
11762
|
+
plog(`Uncaught Exception: ${error.stack || error.message}`);
|
|
11763
|
+
};
|
|
11764
|
+
const getOrInitModel = async (route) => {
|
|
11765
|
+
let m = models.get(route.aliasId);
|
|
11766
|
+
if (!m) {
|
|
11767
|
+
m = await createLanguageModel({
|
|
11768
|
+
npm: route.npm || "@ai-sdk/openai-compatible",
|
|
11769
|
+
modelId: route.realModelId,
|
|
11770
|
+
apiKey: route.apiKey,
|
|
11771
|
+
baseURL: route.baseURL,
|
|
11772
|
+
providerId: route.aliasId,
|
|
11773
|
+
authType: route.authType,
|
|
11774
|
+
oauthAccountId: route.oauthAccountId
|
|
11775
|
+
});
|
|
11776
|
+
models.set(route.aliasId, m);
|
|
11777
|
+
}
|
|
11778
|
+
return m;
|
|
11779
|
+
};
|
|
11780
|
+
const formatGeminiModel = (route) => ({
|
|
11781
|
+
name: `models/${route.aliasId}`,
|
|
11782
|
+
version: "1.0",
|
|
11783
|
+
displayName: route.displayName,
|
|
11784
|
+
description: "Registry model routed through relay-ai proxy",
|
|
11785
|
+
inputTokenLimit: route.contextWindow || 1e6,
|
|
11786
|
+
outputTokenLimit: 8192,
|
|
11787
|
+
supportedGenerationMethods: ["generateContent", "streamGenerateContent"]
|
|
11788
|
+
});
|
|
11789
|
+
let sessionRouteOverride = void 0;
|
|
11790
|
+
const server = createServer4(async (req, res) => {
|
|
11791
|
+
try {
|
|
11792
|
+
const url = req.url ?? "";
|
|
11793
|
+
plog(`${req.method} ${url}`);
|
|
11794
|
+
if (req.method === "GET" && (url.endsWith("/models") || url.includes("/models?"))) {
|
|
11795
|
+
plog("GET models list");
|
|
11796
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
11797
|
+
const payload = JSON.stringify({
|
|
11798
|
+
models: routes.map(formatGeminiModel)
|
|
11799
|
+
});
|
|
11800
|
+
plog(`Response: ${payload}`);
|
|
11801
|
+
res.end(payload);
|
|
11802
|
+
return;
|
|
11803
|
+
}
|
|
11804
|
+
if (req.method === "GET" && url.includes("/models/")) {
|
|
11805
|
+
const modelMatch = url.match(/\/models\/([^?]+)/);
|
|
11806
|
+
if (modelMatch) {
|
|
11807
|
+
const modelId = decodeURIComponent(modelMatch[1]);
|
|
11808
|
+
const route = lookupGeminiRoute(routes, modelId) ?? defaultRoute;
|
|
11809
|
+
plog(`GET model details: ${modelId} -> mapped to route ${route.aliasId}`);
|
|
11810
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
11811
|
+
const payload = JSON.stringify(formatGeminiModel(route));
|
|
11812
|
+
plog(`Response: ${payload}`);
|
|
11813
|
+
res.end(payload);
|
|
11814
|
+
return;
|
|
11815
|
+
}
|
|
11816
|
+
}
|
|
11817
|
+
if (req.method === "POST" && url.includes(":")) {
|
|
11818
|
+
const isStream = url.includes("streamGenerateContent");
|
|
11819
|
+
const rawBody = await readBody(req);
|
|
11820
|
+
plog(`Request body:
|
|
11821
|
+
${rawBody}`);
|
|
11822
|
+
let body;
|
|
11823
|
+
try {
|
|
11824
|
+
body = JSON.parse(rawBody);
|
|
11825
|
+
} catch {
|
|
11826
|
+
plog("Error: Invalid JSON body");
|
|
11827
|
+
res.writeHead(400);
|
|
11828
|
+
res.end("Invalid JSON");
|
|
11829
|
+
return;
|
|
11830
|
+
}
|
|
11831
|
+
const modelMatch = url.match(/\/models\/([^:]+)/);
|
|
11832
|
+
const requestedModel = modelMatch ? decodeURIComponent(modelMatch[1]) : defaultRoute.aliasId;
|
|
11833
|
+
const lastUserTurn = findLastUserTurn(body.contents || []);
|
|
11834
|
+
const modelCommand = parseModelCommand(lastUserTurn);
|
|
11835
|
+
if (modelCommand !== null) {
|
|
11836
|
+
if (modelCommand === "") {
|
|
11837
|
+
const current = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
|
|
11838
|
+
const availableList = routes.map((r) => ` - ${r.aliasId} (${r.displayName})`).join("\n");
|
|
11839
|
+
const exampleId = routes.length > 1 ? routes[1].aliasId : routes[0]?.aliasId ?? "deepseek-v4";
|
|
11840
|
+
const text5 = `Current model: ${current.displayName} (${current.aliasId})
|
|
11841
|
+
|
|
11842
|
+
Available models:
|
|
11843
|
+
${availableList}
|
|
11844
|
+
|
|
11845
|
+
\u{1F4A1} To switch models, type: .model <id>
|
|
11846
|
+
Example: .model ${exampleId}`;
|
|
11847
|
+
sendMockGeminiResponse(res, text5, isStream);
|
|
11848
|
+
return;
|
|
11849
|
+
}
|
|
11850
|
+
const targetRoute = lookupGeminiRoute(routes, modelCommand);
|
|
11851
|
+
if (targetRoute) {
|
|
11852
|
+
sessionRouteOverride = targetRoute;
|
|
11853
|
+
plog(`.model switch: ${targetRoute.aliasId} (${targetRoute.realModelId})`);
|
|
11854
|
+
sendMockGeminiResponse(res, `\u2705 Switched model to ${targetRoute.displayName} (${targetRoute.aliasId})`, isStream);
|
|
11855
|
+
} else {
|
|
11856
|
+
const available = routes.map((r) => r.aliasId).join(", ");
|
|
11857
|
+
sendMockGeminiResponse(res, `\u274C Model '${modelCommand}' not found.
|
|
11858
|
+
|
|
11859
|
+
Available: ${available}`, isStream);
|
|
11860
|
+
}
|
|
11861
|
+
return;
|
|
11862
|
+
}
|
|
11863
|
+
const route = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
|
|
11864
|
+
plog(`Route selected: ${route.aliasId} (upstream model: ${route.realModelId})`);
|
|
11865
|
+
body.contents = sanitizeModelSwitchTurns(body.contents || []);
|
|
11866
|
+
const languageModel = await getOrInitModel(route);
|
|
11867
|
+
const params = translateGeminiRequest(body);
|
|
11868
|
+
plog(`Translated SDK params:
|
|
11869
|
+
${JSON.stringify(params, null, 2)}`);
|
|
11870
|
+
if (isStream) {
|
|
11871
|
+
res.writeHead(200, {
|
|
11872
|
+
"Content-Type": "text/event-stream",
|
|
11873
|
+
"Cache-Control": "no-cache",
|
|
11874
|
+
"Connection": "keep-alive"
|
|
11875
|
+
});
|
|
11876
|
+
plog("Starting streamText...");
|
|
11877
|
+
const { fullStream } = streamText4({
|
|
11878
|
+
model: languageModel,
|
|
11879
|
+
...params
|
|
11880
|
+
});
|
|
11881
|
+
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
11882
|
+
let isThinking = false;
|
|
11883
|
+
for await (const part of fullStream) {
|
|
11884
|
+
const p21 = part;
|
|
11885
|
+
plog(`Stream chunk type: ${p21.type}`);
|
|
11886
|
+
if (isThinking && (p21.type === "tool-input-start" || p21.type === "tool-call" || p21.type === "finish")) {
|
|
11887
|
+
isThinking = false;
|
|
11888
|
+
const chunk = {
|
|
11889
|
+
candidates: [{ content: { role: "model", parts: [{ text: `
|
|
11890
|
+
</thinking>
|
|
11891
|
+
|
|
11892
|
+
` }] } }]
|
|
11893
|
+
};
|
|
11894
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11895
|
+
|
|
11896
|
+
`);
|
|
11897
|
+
}
|
|
11898
|
+
if (p21.type === "reasoning") {
|
|
11899
|
+
let text5 = p21.textDelta ?? p21.text ?? "";
|
|
11900
|
+
if (!isThinking) {
|
|
11901
|
+
isThinking = true;
|
|
11902
|
+
text5 = `<thinking>
|
|
11903
|
+
` + text5;
|
|
11904
|
+
}
|
|
11905
|
+
const chunk = {
|
|
11906
|
+
candidates: [{ content: { role: "model", parts: [{ text: text5 }] } }]
|
|
11907
|
+
};
|
|
11908
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11909
|
+
|
|
11910
|
+
`);
|
|
11911
|
+
} else if (p21.type === "text-delta") {
|
|
11912
|
+
let text5 = p21.textDelta ?? p21.text ?? "";
|
|
11913
|
+
if (isThinking) {
|
|
11914
|
+
isThinking = false;
|
|
11915
|
+
text5 = `
|
|
11916
|
+
</thinking>
|
|
11917
|
+
|
|
11918
|
+
` + text5;
|
|
11919
|
+
}
|
|
11920
|
+
const chunk = {
|
|
11921
|
+
candidates: [{
|
|
11922
|
+
content: {
|
|
11923
|
+
role: "model",
|
|
11924
|
+
parts: [{ text: text5 }]
|
|
11925
|
+
}
|
|
11926
|
+
}]
|
|
11927
|
+
};
|
|
11928
|
+
const data = `data: ${JSON.stringify(chunk)}
|
|
11929
|
+
|
|
11930
|
+
`;
|
|
11931
|
+
plog(`Streaming text delta: ${p21.textDelta}`);
|
|
11932
|
+
res.write(data);
|
|
11933
|
+
} else if (p21.type === "tool-input-start") {
|
|
11934
|
+
toolCallBuffers.set(p21.toolCallId, { name: p21.toolName, json: "" });
|
|
11935
|
+
} else if (p21.type === "tool-input-delta") {
|
|
11936
|
+
const buf = toolCallBuffers.get(p21.toolCallId);
|
|
11937
|
+
if (buf) buf.json += p21.delta;
|
|
11938
|
+
} else if (p21.type === "tool-call") {
|
|
11939
|
+
const buf = toolCallBuffers.get(p21.toolCallId);
|
|
11940
|
+
const args = buf ? JSON.parse(buf.json || "{}") : p21.input || {};
|
|
11941
|
+
const name = buf ? buf.name : p21.toolName;
|
|
11942
|
+
plog(`Streaming tool call: ${name} with args: ${JSON.stringify(args)}`);
|
|
11943
|
+
const chunk = {
|
|
11944
|
+
candidates: [{
|
|
11945
|
+
content: {
|
|
11946
|
+
role: "model",
|
|
11947
|
+
parts: [{
|
|
11948
|
+
functionCall: { name, args }
|
|
11949
|
+
}]
|
|
11950
|
+
}
|
|
11951
|
+
}]
|
|
11952
|
+
};
|
|
11953
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11954
|
+
|
|
11955
|
+
`);
|
|
11956
|
+
} else if (p21.type === "finish") {
|
|
11957
|
+
const chunk = {
|
|
11958
|
+
candidates: [{
|
|
11959
|
+
finishReason: mapFinishReason(p21.finishReason ?? "")
|
|
11960
|
+
}],
|
|
11961
|
+
usageMetadata: {
|
|
11962
|
+
promptTokenCount: p21.totalUsage?.inputTokens || 0,
|
|
11963
|
+
candidatesTokenCount: p21.totalUsage?.outputTokens || 0
|
|
11964
|
+
}
|
|
11965
|
+
};
|
|
11966
|
+
plog(`Stream finish. Reason: ${p21.finishReason}`);
|
|
11967
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
11968
|
+
|
|
11969
|
+
`);
|
|
11970
|
+
}
|
|
11971
|
+
}
|
|
11972
|
+
res.end();
|
|
11973
|
+
plog("Stream ended.");
|
|
11974
|
+
} else {
|
|
11975
|
+
plog("Starting generateText...");
|
|
11976
|
+
const result = await generateText4({
|
|
11977
|
+
model: languageModel,
|
|
11978
|
+
...params
|
|
11979
|
+
});
|
|
11980
|
+
plog("generateText finished.");
|
|
11981
|
+
const parts = [];
|
|
11982
|
+
if (result.reasoning) {
|
|
11983
|
+
parts.push({ text: `<thinking>
|
|
11984
|
+
${result.reasoning}
|
|
11985
|
+
</thinking>
|
|
11986
|
+
|
|
11987
|
+
` });
|
|
11988
|
+
}
|
|
11989
|
+
if (result.text) {
|
|
11990
|
+
parts.push({ text: result.text });
|
|
11991
|
+
}
|
|
11992
|
+
if (result.toolCalls?.length) {
|
|
11993
|
+
for (const tc of result.toolCalls) {
|
|
11994
|
+
parts.push({
|
|
11995
|
+
functionCall: { name: tc.toolName, args: tc.args }
|
|
11996
|
+
});
|
|
11997
|
+
}
|
|
11998
|
+
}
|
|
11999
|
+
const response = {
|
|
12000
|
+
candidates: [{
|
|
12001
|
+
content: {
|
|
12002
|
+
role: "model",
|
|
12003
|
+
parts
|
|
12004
|
+
},
|
|
12005
|
+
finishReason: mapFinishReason(result.finishReason ?? "")
|
|
12006
|
+
}],
|
|
12007
|
+
usageMetadata: {
|
|
12008
|
+
promptTokenCount: result.usage?.inputTokens || 0,
|
|
12009
|
+
candidatesTokenCount: result.usage?.outputTokens || 0
|
|
12010
|
+
}
|
|
12011
|
+
};
|
|
12012
|
+
plog(`Response:
|
|
12013
|
+
${JSON.stringify(response, null, 2)}`);
|
|
12014
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
12015
|
+
res.end(JSON.stringify(response));
|
|
12016
|
+
}
|
|
12017
|
+
return;
|
|
12018
|
+
}
|
|
12019
|
+
plog(`404 Not Found: ${url}`);
|
|
12020
|
+
res.writeHead(404);
|
|
12021
|
+
res.end("Not Found");
|
|
12022
|
+
} catch (err) {
|
|
12023
|
+
plog(`Error handling request: ${err instanceof Error ? err.stack || err.message : String(err)}`);
|
|
12024
|
+
if (debug) {
|
|
12025
|
+
console.error("[Gemini Proxy] Critical error in handler:", err);
|
|
12026
|
+
}
|
|
12027
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12028
|
+
if (!res.headersSent) {
|
|
12029
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
12030
|
+
res.end(JSON.stringify({ error: { message: errMsg } }));
|
|
12031
|
+
} else {
|
|
12032
|
+
try {
|
|
12033
|
+
res.write(`data: ${JSON.stringify({ error: { message: errMsg } })}
|
|
12034
|
+
|
|
12035
|
+
`);
|
|
12036
|
+
} catch {
|
|
12037
|
+
}
|
|
12038
|
+
res.end();
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
12041
|
+
});
|
|
12042
|
+
process.on("unhandledRejection", onRejection);
|
|
12043
|
+
process.on("uncaughtException", onException);
|
|
12044
|
+
const cleanup = () => {
|
|
12045
|
+
process.off("unhandledRejection", onRejection);
|
|
12046
|
+
process.off("uncaughtException", onException);
|
|
12047
|
+
};
|
|
12048
|
+
return new Promise((resolve, reject2) => {
|
|
12049
|
+
server.on("error", (err) => {
|
|
12050
|
+
cleanup();
|
|
12051
|
+
reject2(err);
|
|
12052
|
+
});
|
|
12053
|
+
server.listen(0, "127.0.0.1", () => {
|
|
12054
|
+
const addr = server.address();
|
|
12055
|
+
if (!addr || typeof addr === "string") {
|
|
12056
|
+
cleanup();
|
|
12057
|
+
reject2(new Error("Failed to bind gemini proxy"));
|
|
12058
|
+
return;
|
|
12059
|
+
}
|
|
12060
|
+
resolve({
|
|
12061
|
+
port: addr.port,
|
|
12062
|
+
token: proxyToken,
|
|
12063
|
+
close: () => {
|
|
12064
|
+
cleanup();
|
|
12065
|
+
server.close();
|
|
12066
|
+
}
|
|
12067
|
+
});
|
|
12068
|
+
});
|
|
12069
|
+
});
|
|
12070
|
+
}
|
|
12071
|
+
function sanitizeModelSwitchTurns(contents) {
|
|
12072
|
+
const cleaned = [];
|
|
12073
|
+
let i = 0;
|
|
12074
|
+
while (i < contents.length) {
|
|
12075
|
+
const turn = contents[i];
|
|
12076
|
+
if (isModelSwitchTurn(turn)) {
|
|
12077
|
+
i += 1;
|
|
12078
|
+
if (i < contents.length && contents[i]?.role === "model") {
|
|
12079
|
+
i += 1;
|
|
12080
|
+
}
|
|
12081
|
+
continue;
|
|
12082
|
+
}
|
|
12083
|
+
cleaned.push(turn);
|
|
12084
|
+
i += 1;
|
|
12085
|
+
}
|
|
12086
|
+
return cleaned;
|
|
12087
|
+
}
|
|
12088
|
+
function isModelSwitchTurn(turn) {
|
|
12089
|
+
if (turn?.role !== "user") return false;
|
|
12090
|
+
const parts = turn.parts || [];
|
|
12091
|
+
if (parts.length === 0) return false;
|
|
12092
|
+
const firstText = parts[0]?.text;
|
|
12093
|
+
if (typeof firstText !== "string") return false;
|
|
12094
|
+
return firstText.trim().startsWith(".model");
|
|
12095
|
+
}
|
|
12096
|
+
function findLastUserTurn(contents) {
|
|
12097
|
+
for (let i = contents.length - 1; i >= 0; i--) {
|
|
12098
|
+
if (contents[i]?.role === "user") return contents[i];
|
|
12099
|
+
}
|
|
12100
|
+
return void 0;
|
|
12101
|
+
}
|
|
12102
|
+
function parseModelCommand(turn) {
|
|
12103
|
+
if (!turn || turn.role !== "user") return null;
|
|
12104
|
+
const parts = turn.parts || [];
|
|
12105
|
+
if (parts.length !== 1) return null;
|
|
12106
|
+
const text5 = parts[0]?.text;
|
|
12107
|
+
if (typeof text5 !== "string") return null;
|
|
12108
|
+
const trimmed = text5.trim();
|
|
12109
|
+
if (!trimmed.startsWith(".model")) return null;
|
|
12110
|
+
if (trimmed === ".model") return "";
|
|
12111
|
+
if (trimmed.charAt(6) !== " ") return null;
|
|
12112
|
+
return trimmed.slice(7).trim();
|
|
12113
|
+
}
|
|
12114
|
+
function sendMockGeminiResponse(res, text5, isStream) {
|
|
12115
|
+
if (isStream) {
|
|
12116
|
+
res.writeHead(200, {
|
|
12117
|
+
"Content-Type": "text/event-stream",
|
|
12118
|
+
"Cache-Control": "no-cache",
|
|
12119
|
+
"Connection": "keep-alive"
|
|
12120
|
+
});
|
|
12121
|
+
const chunk = {
|
|
12122
|
+
candidates: [{
|
|
12123
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
12124
|
+
finishReason: "STOP"
|
|
12125
|
+
}],
|
|
12126
|
+
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
|
|
12127
|
+
};
|
|
12128
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
12129
|
+
|
|
12130
|
+
`);
|
|
12131
|
+
const finishChunk = {
|
|
12132
|
+
candidates: [{ finishReason: "STOP" }],
|
|
12133
|
+
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
|
|
12134
|
+
};
|
|
12135
|
+
res.write(`data: ${JSON.stringify(finishChunk)}
|
|
12136
|
+
|
|
12137
|
+
`);
|
|
12138
|
+
res.end();
|
|
12139
|
+
} else {
|
|
12140
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
12141
|
+
res.end(JSON.stringify({
|
|
12142
|
+
candidates: [{
|
|
12143
|
+
content: { role: "model", parts: [{ text: text5 }] },
|
|
12144
|
+
finishReason: "STOP"
|
|
12145
|
+
}],
|
|
12146
|
+
usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
|
|
12147
|
+
}));
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
|
|
12151
|
+
// src/gemini.ts
|
|
12152
|
+
function geminiHelpText() {
|
|
12153
|
+
return `${pc14.bold("relay-ai gemini")} v${VERSION}
|
|
12154
|
+
Launch Google Gemini CLI with OpenCode Zen / Go or local registry providers.
|
|
12155
|
+
|
|
12156
|
+
${pc14.bold("Usage:")}
|
|
12157
|
+
relay-ai gemini [options] [gemini-flags]
|
|
12158
|
+
relay-ai gemini --help
|
|
12159
|
+
relay-ai gemini --version
|
|
12160
|
+
|
|
12161
|
+
${pc14.bold("Options:")}
|
|
12162
|
+
--trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
12163
|
+
--provider Boot provider id (skip wizard when paired with --model or non-interactive)
|
|
12164
|
+
--model Boot model id (skip wizard when paired with --provider or non-interactive)
|
|
12165
|
+
--help Show this command help
|
|
12166
|
+
--version Show version
|
|
12167
|
+
|
|
12168
|
+
${pc14.bold("Description:")}
|
|
12169
|
+
Picks a provider and model from ~/.relay-ai/providers.json, starts a local Gemini-to-SDK translation
|
|
12170
|
+
proxy, and launches the Gemini CLI.
|
|
12171
|
+
All registry models (Anthropic, OpenAI, custom endpoints, etc.) route through the local translation proxy.
|
|
12172
|
+
|
|
12173
|
+
${pc14.bold("Prerequisites:")}
|
|
12174
|
+
npm install -g @google/gemini-cli
|
|
12175
|
+
|
|
12176
|
+
${pc14.bold("Passing flags to Gemini CLI:")}
|
|
12177
|
+
Add Gemini flags directly \u2014 no "--" separator needed.
|
|
12178
|
+
relay-ai manages -m / --model and -p / --prompt; other flags go to Gemini CLI.
|
|
12179
|
+
|
|
12180
|
+
${pc14.bold("Examples:")}
|
|
12181
|
+
relay-ai gemini
|
|
12182
|
+
relay-ai gemini --trace
|
|
12183
|
+
relay-ai gemini --provider zen --model gemini-2.5-flash
|
|
12184
|
+
relay-ai gemini -p "review this file"`;
|
|
12185
|
+
}
|
|
12186
|
+
async function runGeminiCommand(geminiArgs, trace = false, launch = {}) {
|
|
12187
|
+
if (geminiArgs.includes("--help") || geminiArgs.includes("-h")) {
|
|
12188
|
+
console.log(geminiHelpText());
|
|
12189
|
+
return 0;
|
|
12190
|
+
}
|
|
12191
|
+
const geminiPath = findGeminiBinary();
|
|
12192
|
+
if (!geminiPath) {
|
|
12193
|
+
console.error(pc14.red("\nError: gemini binary not found on PATH.\n"));
|
|
12194
|
+
console.error("Install Google Gemini CLI:");
|
|
12195
|
+
console.error(" npm install -g @google/gemini-cli\n");
|
|
12196
|
+
return 1;
|
|
12197
|
+
}
|
|
12198
|
+
const passthroughArgs = rejectGeminiManagedFlags(geminiArgs);
|
|
12199
|
+
const agentStdout = wantsCleanAgentStdout("gemini", passthroughArgs);
|
|
12200
|
+
setAgentStdoutMode(agentStdout);
|
|
12201
|
+
const prefs = loadPreferences();
|
|
12202
|
+
const launchPlan = planLaunchWizard({
|
|
12203
|
+
explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
|
|
12204
|
+
childArgs: passthroughArgs,
|
|
12205
|
+
agent: "gemini",
|
|
12206
|
+
prefs
|
|
12207
|
+
});
|
|
12208
|
+
if (launchPlan.error) {
|
|
12209
|
+
console.error(pc14.red(`
|
|
12210
|
+
Error: ${launchPlan.error}
|
|
12211
|
+
`));
|
|
12212
|
+
return 1;
|
|
12213
|
+
}
|
|
12214
|
+
let catalog;
|
|
12215
|
+
if (agentStdout) {
|
|
12216
|
+
try {
|
|
12217
|
+
catalog = await fetchProviderCatalog({ agent: "gemini" });
|
|
12218
|
+
} catch (err) {
|
|
12219
|
+
console.error(pc14.red(String(err instanceof Error ? err.message : err)));
|
|
12220
|
+
return 1;
|
|
12221
|
+
}
|
|
12222
|
+
} else {
|
|
12223
|
+
const catalogSpinner = p15.spinner();
|
|
12224
|
+
catalogSpinner.start("Loading your providers...");
|
|
12225
|
+
try {
|
|
12226
|
+
catalog = await fetchProviderCatalog({ agent: "gemini" });
|
|
12227
|
+
} catch (err) {
|
|
12228
|
+
catalogSpinner.stop("");
|
|
12229
|
+
console.error(pc14.red(String(err instanceof Error ? err.message : err)));
|
|
12230
|
+
return 1;
|
|
12231
|
+
}
|
|
12232
|
+
catalogSpinner.stop("");
|
|
12233
|
+
}
|
|
12234
|
+
const compatible = providersForPicker(catalog);
|
|
12235
|
+
if (compatible.length === 0) {
|
|
12236
|
+
p15.log.warn("No Gemini-compatible providers in your registry.");
|
|
12237
|
+
p15.log.info("Add a provider with relay-ai providers add, or sign in with relay-ai providers auth openai.");
|
|
12238
|
+
return 0;
|
|
12239
|
+
}
|
|
12240
|
+
let activeProvider = compatible.find((lp) => lp.id === prefs.lastGeminiProvider) ?? compatible[0];
|
|
12241
|
+
let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastGeminiModel) ?? activeProvider.models[0];
|
|
12242
|
+
if (!selectedModel) {
|
|
12243
|
+
p15.log.error(`Provider "${activeProvider.name}" has no models available.`);
|
|
12244
|
+
return 1;
|
|
12245
|
+
}
|
|
12246
|
+
;
|
|
12247
|
+
if (launchPlan.skip && launchPlan.target) {
|
|
12248
|
+
const resolved = findProviderAndModel(compatible, launchPlan.target);
|
|
12249
|
+
if (!resolved) {
|
|
12250
|
+
p15.log.error(
|
|
12251
|
+
`Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
|
|
12252
|
+
);
|
|
12253
|
+
return 1;
|
|
12254
|
+
}
|
|
12255
|
+
activeProvider = resolved.provider;
|
|
12256
|
+
selectedModel = resolved.model;
|
|
12257
|
+
if (!agentStdout) {
|
|
12258
|
+
p15.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
12259
|
+
}
|
|
12260
|
+
} else {
|
|
12261
|
+
if (!agentStdout) {
|
|
12262
|
+
console.log("");
|
|
12263
|
+
p15.log.info(`Launching ${pc14.bold("Gemini CLI")} with relay-ai`);
|
|
12264
|
+
}
|
|
12265
|
+
const chosenProvider = await pickGeminiProvider(
|
|
12266
|
+
compatible,
|
|
12267
|
+
prefs,
|
|
12268
|
+
(prefs.favoriteModels ?? []).length > 0,
|
|
12269
|
+
launch.launchProvider
|
|
12270
|
+
);
|
|
12271
|
+
if (!chosenProvider) return 0;
|
|
12272
|
+
if (chosenProvider === "__favorites__") {
|
|
12273
|
+
const favPick = await pickGeminiFavoriteModel(compatible, prefs.favoriteModels ?? []);
|
|
12274
|
+
if (!favPick || favPick === "back") return 0;
|
|
12275
|
+
activeProvider = favPick.provider;
|
|
12276
|
+
selectedModel = favPick.model;
|
|
12277
|
+
} else {
|
|
12278
|
+
activeProvider = chosenProvider;
|
|
12279
|
+
const chosenModel = await pickGeminiModel(activeProvider, prefs);
|
|
12280
|
+
if (!chosenModel || chosenModel === "back") return 0;
|
|
12281
|
+
selectedModel = chosenModel;
|
|
12282
|
+
}
|
|
12283
|
+
if (!agentStdout) {
|
|
12284
|
+
const ok = await confirmGeminiLaunch(
|
|
12285
|
+
activeProvider.name,
|
|
12286
|
+
selectedModel.name || selectedModel.id,
|
|
12287
|
+
selectedModel.id
|
|
12288
|
+
);
|
|
12289
|
+
if (!ok) return 0;
|
|
12290
|
+
}
|
|
12291
|
+
}
|
|
12292
|
+
recordLaunchSelection("gemini", activeProvider.id, selectedModel.id, prefs);
|
|
12293
|
+
const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
|
|
12294
|
+
if (!launchApiKey?.trim()) {
|
|
12295
|
+
p15.log.error(
|
|
12296
|
+
`No API key found for ${activeProvider.name}. Set it with relay-ai providers add.`
|
|
12297
|
+
);
|
|
12298
|
+
return 1;
|
|
12299
|
+
}
|
|
12300
|
+
const providerRoutes = activeProvider.models.map((m) => ({
|
|
12301
|
+
aliasId: m.id,
|
|
12302
|
+
realModelId: m.upstreamModelId || m.id,
|
|
12303
|
+
displayName: m.name || m.id,
|
|
12304
|
+
upstreamUrl: m.baseUrl || m.apiBaseUrl || "",
|
|
12305
|
+
apiKey: launchApiKey,
|
|
12306
|
+
modelFormat: m.modelFormat,
|
|
12307
|
+
contextWindow: m.contextWindow,
|
|
12308
|
+
npm: m.npm,
|
|
12309
|
+
baseURL: m.apiBaseUrl,
|
|
12310
|
+
providerId: activeProvider.id,
|
|
12311
|
+
authType: activeProvider.authType,
|
|
12312
|
+
oauthAccountId: activeProvider.oauthAccountId,
|
|
12313
|
+
supportedParameters: m.supportedParameters,
|
|
12314
|
+
reasoning: m.reasoning,
|
|
12315
|
+
interleavedReasoningField: m.interleavedReasoningField
|
|
12316
|
+
}));
|
|
12317
|
+
const resolvedFavs = [];
|
|
12318
|
+
const favorites = prefs.favoriteModels ?? [];
|
|
12319
|
+
for (const fav of favorites) {
|
|
12320
|
+
const provider = compatible.find((lp) => lp.id === fav.providerId);
|
|
12321
|
+
const model = provider?.models.find((m) => m.id === fav.modelId);
|
|
12322
|
+
if (provider && model) {
|
|
12323
|
+
const apiKey = await resolveLocalProviderApiKey(provider);
|
|
12324
|
+
if (apiKey) {
|
|
12325
|
+
resolvedFavs.push({
|
|
12326
|
+
aliasId: model.id,
|
|
12327
|
+
realModelId: model.upstreamModelId || model.id,
|
|
12328
|
+
displayName: model.name || model.id,
|
|
12329
|
+
upstreamUrl: model.baseUrl || model.apiBaseUrl || "",
|
|
12330
|
+
apiKey,
|
|
12331
|
+
modelFormat: model.modelFormat,
|
|
12332
|
+
contextWindow: model.contextWindow,
|
|
12333
|
+
npm: model.npm,
|
|
12334
|
+
baseURL: model.apiBaseUrl,
|
|
12335
|
+
providerId: provider.id,
|
|
12336
|
+
authType: provider.authType,
|
|
12337
|
+
oauthAccountId: provider.oauthAccountId,
|
|
12338
|
+
supportedParameters: model.supportedParameters,
|
|
12339
|
+
reasoning: model.reasoning,
|
|
12340
|
+
interleavedReasoningField: model.interleavedReasoningField
|
|
12341
|
+
});
|
|
12342
|
+
}
|
|
12343
|
+
}
|
|
12344
|
+
}
|
|
12345
|
+
const routesMap = /* @__PURE__ */ new Map();
|
|
12346
|
+
for (const route of providerRoutes) {
|
|
12347
|
+
routesMap.set(route.aliasId, route);
|
|
12348
|
+
}
|
|
12349
|
+
for (const route of resolvedFavs) {
|
|
12350
|
+
if (!routesMap.has(route.aliasId)) {
|
|
12351
|
+
routesMap.set(route.aliasId, route);
|
|
12352
|
+
}
|
|
12353
|
+
}
|
|
12354
|
+
const startingRoute = routesMap.get(selectedModel.id);
|
|
12355
|
+
if (!startingRoute) {
|
|
12356
|
+
routesMap.set(selectedModel.id, {
|
|
12357
|
+
aliasId: selectedModel.id,
|
|
12358
|
+
realModelId: selectedModel.upstreamModelId || selectedModel.id,
|
|
12359
|
+
displayName: selectedModel.name || selectedModel.id,
|
|
12360
|
+
upstreamUrl: selectedModel.baseUrl || selectedModel.apiBaseUrl || "",
|
|
12361
|
+
apiKey: launchApiKey,
|
|
12362
|
+
modelFormat: selectedModel.modelFormat,
|
|
12363
|
+
contextWindow: selectedModel.contextWindow,
|
|
12364
|
+
npm: selectedModel.npm,
|
|
12365
|
+
baseURL: selectedModel.apiBaseUrl,
|
|
12366
|
+
providerId: activeProvider.id,
|
|
12367
|
+
authType: activeProvider.authType,
|
|
12368
|
+
oauthAccountId: activeProvider.oauthAccountId,
|
|
12369
|
+
supportedParameters: selectedModel.supportedParameters,
|
|
12370
|
+
reasoning: selectedModel.reasoning,
|
|
12371
|
+
interleavedReasoningField: selectedModel.interleavedReasoningField
|
|
12372
|
+
});
|
|
12373
|
+
}
|
|
12374
|
+
const finalRoutes = [...routesMap.values()];
|
|
12375
|
+
let proxyHandle = null;
|
|
12376
|
+
try {
|
|
12377
|
+
proxyHandle = await startGeminiProxy(finalRoutes, trace);
|
|
12378
|
+
} catch (err) {
|
|
12379
|
+
p15.log.error(`Failed to start Gemini proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
12380
|
+
return 1;
|
|
12381
|
+
}
|
|
12382
|
+
const childEnv = buildGeminiChildEnv(proxyHandle.port, proxyHandle.token);
|
|
12383
|
+
if (!agentStdout) {
|
|
12384
|
+
p15.log.info(`Gemini proxy started on port ${proxyHandle.port}`);
|
|
12385
|
+
p15.log.info(`\u{1F4A1} Type ${pc14.bold(".model <id>")} in the chat to switch models mid-session.`);
|
|
12386
|
+
}
|
|
12387
|
+
const exitCode = await launchGemini(geminiPath, selectedModel.id, childEnv, passthroughArgs);
|
|
12388
|
+
proxyHandle.close();
|
|
12389
|
+
if (!agentStdout) {
|
|
12390
|
+
p15.log.info("Gemini proxy stopped.");
|
|
12391
|
+
}
|
|
12392
|
+
if (trace) {
|
|
12393
|
+
printTraceLog(getGeminiProxyDebugLogPath());
|
|
12394
|
+
}
|
|
12395
|
+
return exitCode;
|
|
12396
|
+
}
|
|
12397
|
+
|
|
12398
|
+
// src/codex-app.ts
|
|
12399
|
+
import pc15 from "picocolors";
|
|
12400
|
+
import * as p17 from "@clack/prompts";
|
|
12401
|
+
|
|
12402
|
+
// src/codex/app-config.ts
|
|
12403
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
|
|
12404
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
12405
|
+
import { parse, stringify } from "smol-toml";
|
|
12406
|
+
function getCodexConfigPath() {
|
|
12407
|
+
return join15(getCodexHome(), "config.toml");
|
|
12408
|
+
}
|
|
12409
|
+
function getCodexAppSidecarProfilePath() {
|
|
12410
|
+
return join15(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
|
|
12411
|
+
}
|
|
12412
|
+
function asRecord(value) {
|
|
12413
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
12414
|
+
}
|
|
12415
|
+
function rootString(config, key) {
|
|
12416
|
+
if (!(key in config)) return { had: false, value: "" };
|
|
12417
|
+
const v = config[key];
|
|
12418
|
+
return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
|
|
12419
|
+
}
|
|
12420
|
+
function readCodexConfigText(path = getCodexConfigPath()) {
|
|
12421
|
+
if (!existsSync14(path)) return "";
|
|
12422
|
+
return readFileSync11(path, "utf8");
|
|
12423
|
+
}
|
|
12424
|
+
function parseCodexConfig(text5) {
|
|
12425
|
+
if (!text5.trim()) return {};
|
|
12426
|
+
return asRecord(parse(text5));
|
|
12427
|
+
}
|
|
12428
|
+
function captureRestoreState(text5) {
|
|
12429
|
+
const config = parseCodexConfig(text5);
|
|
12430
|
+
const profile = rootString(config, "profile");
|
|
12431
|
+
const model = rootString(config, "model");
|
|
12432
|
+
const modelProvider = rootString(config, "model_provider");
|
|
12433
|
+
const modelCatalog = rootString(config, "model_catalog_json");
|
|
12434
|
+
const openAIBaseUrl = rootString(config, "openai_base_url");
|
|
12435
|
+
const reasoning = rootString(config, "model_reasoning_effort");
|
|
12436
|
+
return {
|
|
12437
|
+
hadProfile: profile.had,
|
|
12438
|
+
profile: profile.value,
|
|
12439
|
+
hadModel: model.had,
|
|
12440
|
+
model: model.value,
|
|
12441
|
+
hadModelProvider: modelProvider.had,
|
|
12442
|
+
modelProvider: modelProvider.value,
|
|
12443
|
+
hadModelCatalogJson: modelCatalog.had,
|
|
12444
|
+
modelCatalogJson: modelCatalog.value,
|
|
12445
|
+
hadOpenAIBaseUrl: openAIBaseUrl.had,
|
|
12446
|
+
openAIBaseUrl: openAIBaseUrl.value,
|
|
12447
|
+
hadModelReasoningEffort: reasoning.had,
|
|
12448
|
+
modelReasoningEffort: reasoning.value
|
|
12449
|
+
};
|
|
12450
|
+
}
|
|
12451
|
+
function isAppManagedConfig(text5) {
|
|
12452
|
+
const config = parseCodexConfig(text5);
|
|
12453
|
+
const mp = rootString(config, "model_provider");
|
|
12454
|
+
if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
|
|
12455
|
+
const baseUrl = rootString(config, "openai_base_url");
|
|
12456
|
+
const catalog = rootString(config, "model_catalog_json");
|
|
12457
|
+
return mp.value === "openai" && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
|
|
12458
|
+
}
|
|
12459
|
+
function mergeAppConfig(existing, spec) {
|
|
12460
|
+
const patch = buildCodexAppRootConfig(spec);
|
|
12461
|
+
const out = { ...existing };
|
|
12462
|
+
delete out.profile;
|
|
12463
|
+
out.model = patch.model;
|
|
12464
|
+
out.model_provider = patch.model_provider;
|
|
12465
|
+
out.openai_base_url = patch.openai_base_url;
|
|
12466
|
+
out.model_catalog_json = patch.model_catalog_json;
|
|
12467
|
+
const providers = asRecord(out.model_providers);
|
|
12468
|
+
delete providers[CODEX_APP_PROVIDER_ID];
|
|
12469
|
+
const profiles = asRecord(out.profiles);
|
|
12470
|
+
delete profiles[CODEX_APP_PROVIDER_ID];
|
|
12471
|
+
if (Object.keys(profiles).length === 0) {
|
|
12472
|
+
delete out.profiles;
|
|
12473
|
+
} else {
|
|
12474
|
+
out.profiles = profiles;
|
|
12475
|
+
}
|
|
12476
|
+
if (Object.keys(providers).length === 0) {
|
|
12477
|
+
delete out.model_providers;
|
|
12478
|
+
} else {
|
|
12479
|
+
out.model_providers = providers;
|
|
12480
|
+
}
|
|
12481
|
+
const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
|
|
12482
|
+
if (existingEffort !== void 0) {
|
|
12483
|
+
const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
|
|
12484
|
+
providerId: spec.route.providerId,
|
|
12485
|
+
apiBaseUrl: spec.route.baseURL,
|
|
12486
|
+
supportedParameters: spec.route.supportedParameters,
|
|
12487
|
+
reasoning: spec.route.reasoning,
|
|
12488
|
+
interleavedReasoningField: spec.route.interleavedReasoningField
|
|
12489
|
+
});
|
|
12490
|
+
if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
|
|
12491
|
+
if (caps.levels.length > 0 && caps.defaultLevel) {
|
|
12492
|
+
out.model_reasoning_effort = caps.defaultLevel;
|
|
12493
|
+
} else {
|
|
12494
|
+
delete out.model_reasoning_effort;
|
|
12495
|
+
}
|
|
12496
|
+
}
|
|
12497
|
+
}
|
|
12498
|
+
return out;
|
|
12499
|
+
}
|
|
12500
|
+
function validateAppConfigText(text5, spec) {
|
|
12501
|
+
const config = parseCodexConfig(text5);
|
|
12502
|
+
if ("profile" in config) {
|
|
12503
|
+
throw new Error("Generated config still contains legacy root profile key");
|
|
12504
|
+
}
|
|
12505
|
+
const profiles = asRecord(config.profiles);
|
|
12506
|
+
if (profiles[CODEX_APP_PROVIDER_ID]) {
|
|
12507
|
+
throw new Error("Generated config still contains legacy profiles table");
|
|
12508
|
+
}
|
|
12509
|
+
const mp = rootString(config, "model_provider");
|
|
12510
|
+
if (mp.value !== "openai") {
|
|
12511
|
+
throw new Error("Generated config must keep the built-in OpenAI model_provider");
|
|
12512
|
+
}
|
|
12513
|
+
const baseUrl = rootString(config, "openai_base_url");
|
|
12514
|
+
if (baseUrl.value !== `http://127.0.0.1:${spec.proxyPort}/v1`) {
|
|
12515
|
+
throw new Error("Generated config openai_base_url mismatch");
|
|
12516
|
+
}
|
|
12517
|
+
const catalog = rootString(config, "model_catalog_json");
|
|
11362
12518
|
if (catalog.value !== spec.catalogPath) {
|
|
11363
12519
|
throw new Error("Generated config model_catalog_json mismatch");
|
|
11364
12520
|
}
|
|
@@ -11409,13 +12565,13 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
|
|
|
11409
12565
|
}
|
|
11410
12566
|
applyRestoreKey(config, "model_reasoning_effort", state.hadModelReasoningEffort, state.modelReasoningEffort);
|
|
11411
12567
|
const sidecar = getCodexAppSidecarProfilePath();
|
|
11412
|
-
if (
|
|
12568
|
+
if (existsSync14(sidecar)) {
|
|
11413
12569
|
try {
|
|
11414
12570
|
rmSync2(sidecar, { force: true });
|
|
11415
12571
|
} catch {
|
|
11416
12572
|
}
|
|
11417
12573
|
}
|
|
11418
|
-
const hadFile =
|
|
12574
|
+
const hadFile = existsSync14(configPath);
|
|
11419
12575
|
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
12576
|
if (!hadFile && empty) return false;
|
|
11421
12577
|
if (empty) {
|
|
@@ -11436,25 +12592,25 @@ function previewAppConfigToml(spec) {
|
|
|
11436
12592
|
// src/codex/app-session.ts
|
|
11437
12593
|
import {
|
|
11438
12594
|
copyFileSync as copyFileSync4,
|
|
11439
|
-
existsSync as
|
|
12595
|
+
existsSync as existsSync15,
|
|
11440
12596
|
mkdirSync as mkdirSync8,
|
|
11441
12597
|
readdirSync as readdirSync2,
|
|
11442
12598
|
readFileSync as readFileSync12,
|
|
11443
12599
|
rmSync as rmSync3
|
|
11444
12600
|
} from "fs";
|
|
11445
|
-
import { basename as basename2, join as
|
|
12601
|
+
import { basename as basename2, join as join16 } from "path";
|
|
11446
12602
|
function getAppSessionLockPath(env = process.env) {
|
|
11447
|
-
return
|
|
12603
|
+
return join16(getRelayAiCodexDir(env), "session-app.json");
|
|
11448
12604
|
}
|
|
11449
12605
|
function getAppRestoreStatePath(env = process.env) {
|
|
11450
|
-
return
|
|
12606
|
+
return join16(getRelayAiCodexDir(env), "app-restore-state.json");
|
|
11451
12607
|
}
|
|
11452
12608
|
function getAppCatalogPath(providerId, env = process.env) {
|
|
11453
|
-
return
|
|
12609
|
+
return join16(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
|
|
11454
12610
|
}
|
|
11455
12611
|
function readAppSessionLock(env = process.env) {
|
|
11456
12612
|
const path = getAppSessionLockPath(env);
|
|
11457
|
-
if (!
|
|
12613
|
+
if (!existsSync15(path)) return null;
|
|
11458
12614
|
try {
|
|
11459
12615
|
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
11460
12616
|
if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
|
|
@@ -11468,11 +12624,11 @@ function writeAppSessionLock(lock, env = process.env) {
|
|
|
11468
12624
|
}
|
|
11469
12625
|
function clearAppSessionLock(env = process.env) {
|
|
11470
12626
|
const path = getAppSessionLockPath(env);
|
|
11471
|
-
if (
|
|
12627
|
+
if (existsSync15(path)) rmSync3(path, { force: true });
|
|
11472
12628
|
}
|
|
11473
12629
|
function readAppRestoreState(env = process.env) {
|
|
11474
12630
|
const path = getAppRestoreStatePath(env);
|
|
11475
|
-
if (!
|
|
12631
|
+
if (!existsSync15(path)) return null;
|
|
11476
12632
|
try {
|
|
11477
12633
|
return JSON.parse(readFileSync12(path, "utf8"));
|
|
11478
12634
|
} catch {
|
|
@@ -11486,16 +12642,16 @@ function writeAppRestoreState(state, env = process.env) {
|
|
|
11486
12642
|
}
|
|
11487
12643
|
function clearAppRestoreState(env = process.env) {
|
|
11488
12644
|
const path = getAppRestoreStatePath(env);
|
|
11489
|
-
if (
|
|
12645
|
+
if (existsSync15(path)) rmSync3(path, { force: true });
|
|
11490
12646
|
}
|
|
11491
12647
|
function backupConfigToml(env = process.env) {
|
|
11492
12648
|
const configPath = getCodexConfigPath();
|
|
11493
|
-
if (!
|
|
12649
|
+
if (!existsSync15(configPath)) return void 0;
|
|
11494
12650
|
rotateBackups(configPath, env);
|
|
11495
12651
|
const backupsDir = getBackupsDir(env);
|
|
11496
12652
|
mkdirSync8(backupsDir, { recursive: true });
|
|
11497
12653
|
const base = basename2(configPath);
|
|
11498
|
-
const backupPath =
|
|
12654
|
+
const backupPath = join16(backupsDir, `${base}.${Date.now()}.bak`);
|
|
11499
12655
|
copyFileSync4(configPath, backupPath);
|
|
11500
12656
|
return backupPath;
|
|
11501
12657
|
}
|
|
@@ -11511,8 +12667,8 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
|
|
|
11511
12667
|
}
|
|
11512
12668
|
function ownedAppCatalogPaths(env = process.env) {
|
|
11513
12669
|
const codexDir = getRelayAiCodexDir(env);
|
|
11514
|
-
if (!
|
|
11515
|
-
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) =>
|
|
12670
|
+
if (!existsSync15(codexDir)) return [];
|
|
12671
|
+
return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join16(codexDir, n));
|
|
11516
12672
|
}
|
|
11517
12673
|
function removeAppCatalogs(env = process.env) {
|
|
11518
12674
|
const removed = [];
|
|
@@ -11544,7 +12700,7 @@ function restoreCodexAppOverlay(env = process.env) {
|
|
|
11544
12700
|
}
|
|
11545
12701
|
if (restoreState) {
|
|
11546
12702
|
restoreConfigFromState(restoreState);
|
|
11547
|
-
} else if (lock?.backupPath &&
|
|
12703
|
+
} else if (lock?.backupPath && existsSync15(lock.backupPath)) {
|
|
11548
12704
|
copyFileSync4(lock.backupPath, getCodexConfigPath());
|
|
11549
12705
|
}
|
|
11550
12706
|
removeAppCatalogs(env);
|
|
@@ -11590,11 +12746,11 @@ function waitForShutdown2() {
|
|
|
11590
12746
|
}
|
|
11591
12747
|
|
|
11592
12748
|
// src/codex/app-launch.ts
|
|
11593
|
-
import { execSync as
|
|
11594
|
-
import { existsSync as
|
|
11595
|
-
import { homedir as
|
|
11596
|
-
import { join as
|
|
11597
|
-
import * as
|
|
12749
|
+
import { execSync as execSync5 } from "child_process";
|
|
12750
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
12751
|
+
import { homedir as homedir11 } from "os";
|
|
12752
|
+
import { join as join17 } from "path";
|
|
12753
|
+
import * as p16 from "@clack/prompts";
|
|
11598
12754
|
var CODEX_BUNDLE_ID = "com.openai.codex";
|
|
11599
12755
|
function codexAppSupported() {
|
|
11600
12756
|
if (process.platform !== "darwin" && process.platform !== "win32") {
|
|
@@ -11602,7 +12758,7 @@ function codexAppSupported() {
|
|
|
11602
12758
|
}
|
|
11603
12759
|
}
|
|
11604
12760
|
function run(cmd, encoding = "utf8") {
|
|
11605
|
-
return
|
|
12761
|
+
return execSync5(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
11606
12762
|
}
|
|
11607
12763
|
function runPowerShell(script) {
|
|
11608
12764
|
return run(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
|
|
@@ -11610,30 +12766,30 @@ function runPowerShell(script) {
|
|
|
11610
12766
|
function darwinAppCandidates() {
|
|
11611
12767
|
return [
|
|
11612
12768
|
"/Applications/Codex.app",
|
|
11613
|
-
|
|
12769
|
+
join17(homedir11(), "Applications", "Codex.app")
|
|
11614
12770
|
];
|
|
11615
12771
|
}
|
|
11616
12772
|
function winLocalAppData() {
|
|
11617
|
-
return process.env.LOCALAPPDATA ??
|
|
12773
|
+
return process.env.LOCALAPPDATA ?? join17(homedir11(), "AppData", "Local");
|
|
11618
12774
|
}
|
|
11619
12775
|
function winCodexExeCandidates() {
|
|
11620
12776
|
const local = winLocalAppData();
|
|
11621
12777
|
const bases = [
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
12778
|
+
join17(local, "Programs", "Codex"),
|
|
12779
|
+
join17(local, "Programs", "OpenAI Codex"),
|
|
12780
|
+
join17(local, "Codex"),
|
|
12781
|
+
join17(local, "OpenAI Codex"),
|
|
12782
|
+
join17(local, "OpenAI", "Codex"),
|
|
12783
|
+
join17(local, "openai-codex-electron")
|
|
11628
12784
|
];
|
|
11629
12785
|
const out = [];
|
|
11630
12786
|
for (const base of bases) {
|
|
11631
|
-
out.push(
|
|
12787
|
+
out.push(join17(base, "Codex.exe"));
|
|
11632
12788
|
try {
|
|
11633
|
-
if (
|
|
12789
|
+
if (existsSync16(base)) {
|
|
11634
12790
|
for (const name of readdirSync3(base)) {
|
|
11635
12791
|
if (name.startsWith("app-")) {
|
|
11636
|
-
out.push(
|
|
12792
|
+
out.push(join17(base, name, "Codex.exe"));
|
|
11637
12793
|
}
|
|
11638
12794
|
}
|
|
11639
12795
|
}
|
|
@@ -11646,7 +12802,7 @@ function mdfindCodexApp() {
|
|
|
11646
12802
|
try {
|
|
11647
12803
|
const out = run(`mdfind "kMDItemCFBundleIdentifier == '${CODEX_BUNDLE_ID}'"`);
|
|
11648
12804
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
11649
|
-
return first &&
|
|
12805
|
+
return first && existsSync16(first) ? first : null;
|
|
11650
12806
|
} catch {
|
|
11651
12807
|
return null;
|
|
11652
12808
|
}
|
|
@@ -11654,14 +12810,14 @@ function mdfindCodexApp() {
|
|
|
11654
12810
|
function findCodexApp() {
|
|
11655
12811
|
if (process.platform === "darwin") {
|
|
11656
12812
|
for (const path of darwinAppCandidates()) {
|
|
11657
|
-
if (
|
|
12813
|
+
if (existsSync16(path)) return path;
|
|
11658
12814
|
}
|
|
11659
12815
|
return mdfindCodexApp();
|
|
11660
12816
|
}
|
|
11661
12817
|
if (process.platform === "win32") {
|
|
11662
12818
|
for (const path of winCodexExeCandidates()) {
|
|
11663
12819
|
try {
|
|
11664
|
-
if (
|
|
12820
|
+
if (existsSync16(path) && statSync4(path).isFile()) return path;
|
|
11665
12821
|
} catch {
|
|
11666
12822
|
}
|
|
11667
12823
|
}
|
|
@@ -11725,9 +12881,9 @@ async function waitForQuit(timeoutMs) {
|
|
|
11725
12881
|
function openCodexAppAt(path) {
|
|
11726
12882
|
if (process.platform === "darwin") {
|
|
11727
12883
|
if (path.endsWith(".app")) {
|
|
11728
|
-
|
|
12884
|
+
execSync5(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
|
|
11729
12885
|
} else {
|
|
11730
|
-
|
|
12886
|
+
execSync5(`open -b ${CODEX_BUNDLE_ID}`, { stdio: "inherit" });
|
|
11731
12887
|
}
|
|
11732
12888
|
return;
|
|
11733
12889
|
}
|
|
@@ -11750,9 +12906,9 @@ function openCodexApp() {
|
|
|
11750
12906
|
}
|
|
11751
12907
|
function darwinQuit() {
|
|
11752
12908
|
try {
|
|
11753
|
-
|
|
12909
|
+
execSync5(`osascript -e 'tell application "Codex" to quit'`, { stdio: "pipe" });
|
|
11754
12910
|
} catch {
|
|
11755
|
-
|
|
12911
|
+
execSync5(`osascript -e 'tell application id "${CODEX_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
|
|
11756
12912
|
}
|
|
11757
12913
|
}
|
|
11758
12914
|
function winQuitGraceful() {
|
|
@@ -11780,9 +12936,9 @@ async function launchOrRestartCodexApp(prompt = "Restart Codex to apply relay-ai
|
|
|
11780
12936
|
openCodexAppAt(appPath);
|
|
11781
12937
|
return;
|
|
11782
12938
|
}
|
|
11783
|
-
const restart = await
|
|
11784
|
-
if (
|
|
11785
|
-
|
|
12939
|
+
const restart = await p16.confirm({ message: prompt, initialValue: true });
|
|
12940
|
+
if (p16.isCancel(restart) || !restart) {
|
|
12941
|
+
p16.log.info("Quit and reopen Codex when you are ready for the new model to take effect.");
|
|
11786
12942
|
return;
|
|
11787
12943
|
}
|
|
11788
12944
|
if (process.platform === "darwin") darwinQuit();
|
|
@@ -11800,9 +12956,9 @@ function codexAppInstallHint() {
|
|
|
11800
12956
|
|
|
11801
12957
|
// src/codex-app.ts
|
|
11802
12958
|
function codexAppHelpText() {
|
|
11803
|
-
return `${
|
|
12959
|
+
return `${pc15.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
|
|
11804
12960
|
|
|
11805
|
-
${
|
|
12961
|
+
${pc15.bold("Usage:")}
|
|
11806
12962
|
relay-ai codex-app [options]
|
|
11807
12963
|
relay-ai codex-app --vertex
|
|
11808
12964
|
relay-ai codex-app --restore
|
|
@@ -11810,7 +12966,7 @@ ${pc14.bold("Usage:")}
|
|
|
11810
12966
|
relay-ai codex-app --help
|
|
11811
12967
|
relay-ai codex-app --version
|
|
11812
12968
|
|
|
11813
|
-
${
|
|
12969
|
+
${pc15.bold("Options:")}
|
|
11814
12970
|
--vertex Use Claude models through Google Vertex AI
|
|
11815
12971
|
--restore Restore Codex config after an interrupted app session
|
|
11816
12972
|
--config Preview the generated Codex app configuration without launching
|
|
@@ -11818,31 +12974,31 @@ ${pc14.bold("Options:")}
|
|
|
11818
12974
|
--help Show this command help
|
|
11819
12975
|
--version Show version
|
|
11820
12976
|
|
|
11821
|
-
${
|
|
12977
|
+
${pc15.bold("Description:")}
|
|
11822
12978
|
Picks a provider and model from ~/.relay-ai/providers.json, patches ~/.codex/config.toml
|
|
11823
12979
|
(with backup + restore on Ctrl+C), starts a local Responses proxy, and opens the
|
|
11824
12980
|
Codex desktop app. Keep this terminal open while using Codex.
|
|
11825
12981
|
|
|
11826
|
-
${
|
|
12982
|
+
${pc15.bold("Platforms:")}
|
|
11827
12983
|
macOS and Windows. Linux is not supported (no Codex desktop app).
|
|
11828
12984
|
|
|
11829
|
-
${
|
|
12985
|
+
${pc15.bold("Cleanup:")}
|
|
11830
12986
|
Ctrl+C stops the proxy and restores your previous Codex config.
|
|
11831
12987
|
After crash: relay-ai codex-app --restore
|
|
11832
12988
|
|
|
11833
|
-
${
|
|
12989
|
+
${pc15.bold("Preview (no writes):")}
|
|
11834
12990
|
relay-ai codex-app --config
|
|
11835
12991
|
|
|
11836
12992
|
See docs/CODEX.md for CLI vs app, files touched, and restore.
|
|
11837
12993
|
|
|
11838
|
-
${
|
|
12994
|
+
${pc15.bold("Examples:")}
|
|
11839
12995
|
relay-ai codex-app
|
|
11840
12996
|
relay-ai codex-app --vertex
|
|
11841
12997
|
relay-ai codex-app --config
|
|
11842
12998
|
relay-ai codex-app --restore
|
|
11843
12999
|
|
|
11844
|
-
${
|
|
11845
|
-
When you have saved favorites via ${
|
|
13000
|
+
${pc15.bold("Favorites:")}
|
|
13001
|
+
When you have saved favorites via ${pc15.cyan("relay-ai models")}, the Codex App
|
|
11846
13002
|
picker will show your starting model + favorites for mid-session switching.
|
|
11847
13003
|
Zen/Go favorites are included when an OpenCode API key is available.`;
|
|
11848
13004
|
}
|
|
@@ -11864,26 +13020,26 @@ function vertexEntryToLocalModel2(entry) {
|
|
|
11864
13020
|
}
|
|
11865
13021
|
async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
11866
13022
|
if (!hasApplicationDefaultCredentials()) {
|
|
11867
|
-
|
|
11868
|
-
|
|
13023
|
+
p17.log.error("Google Application Default Credentials not found.");
|
|
13024
|
+
p17.log.info("Run: gcloud auth application-default login");
|
|
11869
13025
|
return 1;
|
|
11870
13026
|
}
|
|
11871
13027
|
const config = buildVertexRuntimeConfig();
|
|
11872
13028
|
if (!config) {
|
|
11873
|
-
|
|
11874
|
-
|
|
13029
|
+
p17.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
|
|
13030
|
+
p17.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
|
|
11875
13031
|
return 1;
|
|
11876
13032
|
}
|
|
11877
13033
|
let selectedEntry;
|
|
11878
13034
|
if (config.models.length === 1) {
|
|
11879
13035
|
selectedEntry = config.models[0];
|
|
11880
13036
|
} else {
|
|
11881
|
-
const choice = await
|
|
13037
|
+
const choice = await p17.select({
|
|
11882
13038
|
message: "Select a starting Vertex AI model:",
|
|
11883
13039
|
options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
|
|
11884
13040
|
});
|
|
11885
|
-
if (
|
|
11886
|
-
|
|
13041
|
+
if (p17.isCancel(choice)) {
|
|
13042
|
+
p17.cancel("Cancelled.");
|
|
11887
13043
|
return 0;
|
|
11888
13044
|
}
|
|
11889
13045
|
selectedEntry = choice;
|
|
@@ -11905,19 +13061,19 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11905
13061
|
const home = process.env["HOME"] ?? "";
|
|
11906
13062
|
const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
|
|
11907
13063
|
console.log("");
|
|
11908
|
-
console.log(
|
|
13064
|
+
console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
|
|
11909
13065
|
console.log("");
|
|
11910
|
-
console.log(` ${
|
|
11911
|
-
console.log(` ${
|
|
11912
|
-
console.log(` ${
|
|
11913
|
-
console.log(` ${
|
|
11914
|
-
console.log(` ${
|
|
13066
|
+
console.log(` ${pc15.bold("Mode:")} Vertex AI`);
|
|
13067
|
+
console.log(` ${pc15.bold("Project:")} ${config.project}`);
|
|
13068
|
+
console.log(` ${pc15.bold("Location:")} ${config.location}`);
|
|
13069
|
+
console.log(` ${pc15.bold("Model:")} ${selectedEntry.display_name}`);
|
|
13070
|
+
console.log(` ${pc15.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
|
|
11915
13071
|
console.log("");
|
|
11916
|
-
console.log(` ${
|
|
11917
|
-
console.log(` ${
|
|
13072
|
+
console.log(` ${pc15.bold("Catalog file:")}`);
|
|
13073
|
+
console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
|
|
11918
13074
|
console.log("");
|
|
11919
|
-
console.log(
|
|
11920
|
-
console.log(
|
|
13075
|
+
console.log(pc15.dim(" No app was launched."));
|
|
13076
|
+
console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app --vertex") + pc15.dim(" to launch."));
|
|
11921
13077
|
console.log("");
|
|
11922
13078
|
return 0;
|
|
11923
13079
|
}
|
|
@@ -11956,14 +13112,14 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11956
13112
|
proxyPort
|
|
11957
13113
|
});
|
|
11958
13114
|
sessionActive = true;
|
|
11959
|
-
|
|
13115
|
+
p17.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
|
|
11960
13116
|
logProxy(proxyPort);
|
|
11961
13117
|
logActiveModel(selectedEntry.display_name, selectedEntry.id);
|
|
11962
13118
|
try {
|
|
11963
13119
|
await launchOrRestartCodexApp();
|
|
11964
13120
|
} catch (err) {
|
|
11965
|
-
|
|
11966
|
-
|
|
13121
|
+
p17.log.warn(String(err instanceof Error ? err.message : err));
|
|
13122
|
+
p17.log.info(codexAppInstallHint());
|
|
11967
13123
|
}
|
|
11968
13124
|
printCodexAppSessionPanel({
|
|
11969
13125
|
modelLabel: selectedEntry.display_name,
|
|
@@ -11979,8 +13135,8 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
|
|
|
11979
13135
|
sessionActive = false;
|
|
11980
13136
|
}
|
|
11981
13137
|
if (isCodexAppRunning()) {
|
|
11982
|
-
const shouldClose = await
|
|
11983
|
-
if (shouldClose && !
|
|
13138
|
+
const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
|
|
13139
|
+
if (shouldClose && !p17.isCancel(shouldClose)) {
|
|
11984
13140
|
quitCodexAppGracefully();
|
|
11985
13141
|
}
|
|
11986
13142
|
}
|
|
@@ -12003,7 +13159,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12003
13159
|
try {
|
|
12004
13160
|
codexAppSupported();
|
|
12005
13161
|
} catch (err) {
|
|
12006
|
-
console.error(
|
|
13162
|
+
console.error(pc15.red(String(err instanceof Error ? err.message : err)));
|
|
12007
13163
|
return 1;
|
|
12008
13164
|
}
|
|
12009
13165
|
const interrupted = recoverInterruptedCodexAppSession();
|
|
@@ -12011,17 +13167,17 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12011
13167
|
const trace = args.includes("--trace");
|
|
12012
13168
|
const debugLogPath = getCodexProxyDebugLogPath();
|
|
12013
13169
|
if (trace && !configOnly) {
|
|
12014
|
-
|
|
13170
|
+
p17.log.info(`Debug log: ${debugLogPath}`);
|
|
12015
13171
|
}
|
|
12016
13172
|
const isTty = Boolean(process.stdin.isTTY);
|
|
12017
13173
|
if (!configOnly) {
|
|
12018
13174
|
const sessionCheck = checkAppSessionLock(isTty);
|
|
12019
13175
|
if (!sessionCheck.ok) {
|
|
12020
13176
|
if (sessionCheck.reason === "non_tty") {
|
|
12021
|
-
console.error(
|
|
13177
|
+
console.error(pc15.red("relay-ai codex-app requires an interactive terminal."));
|
|
12022
13178
|
return 1;
|
|
12023
13179
|
}
|
|
12024
|
-
console.error(
|
|
13180
|
+
console.error(pc15.yellow(`Another relay-ai codex-app session may be running (pid ${sessionCheck.lock.pid}).`));
|
|
12025
13181
|
console.error("Stop it with Ctrl+C in that terminal, or run relay-ai codex-app --restore after it exits.");
|
|
12026
13182
|
return 1;
|
|
12027
13183
|
}
|
|
@@ -12029,28 +13185,28 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12029
13185
|
if (!configOnly) {
|
|
12030
13186
|
codexAppIntro();
|
|
12031
13187
|
if (interrupted.recovered) {
|
|
12032
|
-
|
|
13188
|
+
p17.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
|
|
12033
13189
|
}
|
|
12034
13190
|
}
|
|
12035
13191
|
if (opts.vertex) {
|
|
12036
13192
|
return runCodexAppVertexLaunch(configOnly, trace);
|
|
12037
13193
|
}
|
|
12038
|
-
const catalogSpinner =
|
|
13194
|
+
const catalogSpinner = p17.spinner();
|
|
12039
13195
|
catalogSpinner.start("Loading your providers...");
|
|
12040
13196
|
let catalog;
|
|
12041
13197
|
try {
|
|
12042
13198
|
catalog = await fetchProviderCatalog({ agent: "codex-app" });
|
|
12043
13199
|
} catch (err) {
|
|
12044
13200
|
catalogSpinner.stop("");
|
|
12045
|
-
console.error(
|
|
13201
|
+
console.error(pc15.red(String(err instanceof Error ? err.message : err)));
|
|
12046
13202
|
return 1;
|
|
12047
13203
|
}
|
|
12048
13204
|
catalogSpinner.stop("");
|
|
12049
13205
|
const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
|
|
12050
13206
|
if (compatible.length === 0) {
|
|
12051
13207
|
if (!configOnly) {
|
|
12052
|
-
|
|
12053
|
-
|
|
13208
|
+
p17.log.warn("No Codex-compatible providers in your registry.");
|
|
13209
|
+
p17.log.info("Add a provider with relay-ai providers add.");
|
|
12054
13210
|
}
|
|
12055
13211
|
return 0;
|
|
12056
13212
|
}
|
|
@@ -12058,10 +13214,10 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12058
13214
|
const favorites = prefs.favoriteModels ?? [];
|
|
12059
13215
|
const favoritesActive = favorites.length > 0;
|
|
12060
13216
|
if (favoritesActive && !configOnly) {
|
|
12061
|
-
|
|
13217
|
+
p17.log.info(
|
|
12062
13218
|
`Favorites mode active \u2014 Codex App picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
|
|
12063
13219
|
);
|
|
12064
|
-
|
|
13220
|
+
p17.log.info("Edit with `relay-ai models`.");
|
|
12065
13221
|
}
|
|
12066
13222
|
let activeProvider = providerForCodexPicker(
|
|
12067
13223
|
compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
|
|
@@ -12076,12 +13232,12 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12076
13232
|
const favoriteProviders = compatible.map(providerForCodexPicker);
|
|
12077
13233
|
const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
|
|
12078
13234
|
if (!favoriteStart) {
|
|
12079
|
-
|
|
13235
|
+
p17.log.warn("No saved Codex App favorites are currently available.");
|
|
12080
13236
|
return 0;
|
|
12081
13237
|
}
|
|
12082
13238
|
activeProvider = favoriteStart.provider;
|
|
12083
13239
|
selectedModel = favoriteStart.model;
|
|
12084
|
-
|
|
13240
|
+
p17.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
12085
13241
|
break;
|
|
12086
13242
|
} else {
|
|
12087
13243
|
activeProvider = providerForCodexPicker(pickedProvider);
|
|
@@ -12101,7 +13257,7 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12101
13257
|
const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
|
|
12102
13258
|
if (!apiKey) {
|
|
12103
13259
|
if (!configOnly) {
|
|
12104
|
-
|
|
13260
|
+
p17.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
|
|
12105
13261
|
}
|
|
12106
13262
|
return 1;
|
|
12107
13263
|
}
|
|
@@ -12161,36 +13317,36 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12161
13317
|
const home = process.env["HOME"] ?? "";
|
|
12162
13318
|
const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
|
|
12163
13319
|
console.log("");
|
|
12164
|
-
console.log(
|
|
13320
|
+
console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
|
|
12165
13321
|
console.log("");
|
|
12166
13322
|
if (favoritesActive) {
|
|
12167
|
-
console.log(` ${
|
|
13323
|
+
console.log(` ${pc15.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
|
|
12168
13324
|
console.log("");
|
|
12169
|
-
console.log(` ${
|
|
13325
|
+
console.log(` ${pc15.bold("Models:")}`);
|
|
12170
13326
|
for (const r of resolvedFavorites) {
|
|
12171
|
-
console.log(` ${
|
|
13327
|
+
console.log(` ${pc15.cyan(r.model.id)} ${pc15.dim(`(${r.providerName})`)}`);
|
|
12172
13328
|
}
|
|
12173
13329
|
} else {
|
|
12174
|
-
console.log(` ${
|
|
12175
|
-
console.log(` ${
|
|
12176
|
-
console.log(` ${
|
|
12177
|
-
console.log(` ${
|
|
13330
|
+
console.log(` ${pc15.bold("Mode:")} Single model`);
|
|
13331
|
+
console.log(` ${pc15.bold("Provider:")} ${activeProvider.name}`);
|
|
13332
|
+
console.log(` ${pc15.bold("Model:")} ${formatCodexModelLabel(selectedModel)}`);
|
|
13333
|
+
console.log(` ${pc15.bold("Catalog:")} ${routable.length} model${routable.length !== 1 ? "s" : ""} available`);
|
|
12178
13334
|
}
|
|
12179
13335
|
console.log("");
|
|
12180
|
-
console.log(` ${
|
|
13336
|
+
console.log(` ${pc15.bold("config.toml patch preview:")}`);
|
|
12181
13337
|
const tomlPreview = previewAppConfigToml({
|
|
12182
13338
|
...specBase,
|
|
12183
13339
|
proxyPort: PREVIEW_PROXY_PORT
|
|
12184
13340
|
});
|
|
12185
13341
|
for (const line of tomlPreview.split("\n")) {
|
|
12186
|
-
console.log(` ${
|
|
13342
|
+
console.log(` ${pc15.dim(line)}`);
|
|
12187
13343
|
}
|
|
12188
13344
|
console.log("");
|
|
12189
|
-
console.log(` ${
|
|
12190
|
-
console.log(` ${
|
|
13345
|
+
console.log(` ${pc15.bold("Catalog file:")}`);
|
|
13346
|
+
console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
|
|
12191
13347
|
console.log("");
|
|
12192
|
-
console.log(
|
|
12193
|
-
console.log(
|
|
13348
|
+
console.log(pc15.dim(" No app was launched."));
|
|
13349
|
+
console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app") + pc15.dim(" to launch."));
|
|
12194
13350
|
console.log("");
|
|
12195
13351
|
return 0;
|
|
12196
13352
|
}
|
|
@@ -12234,8 +13390,8 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12234
13390
|
try {
|
|
12235
13391
|
await launchOrRestartCodexApp();
|
|
12236
13392
|
} catch (err) {
|
|
12237
|
-
|
|
12238
|
-
|
|
13393
|
+
p17.log.warn(String(err instanceof Error ? err.message : err));
|
|
13394
|
+
p17.log.info(codexAppInstallHint());
|
|
12239
13395
|
}
|
|
12240
13396
|
printCodexAppSessionPanel({
|
|
12241
13397
|
modelLabel,
|
|
@@ -12252,8 +13408,8 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12252
13408
|
sessionActive = false;
|
|
12253
13409
|
}
|
|
12254
13410
|
if (isCodexAppRunning()) {
|
|
12255
|
-
const shouldClose = await
|
|
12256
|
-
if (shouldClose && !
|
|
13411
|
+
const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
|
|
13412
|
+
if (shouldClose && !p17.isCancel(shouldClose)) {
|
|
12257
13413
|
quitCodexAppGracefully();
|
|
12258
13414
|
}
|
|
12259
13415
|
}
|
|
@@ -12265,29 +13421,29 @@ async function runCodexAppCommand(args, opts = {}) {
|
|
|
12265
13421
|
}
|
|
12266
13422
|
|
|
12267
13423
|
// src/claude-app.ts
|
|
12268
|
-
import
|
|
12269
|
-
import * as
|
|
13424
|
+
import pc16 from "picocolors";
|
|
13425
|
+
import * as p19 from "@clack/prompts";
|
|
12270
13426
|
|
|
12271
13427
|
// src/claude-desktop/app-config.ts
|
|
12272
|
-
import { existsSync as
|
|
12273
|
-
import { homedir as
|
|
12274
|
-
import { join as
|
|
12275
|
-
import { randomUUID as
|
|
13428
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
|
|
13429
|
+
import { homedir as homedir12 } from "os";
|
|
13430
|
+
import { join as join18, dirname as dirname7 } from "path";
|
|
13431
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
12276
13432
|
function getClaudeDesktopHome() {
|
|
12277
13433
|
if (process.platform === "win32") {
|
|
12278
|
-
return
|
|
13434
|
+
return join18(process.env.APPDATA || join18(homedir12(), "AppData", "Roaming"), "Claude-3p");
|
|
12279
13435
|
}
|
|
12280
|
-
return
|
|
13436
|
+
return join18(homedir12(), "Library", "Application Support", "Claude-3p");
|
|
12281
13437
|
}
|
|
12282
13438
|
function getConfigLibraryPath() {
|
|
12283
|
-
return
|
|
13439
|
+
return join18(getClaudeDesktopHome(), "configLibrary");
|
|
12284
13440
|
}
|
|
12285
13441
|
function getMetaJsonPath() {
|
|
12286
|
-
return
|
|
13442
|
+
return join18(getConfigLibraryPath(), "_meta.json");
|
|
12287
13443
|
}
|
|
12288
13444
|
function readMetaJson() {
|
|
12289
13445
|
const metaPath = getMetaJsonPath();
|
|
12290
|
-
if (!
|
|
13446
|
+
if (!existsSync17(metaPath)) return null;
|
|
12291
13447
|
try {
|
|
12292
13448
|
return JSON.parse(readFileSync13(metaPath, "utf8"));
|
|
12293
13449
|
} catch {
|
|
@@ -12310,8 +13466,8 @@ function buildRelayAiConfig(proxyPort) {
|
|
|
12310
13466
|
};
|
|
12311
13467
|
}
|
|
12312
13468
|
function writeRelayAiConfig(proxyPort) {
|
|
12313
|
-
const uuid =
|
|
12314
|
-
const configPath =
|
|
13469
|
+
const uuid = randomUUID3();
|
|
13470
|
+
const configPath = join18(getConfigLibraryPath(), `${uuid}.json`);
|
|
12315
13471
|
const config = buildRelayAiConfig(proxyPort);
|
|
12316
13472
|
mkdirSync9(dirname7(configPath), { recursive: true });
|
|
12317
13473
|
writeFileSync7(configPath, `${JSON.stringify(config, null, 2)}
|
|
@@ -12326,14 +13482,14 @@ function writeRelayAiConfig(proxyPort) {
|
|
|
12326
13482
|
}
|
|
12327
13483
|
|
|
12328
13484
|
// src/claude-desktop/app-session.ts
|
|
12329
|
-
import { existsSync as
|
|
12330
|
-
import { join as
|
|
13485
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync8, copyFileSync as copyFileSync5, unlinkSync as unlinkSync3 } from "fs";
|
|
13486
|
+
import { join as join19 } from "path";
|
|
12331
13487
|
function getSessionLockPath2() {
|
|
12332
|
-
return
|
|
13488
|
+
return join19(getClaudeDesktopHome(), ".relay-ai.lock");
|
|
12333
13489
|
}
|
|
12334
13490
|
function readSessionLock2() {
|
|
12335
13491
|
const path = getSessionLockPath2();
|
|
12336
|
-
if (!
|
|
13492
|
+
if (!existsSync18(path)) return null;
|
|
12337
13493
|
try {
|
|
12338
13494
|
const parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
12339
13495
|
if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
|
|
@@ -12358,21 +13514,21 @@ function isProcessAlive3(pid) {
|
|
|
12358
13514
|
function backupMetaJson() {
|
|
12359
13515
|
const metaPath = getMetaJsonPath();
|
|
12360
13516
|
const backupPath = `${metaPath}.bak`;
|
|
12361
|
-
if (
|
|
13517
|
+
if (existsSync18(metaPath)) {
|
|
12362
13518
|
copyFileSync5(metaPath, backupPath);
|
|
12363
13519
|
}
|
|
12364
13520
|
}
|
|
12365
13521
|
function restoreMetaJson() {
|
|
12366
13522
|
const metaPath = getMetaJsonPath();
|
|
12367
13523
|
const backupPath = `${metaPath}.bak`;
|
|
12368
|
-
if (
|
|
13524
|
+
if (existsSync18(backupPath)) {
|
|
12369
13525
|
copyFileSync5(backupPath, metaPath);
|
|
12370
13526
|
unlinkSync3(backupPath);
|
|
12371
13527
|
}
|
|
12372
13528
|
}
|
|
12373
13529
|
function removeRelayAiConfig(uuid) {
|
|
12374
|
-
const configPath =
|
|
12375
|
-
if (
|
|
13530
|
+
const configPath = join19(getConfigLibraryPath(), `${uuid}.json`);
|
|
13531
|
+
if (existsSync18(configPath)) {
|
|
12376
13532
|
try {
|
|
12377
13533
|
rmSync4(configPath, { force: true });
|
|
12378
13534
|
} catch {
|
|
@@ -12436,11 +13592,11 @@ function setupExitCleanup(uuid) {
|
|
|
12436
13592
|
}
|
|
12437
13593
|
|
|
12438
13594
|
// 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
|
|
13595
|
+
import { execSync as execSync6 } from "child_process";
|
|
13596
|
+
import { existsSync as existsSync19, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
13597
|
+
import { homedir as homedir13 } from "os";
|
|
13598
|
+
import { join as join20 } from "path";
|
|
13599
|
+
import * as p18 from "@clack/prompts";
|
|
12444
13600
|
var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
|
|
12445
13601
|
function claudeAppSupported() {
|
|
12446
13602
|
if (process.platform !== "darwin" && process.platform !== "win32") {
|
|
@@ -12448,7 +13604,7 @@ function claudeAppSupported() {
|
|
|
12448
13604
|
}
|
|
12449
13605
|
}
|
|
12450
13606
|
function run2(cmd, encoding = "utf8") {
|
|
12451
|
-
return
|
|
13607
|
+
return execSync6(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
12452
13608
|
}
|
|
12453
13609
|
function runPowerShell2(script) {
|
|
12454
13610
|
return run2(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
|
|
@@ -12456,26 +13612,26 @@ function runPowerShell2(script) {
|
|
|
12456
13612
|
function darwinAppCandidates2() {
|
|
12457
13613
|
return [
|
|
12458
13614
|
"/Applications/Claude.app",
|
|
12459
|
-
|
|
13615
|
+
join20(homedir13(), "Applications", "Claude.app")
|
|
12460
13616
|
];
|
|
12461
13617
|
}
|
|
12462
13618
|
function winLocalAppData2() {
|
|
12463
|
-
return process.env.LOCALAPPDATA ??
|
|
13619
|
+
return process.env.LOCALAPPDATA ?? join20(homedir13(), "AppData", "Local");
|
|
12464
13620
|
}
|
|
12465
13621
|
function winClaudeExeCandidates() {
|
|
12466
13622
|
const local = winLocalAppData2();
|
|
12467
13623
|
const bases = [
|
|
12468
|
-
|
|
12469
|
-
|
|
13624
|
+
join20(local, "Programs", "Claude"),
|
|
13625
|
+
join20(local, "Claude")
|
|
12470
13626
|
];
|
|
12471
13627
|
const out = [];
|
|
12472
13628
|
for (const base of bases) {
|
|
12473
|
-
out.push(
|
|
13629
|
+
out.push(join20(base, "Claude.exe"));
|
|
12474
13630
|
try {
|
|
12475
|
-
if (
|
|
13631
|
+
if (existsSync19(base)) {
|
|
12476
13632
|
for (const name of readdirSync4(base)) {
|
|
12477
13633
|
if (name.startsWith("app-")) {
|
|
12478
|
-
out.push(
|
|
13634
|
+
out.push(join20(base, name, "Claude.exe"));
|
|
12479
13635
|
}
|
|
12480
13636
|
}
|
|
12481
13637
|
}
|
|
@@ -12488,7 +13644,7 @@ function mdfindClaudeApp() {
|
|
|
12488
13644
|
try {
|
|
12489
13645
|
const out = run2(`mdfind "kMDItemCFBundleIdentifier == '${CLAUDE_BUNDLE_ID}'"`);
|
|
12490
13646
|
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
12491
|
-
return first &&
|
|
13647
|
+
return first && existsSync19(first) ? first : null;
|
|
12492
13648
|
} catch {
|
|
12493
13649
|
return null;
|
|
12494
13650
|
}
|
|
@@ -12496,14 +13652,14 @@ function mdfindClaudeApp() {
|
|
|
12496
13652
|
function findClaudeApp() {
|
|
12497
13653
|
if (process.platform === "darwin") {
|
|
12498
13654
|
for (const path of darwinAppCandidates2()) {
|
|
12499
|
-
if (
|
|
13655
|
+
if (existsSync19(path)) return path;
|
|
12500
13656
|
}
|
|
12501
13657
|
return mdfindClaudeApp();
|
|
12502
13658
|
}
|
|
12503
13659
|
if (process.platform === "win32") {
|
|
12504
13660
|
for (const path of winClaudeExeCandidates()) {
|
|
12505
13661
|
try {
|
|
12506
|
-
if (
|
|
13662
|
+
if (existsSync19(path) && statSync5(path).isFile()) return path;
|
|
12507
13663
|
} catch {
|
|
12508
13664
|
}
|
|
12509
13665
|
}
|
|
@@ -12567,9 +13723,9 @@ async function waitForQuit2(timeoutMs) {
|
|
|
12567
13723
|
function openClaudeAppAt(path) {
|
|
12568
13724
|
if (process.platform === "darwin") {
|
|
12569
13725
|
if (path.endsWith(".app")) {
|
|
12570
|
-
|
|
13726
|
+
execSync6(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
|
|
12571
13727
|
} else {
|
|
12572
|
-
|
|
13728
|
+
execSync6(`open -b ${CLAUDE_BUNDLE_ID}`, { stdio: "inherit" });
|
|
12573
13729
|
}
|
|
12574
13730
|
return;
|
|
12575
13731
|
}
|
|
@@ -12592,9 +13748,9 @@ function openClaudeApp() {
|
|
|
12592
13748
|
}
|
|
12593
13749
|
function darwinQuit2() {
|
|
12594
13750
|
try {
|
|
12595
|
-
|
|
13751
|
+
execSync6(`osascript -e 'tell application "Claude" to quit'`, { stdio: "pipe" });
|
|
12596
13752
|
} catch {
|
|
12597
|
-
|
|
13753
|
+
execSync6(`osascript -e 'tell application id "${CLAUDE_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
|
|
12598
13754
|
}
|
|
12599
13755
|
}
|
|
12600
13756
|
function winQuitGraceful2() {
|
|
@@ -12620,9 +13776,9 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
|
|
|
12620
13776
|
openClaudeAppAt(appPath);
|
|
12621
13777
|
return;
|
|
12622
13778
|
}
|
|
12623
|
-
const restart = await
|
|
12624
|
-
if (
|
|
12625
|
-
|
|
13779
|
+
const restart = await p18.confirm({ message: prompt, initialValue: true });
|
|
13780
|
+
if (p18.isCancel(restart) || !restart) {
|
|
13781
|
+
p18.log.info("Quit and reopen Claude Desktop when you are ready for the new model to take effect.");
|
|
12626
13782
|
return;
|
|
12627
13783
|
}
|
|
12628
13784
|
if (process.platform === "darwin") darwinQuit2();
|
|
@@ -12637,30 +13793,30 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
|
|
|
12637
13793
|
|
|
12638
13794
|
// src/claude-app.ts
|
|
12639
13795
|
function claudeAppHelpText() {
|
|
12640
|
-
return `${
|
|
13796
|
+
return `${pc16.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
|
|
12641
13797
|
|
|
12642
|
-
${
|
|
13798
|
+
${pc16.bold("Usage:")}
|
|
12643
13799
|
relay-ai claude-app [options]
|
|
12644
13800
|
relay-ai claude-app --trace
|
|
12645
13801
|
relay-ai claude-app --restore
|
|
12646
13802
|
relay-ai claude-app --help
|
|
12647
13803
|
relay-ai claude-app --version
|
|
12648
13804
|
|
|
12649
|
-
${
|
|
13805
|
+
${pc16.bold("Options:")}
|
|
12650
13806
|
--trace Write proxy debug logs to ~/.relay-ai/logs/
|
|
12651
13807
|
--restore Restore Claude Desktop config after an interrupted app session
|
|
12652
13808
|
--help Show this command help
|
|
12653
13809
|
--version Show version
|
|
12654
13810
|
|
|
12655
|
-
${
|
|
13811
|
+
${pc16.bold("Description:")}
|
|
12656
13812
|
Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
|
|
12657
13813
|
(with backup + restore on Ctrl+C), starts a local Responses proxy, and opens
|
|
12658
13814
|
the Claude Desktop app. Keep this terminal open while using Claude.
|
|
12659
13815
|
|
|
12660
|
-
${
|
|
13816
|
+
${pc16.bold("Platforms:")}
|
|
12661
13817
|
macOS and Windows. Linux is not supported.
|
|
12662
13818
|
|
|
12663
|
-
${
|
|
13819
|
+
${pc16.bold("Cleanup:")}
|
|
12664
13820
|
Ctrl+C stops the proxy and restores your previous Claude config.
|
|
12665
13821
|
After a crash: relay-ai claude-app --restore
|
|
12666
13822
|
`;
|
|
@@ -12684,37 +13840,37 @@ async function runClaudeAppCommand(args) {
|
|
|
12684
13840
|
try {
|
|
12685
13841
|
claudeAppSupported();
|
|
12686
13842
|
} catch (err) {
|
|
12687
|
-
console.error(
|
|
13843
|
+
console.error(pc16.red(String(err instanceof Error ? err.message : err)));
|
|
12688
13844
|
return 1;
|
|
12689
13845
|
}
|
|
12690
13846
|
const isTty = Boolean(process.stdin.isTTY);
|
|
12691
13847
|
if (!isTty) {
|
|
12692
|
-
console.error(
|
|
13848
|
+
console.error(pc16.red("relay-ai claude-app requires an interactive terminal."));
|
|
12693
13849
|
return 1;
|
|
12694
13850
|
}
|
|
12695
13851
|
if (isConcurrentLiveSession()) {
|
|
12696
|
-
console.error(
|
|
13852
|
+
console.error(pc16.yellow(`Another relay-ai claude-app session may be running.`));
|
|
12697
13853
|
console.error("Stop it with Ctrl+C in that terminal.");
|
|
12698
13854
|
return 1;
|
|
12699
13855
|
}
|
|
12700
13856
|
if (hasStaleSession()) {
|
|
12701
|
-
|
|
13857
|
+
p19.log.warn("Recovered from an interrupted claude-app session.");
|
|
12702
13858
|
recoverSession();
|
|
12703
13859
|
}
|
|
12704
|
-
const catalogSpinner =
|
|
13860
|
+
const catalogSpinner = p19.spinner();
|
|
12705
13861
|
catalogSpinner.start("Loading your providers...");
|
|
12706
13862
|
let catalog;
|
|
12707
13863
|
try {
|
|
12708
13864
|
catalog = await fetchProviderCatalog({ agent: "codex-app" });
|
|
12709
13865
|
} catch (err) {
|
|
12710
13866
|
catalogSpinner.stop("");
|
|
12711
|
-
console.error(
|
|
13867
|
+
console.error(pc16.red(String(err instanceof Error ? err.message : err)));
|
|
12712
13868
|
return 1;
|
|
12713
13869
|
}
|
|
12714
13870
|
catalogSpinner.stop("");
|
|
12715
13871
|
const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
|
|
12716
13872
|
if (compatible.length === 0) {
|
|
12717
|
-
|
|
13873
|
+
p19.log.warn("No compatible providers in your registry.");
|
|
12718
13874
|
return 0;
|
|
12719
13875
|
}
|
|
12720
13876
|
const prefs = loadPreferences();
|
|
@@ -12736,7 +13892,7 @@ async function runClaudeAppCommand(args) {
|
|
|
12736
13892
|
const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
|
|
12737
13893
|
const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
|
|
12738
13894
|
if (!apiKey) {
|
|
12739
|
-
|
|
13895
|
+
p19.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
|
|
12740
13896
|
return 1;
|
|
12741
13897
|
}
|
|
12742
13898
|
activeProvider.apiKey = apiKey;
|
|
@@ -12799,28 +13955,28 @@ async function runClaudeAppCommand(args) {
|
|
|
12799
13955
|
});
|
|
12800
13956
|
}
|
|
12801
13957
|
console.log(`
|
|
12802
|
-
${
|
|
13958
|
+
${pc16.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
|
|
12803
13959
|
try {
|
|
12804
13960
|
await launchOrRestartClaudeApp();
|
|
12805
13961
|
} catch (err) {
|
|
12806
|
-
|
|
13962
|
+
p19.log.warn(String(err instanceof Error ? err.message : err));
|
|
12807
13963
|
}
|
|
12808
13964
|
console.log(`
|
|
12809
|
-
${
|
|
13965
|
+
${pc16.bold("Claude Desktop 3P Mode Active")}`);
|
|
12810
13966
|
if (useFavorites) {
|
|
12811
|
-
console.log(`${
|
|
13967
|
+
console.log(`${pc16.dim("Catalog:")} Favorite models only`);
|
|
12812
13968
|
} else {
|
|
12813
|
-
console.log(`${
|
|
12814
|
-
console.log(`${
|
|
13969
|
+
console.log(`${pc16.dim("Model:")} ${selectedModel.id}`);
|
|
13970
|
+
console.log(`${pc16.dim("Provider:")} ${activeProvider.name}`);
|
|
12815
13971
|
}
|
|
12816
|
-
console.log(`${
|
|
13972
|
+
console.log(`${pc16.cyan("Press Ctrl+C to stop and restore config.")}`);
|
|
12817
13973
|
await waitForShutdown3();
|
|
12818
13974
|
console.log("");
|
|
12819
13975
|
cleanupSession(uuid);
|
|
12820
13976
|
sessionActive = false;
|
|
12821
13977
|
if (isClaudeAppRunning()) {
|
|
12822
|
-
const shouldClose = await
|
|
12823
|
-
if (shouldClose && !
|
|
13978
|
+
const shouldClose = await p19.confirm({ message: "Claude Desktop is still running. Close it?" });
|
|
13979
|
+
if (shouldClose && !p19.isCancel(shouldClose)) {
|
|
12824
13980
|
quitClaudeAppGracefully();
|
|
12825
13981
|
}
|
|
12826
13982
|
}
|
|
@@ -12835,17 +13991,17 @@ ${pc15.bold("Claude Desktop 3P Mode Active")}`);
|
|
|
12835
13991
|
}
|
|
12836
13992
|
|
|
12837
13993
|
// src/ai-doc.ts
|
|
12838
|
-
import { existsSync as
|
|
12839
|
-
import { homedir as
|
|
12840
|
-
import { join as
|
|
13994
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
13995
|
+
import { homedir as homedir14 } from "os";
|
|
13996
|
+
import { join as join21 } from "path";
|
|
12841
13997
|
var SKILL_DIR_NAME = "relay-ai-cli";
|
|
12842
13998
|
var SKILL_INSTALL_DIRS = [
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
13999
|
+
join21(getAppHome(), "skills"),
|
|
14000
|
+
join21(homedir14(), ".claude", "skills"),
|
|
14001
|
+
join21(homedir14(), ".agents", "skills"),
|
|
14002
|
+
join21(homedir14(), ".codex", "skills"),
|
|
14003
|
+
join21(homedir14(), ".cursor", "skills"),
|
|
14004
|
+
join21(homedir14(), ".cursor", "skills-cursor")
|
|
12849
14005
|
];
|
|
12850
14006
|
function parseSkillVersion(content) {
|
|
12851
14007
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -12859,8 +14015,8 @@ function parseSkillVersion(content) {
|
|
|
12859
14015
|
return null;
|
|
12860
14016
|
}
|
|
12861
14017
|
function readInstalledSkillVersion(skillDir) {
|
|
12862
|
-
const skillPath =
|
|
12863
|
-
if (!
|
|
14018
|
+
const skillPath = join21(skillDir, "SKILL.md");
|
|
14019
|
+
if (!existsSync20(skillPath)) return null;
|
|
12864
14020
|
try {
|
|
12865
14021
|
const head = readFileSync15(skillPath, "utf-8").slice(0, 1024);
|
|
12866
14022
|
return parseSkillVersion(head.includes("---", 4) ? head : `${head}
|
|
@@ -12872,8 +14028,8 @@ function readInstalledSkillVersion(skillDir) {
|
|
|
12872
14028
|
}
|
|
12873
14029
|
function skillInstallTargets() {
|
|
12874
14030
|
return SKILL_INSTALL_DIRS.map((dir) => {
|
|
12875
|
-
const skillDir =
|
|
12876
|
-
return { skillDir, skillPath:
|
|
14031
|
+
const skillDir = join21(dir, SKILL_DIR_NAME);
|
|
14032
|
+
return { skillDir, skillPath: join21(skillDir, "SKILL.md") };
|
|
12877
14033
|
});
|
|
12878
14034
|
}
|
|
12879
14035
|
function formatProviderModels(provider) {
|
|
@@ -12886,7 +14042,7 @@ function formatProviderModels(provider) {
|
|
|
12886
14042
|
function buildLiveStateSection() {
|
|
12887
14043
|
const prefs = loadPreferences();
|
|
12888
14044
|
const registry = loadRegistry();
|
|
12889
|
-
const enabled = registry.providers.filter((
|
|
14045
|
+
const enabled = registry.providers.filter((p21) => p21.enabled);
|
|
12890
14046
|
const prefLines = [];
|
|
12891
14047
|
if (prefs.lastProvider || prefs.lastModel) {
|
|
12892
14048
|
prefLines.push(` Claude last launch: provider=${prefs.lastProvider ?? "(none)"} model=${prefs.lastModel ?? "(none)"}`);
|
|
@@ -12894,15 +14050,18 @@ function buildLiveStateSection() {
|
|
|
12894
14050
|
if (prefs.lastCodexProvider || prefs.lastCodexModel) {
|
|
12895
14051
|
prefLines.push(` Codex last launch: provider=${prefs.lastCodexProvider ?? "(none)"} model=${prefs.lastCodexModel ?? "(none)"}`);
|
|
12896
14052
|
}
|
|
14053
|
+
if (prefs.lastGeminiProvider || prefs.lastGeminiModel) {
|
|
14054
|
+
prefLines.push(` Gemini last launch: provider=${prefs.lastGeminiProvider ?? "(none)"} model=${prefs.lastGeminiModel ?? "(none)"}`);
|
|
14055
|
+
}
|
|
12897
14056
|
if (prefs.favoriteModels?.length) {
|
|
12898
14057
|
prefLines.push(` Favorites (${prefs.favoriteModels.length}/${MAX_MODEL_CATALOG}):`);
|
|
12899
14058
|
for (const f of prefs.favoriteModels) {
|
|
12900
14059
|
prefLines.push(` ${f.providerId} / ${f.modelId}`);
|
|
12901
14060
|
}
|
|
12902
14061
|
}
|
|
12903
|
-
const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((
|
|
12904
|
-
` ${
|
|
12905
|
-
formatProviderModels(
|
|
14062
|
+
const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((p21) => [
|
|
14063
|
+
` ${p21.name} (${p21.id}) \u2014 ${p21.modelsCache?.models.length ?? 0} cached model(s)`,
|
|
14064
|
+
formatProviderModels(p21)
|
|
12906
14065
|
].join("\n"));
|
|
12907
14066
|
return `
|
|
12908
14067
|
================================================================================
|
|
@@ -12938,8 +14097,8 @@ function staticAiDocBody() {
|
|
|
12938
14097
|
RELAY-AI \u2014 AI AGENT REFERENCE (v${VERSION})
|
|
12939
14098
|
================================================================================
|
|
12940
14099
|
|
|
12941
|
-
relay-ai launches Claude Code, OpenAI Codex, and desktop apps
|
|
12942
|
-
provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
|
|
14100
|
+
relay-ai launches Claude Code, OpenAI Codex, Google Gemini CLI, and desktop apps
|
|
14101
|
+
against YOUR provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
|
|
12943
14102
|
It handles API translation, local proxies, env isolation, and model routing.
|
|
12944
14103
|
|
|
12945
14104
|
SKILL VERSIONING
|
|
@@ -12991,13 +14150,14 @@ PREVIEW LAUNCH WITHOUT STARTING A SESSION:
|
|
|
12991
14150
|
INTERACTIVE BROWSE (requires TTY \u2014 avoid in agent scripts):
|
|
12992
14151
|
relay-ai claude provider + model wizard
|
|
12993
14152
|
relay-ai codex provider + model wizard
|
|
14153
|
+
relay-ai gemini provider + model wizard
|
|
12994
14154
|
relay-ai providers provider management hub
|
|
12995
14155
|
|
|
12996
14156
|
================================================================================
|
|
12997
14157
|
AGENT PLATFORM PATTERNS \u2014 MULTI-MODEL / ONE-SHOT QUERIES
|
|
12998
14158
|
================================================================================
|
|
12999
14159
|
|
|
13000
|
-
relay-ai is designed so agents can use Claude Code or
|
|
14160
|
+
relay-ai is designed so agents can use Claude Code, Codex, or Gemini CLI as a PLATFORM:
|
|
13001
14161
|
run many models sequentially or in parallel shell jobs, each with a focused
|
|
13002
14162
|
prompt, without interactive wizards.
|
|
13003
14163
|
|
|
@@ -13045,6 +14205,24 @@ OPENAI CODEX \u2014 NON-INTERACTIVE (exec / positional prompt)
|
|
|
13045
14205
|
--provider <id>
|
|
13046
14206
|
--model <id> or provider__model-id slug
|
|
13047
14207
|
|
|
14208
|
+
GOOGLE GEMINI CLI \u2014 NON-INTERACTIVE (-p / --prompt)
|
|
14209
|
+
Skips the provider/model wizard when:
|
|
14210
|
+
\u2022 Both --provider and --model are set, OR
|
|
14211
|
+
\u2022 Non-interactive args (-p / --prompt, -i / --prompt-interactive, or positional query)
|
|
14212
|
+
and saved preferences exist
|
|
14213
|
+
|
|
14214
|
+
Examples:
|
|
14215
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "Review this file"
|
|
14216
|
+
relay-ai gemini -p "What is the capital of France?"
|
|
14217
|
+
|
|
14218
|
+
Machine-readable stdout:
|
|
14219
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o json
|
|
14220
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o stream-json
|
|
14221
|
+
|
|
14222
|
+
Boot flags (relay-ai \u2014 NOT passed to Gemini):
|
|
14223
|
+
--provider <id>
|
|
14224
|
+
--model <id> or provider__model-id slug
|
|
14225
|
+
|
|
13048
14226
|
MULTI-MODEL LOOP (shell pattern):
|
|
13049
14227
|
for model in llama-3.3-70b-versatile mixtral-8x7b-32768; do
|
|
13050
14228
|
relay-ai claude --provider groq --model "$model" -p "Same prompt for all models"
|
|
@@ -13054,10 +14232,14 @@ MULTI-MODEL LOOP (shell pattern):
|
|
|
13054
14232
|
relay-ai codex --provider zen --model "$model" exec "Same task"
|
|
13055
14233
|
done
|
|
13056
14234
|
|
|
14235
|
+
for model in gemini-2.5-flash gemini-2.5-pro; do
|
|
14236
|
+
relay-ai gemini --provider google --model "$model" -p "Same task"
|
|
14237
|
+
done
|
|
14238
|
+
|
|
13057
14239
|
FAVORITES / MID-SESSION SWITCHING:
|
|
13058
14240
|
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
|
|
14241
|
+
When favorites exist, interactive claude/codex/gemini launches expose /model switching.
|
|
14242
|
+
Boot flags (--provider + --model) or print/exec/-p mode use SINGLE-MODEL launch
|
|
13061
14243
|
(favorites catalog is skipped \u2014 better for agent one-shots).
|
|
13062
14244
|
|
|
13063
14245
|
================================================================================
|
|
@@ -13092,6 +14274,18 @@ CLAUDE CODE
|
|
|
13092
14274
|
relay-ai claude --provider anthropic --model claude-sonnet-4-6 -p "review file.ts"
|
|
13093
14275
|
relay-ai claude --dry-run --provider groq --model llama-3.3-70b-versatile
|
|
13094
14276
|
|
|
14277
|
+
GOOGLE GEMINI CLI
|
|
14278
|
+
relay-ai gemini [relay-options] [gemini-flags]
|
|
14279
|
+
|
|
14280
|
+
Relay options:
|
|
14281
|
+
--provider <id> Boot provider (skip wizard with --model)
|
|
14282
|
+
--model <id> Boot model id or provider__model slug
|
|
14283
|
+
--trace Debug logs in ~/.relay-ai/logs/
|
|
14284
|
+
|
|
14285
|
+
Examples:
|
|
14286
|
+
relay-ai gemini
|
|
14287
|
+
relay-ai gemini --provider google --model gemini-2.5-flash -p "What is the capital of France?"
|
|
14288
|
+
|
|
13095
14289
|
OPENAI CODEX CLI
|
|
13096
14290
|
relay-ai codex [relay-options] [codex-flags]
|
|
13097
14291
|
|
|
@@ -13125,7 +14319,7 @@ PROVIDERS REGISTRY
|
|
|
13125
14319
|
|
|
13126
14320
|
MODELS / FAVORITES
|
|
13127
14321
|
relay-ai models manage favoriteModels in config (alias: favorites)
|
|
13128
|
-
Used for mid-session /model switching in interactive Claude/Codex sessions.
|
|
14322
|
+
Used for mid-session /model switching in interactive Claude/Codex/Gemini sessions.
|
|
13129
14323
|
|
|
13130
14324
|
API GATEWAY (for tools that speak Anthropic/OpenAI HTTP)
|
|
13131
14325
|
relay-ai server foreground gateway on port 17645
|
|
@@ -13161,10 +14355,10 @@ DO:
|
|
|
13161
14355
|
|
|
13162
14356
|
DO NOT:
|
|
13163
14357
|
\u2022 Rely on interactive wizards in CI, scripts, or headless agent loops
|
|
13164
|
-
\u2022 Pass --provider / --model to Claude or
|
|
14358
|
+
\u2022 Pass --provider / --model to Claude, Codex, or Gemini directly \u2014 relay-ai consumes them
|
|
13165
14359
|
\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
|
|
14360
|
+
\u2022 Assume favorites catalog in print/exec/-p mode \u2014 use explicit boot flags
|
|
14361
|
+
\u2022 Mutate settings files (e.g. ~/.claude/settings.json, ~/.codex/config.toml, ~/.gemini/config/config.json) \u2014 relay-ai uses env +
|
|
13168
14362
|
temporary overlay profiles only
|
|
13169
14363
|
|
|
13170
14364
|
NON-TTY:
|
|
@@ -13178,6 +14372,7 @@ TROUBLESHOOTING
|
|
|
13178
14372
|
relay-ai codex --restore clean stale overlay after crash
|
|
13179
14373
|
relay-ai claude --trace proxy + Claude debug logs
|
|
13180
14374
|
relay-ai codex --trace proxy debug log on exit
|
|
14375
|
+
relay-ai gemini --trace proxy debug log on exit
|
|
13181
14376
|
relay-ai providers list verify provider ids
|
|
13182
14377
|
relay-ai providers refresh-models repopulate model cache
|
|
13183
14378
|
|
|
@@ -13196,7 +14391,7 @@ Human-readable guide: docs/AI-AGENTS.md in the relay-ai repo.
|
|
|
13196
14391
|
ALEF AGENT INTEGRATION
|
|
13197
14392
|
================================================================================
|
|
13198
14393
|
|
|
13199
|
-
alef-agent shells out to relay-ai to run Claude Code or
|
|
14394
|
+
alef-agent shells out to relay-ai to run Claude Code, Codex, or Gemini CLI against any
|
|
13200
14395
|
provider in ~/.relay-ai/providers.json. relay-ai is a launcher + proxy; the
|
|
13201
14396
|
child CLI owns NDJSON/JSONL on stdout.
|
|
13202
14397
|
|
|
@@ -13253,7 +14448,8 @@ ALEF CHECKLIST
|
|
|
13253
14448
|
\u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
|
|
13254
14449
|
\u25A1 Claude: --output-format stream-json (or json) with -p
|
|
13255
14450
|
\u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
|
|
13256
|
-
\u25A1
|
|
14451
|
+
\u25A1 Gemini: -o json (or stream-json) with -p
|
|
14452
|
+
\u25A1 Parse stdout only; ignore stderr for JSONL/NDJSON stream
|
|
13257
14453
|
\u25A1 Zen/Go: --provider zen explicitly + OPENCODE_API_KEY available
|
|
13258
14454
|
\u25A1 Codex network: default danger-full-access \u2014 no extra -s needed for nlm/curl/npm
|
|
13259
14455
|
\u25A1 MCP (Claude): --allowed-tools mcp__server__tool on claude args after relay-ai flags
|
|
@@ -13442,10 +14638,12 @@ function parseArgs(args) {
|
|
|
13442
14638
|
}
|
|
13443
14639
|
if (first === "providers") {
|
|
13444
14640
|
const parsed2 = emptyParsed("providers");
|
|
13445
|
-
parsed2.claudeArgs =
|
|
14641
|
+
parsed2.claudeArgs = [];
|
|
13446
14642
|
for (const arg of rest) {
|
|
13447
|
-
if (arg === "--
|
|
14643
|
+
if (arg === "--trace") parsed2.trace = true;
|
|
14644
|
+
else if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
|
|
13448
14645
|
else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
|
|
14646
|
+
else parsed2.claudeArgs.push(arg);
|
|
13449
14647
|
}
|
|
13450
14648
|
return parsed2;
|
|
13451
14649
|
}
|
|
@@ -13498,6 +14696,32 @@ function parseArgs(args) {
|
|
|
13498
14696
|
}
|
|
13499
14697
|
return parsed2;
|
|
13500
14698
|
}
|
|
14699
|
+
if (first === "gemini") {
|
|
14700
|
+
const parsed2 = emptyParsed("gemini");
|
|
14701
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
14702
|
+
const arg = rest[i];
|
|
14703
|
+
if (arg === "--trace") {
|
|
14704
|
+
parsed2.trace = true;
|
|
14705
|
+
continue;
|
|
14706
|
+
}
|
|
14707
|
+
if (arg === "--help" || arg === "-h") {
|
|
14708
|
+
parsed2.showHelp = true;
|
|
14709
|
+
continue;
|
|
14710
|
+
}
|
|
14711
|
+
if (arg === "--version" || arg === "-v") {
|
|
14712
|
+
parsed2.showVersion = true;
|
|
14713
|
+
continue;
|
|
14714
|
+
}
|
|
14715
|
+
const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
|
|
14716
|
+
if (consumed !== null) {
|
|
14717
|
+
if ("error" in consumed) return parsed2;
|
|
14718
|
+
i = consumed.next;
|
|
14719
|
+
continue;
|
|
14720
|
+
}
|
|
14721
|
+
parsed2.claudeArgs.push(arg);
|
|
14722
|
+
}
|
|
14723
|
+
return parsed2;
|
|
14724
|
+
}
|
|
13501
14725
|
if (first !== "claude") {
|
|
13502
14726
|
return {
|
|
13503
14727
|
...emptyParsed("root"),
|
|
@@ -13530,15 +14754,16 @@ function parseArgs(args) {
|
|
|
13530
14754
|
return parsed;
|
|
13531
14755
|
}
|
|
13532
14756
|
function rootHelpText() {
|
|
13533
|
-
return `${
|
|
14757
|
+
return `${pc17.bold("relay-ai")} v${VERSION}
|
|
13534
14758
|
Launch AI coding tools with OpenCode Zen / Go or local providers (Groq, Mistral,
|
|
13535
14759
|
OpenAI, Gemini, Ollama, and more).
|
|
13536
14760
|
|
|
13537
|
-
${
|
|
14761
|
+
${pc17.bold("Usage:")}
|
|
13538
14762
|
relay-ai claude [options] [claude-flags]
|
|
13539
14763
|
relay-ai claude-app [options]
|
|
13540
14764
|
relay-ai codex [options] [codex-flags]
|
|
13541
14765
|
relay-ai codex-app [options]
|
|
14766
|
+
relay-ai gemini [options] [gemini-flags]
|
|
13542
14767
|
relay-ai server [options]
|
|
13543
14768
|
relay-ai models
|
|
13544
14769
|
relay-ai favorites
|
|
@@ -13549,32 +14774,34 @@ ${pc16.bold("Usage:")}
|
|
|
13549
14774
|
relay-ai --ai --install Install or upgrade agent skill when version changed
|
|
13550
14775
|
relay-ai --ai --install --force Reinstall skill even if already current
|
|
13551
14776
|
|
|
13552
|
-
${
|
|
14777
|
+
${pc17.bold("Root options:")}
|
|
13553
14778
|
-h, --help Show this help
|
|
13554
14779
|
-v, --version Show version
|
|
13555
14780
|
--ai Print the full reference for AI agents
|
|
13556
14781
|
--ai --install Install or upgrade the relay-ai agent skill
|
|
13557
14782
|
--force Reinstall the agent skill when used with --ai --install
|
|
13558
14783
|
|
|
13559
|
-
${
|
|
14784
|
+
${pc17.bold("Commands:")}
|
|
13560
14785
|
claude Launch Claude Code \u2014 pick a provider from your registry
|
|
13561
14786
|
models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
|
|
13562
14787
|
favorites Alias for models
|
|
13563
14788
|
providers Add, import, and manage your AI providers
|
|
13564
14789
|
server Run a foreground API gateway (OpenCode Zen / Go and local providers)
|
|
13565
14790
|
codex Launch OpenAI Codex CLI with registry providers
|
|
14791
|
+
gemini Launch Google Gemini CLI with registry providers
|
|
13566
14792
|
codex-app Launch Codex desktop app with registry providers (macOS + Windows)
|
|
13567
14793
|
claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
|
|
13568
14794
|
|
|
13569
|
-
${
|
|
14795
|
+
${pc17.bold("Migration:")}
|
|
13570
14796
|
Bare relay-ai prints this help instead of launching Claude Code.
|
|
13571
14797
|
Use relay-ai claude for the wizard and launcher.
|
|
13572
14798
|
|
|
13573
|
-
${
|
|
14799
|
+
${pc17.bold("Examples:")}
|
|
13574
14800
|
relay-ai claude
|
|
13575
14801
|
relay-ai models
|
|
13576
14802
|
relay-ai providers
|
|
13577
14803
|
relay-ai codex
|
|
14804
|
+
relay-ai gemini
|
|
13578
14805
|
relay-ai codex-app
|
|
13579
14806
|
relay-ai claude-app
|
|
13580
14807
|
relay-ai server
|
|
@@ -13583,15 +14810,15 @@ ${pc16.bold("Examples:")}
|
|
|
13583
14810
|
relay-ai claude -- --print "hello"`;
|
|
13584
14811
|
}
|
|
13585
14812
|
function claudeHelpText() {
|
|
13586
|
-
return `${
|
|
14813
|
+
return `${pc17.bold("relay-ai claude")} v${VERSION}
|
|
13587
14814
|
Launch Claude Code with OpenCode Zen, Go, or local providers as the API backend.
|
|
13588
14815
|
|
|
13589
|
-
${
|
|
14816
|
+
${pc17.bold("Usage:")}
|
|
13590
14817
|
relay-ai claude [options] [claude-flags]
|
|
13591
14818
|
relay-ai claude --help
|
|
13592
14819
|
relay-ai claude --version
|
|
13593
14820
|
|
|
13594
|
-
${
|
|
14821
|
+
${pc17.bold("Options:")}
|
|
13595
14822
|
--dry-run Run the wizard but show a preview instead of launching Claude Code
|
|
13596
14823
|
--setup Hint: use relay-ai providers to add or manage providers
|
|
13597
14824
|
--trace Write debug logs to ~/.relay-ai/logs/ and show errors on exit
|
|
@@ -13600,22 +14827,22 @@ ${pc16.bold("Options:")}
|
|
|
13600
14827
|
--help Show this command help
|
|
13601
14828
|
--version Show version
|
|
13602
14829
|
|
|
13603
|
-
${
|
|
14830
|
+
${pc17.bold("Providers:")}
|
|
13604
14831
|
Cloud (Zen/Go) Requires OPENCODE_API_KEY \u2014 get one at https://opencode.ai/auth
|
|
13605
14832
|
Registry Configure with relay-ai providers add or import (Groq, Mistral,
|
|
13606
14833
|
Nvidia, DeepSeek, OpenAI, custom endpoints, etc.).
|
|
13607
14834
|
|
|
13608
|
-
${
|
|
14835
|
+
${pc17.bold("Model switching:")}
|
|
13609
14836
|
Run relay-ai models to save favorites (max ${MAX_MODEL_CATALOG}).
|
|
13610
14837
|
When favorites exist, launch starts a multi-route proxy and Claude Code /model
|
|
13611
14838
|
lists your starting model plus favorites for live switching.
|
|
13612
14839
|
With no favorites, launch uses a single model as before.
|
|
13613
14840
|
|
|
13614
|
-
${
|
|
14841
|
+
${pc17.bold("Note:")}
|
|
13615
14842
|
Claude Code may save the launched model to ~/.claude/settings.json.
|
|
13616
14843
|
Bare claude later can still show that model \u2014 reset with claude --model sonnet.
|
|
13617
14844
|
|
|
13618
|
-
${
|
|
14845
|
+
${pc17.bold("Examples:")}
|
|
13619
14846
|
relay-ai claude
|
|
13620
14847
|
relay-ai claude -c
|
|
13621
14848
|
relay-ai claude --resume abc-123
|
|
@@ -13629,54 +14856,54 @@ ${pc16.bold("Examples:")}
|
|
|
13629
14856
|
relay-ai claude -- --dangerously-skip-permissions`;
|
|
13630
14857
|
}
|
|
13631
14858
|
function serverHelpText() {
|
|
13632
|
-
return `${
|
|
14859
|
+
return `${pc17.bold("relay-ai server")} v${VERSION}
|
|
13633
14860
|
Run a foreground API gateway for registry providers, Zen/Go, or Vertex AI.
|
|
13634
14861
|
|
|
13635
|
-
${
|
|
14862
|
+
${pc17.bold("Usage:")}
|
|
13636
14863
|
relay-ai server
|
|
13637
14864
|
relay-ai server --vertex
|
|
13638
14865
|
relay-ai server --help
|
|
13639
14866
|
relay-ai server --version
|
|
13640
14867
|
|
|
13641
|
-
${
|
|
14868
|
+
${pc17.bold("Behavior:")}
|
|
13642
14869
|
Default: interactive wizard for exposed providers, discovery id masking (for
|
|
13643
14870
|
Claude Desktop / Cowork), optional favorites-only catalog, then listen mode.
|
|
13644
14871
|
--vertex: Anthropic-compatible gateway to Claude on Google Vertex AI using
|
|
13645
14872
|
local gcloud Application Default Credentials (no OpenCode API key).
|
|
13646
14873
|
Binds to port 17645. Network mode asks for a server password.
|
|
13647
14874
|
|
|
13648
|
-
${
|
|
14875
|
+
${pc17.bold("Vertex env:")}
|
|
13649
14876
|
ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
|
|
13650
14877
|
GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
|
|
13651
14878
|
Optional catalog: ~/.relay-ai/vertex-models.json (see assets/vertex-models.example.json)
|
|
13652
14879
|
|
|
13653
|
-
${
|
|
14880
|
+
${pc17.bold("Endpoints:")}
|
|
13654
14881
|
Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
|
|
13655
14882
|
OpenAI-compatible: OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
|
|
13656
14883
|
API key: use anything locally; use the server password in network mode.`;
|
|
13657
14884
|
}
|
|
13658
14885
|
function modelsHelpText() {
|
|
13659
|
-
return `${
|
|
14886
|
+
return `${pc17.bold("relay-ai models")} v${VERSION}
|
|
13660
14887
|
Manage favorite models for mid-session switching in Claude Code.
|
|
13661
14888
|
|
|
13662
|
-
${
|
|
14889
|
+
${pc17.bold("Usage:")}
|
|
13663
14890
|
relay-ai models
|
|
13664
14891
|
relay-ai models --help
|
|
13665
14892
|
relay-ai models --version
|
|
13666
14893
|
|
|
13667
|
-
${
|
|
14894
|
+
${pc17.bold("Behavior:")}
|
|
13668
14895
|
Opens an interactive manager to add or remove favorites.
|
|
13669
14896
|
Search all providers at once (paginated results) or browse one provider at a time.
|
|
13670
14897
|
Pick from Zen, Go, or any provider in your registry.
|
|
13671
14898
|
Favorites are saved to ~/.relay-ai/config.json (max ${MAX_MODEL_CATALOG}).
|
|
13672
14899
|
|
|
13673
|
-
${
|
|
14900
|
+
${pc17.bold("How it works:")}
|
|
13674
14901
|
When favorites exist, relay-ai claude starts a multi-route catalog proxy.
|
|
13675
14902
|
Claude Code /model lists your starting model plus favorites \u2014 switch live
|
|
13676
14903
|
without restarting. Mix cloud and local favorites in one session.
|
|
13677
14904
|
With no favorites, launch uses a single model as before.
|
|
13678
14905
|
|
|
13679
|
-
${
|
|
14906
|
+
${pc17.bold("Examples:")}
|
|
13680
14907
|
relay-ai models
|
|
13681
14908
|
relay-ai claude # switch menu active when favorites are set`;
|
|
13682
14909
|
}
|
|
@@ -13689,11 +14916,11 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13689
14916
|
let proxyHandle;
|
|
13690
14917
|
try {
|
|
13691
14918
|
proxyHandle = await startProxyCatalog(catalogRoutes, startingRoute.aliasId, trace);
|
|
13692
|
-
|
|
13693
|
-
`Switch menu active \u2014 proxy on port ${proxyHandle.port} ` +
|
|
14919
|
+
p20.log.info(
|
|
14920
|
+
`Switch menu active \u2014 proxy on port ${proxyHandle.port} ` + pc17.dim(`(${catalogRoutes.length} model${catalogRoutes.length !== 1 ? "s" : ""} in /model)`)
|
|
13694
14921
|
);
|
|
13695
14922
|
} catch (err) {
|
|
13696
|
-
|
|
14923
|
+
p20.log.error(`Failed to start proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
13697
14924
|
return 1;
|
|
13698
14925
|
}
|
|
13699
14926
|
const childEnv = buildChildEnv(
|
|
@@ -13706,7 +14933,7 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13706
14933
|
);
|
|
13707
14934
|
const debugLogPath = prepareClaudeTraceLog();
|
|
13708
14935
|
const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
|
|
13709
|
-
if (trace)
|
|
14936
|
+
if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
|
|
13710
14937
|
const exitCode = await launchClaude(
|
|
13711
14938
|
childEnv,
|
|
13712
14939
|
claudeCodeClientModelId(startingRoute.aliasId, contextWindow),
|
|
@@ -13718,14 +14945,14 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
|
|
|
13718
14945
|
}
|
|
13719
14946
|
async function runModelsCommand() {
|
|
13720
14947
|
relayIntro("Favorite Models");
|
|
13721
|
-
const
|
|
13722
|
-
|
|
14948
|
+
const spinner10 = p20.spinner();
|
|
14949
|
+
spinner10.start("Loading providers...");
|
|
13723
14950
|
const catalog = await fetchProviderCatalog();
|
|
13724
|
-
|
|
14951
|
+
spinner10.stop("");
|
|
13725
14952
|
const allProviders = providersForPicker(catalog);
|
|
13726
14953
|
if (allProviders.length === 0) {
|
|
13727
|
-
|
|
13728
|
-
|
|
14954
|
+
p20.log.warn("No providers found.");
|
|
14955
|
+
p20.log.info(`${pc17.dim("OpenCode Zen/Go is always available. Add providers with ")}${pc17.cyan("relay-ai providers")}${pc17.dim(".")}`);
|
|
13729
14956
|
relayOutro("Done");
|
|
13730
14957
|
return 0;
|
|
13731
14958
|
}
|
|
@@ -13743,45 +14970,45 @@ async function runModelsCommand() {
|
|
|
13743
14970
|
for (let i = 0; i < favorites.length; i++) {
|
|
13744
14971
|
const fav = favorites[i];
|
|
13745
14972
|
const entry = modelLookup.get(`${fav.providerId}:${fav.modelId}`);
|
|
13746
|
-
const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${
|
|
14973
|
+
const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${pc17.dim(`(${entry.providerName})`)}` : pc17.dim(`\u2605 ${fav.modelId} \u2014 provider gone`);
|
|
13747
14974
|
options.push({ value: `fav-${i}`, label, hint: "select to remove" });
|
|
13748
14975
|
}
|
|
13749
14976
|
const atCap = favorites.length >= MAX_MODEL_CATALOG;
|
|
13750
14977
|
options.push({
|
|
13751
14978
|
value: "__add__",
|
|
13752
|
-
label: atCap ?
|
|
14979
|
+
label: atCap ? pc17.dim(`+ Add a model \u2192 (limit of ${MAX_MODEL_CATALOG} reached)`) : pc17.cyan("+ Add a model \u2192"),
|
|
13753
14980
|
hint: atCap ? "Remove a favorite first to make room" : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
|
|
13754
14981
|
});
|
|
13755
14982
|
options.push({ value: "__done__", label: "Done", hint: "" });
|
|
13756
14983
|
const header = favorites.length === 0 ? `Favorites (0/${MAX_MODEL_CATALOG})` : `Favorites (${favorites.length}/${MAX_MODEL_CATALOG}) \u2014 select to remove`;
|
|
13757
|
-
const choice = await
|
|
14984
|
+
const choice = await p20.select({
|
|
13758
14985
|
message: header,
|
|
13759
14986
|
options,
|
|
13760
14987
|
initialValue: "__done__"
|
|
13761
14988
|
});
|
|
13762
|
-
if (
|
|
14989
|
+
if (p20.isCancel(choice) || choice === "__done__") break;
|
|
13763
14990
|
if (choice === "__add__") {
|
|
13764
14991
|
if (atCap) {
|
|
13765
|
-
|
|
14992
|
+
p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 remove one first.`);
|
|
13766
14993
|
continue;
|
|
13767
14994
|
}
|
|
13768
14995
|
const globalCount = buildGlobalFavoriteIndex(allProviders).length;
|
|
13769
|
-
const addPath = await
|
|
14996
|
+
const addPath = await p20.select({
|
|
13770
14997
|
message: "Add a favorite",
|
|
13771
14998
|
options: [
|
|
13772
14999
|
{
|
|
13773
15000
|
value: "global",
|
|
13774
|
-
label:
|
|
15001
|
+
label: pc17.cyan("Search all providers"),
|
|
13775
15002
|
hint: `${globalCount} models \xB7 ${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""}`
|
|
13776
15003
|
},
|
|
13777
15004
|
{
|
|
13778
15005
|
value: "provider",
|
|
13779
|
-
label:
|
|
15006
|
+
label: pc17.cyan("Browse by provider \u2192"),
|
|
13780
15007
|
hint: "Pick one provider first"
|
|
13781
15008
|
}
|
|
13782
15009
|
]
|
|
13783
15010
|
});
|
|
13784
|
-
if (
|
|
15011
|
+
if (p20.isCancel(addPath)) continue;
|
|
13785
15012
|
let provider;
|
|
13786
15013
|
let browsedMultiple = [];
|
|
13787
15014
|
if (addPath === "global") {
|
|
@@ -13796,12 +15023,12 @@ async function runModelsCommand() {
|
|
|
13796
15023
|
let currentInitialProvider = void 0;
|
|
13797
15024
|
while (true) {
|
|
13798
15025
|
const providerOptions = allProviders.map((ap) => providerSelectOption(ap));
|
|
13799
|
-
const pickedProviderId = await
|
|
15026
|
+
const pickedProviderId = await p20.select({
|
|
13800
15027
|
message: "Which provider?",
|
|
13801
15028
|
options: providerOptions,
|
|
13802
15029
|
initialValue: currentInitialProvider
|
|
13803
15030
|
});
|
|
13804
|
-
if (
|
|
15031
|
+
if (p20.isCancel(pickedProviderId)) break;
|
|
13805
15032
|
provider = allProviders.find((ap) => ap.id === pickedProviderId);
|
|
13806
15033
|
const options2 = provider.models.map((m) => {
|
|
13807
15034
|
const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
|
|
@@ -13809,15 +15036,15 @@ async function runModelsCommand() {
|
|
|
13809
15036
|
return {
|
|
13810
15037
|
value: m.id,
|
|
13811
15038
|
label: fmtModel(label, m.id),
|
|
13812
|
-
hint: favorited ?
|
|
15039
|
+
hint: favorited ? pc17.yellow("\u2605 already favorite") : ""
|
|
13813
15040
|
};
|
|
13814
15041
|
});
|
|
13815
|
-
const pickedModelIds = await
|
|
13816
|
-
message: `Select models to add from ${provider.name} ${
|
|
15042
|
+
const pickedModelIds = await p20.multiselect({
|
|
15043
|
+
message: `Select models to add from ${provider.name} ${pc17.dim("(Space to select, Enter to confirm)")}`,
|
|
13817
15044
|
options: options2,
|
|
13818
15045
|
required: false
|
|
13819
15046
|
});
|
|
13820
|
-
if (
|
|
15047
|
+
if (p20.isCancel(pickedModelIds)) {
|
|
13821
15048
|
currentInitialProvider = provider.id;
|
|
13822
15049
|
continue;
|
|
13823
15050
|
}
|
|
@@ -13852,16 +15079,16 @@ async function runModelsCommand() {
|
|
|
13852
15079
|
if (addedModels.length > 0) {
|
|
13853
15080
|
if (addedModels.length === 1) {
|
|
13854
15081
|
const modelName = addedModels[0].name || addedModels[0].id;
|
|
13855
|
-
|
|
15082
|
+
p20.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
|
|
13856
15083
|
} else {
|
|
13857
|
-
|
|
15084
|
+
p20.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
|
|
13858
15085
|
}
|
|
13859
15086
|
}
|
|
13860
15087
|
if (duplicateCount > 0) {
|
|
13861
|
-
|
|
15088
|
+
p20.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
|
|
13862
15089
|
}
|
|
13863
15090
|
if (limitReached) {
|
|
13864
|
-
|
|
15091
|
+
p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
|
|
13865
15092
|
}
|
|
13866
15093
|
} else if (choice.startsWith("fav-")) {
|
|
13867
15094
|
const idx = parseInt(choice.slice(4), 10);
|
|
@@ -13870,7 +15097,7 @@ async function runModelsCommand() {
|
|
|
13870
15097
|
const label = entry ? `${entry.modelName} (${entry.providerName})` : fav.modelId;
|
|
13871
15098
|
favorites = removeFavorite(favorites, fav);
|
|
13872
15099
|
favoritesDirty = true;
|
|
13873
|
-
|
|
15100
|
+
p20.log.success(`Removed ${label} from favorites.`);
|
|
13874
15101
|
}
|
|
13875
15102
|
}
|
|
13876
15103
|
if (favoritesDirty) {
|
|
@@ -13878,7 +15105,7 @@ async function runModelsCommand() {
|
|
|
13878
15105
|
}
|
|
13879
15106
|
relayOutro(
|
|
13880
15107
|
favorites.length === 0 ? "No favorites saved" : `${favorites.length} favorite${favorites.length !== 1 ? "s" : ""} saved`,
|
|
13881
|
-
favorites.length === 0 ?
|
|
15108
|
+
favorites.length === 0 ? pc17.dim("Launch uses single-model mode") : pc17.cyan("/model menu ready on next launch")
|
|
13882
15109
|
);
|
|
13883
15110
|
return 0;
|
|
13884
15111
|
}
|
|
@@ -13889,7 +15116,7 @@ async function runClaudeCommand(parsed) {
|
|
|
13889
15116
|
setAgentStdoutMode(agentStdout);
|
|
13890
15117
|
const claudePath = findClaudeBinary();
|
|
13891
15118
|
if (!claudePath) {
|
|
13892
|
-
console.error(
|
|
15119
|
+
console.error(pc17.red("\nError: claude binary not found on PATH.\n"));
|
|
13893
15120
|
console.error("Install Claude Code:");
|
|
13894
15121
|
console.error(" npm install -g @anthropic-ai/claude-code\n");
|
|
13895
15122
|
return 1;
|
|
@@ -13904,7 +15131,7 @@ async function runClaudeCommand(parsed) {
|
|
|
13904
15131
|
prefs
|
|
13905
15132
|
});
|
|
13906
15133
|
if (launchPlan.error) {
|
|
13907
|
-
console.error(
|
|
15134
|
+
console.error(pc17.red(`
|
|
13908
15135
|
Error: ${launchPlan.error}
|
|
13909
15136
|
`));
|
|
13910
15137
|
return 1;
|
|
@@ -13912,7 +15139,7 @@ Error: ${launchPlan.error}
|
|
|
13912
15139
|
const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
|
|
13913
15140
|
if (!agentStdout) relayIntro("Claude Code");
|
|
13914
15141
|
if (setup && !dryRun && !agentStdout) {
|
|
13915
|
-
|
|
15142
|
+
p20.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
|
|
13916
15143
|
}
|
|
13917
15144
|
if (!dryRun && await needsFirstRunSetup()) {
|
|
13918
15145
|
const firstRun = await runFirstRunWizard(trace);
|
|
@@ -13923,25 +15150,25 @@ Error: ${launchPlan.error}
|
|
|
13923
15150
|
try {
|
|
13924
15151
|
catalog = await fetchProviderCatalog();
|
|
13925
15152
|
} catch (err) {
|
|
13926
|
-
console.error(
|
|
15153
|
+
console.error(pc17.red(String(err instanceof Error ? err.message : err)));
|
|
13927
15154
|
return 1;
|
|
13928
15155
|
}
|
|
13929
15156
|
} else {
|
|
13930
|
-
const catalogSpinner =
|
|
15157
|
+
const catalogSpinner = p20.spinner();
|
|
13931
15158
|
catalogSpinner.start("Loading your providers...");
|
|
13932
15159
|
try {
|
|
13933
15160
|
catalog = await fetchProviderCatalog();
|
|
13934
15161
|
} catch (err) {
|
|
13935
15162
|
catalogSpinner.stop("");
|
|
13936
|
-
console.error(
|
|
15163
|
+
console.error(pc17.red(String(err instanceof Error ? err.message : err)));
|
|
13937
15164
|
return 1;
|
|
13938
15165
|
}
|
|
13939
15166
|
catalogSpinner.stop("");
|
|
13940
15167
|
}
|
|
13941
15168
|
const allProviders = providersForPicker(catalog);
|
|
13942
15169
|
if (allProviders.length === 0) {
|
|
13943
|
-
|
|
13944
|
-
|
|
15170
|
+
p20.log.warn("No providers available.");
|
|
15171
|
+
p20.log.info(pc17.dim("Run relay-ai providers add or import to get started."));
|
|
13945
15172
|
return 0;
|
|
13946
15173
|
}
|
|
13947
15174
|
const providerOptions = allProviders.map((lp) => providerSelectOption(lp));
|
|
@@ -13958,7 +15185,7 @@ Error: ${launchPlan.error}
|
|
|
13958
15185
|
if (launchPlan.skip && launchPlan.target) {
|
|
13959
15186
|
const resolved = findProviderAndModel(allProviders, launchPlan.target);
|
|
13960
15187
|
if (!resolved) {
|
|
13961
|
-
|
|
15188
|
+
p20.log.error(
|
|
13962
15189
|
`Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
|
|
13963
15190
|
);
|
|
13964
15191
|
return 1;
|
|
@@ -13966,31 +15193,31 @@ Error: ${launchPlan.error}
|
|
|
13966
15193
|
activeProvider = resolved.provider;
|
|
13967
15194
|
selectedModel = resolved.model;
|
|
13968
15195
|
if (!agentStdout) {
|
|
13969
|
-
|
|
15196
|
+
p20.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
13970
15197
|
}
|
|
13971
15198
|
if (!dryRun) recordLaunchSelection("claude", activeProvider.id, selectedModel.id, prefs);
|
|
13972
15199
|
} else {
|
|
13973
15200
|
let currentInitialProvider = initialProvider;
|
|
13974
15201
|
while (true) {
|
|
13975
|
-
const chosen = await
|
|
15202
|
+
const chosen = await p20.select({
|
|
13976
15203
|
message: "Which provider?",
|
|
13977
15204
|
options: providerOptions,
|
|
13978
15205
|
initialValue: currentInitialProvider
|
|
13979
15206
|
});
|
|
13980
|
-
if (
|
|
13981
|
-
|
|
15207
|
+
if (p20.isCancel(chosen)) {
|
|
15208
|
+
p20.cancel("Cancelled.");
|
|
13982
15209
|
return 0;
|
|
13983
15210
|
}
|
|
13984
15211
|
const providerChoice = chosen;
|
|
13985
15212
|
if (providerChoice === "__favorites__") {
|
|
13986
15213
|
const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
|
|
13987
15214
|
if (!favoriteStart) {
|
|
13988
|
-
|
|
15215
|
+
p20.log.warn("No saved favorites are currently available.");
|
|
13989
15216
|
return 0;
|
|
13990
15217
|
}
|
|
13991
15218
|
activeProvider = favoriteStart.provider;
|
|
13992
15219
|
selectedModel = favoriteStart.model;
|
|
13993
|
-
|
|
15220
|
+
p20.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
|
|
13994
15221
|
break;
|
|
13995
15222
|
} else {
|
|
13996
15223
|
activeProvider = allProviders.find((lp) => lp.id === providerChoice);
|
|
@@ -14017,27 +15244,27 @@ Error: ${launchPlan.error}
|
|
|
14017
15244
|
);
|
|
14018
15245
|
const startingRoute = resolveRoute(activeProvider.id, selectedModel.id) ?? null;
|
|
14019
15246
|
if (!startingRoute) {
|
|
14020
|
-
|
|
15247
|
+
p20.log.error("Could not resolve a proxy route for the selected model.");
|
|
14021
15248
|
return 1;
|
|
14022
15249
|
}
|
|
14023
15250
|
const { routes: catalogRoutes, droppedFavorites } = buildCatalogRoutes(startingRoute, favorites, resolveRoute);
|
|
14024
15251
|
if (droppedFavorites.length > 0) {
|
|
14025
|
-
|
|
15252
|
+
p20.log.warn(
|
|
14026
15253
|
`Skipping ${droppedFavorites.length} favorite${droppedFavorites.length === 1 ? "" : "s"} that are no longer available in /model`
|
|
14027
15254
|
);
|
|
14028
15255
|
}
|
|
14029
15256
|
if (dryRun) {
|
|
14030
15257
|
const endpoint = selectedModel.baseUrl ?? selectedModel.completionsUrl ?? "(unknown)";
|
|
14031
15258
|
console.log("");
|
|
14032
|
-
console.log(
|
|
15259
|
+
console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute (switch-menu mode):")));
|
|
14033
15260
|
console.log("");
|
|
14034
|
-
console.log(` ${
|
|
14035
|
-
console.log(` ${
|
|
14036
|
-
console.log(` ${
|
|
14037
|
-
console.log(` ${
|
|
14038
|
-
catalogRoutes.forEach((r) => console.log(` ${
|
|
15261
|
+
console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
|
|
15262
|
+
console.log(` ${pc17.bold("Starting model:")} ${selectedModel.id}`);
|
|
15263
|
+
console.log(` ${pc17.bold("Endpoint:")} ${endpoint}`);
|
|
15264
|
+
console.log(` ${pc17.bold("/model catalog:")} ${catalogRoutes.length} model(s)`);
|
|
15265
|
+
catalogRoutes.forEach((r) => console.log(` ${pc17.dim(r.displayName)}`));
|
|
14039
15266
|
console.log("");
|
|
14040
|
-
console.log(
|
|
15267
|
+
console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
|
|
14041
15268
|
console.log("");
|
|
14042
15269
|
return 0;
|
|
14043
15270
|
}
|
|
@@ -14053,21 +15280,21 @@ Error: ${launchPlan.error}
|
|
|
14053
15280
|
const formatDesc = selectedModel.modelFormat === "anthropic" ? "direct passthrough" : "via SDK adapter proxy";
|
|
14054
15281
|
const endpoint = selectedModel.modelFormat === "anthropic" ? selectedModel.baseUrl ?? "(unknown)" : selectedModel.npm ?? "SDK";
|
|
14055
15282
|
console.log("");
|
|
14056
|
-
console.log(
|
|
15283
|
+
console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute:")));
|
|
14057
15284
|
console.log("");
|
|
14058
|
-
console.log(` ${
|
|
14059
|
-
console.log(` ${
|
|
14060
|
-
console.log(` ${
|
|
14061
|
-
console.log(` ${
|
|
14062
|
-
console.log(` ${
|
|
15285
|
+
console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
|
|
15286
|
+
console.log(` ${pc17.bold("Model:")} ${selectedModel.id}`);
|
|
15287
|
+
console.log(` ${pc17.bold("Format:")} ${selectedModel.modelFormat} (${formatDesc})`);
|
|
15288
|
+
console.log(` ${pc17.bold(selectedModel.modelFormat === "anthropic" ? "Endpoint:" : "SDK npm:")} ${endpoint}`);
|
|
15289
|
+
console.log(` ${pc17.bold("Key:")} ${activeProvider.name} provider key`);
|
|
14063
15290
|
console.log("");
|
|
14064
|
-
console.log(
|
|
15291
|
+
console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
|
|
14065
15292
|
console.log("");
|
|
14066
15293
|
return 0;
|
|
14067
15294
|
}
|
|
14068
15295
|
const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
|
|
14069
15296
|
if (!launchApiKey?.trim()) {
|
|
14070
|
-
|
|
15297
|
+
p20.log.error(
|
|
14071
15298
|
`No credential found for ${activeProvider.name}. Add a key with relay-ai providers or set OPENCODE_API_KEY.`
|
|
14072
15299
|
);
|
|
14073
15300
|
return 1;
|
|
@@ -14103,12 +15330,12 @@ Error: ${launchPlan.error}
|
|
|
14103
15330
|
launchApiKey
|
|
14104
15331
|
);
|
|
14105
15332
|
if (!isAgentStdoutMode()) {
|
|
14106
|
-
|
|
14107
|
-
`SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ?
|
|
15333
|
+
p20.log.info(
|
|
15334
|
+
`SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ? pc17.dim(` (${selectedModel.npm})`) : "")
|
|
14108
15335
|
);
|
|
14109
15336
|
}
|
|
14110
15337
|
} catch (err) {
|
|
14111
|
-
|
|
15338
|
+
p20.log.error(`Failed to start SDK adapter proxy: ${err instanceof Error ? err.message : String(err)}`);
|
|
14112
15339
|
return 1;
|
|
14113
15340
|
}
|
|
14114
15341
|
childEnv = buildChildEnv(
|
|
@@ -14124,7 +15351,7 @@ Error: ${launchPlan.error}
|
|
|
14124
15351
|
}
|
|
14125
15352
|
const debugLogPath = prepareClaudeTraceLog();
|
|
14126
15353
|
const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
|
|
14127
|
-
if (trace)
|
|
15354
|
+
if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
|
|
14128
15355
|
const exitCode = await launchClaude(
|
|
14129
15356
|
childEnv,
|
|
14130
15357
|
claudeCodeClientModelId(selectedModel.id, selectedModel.contextWindow),
|
|
@@ -14137,7 +15364,7 @@ Error: ${launchPlan.error}
|
|
|
14137
15364
|
async function main(args = process.argv.slice(2)) {
|
|
14138
15365
|
const parsed = parseArgs(args);
|
|
14139
15366
|
if (parsed.error) {
|
|
14140
|
-
console.error(
|
|
15367
|
+
console.error(pc17.red(`
|
|
14141
15368
|
Error: ${parsed.error}
|
|
14142
15369
|
`));
|
|
14143
15370
|
printHelp(rootHelpText());
|
|
@@ -14192,6 +15419,9 @@ Error: ${parsed.error}
|
|
|
14192
15419
|
printHelp(providersHelpText());
|
|
14193
15420
|
return 0;
|
|
14194
15421
|
}
|
|
15422
|
+
if (parsed.trace) {
|
|
15423
|
+
process.env.RELAY_AI_TRACE = "1";
|
|
15424
|
+
}
|
|
14195
15425
|
return runProvidersCommand(parsed.claudeArgs);
|
|
14196
15426
|
}
|
|
14197
15427
|
if (parsed.command === "codex-app") {
|
|
@@ -14223,6 +15453,20 @@ Error: ${parsed.error}
|
|
|
14223
15453
|
vertex: parsed.vertex
|
|
14224
15454
|
});
|
|
14225
15455
|
}
|
|
15456
|
+
if (parsed.command === "gemini") {
|
|
15457
|
+
if (parsed.showVersion) {
|
|
15458
|
+
console.log(VERSION);
|
|
15459
|
+
return 0;
|
|
15460
|
+
}
|
|
15461
|
+
if (parsed.showHelp) {
|
|
15462
|
+
console.log(geminiHelpText());
|
|
15463
|
+
return 0;
|
|
15464
|
+
}
|
|
15465
|
+
return runGeminiCommand(parsed.claudeArgs, parsed.trace, {
|
|
15466
|
+
launchProvider: parsed.launchProvider,
|
|
15467
|
+
launchModel: parsed.launchModel
|
|
15468
|
+
});
|
|
15469
|
+
}
|
|
14226
15470
|
if (parsed.showVersion) {
|
|
14227
15471
|
console.log(VERSION);
|
|
14228
15472
|
return 0;
|
|
@@ -14248,7 +15492,7 @@ if (isCliEntryPoint()) {
|
|
|
14248
15492
|
if (err === /* @__PURE__ */ Symbol.for("clack:cancel")) {
|
|
14249
15493
|
process.exit(0);
|
|
14250
15494
|
}
|
|
14251
|
-
console.error(
|
|
15495
|
+
console.error(pc17.red("\nUnexpected error:"), err);
|
|
14252
15496
|
process.exit(1);
|
|
14253
15497
|
});
|
|
14254
15498
|
}
|