CLI: add OpenAI-compatible endpoint auth choice
This commit is contained in:
parent
6372242da7
commit
ad6d4f6389
@ -3,6 +3,7 @@ import { createServer } from "node:http";
|
|||||||
import type { AddressInfo } from "node:net";
|
import type { AddressInfo } from "node:net";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { rawDataToString } from "../infra/ws.js";
|
import { rawDataToString } from "../infra/ws.js";
|
||||||
@ -202,6 +203,15 @@ describe("canvas host", () => {
|
|||||||
|
|
||||||
it("serves the gateway-hosted A2UI scaffold", async () => {
|
it("serves the gateway-hosted A2UI scaffold", async () => {
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-canvas-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-canvas-"));
|
||||||
|
const a2uiDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "a2ui");
|
||||||
|
const bundlePath = path.join(a2uiDir, "a2ui.bundle.js");
|
||||||
|
let wroteBundle = false;
|
||||||
|
try {
|
||||||
|
await fs.stat(bundlePath);
|
||||||
|
} catch {
|
||||||
|
await fs.writeFile(bundlePath, "window.moltbotA2UI = {};", "utf8");
|
||||||
|
wroteBundle = true;
|
||||||
|
}
|
||||||
|
|
||||||
const server = await startCanvasHost({
|
const server = await startCanvasHost({
|
||||||
runtime: defaultRuntime,
|
runtime: defaultRuntime,
|
||||||
@ -227,6 +237,9 @@ describe("canvas host", () => {
|
|||||||
} finally {
|
} finally {
|
||||||
await server.close();
|
await server.close();
|
||||||
await fs.rm(dir, { recursive: true, force: true });
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
|
if (wroteBundle) {
|
||||||
|
await fs.rm(bundlePath, { force: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
value: "openai",
|
value: "openai",
|
||||||
label: "OpenAI",
|
label: "OpenAI",
|
||||||
hint: "Codex OAuth + API key",
|
hint: "Codex OAuth + API key",
|
||||||
choices: ["openai-codex", "openai-api-key"],
|
choices: ["openai-codex", "openai-api-key", "openai-custom-endpoint"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "anthropic",
|
value: "anthropic",
|
||||||
@ -134,6 +134,11 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
});
|
});
|
||||||
options.push({ value: "chutes", label: "Chutes (OAuth)" });
|
options.push({ value: "chutes", label: "Chutes (OAuth)" });
|
||||||
options.push({ value: "openai-api-key", label: "OpenAI API key" });
|
options.push({ value: "openai-api-key", label: "OpenAI API key" });
|
||||||
|
options.push({
|
||||||
|
value: "openai-custom-endpoint",
|
||||||
|
label: "OpenAI-compatible endpoint",
|
||||||
|
hint: "Use a private base URL + bearer token",
|
||||||
|
});
|
||||||
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
|
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
|
||||||
options.push({
|
options.push({
|
||||||
value: "ai-gateway-api-key",
|
value: "ai-gateway-api-key",
|
||||||
|
|||||||
@ -16,6 +16,34 @@ import {
|
|||||||
OPENAI_CODEX_DEFAULT_MODEL,
|
OPENAI_CODEX_DEFAULT_MODEL,
|
||||||
} from "./openai-codex-model-default.js";
|
} from "./openai-codex-model-default.js";
|
||||||
|
|
||||||
|
function readConfigEnvValue(cfg: { env?: Record<string, unknown> }, key: string): string | null {
|
||||||
|
const env = cfg.env;
|
||||||
|
if (!env) return null;
|
||||||
|
const direct = env[key];
|
||||||
|
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
||||||
|
const vars = env.vars as Record<string, unknown> | undefined;
|
||||||
|
const fromVars = vars?.[key];
|
||||||
|
if (typeof fromVars === "string" && fromVars.trim()) return fromVars.trim();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigEnvValue<T extends { env?: Record<string, unknown> }>(
|
||||||
|
cfg: T,
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
): T {
|
||||||
|
const existing = cfg.env ?? {};
|
||||||
|
const vars =
|
||||||
|
existing.vars && typeof existing.vars === "object"
|
||||||
|
? { ...(existing.vars as Record<string, string>) }
|
||||||
|
: undefined;
|
||||||
|
if (vars && Object.prototype.hasOwnProperty.call(vars, key)) {
|
||||||
|
vars[key] = value;
|
||||||
|
return { ...cfg, env: { ...existing, vars } } as T;
|
||||||
|
}
|
||||||
|
return { ...cfg, env: { ...existing, [key]: value, ...(vars ? { vars } : {}) } } as T;
|
||||||
|
}
|
||||||
|
|
||||||
export async function applyAuthChoiceOpenAI(
|
export async function applyAuthChoiceOpenAI(
|
||||||
params: ApplyAuthChoiceParams,
|
params: ApplyAuthChoiceParams,
|
||||||
): Promise<ApplyAuthChoiceResult | null> {
|
): Promise<ApplyAuthChoiceResult | null> {
|
||||||
@ -24,6 +52,59 @@ export async function applyAuthChoiceOpenAI(
|
|||||||
authChoice = "openai-api-key";
|
authChoice = "openai-api-key";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authChoice === "openai-custom-endpoint") {
|
||||||
|
let nextConfig = params.config;
|
||||||
|
const existingBaseUrl =
|
||||||
|
readConfigEnvValue(nextConfig, "OPENAI_BASE_URL") ??
|
||||||
|
process.env.OPENAI_BASE_URL?.trim() ??
|
||||||
|
"";
|
||||||
|
const existingToken =
|
||||||
|
readConfigEnvValue(nextConfig, "OPENAI_API_KEY") ?? process.env.OPENAI_API_KEY?.trim() ?? "";
|
||||||
|
|
||||||
|
let baseUrl = existingBaseUrl;
|
||||||
|
if (existingBaseUrl) {
|
||||||
|
const useExisting = await params.prompter.confirm({
|
||||||
|
message: `Use existing OPENAI_BASE_URL (${existingBaseUrl})?`,
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (!useExisting) baseUrl = "";
|
||||||
|
}
|
||||||
|
if (!baseUrl) {
|
||||||
|
const input = await params.prompter.text({
|
||||||
|
message: "OpenAI-compatible base URL",
|
||||||
|
placeholder: "https://api.example.com/v1",
|
||||||
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||||
|
});
|
||||||
|
baseUrl = String(input ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = existingToken;
|
||||||
|
if (existingToken) {
|
||||||
|
const useExisting = await params.prompter.confirm({
|
||||||
|
message: `Use existing OPENAI_API_KEY (${formatApiKeyPreview(existingToken)})?`,
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (!useExisting) token = "";
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
const input = await params.prompter.text({
|
||||||
|
message: "Bearer token",
|
||||||
|
validate: validateApiKeyInput,
|
||||||
|
});
|
||||||
|
token = normalizeApiKeyInput(String(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
nextConfig = setConfigEnvValue(nextConfig, "OPENAI_BASE_URL", baseUrl);
|
||||||
|
nextConfig = setConfigEnvValue(nextConfig, "OPENAI_API_KEY", token);
|
||||||
|
process.env.OPENAI_BASE_URL = baseUrl;
|
||||||
|
process.env.OPENAI_API_KEY = token;
|
||||||
|
await params.prompter.note(
|
||||||
|
"Saved OPENAI_BASE_URL and OPENAI_API_KEY in config env.",
|
||||||
|
"OpenAI endpoint",
|
||||||
|
);
|
||||||
|
return { config: nextConfig };
|
||||||
|
}
|
||||||
|
|
||||||
if (authChoice === "openai-api-key") {
|
if (authChoice === "openai-api-key") {
|
||||||
const envKey = resolveEnvApiKey("openai");
|
const envKey = resolveEnvApiKey("openai");
|
||||||
if (envKey) {
|
if (envKey) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
|||||||
"codex-cli": "openai-codex",
|
"codex-cli": "openai-codex",
|
||||||
chutes: "chutes",
|
chutes: "chutes",
|
||||||
"openai-api-key": "openai",
|
"openai-api-key": "openai",
|
||||||
|
"openai-custom-endpoint": "openai",
|
||||||
"openrouter-api-key": "openrouter",
|
"openrouter-api-key": "openrouter",
|
||||||
"ai-gateway-api-key": "vercel-ai-gateway",
|
"ai-gateway-api-key": "vercel-ai-gateway",
|
||||||
"moonshot-api-key": "moonshot",
|
"moonshot-api-key": "moonshot",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export type AuthChoice =
|
|||||||
| "chutes"
|
| "chutes"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
| "openai-api-key"
|
| "openai-api-key"
|
||||||
|
| "openai-custom-endpoint"
|
||||||
| "openrouter-api-key"
|
| "openrouter-api-key"
|
||||||
| "ai-gateway-api-key"
|
| "ai-gateway-api-key"
|
||||||
| "moonshot-api-key"
|
| "moonshot-api-key"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user