Merge branch 'main' into qianfan

This commit is contained in:
ide-rea
2026-02-04 22:39:13 +08:00
committed by GitHub
153 changed files with 4282 additions and 1535 deletions

View File

@@ -65,6 +65,97 @@ describe("cron cli", () => {
expect(params?.payload?.thinking).toBe("low");
});
it("defaults isolated cron add to announce delivery", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
await program.parseAsync(
[
"cron",
"add",
"--name",
"Daily",
"--cron",
"* * * * *",
"--session",
"isolated",
"--message",
"hello",
],
{ from: "user" },
);
const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
const params = addCall?.[2] as { delivery?: { mode?: string } };
expect(params?.delivery?.mode).toBe("announce");
});
it("infers sessionTarget from payload when --session is omitted", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
await program.parseAsync(
["cron", "add", "--name", "Main reminder", "--cron", "* * * * *", "--system-event", "hi"],
{ from: "user" },
);
let addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
let params = addCall?.[2] as { sessionTarget?: string; payload?: { kind?: string } };
expect(params?.sessionTarget).toBe("main");
expect(params?.payload?.kind).toBe("systemEvent");
callGatewayFromCli.mockClear();
await program.parseAsync(
["cron", "add", "--name", "Isolated task", "--cron", "* * * * *", "--message", "hello"],
{ from: "user" },
);
addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
params = addCall?.[2] as { sessionTarget?: string; payload?: { kind?: string } };
expect(params?.sessionTarget).toBe("isolated");
expect(params?.payload?.kind).toBe("agentTurn");
});
it("supports --keep-after-run on cron add", async () => {
callGatewayFromCli.mockClear();
const { registerCronCli } = await import("./cron-cli.js");
const program = new Command();
program.exitOverride();
registerCronCli(program);
await program.parseAsync(
[
"cron",
"add",
"--name",
"Keep me",
"--at",
"20m",
"--session",
"main",
"--system-event",
"hello",
"--keep-after-run",
],
{ from: "user" },
);
const addCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.add");
const params = addCall?.[2] as { deleteAfterRun?: boolean };
expect(params?.deleteAfterRun).toBe(false);
});
it("sends agent id on cron add", async () => {
callGatewayFromCli.mockClear();
@@ -213,20 +304,15 @@ describe("cron cli", () => {
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as {
patch?: {
payload?: {
kind?: string;
message?: string;
deliver?: boolean;
channel?: string;
to?: string;
};
payload?: { kind?: string; message?: string };
delivery?: { mode?: string; channel?: string; to?: string };
};
};
expect(patch?.patch?.payload?.kind).toBe("agentTurn");
expect(patch?.patch?.payload?.deliver).toBe(true);
expect(patch?.patch?.payload?.channel).toBe("telegram");
expect(patch?.patch?.payload?.to).toBe("19098680");
expect(patch?.patch?.delivery?.mode).toBe("announce");
expect(patch?.patch?.delivery?.channel).toBe("telegram");
expect(patch?.patch?.delivery?.to).toBe("19098680");
expect(patch?.patch?.payload?.message).toBeUndefined();
});
@@ -242,11 +328,11 @@ describe("cron cli", () => {
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as {
patch?: { payload?: { kind?: string; deliver?: boolean } };
patch?: { payload?: { kind?: string }; delivery?: { mode?: string } };
};
expect(patch?.patch?.payload?.kind).toBe("agentTurn");
expect(patch?.patch?.payload?.deliver).toBe(false);
expect(patch?.patch?.delivery?.mode).toBe("none");
});
it("does not include undefined delivery fields when updating message", async () => {
@@ -272,6 +358,7 @@ describe("cron cli", () => {
to?: string;
bestEffortDeliver?: boolean;
};
delivery?: unknown;
};
};
@@ -283,6 +370,7 @@ describe("cron cli", () => {
expect(patch?.patch?.payload).not.toHaveProperty("channel");
expect(patch?.patch?.payload).not.toHaveProperty("to");
expect(patch?.patch?.payload).not.toHaveProperty("bestEffortDeliver");
expect(patch?.patch).not.toHaveProperty("delivery");
});
it("includes delivery fields when explicitly provided with message", async () => {
@@ -313,20 +401,16 @@ describe("cron cli", () => {
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as {
patch?: {
payload?: {
message?: string;
deliver?: boolean;
channel?: string;
to?: string;
};
payload?: { message?: string };
delivery?: { mode?: string; channel?: string; to?: string };
};
};
// Should include everything
expect(patch?.patch?.payload?.message).toBe("Updated message");
expect(patch?.patch?.payload?.deliver).toBe(true);
expect(patch?.patch?.payload?.channel).toBe("telegram");
expect(patch?.patch?.payload?.to).toBe("19098680");
expect(patch?.patch?.delivery?.mode).toBe("announce");
expect(patch?.patch?.delivery?.channel).toBe("telegram");
expect(patch?.patch?.delivery?.to).toBe("19098680");
});
it("includes best-effort delivery when provided with message", async () => {
@@ -344,11 +428,15 @@ describe("cron cli", () => {
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as {
patch?: { payload?: { message?: string; bestEffortDeliver?: boolean } };
patch?: {
payload?: { message?: string };
delivery?: { bestEffort?: boolean; mode?: string };
};
};
expect(patch?.patch?.payload?.message).toBe("Updated message");
expect(patch?.patch?.payload?.bestEffortDeliver).toBe(true);
expect(patch?.patch?.delivery?.mode).toBe("announce");
expect(patch?.patch?.delivery?.bestEffort).toBe(true);
});
it("includes no-best-effort delivery when provided with message", async () => {
@@ -366,10 +454,14 @@ describe("cron cli", () => {
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
const patch = updateCall?.[2] as {
patch?: { payload?: { message?: string; bestEffortDeliver?: boolean } };
patch?: {
payload?: { message?: string };
delivery?: { bestEffort?: boolean; mode?: string };
};
};
expect(patch?.patch?.payload?.message).toBe("Updated message");
expect(patch?.patch?.payload?.bestEffortDeliver).toBe(false);
expect(patch?.patch?.delivery?.mode).toBe("announce");
expect(patch?.patch?.delivery?.bestEffort).toBe(false);
});
});

View File

@@ -8,7 +8,7 @@ import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
import {
getCronChannelOptions,
parseAtMs,
parseAt,
parseDurationMs,
printCronList,
warnIfCronSchedulerDisabled,
@@ -68,8 +68,9 @@ export function registerCronAddCommand(cron: Command) {
.option("--description <text>", "Optional description")
.option("--disabled", "Create job disabled", false)
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
.option("--agent <id>", "Agent id for this job")
.option("--session <target>", "Session target (main|isolated)", "main")
.option("--session <target>", "Session target (main|isolated)")
.option("--wake <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
.option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)")
.option("--every <duration>", "Run every duration (e.g. 10m, 1h)")
@@ -80,26 +81,17 @@ export function registerCronAddCommand(cron: Command) {
.option("--thinking <level>", "Thinking level for agent jobs (off|minimal|low|medium|high)")
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
.option(
"--deliver",
"Deliver agent output (required when using last-route delivery without --to)",
false,
)
.option("--announce", "Announce summary to a chat (subagent-style)", false)
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
.option("--no-deliver", "Disable announce delivery and skip main-session summary")
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`, "last")
.option(
"--to <dest>",
"Delivery destination (E.164, Telegram chatId, or Discord channel/user)",
)
.option("--best-effort-deliver", "Do not fail the job if delivery fails", false)
.option("--post-prefix <prefix>", "Prefix for main-session post", "Cron")
.option(
"--post-mode <mode>",
"What to post back to main for isolated jobs (summary|full)",
"summary",
)
.option("--post-max-chars <n>", "Max chars when --post-mode=full (default 8000)", "8000")
.option("--json", "Output JSON", false)
.action(async (opts: GatewayRpcOpts & Record<string, unknown>) => {
.action(async (opts: GatewayRpcOpts & Record<string, unknown>, cmd?: Command) => {
try {
const schedule = (() => {
const at = typeof opts.at === "string" ? opts.at : "";
@@ -110,11 +102,11 @@ export function registerCronAddCommand(cron: Command) {
throw new Error("Choose exactly one schedule: --at, --every, or --cron");
}
if (at) {
const atMs = parseAtMs(at);
if (!atMs) {
const atIso = parseAt(at);
if (!atIso) {
throw new Error("Invalid --at; use ISO time or duration like 20m");
}
return { kind: "at" as const, atMs };
return { kind: "at" as const, at: atIso };
}
if (every) {
const everyMs = parseDurationMs(every);
@@ -130,12 +122,6 @@ export function registerCronAddCommand(cron: Command) {
};
})();
const sessionTargetRaw = typeof opts.session === "string" ? opts.session : "main";
const sessionTarget = sessionTargetRaw.trim() || "main";
if (sessionTarget !== "main" && sessionTarget !== "isolated") {
throw new Error("--session must be main or isolated");
}
const wakeModeRaw = typeof opts.wake === "string" ? opts.wake : "next-heartbeat";
const wakeMode = wakeModeRaw.trim() || "next-heartbeat";
if (wakeMode !== "now" && wakeMode !== "next-heartbeat") {
@@ -147,6 +133,13 @@ export function registerCronAddCommand(cron: Command) {
? sanitizeAgentId(opts.agent.trim())
: undefined;
const hasAnnounce = Boolean(opts.announce) || opts.deliver === true;
const hasNoDeliver = opts.deliver === false;
const deliveryFlagCount = [hasAnnounce, hasNoDeliver].filter(Boolean).length;
if (deliveryFlagCount > 1) {
throw new Error("Choose at most one of --announce or --no-deliver");
}
const payload = (() => {
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
const message = typeof opts.message === "string" ? opts.message.trim() : "";
@@ -169,36 +162,46 @@ export function registerCronAddCommand(cron: Command) {
: undefined,
timeoutSeconds:
timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined,
deliver: opts.deliver ? true : undefined,
channel: typeof opts.channel === "string" ? opts.channel : "last",
to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined,
bestEffortDeliver: opts.bestEffortDeliver ? true : undefined,
};
})();
const optionSource =
typeof cmd?.getOptionValueSource === "function"
? (name: string) => cmd.getOptionValueSource(name)
: () => undefined;
const sessionSource = optionSource("session");
const sessionTargetRaw = typeof opts.session === "string" ? opts.session.trim() : "";
const inferredSessionTarget = payload.kind === "agentTurn" ? "isolated" : "main";
const sessionTarget =
sessionSource === "cli" ? sessionTargetRaw || "" : inferredSessionTarget;
if (sessionTarget !== "main" && sessionTarget !== "isolated") {
throw new Error("--session must be main or isolated");
}
if (opts.deleteAfterRun && opts.keepAfterRun) {
throw new Error("Choose --delete-after-run or --keep-after-run, not both");
}
if (sessionTarget === "main" && payload.kind !== "systemEvent") {
throw new Error("Main jobs require --system-event (systemEvent).");
}
if (sessionTarget === "isolated" && payload.kind !== "agentTurn") {
throw new Error("Isolated jobs require --message (agentTurn).");
}
if (
(opts.announce || typeof opts.deliver === "boolean") &&
(sessionTarget !== "isolated" || payload.kind !== "agentTurn")
) {
throw new Error("--announce/--no-deliver require --session isolated.");
}
const isolation =
sessionTarget === "isolated"
? {
postToMainPrefix:
typeof opts.postPrefix === "string" && opts.postPrefix.trim()
? opts.postPrefix.trim()
: "Cron",
postToMainMode:
opts.postMode === "full" || opts.postMode === "summary"
? opts.postMode
: undefined,
postToMainMaxChars:
typeof opts.postMaxChars === "string" && /^\d+$/.test(opts.postMaxChars)
? Number.parseInt(opts.postMaxChars, 10)
: undefined,
}
const deliveryMode =
sessionTarget === "isolated" && payload.kind === "agentTurn"
? hasAnnounce
? "announce"
: hasNoDeliver
? "none"
: "announce"
: undefined;
const nameRaw = typeof opts.name === "string" ? opts.name : "";
@@ -216,13 +219,23 @@ export function registerCronAddCommand(cron: Command) {
name,
description,
enabled: !opts.disabled,
deleteAfterRun: Boolean(opts.deleteAfterRun),
deleteAfterRun: opts.deleteAfterRun ? true : opts.keepAfterRun ? false : undefined,
agentId,
schedule,
sessionTarget,
wakeMode,
payload,
isolation,
delivery: deliveryMode
? {
mode: deliveryMode,
channel:
typeof opts.channel === "string" && opts.channel.trim()
? opts.channel.trim()
: undefined,
to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined,
bestEffort: opts.bestEffortDeliver ? true : undefined,
}
: undefined,
};
const res = await callGatewayFromCli("cron.add", opts, params);

View File

@@ -5,7 +5,7 @@ import { defaultRuntime } from "../../runtime.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import {
getCronChannelOptions,
parseAtMs,
parseAt,
parseDurationMs,
warnIfCronSchedulerDisabled,
} from "./shared.js";
@@ -46,11 +46,9 @@ export function registerCronEditCommand(cron: Command) {
.option("--thinking <level>", "Thinking level for agent jobs")
.option("--model <model>", "Model override for agent jobs")
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
.option(
"--deliver",
"Deliver agent output (required when using last-route delivery without --to)",
)
.option("--no-deliver", "Disable delivery")
.option("--announce", "Announce summary to a chat (subagent-style)")
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
.option("--no-deliver", "Disable announce delivery")
.option("--channel <channel>", `Delivery channel (${getCronChannelOptions()})`)
.option(
"--to <dest>",
@@ -58,7 +56,6 @@ export function registerCronEditCommand(cron: Command) {
)
.option("--best-effort-deliver", "Do not fail job if delivery fails")
.option("--no-best-effort-deliver", "Fail job when delivery fails")
.option("--post-prefix <prefix>", "Prefix for summary system event")
.action(async (id, opts) => {
try {
if (opts.session === "main" && opts.message) {
@@ -71,8 +68,8 @@ export function registerCronEditCommand(cron: Command) {
"Isolated jobs cannot use --system-event; use --message or --session main.",
);
}
if (opts.session === "main" && typeof opts.postPrefix === "string") {
throw new Error("--post-prefix only applies to isolated jobs.");
if (opts.announce && typeof opts.deliver === "boolean") {
throw new Error("Choose --announce or --no-deliver (not multiple).");
}
const patch: Record<string, unknown> = {};
@@ -121,11 +118,11 @@ export function registerCronEditCommand(cron: Command) {
throw new Error("Choose at most one schedule change");
}
if (opts.at) {
const atMs = parseAtMs(String(opts.at));
if (!atMs) {
const atIso = parseAt(String(opts.at));
if (!atIso) {
throw new Error("Invalid --at");
}
patch.schedule = { kind: "at", atMs };
patch.schedule = { kind: "at", at: atIso };
} else if (opts.every) {
const everyMs = parseDurationMs(String(opts.every));
if (!everyMs) {
@@ -151,15 +148,17 @@ export function registerCronEditCommand(cron: Command) {
? Number.parseInt(String(opts.timeoutSeconds), 10)
: undefined;
const hasTimeoutSeconds = Boolean(timeoutSeconds && Number.isFinite(timeoutSeconds));
const hasDeliveryModeFlag = opts.announce || typeof opts.deliver === "boolean";
const hasDeliveryTarget = typeof opts.channel === "string" || typeof opts.to === "string";
const hasBestEffort = typeof opts.bestEffortDeliver === "boolean";
const hasAgentTurnPatch =
typeof opts.message === "string" ||
Boolean(model) ||
Boolean(thinking) ||
hasTimeoutSeconds ||
typeof opts.deliver === "boolean" ||
typeof opts.channel === "string" ||
typeof opts.to === "string" ||
typeof opts.bestEffortDeliver === "boolean";
hasDeliveryModeFlag ||
hasDeliveryTarget ||
hasBestEffort;
if (hasSystemEventPatch && hasAgentTurnPatch) {
throw new Error("Choose at most one payload change");
}
@@ -174,22 +173,29 @@ export function registerCronEditCommand(cron: Command) {
assignIf(payload, "model", model, Boolean(model));
assignIf(payload, "thinking", thinking, Boolean(thinking));
assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds);
assignIf(payload, "deliver", opts.deliver, typeof opts.deliver === "boolean");
assignIf(payload, "channel", opts.channel, typeof opts.channel === "string");
assignIf(payload, "to", opts.to, typeof opts.to === "string");
assignIf(
payload,
"bestEffortDeliver",
opts.bestEffortDeliver,
typeof opts.bestEffortDeliver === "boolean",
);
patch.payload = payload;
}
if (typeof opts.postPrefix === "string") {
patch.isolation = {
postToMainPrefix: opts.postPrefix.trim() ? opts.postPrefix : "Cron",
};
if (hasDeliveryModeFlag || hasDeliveryTarget || hasBestEffort) {
const deliveryMode =
opts.announce || opts.deliver === true
? "announce"
: opts.deliver === false
? "none"
: "announce";
const delivery: Record<string, unknown> = { mode: deliveryMode };
if (typeof opts.channel === "string") {
const channel = opts.channel.trim();
delivery.channel = channel ? channel : undefined;
}
if (typeof opts.to === "string") {
const to = opts.to.trim();
delivery.to = to ? to : undefined;
}
if (typeof opts.bestEffortDeliver === "boolean") {
delivery.bestEffort = opts.bestEffortDeliver;
}
patch.delivery = delivery;
}
const res = await callGatewayFromCli("cron.update", opts, {

View File

@@ -60,18 +60,18 @@ export function parseDurationMs(input: string): number | null {
return Math.floor(n * factor);
}
export function parseAtMs(input: string): number | null {
export function parseAt(input: string): string | null {
const raw = input.trim();
if (!raw) {
return null;
}
const absolute = parseAbsoluteTimeMs(raw);
if (absolute) {
return absolute;
if (absolute !== null) {
return new Date(absolute).toISOString();
}
const dur = parseDurationMs(raw);
if (dur) {
return Date.now() + dur;
if (dur !== null) {
return new Date(Date.now() + dur).toISOString();
}
return null;
}
@@ -97,13 +97,14 @@ const truncate = (value: string, width: number) => {
return `${value.slice(0, width - 3)}...`;
};
const formatIsoMinute = (ms: number) => {
const d = new Date(ms);
const formatIsoMinute = (iso: string) => {
const parsed = parseAbsoluteTimeMs(iso);
const d = new Date(parsed ?? NaN);
if (Number.isNaN(d.getTime())) {
return "-";
}
const iso = d.toISOString();
return `${iso.slice(0, 10)} ${iso.slice(11, 16)}Z`;
const isoStr = d.toISOString();
return `${isoStr.slice(0, 10)} ${isoStr.slice(11, 16)}Z`;
};
const formatDuration = (ms: number) => {
@@ -143,7 +144,7 @@ const formatRelative = (ms: number | null | undefined, nowMs: number) => {
const formatSchedule = (schedule: CronSchedule) => {
if (schedule.kind === "at") {
return `at ${formatIsoMinute(schedule.atMs)}`;
return `at ${formatIsoMinute(schedule.at)}`;
}
if (schedule.kind === "every") {
return `every ${formatDuration(schedule.everyMs)}`;

View File

@@ -15,7 +15,7 @@ export function addGatewayClientOptions(cmd: Command) {
return cmd
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
.option("--token <token>", "Gateway token (if required)")
.option("--timeout <ms>", "Timeout in ms", "10000")
.option("--timeout <ms>", "Timeout in ms", "30000")
.option("--expect-final", "Wait for final response (agent)", false);
}

View File

@@ -164,6 +164,12 @@ describe("cli program (smoke)", () => {
key: "sk-moonshot-test",
field: "moonshotApiKey",
},
{
authChoice: "moonshot-api-key-cn",
flag: "--moonshot-api-key",
key: "sk-moonshot-cn-test",
field: "moonshotApiKey",
},
{
authChoice: "kimi-code-api-key",
flag: "--kimi-code-api-key",

View File

@@ -58,7 +58,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
"--auth-choice <choice>",
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|qianfan-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip|qianfan-api-key",
)
.option(
"--token-provider <id>",
@@ -74,6 +74,9 @@ export function registerOnboardCommand(program: Command) {
.option("--openai-api-key <key>", "OpenAI API key")
.option("--openrouter-api-key <key>", "OpenRouter API key")
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
.option("--cloudflare-ai-gateway-account-id <id>", "Cloudflare Account ID")
.option("--cloudflare-ai-gateway-gateway-id <id>", "Cloudflare AI Gateway ID")
.option("--cloudflare-ai-gateway-api-key <key>", "Cloudflare AI Gateway API key")
.option("--moonshot-api-key <key>", "Moonshot API key")
.option("--kimi-code-api-key <key>", "Kimi Coding API key")
.option("--gemini-api-key <key>", "Gemini API key")
@@ -126,6 +129,9 @@ export function registerOnboardCommand(program: Command) {
openaiApiKey: opts.openaiApiKey as string | undefined,
openrouterApiKey: opts.openrouterApiKey as string | undefined,
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
cloudflareAiGatewayAccountId: opts.cloudflareAiGatewayAccountId as string | undefined,
cloudflareAiGatewayGatewayId: opts.cloudflareAiGatewayGatewayId as string | undefined,
cloudflareAiGatewayApiKey: opts.cloudflareAiGatewayApiKey as string | undefined,
moonshotApiKey: opts.moonshotApiKey as string | undefined,
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
geminiApiKey: opts.geminiApiKey as string | undefined,