Replace NodeJS dict aliases with maps and named field reads
Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #24.
This commit is contained in:
@@ -9,4 +9,9 @@ export {
|
||||
sanitizedEnvironment,
|
||||
withProfile,
|
||||
} from "./profiles.ts";
|
||||
export type { BotProfile, FailureReason, ProfileFailure, ProfileIdentity } from "./profiles.ts";
|
||||
export type {
|
||||
BotProfile,
|
||||
FailureReason,
|
||||
ProfileFailure,
|
||||
ProfileIdentity,
|
||||
} from "./profiles.ts";
|
||||
|
||||
+109
-83
@@ -11,42 +11,44 @@ import {
|
||||
} from "./profiles.ts";
|
||||
|
||||
test("formats profile attribution as a Markdown quote", () => {
|
||||
expect(profileFooter({ type: "opencode", model: "openrouter/qwen/qwen3.8-27b" })).toBe(
|
||||
"> Profile: `opencode` · `openrouter/qwen/qwen3.8-27b`",
|
||||
);
|
||||
expect(
|
||||
profileFooter({ type: "OPENCODE", model: "openrouter/qwen/qwen3.8-27b" }),
|
||||
).toBe("> Profile: `opencode` · `openrouter/qwen/qwen3.8-27b`");
|
||||
});
|
||||
|
||||
test("falls through only after usage exhaustion", async () => {
|
||||
const visited: string[] = [];
|
||||
const result = await runProfiles(
|
||||
[
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
],
|
||||
(profile) => {
|
||||
async (profile) => {
|
||||
visited.push(profile.type);
|
||||
if (profile.type === "claude") {
|
||||
if (profile.type === "CLAUDE") {
|
||||
throw new Error("You've hit your usage limit");
|
||||
}
|
||||
return Promise.resolve("done");
|
||||
const resolved = await Promise.resolve("done");
|
||||
return resolved;
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toBe("done");
|
||||
expect(visited).toStrictEqual(["claude", "codex"]);
|
||||
expect(visited).toStrictEqual(["CLAUDE", "CODEX"]);
|
||||
});
|
||||
|
||||
test("recognizes Claude's weekly limit message", async () => {
|
||||
const result = await runProfiles(
|
||||
[
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
],
|
||||
(profile) => {
|
||||
if (profile.type === "claude") {
|
||||
async (profile) => {
|
||||
if (profile.type === "CLAUDE") {
|
||||
throw new Error("You've hit your weekly limit · resets 12am (UTC)");
|
||||
}
|
||||
return Promise.resolve("fallback");
|
||||
const resolved = await Promise.resolve("fallback");
|
||||
return resolved;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -55,17 +57,23 @@ test("recognizes Claude's weekly limit message", async () => {
|
||||
|
||||
test("recognizes provider API exhaustion status", async () => {
|
||||
const results = await Promise.all(
|
||||
["API Error: 402 Insufficient funds", "HTTP error: 429"].map((message) =>
|
||||
runProfiles(
|
||||
[
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
],
|
||||
(profile) => {
|
||||
if (profile.type === "claude") throw new Error(message);
|
||||
return Promise.resolve("fallback");
|
||||
},
|
||||
),
|
||||
["API Error: 402 Insufficient funds", "HTTP error: 429"].map(
|
||||
async (message) => {
|
||||
const result = await runProfiles(
|
||||
[
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
],
|
||||
async (profile) => {
|
||||
if (profile.type === "CLAUDE") {
|
||||
throw new Error(message);
|
||||
}
|
||||
const resolved = await Promise.resolve("fallback");
|
||||
return resolved;
|
||||
},
|
||||
);
|
||||
return result;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -76,24 +84,27 @@ test("falls through after a sign-in failure and records each try", async () => {
|
||||
const failures: ProfileFailure[] = [];
|
||||
const result = await runProfiles(
|
||||
[
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "opencode", model: "openrouter/qwen/qwen3.8-27b" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "OPENCODE", model: "openrouter/qwen/qwen3.8-27b" },
|
||||
],
|
||||
(profile) => {
|
||||
if (profile.type === "codex") {
|
||||
async (profile) => {
|
||||
if (profile.type === "CODEX") {
|
||||
throw new AgentAuthError("codex sign-in failed");
|
||||
}
|
||||
if (profile.type === "claude") throw new Error("API Error: 429");
|
||||
return Promise.resolve("fallback");
|
||||
if (profile.type === "CLAUDE") {
|
||||
throw new Error("API Error: 429");
|
||||
}
|
||||
const resolved = await Promise.resolve("fallback");
|
||||
return resolved;
|
||||
},
|
||||
failures,
|
||||
);
|
||||
|
||||
expect(result).toBe("fallback");
|
||||
expect(failures).toStrictEqual([
|
||||
{ type: "codex", model: "gpt-5.6-terra", reason: "sign-in failed" },
|
||||
{ type: "claude", model: "claude-sonnet-5", reason: "usage exhausted" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra", reason: "SIGN_IN_FAILED" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", reason: "USAGE_EXHAUSTED" },
|
||||
]);
|
||||
expect(failureFooter(failures)).toStrictEqual(
|
||||
"> Tried: `codex` · `gpt-5.6-terra` (sign-in failed), " +
|
||||
@@ -103,11 +114,11 @@ test("falls through after a sign-in failure and records each try", async () => {
|
||||
|
||||
test("throws the last sign-in failure when every profile fails", async () => {
|
||||
const failures: ProfileFailure[] = [];
|
||||
await expect(() =>
|
||||
await expect(
|
||||
runProfiles(
|
||||
[
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
],
|
||||
(profile) => {
|
||||
throw new AgentAuthError(`${profile.type} sign-in failed`);
|
||||
@@ -116,19 +127,24 @@ test("throws the last sign-in failure when every profile fails", async () => {
|
||||
),
|
||||
).rejects.toThrow(
|
||||
expect.toSatisfy(
|
||||
(e: unknown) => e instanceof AgentAuthError && e.message.includes("claude sign-in failed"),
|
||||
(error: unknown) =>
|
||||
error instanceof AgentAuthError &&
|
||||
error.message.includes("CLAUDE sign-in failed"),
|
||||
),
|
||||
);
|
||||
expect(failures.map(({ reason }) => reason)).toStrictEqual(["sign-in failed", "sign-in failed"]);
|
||||
expect(failures.map(({ reason }) => reason)).toStrictEqual([
|
||||
"SIGN_IN_FAILED",
|
||||
"SIGN_IN_FAILED",
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves an ordinary agent failure", async () => {
|
||||
const visited: string[] = [];
|
||||
await expect(() =>
|
||||
await expect(
|
||||
runProfiles(
|
||||
[
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
],
|
||||
(profile) => {
|
||||
visited.push(profile.type);
|
||||
@@ -136,16 +152,16 @@ test("preserves an ordinary agent failure", async () => {
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("tests failed");
|
||||
expect(visited).toStrictEqual(["claude"]);
|
||||
expect(visited).toStrictEqual(["CLAUDE"]);
|
||||
});
|
||||
|
||||
test("does not mistake incidental quota text for exhaustion", async () => {
|
||||
const visited: string[] = [];
|
||||
await expect(() =>
|
||||
await expect(
|
||||
runProfiles(
|
||||
[
|
||||
{ type: "claude", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "codex", model: "gpt-5.6-terra" },
|
||||
{ type: "CLAUDE", model: "claude-sonnet-5", token: "first" },
|
||||
{ type: "CODEX", model: "gpt-5.6-terra" },
|
||||
],
|
||||
(profile) => {
|
||||
visited.push(profile.type);
|
||||
@@ -153,27 +169,27 @@ test("does not mistake incidental quota text for exhaustion", async () => {
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("quota parser test failed");
|
||||
expect(visited).toStrictEqual(["claude"]);
|
||||
expect(visited).toStrictEqual(["CLAUDE"]);
|
||||
});
|
||||
|
||||
test("exposes only the selected profile credential to the agent", () => {
|
||||
const base = {
|
||||
PATH: "/usr/bin",
|
||||
HOME: "/home/bot",
|
||||
UNRELATED_SECRET: "must-not-reach-agent",
|
||||
BOT_AGENTS_MASTER_KEY: "master",
|
||||
PROFILES: "contains every secret",
|
||||
REVIEWER_TOKEN: "reviewer",
|
||||
CLAUDE_CODE_OAUTH_TOKEN: "old-claude",
|
||||
ANTHROPIC_API_KEY: "old-anthropic-key",
|
||||
ANTHROPIC_AUTH_TOKEN: "old-anthropic-token",
|
||||
OPENAI_API_KEY: "old-openai",
|
||||
CODEX_API_KEY: "old-codex-key",
|
||||
CODEX_ACCESS_TOKEN: "old-codex-token",
|
||||
};
|
||||
const base = new Map([
|
||||
["PATH", "/usr/bin"],
|
||||
["HOME", "/home/bot"],
|
||||
["UNRELATED_SECRET", "must-not-reach-agent"],
|
||||
["BOT_AGENTS_MASTER_KEY", "master"],
|
||||
["PROFILES", "contains every secret"],
|
||||
["REVIEWER_TOKEN", "reviewer"],
|
||||
["CLAUDE_CODE_OAUTH_TOKEN", "old-claude"],
|
||||
["ANTHROPIC_API_KEY", "old-anthropic-key"],
|
||||
["ANTHROPIC_AUTH_TOKEN", "old-anthropic-token"],
|
||||
["OPENAI_API_KEY", "old-openai"],
|
||||
["CODEX_API_KEY", "old-codex-key"],
|
||||
["CODEX_ACCESS_TOKEN", "old-codex-token"],
|
||||
]);
|
||||
const profile = {
|
||||
index: 0,
|
||||
type: "claude" as const,
|
||||
type: "CLAUDE" as const,
|
||||
model: "claude-sonnet-5",
|
||||
uid: 20_000,
|
||||
gid: 20_000,
|
||||
@@ -181,21 +197,25 @@ test("exposes only the selected profile credential to the agent", () => {
|
||||
tokenFile: "/runtime/profile-0/token",
|
||||
};
|
||||
|
||||
expect(agentEnvironment(base, profile, "selected")).toStrictEqual({
|
||||
PATH: "/usr/bin",
|
||||
HOME: "/runtime/profile-0",
|
||||
CLAUDE_CODE_OAUTH_TOKEN: "selected",
|
||||
});
|
||||
expect(sanitizedEnvironment(base)).toStrictEqual({
|
||||
PATH: "/usr/bin",
|
||||
HOME: "/home/bot",
|
||||
});
|
||||
expect(agentEnvironment(base, profile, "selected")).toStrictEqual(
|
||||
new Map([
|
||||
["PATH", "/usr/bin"],
|
||||
["HOME", "/runtime/profile-0"],
|
||||
["CLAUDE_CODE_OAUTH_TOKEN", "selected"],
|
||||
]),
|
||||
);
|
||||
expect(sanitizedEnvironment(base)).toStrictEqual(
|
||||
new Map([
|
||||
["PATH", "/usr/bin"],
|
||||
["HOME", "/home/bot"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test("uses a private Codex home without inventing an API credential", () => {
|
||||
const profile = {
|
||||
index: 1,
|
||||
type: "codex" as const,
|
||||
type: "CODEX" as const,
|
||||
model: "gpt-5.6-terra",
|
||||
uid: 20_001,
|
||||
gid: 20_001,
|
||||
@@ -203,17 +223,19 @@ test("uses a private Codex home without inventing an API credential", () => {
|
||||
tokenFile: null,
|
||||
};
|
||||
|
||||
expect(agentEnvironment({ PATH: "/bin" }, profile)).toStrictEqual({
|
||||
PATH: "/bin",
|
||||
HOME: "/runtime/profile-1",
|
||||
CODEX_HOME: "/runtime/profile-1/.codex",
|
||||
});
|
||||
expect(agentEnvironment(new Map([["PATH", "/bin"]]), profile)).toStrictEqual(
|
||||
new Map([
|
||||
["PATH", "/bin"],
|
||||
["HOME", "/runtime/profile-1"],
|
||||
["CODEX_HOME", "/runtime/profile-1/.codex"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test("exposes an OpenRouter key only to the OpenCode profile", () => {
|
||||
const profile = {
|
||||
index: 2,
|
||||
type: "opencode" as const,
|
||||
type: "OPENCODE" as const,
|
||||
model: "openrouter/qwen/qwen3.8-27b",
|
||||
uid: 20_002,
|
||||
gid: 20_002,
|
||||
@@ -221,9 +243,13 @@ test("exposes an OpenRouter key only to the OpenCode profile", () => {
|
||||
tokenFile: "/runtime/profile-2/token",
|
||||
};
|
||||
|
||||
expect(agentEnvironment({ PATH: "/bin" }, profile, "selected")).toStrictEqual({
|
||||
PATH: "/bin",
|
||||
HOME: "/runtime/profile-2",
|
||||
OPENROUTER_API_KEY: "selected",
|
||||
});
|
||||
expect(
|
||||
agentEnvironment(new Map([["PATH", "/bin"]]), profile, "selected"),
|
||||
).toStrictEqual(
|
||||
new Map([
|
||||
["PATH", "/bin"],
|
||||
["HOME", "/runtime/profile-2"],
|
||||
["OPENROUTER_API_KEY", "selected"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
+92
-47
@@ -1,6 +1,6 @@
|
||||
// The agent and model a profile runs, which every report names.
|
||||
export interface ProfileIdentity {
|
||||
type: "claude" | "codex" | "opencode";
|
||||
type: "CLAUDE" | "CODEX" | "OPENCODE";
|
||||
model: string;
|
||||
}
|
||||
|
||||
@@ -8,15 +8,39 @@ export interface BotProfile extends ProfileIdentity {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
// The lowercase name a profile type reads as in posted comments.
|
||||
function profileName(type: ProfileIdentity["type"]): string {
|
||||
switch (type) {
|
||||
case "CLAUDE":
|
||||
return "claude";
|
||||
case "CODEX":
|
||||
return "codex";
|
||||
case "OPENCODE":
|
||||
return "opencode";
|
||||
}
|
||||
}
|
||||
|
||||
export function profileFooter(profile: ProfileIdentity): string {
|
||||
return `> Profile: \`${profile.type}\` · \`${profile.model}\``;
|
||||
return `> Profile: \`${profileName(profile.type)}\` · \`${profile.model}\``;
|
||||
}
|
||||
|
||||
export function withProfile(body: string, profile: ProfileIdentity): string {
|
||||
return `${body}\n\n${profileFooter(profile)}`;
|
||||
}
|
||||
|
||||
export type FailureReason = "sign-in failed" | "usage exhausted" | "failed";
|
||||
export type FailureReason = "SIGN_IN_FAILED" | "USAGE_EXHAUSTED" | "FAILED";
|
||||
|
||||
// The words a failure reason reads as in logs and posted comments.
|
||||
function toMessage(reason: FailureReason): string {
|
||||
switch (reason) {
|
||||
case "SIGN_IN_FAILED":
|
||||
return "sign-in failed";
|
||||
case "USAGE_EXHAUSTED":
|
||||
return "usage exhausted";
|
||||
case "FAILED":
|
||||
return "failed";
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProfileFailure extends ProfileIdentity {
|
||||
reason: FailureReason;
|
||||
@@ -29,7 +53,12 @@ interface ProfileHome {
|
||||
}
|
||||
|
||||
// Raised once an agent's credentials are rejected and any re-login is spent.
|
||||
export class AgentAuthError extends Error {}
|
||||
export class AgentAuthError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = "AgentAuthError";
|
||||
}
|
||||
}
|
||||
|
||||
function isUsageExhausted(error: unknown): boolean {
|
||||
return /(?:usage|rate) limit|hit your (?:weekly )?limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/iu.test(
|
||||
@@ -38,50 +67,54 @@ function isUsageExhausted(error: unknown): boolean {
|
||||
}
|
||||
|
||||
function failureReason(error: unknown): FailureReason {
|
||||
if (error instanceof AgentAuthError) return "sign-in failed";
|
||||
return isUsageExhausted(error) ? "usage exhausted" : "failed";
|
||||
if (error instanceof AgentAuthError) {
|
||||
return "SIGN_IN_FAILED";
|
||||
}
|
||||
return isUsageExhausted(error) ? "USAGE_EXHAUSTED" : "FAILED";
|
||||
}
|
||||
|
||||
export function failureFooter(failures: ProfileFailure[]): string {
|
||||
if (failures.length === 0) return "> Profile: none";
|
||||
if (failures.length === 0) {
|
||||
return "> Profile: none";
|
||||
}
|
||||
return `> Tried: ${failures
|
||||
.map(({ type, model, reason }) => `\`${type}\` · \`${model}\` (${reason})`)
|
||||
.map(
|
||||
({ type, model, reason }) =>
|
||||
`\`${profileName(type)}\` · \`${model}\` (${toMessage(reason)})`,
|
||||
)
|
||||
.join(", ")}`;
|
||||
}
|
||||
|
||||
// Tries the profiles from `index` on, moving on only after a sign-in failure
|
||||
// or exhausted usage, and records every profile it tried.
|
||||
async function attemptFrom<T, P extends ProfileIdentity>(
|
||||
profiles: P[],
|
||||
attempt: (profile: P) => Promise<T>,
|
||||
failures: ProfileFailure[],
|
||||
index: number,
|
||||
): Promise<T> {
|
||||
const profile = profiles[index];
|
||||
if (profile === undefined) {
|
||||
throw new Error("profiles must contain at least one profile");
|
||||
}
|
||||
try {
|
||||
return await attempt(profile);
|
||||
} catch (error) {
|
||||
const reason = failureReason(error);
|
||||
failures.push({ type: profile.type, model: profile.model, reason });
|
||||
if (reason === "failed" || index === profiles.length - 1) throw error;
|
||||
console.error(`${profile.type} ${reason}; trying next profile`);
|
||||
return attemptFrom(profiles, attempt, failures, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function runProfiles<T, P extends ProfileIdentity>(
|
||||
// Tries the profiles in order, moving on only after a sign-in failure or
|
||||
// exhausted usage, and records every profile it tried.
|
||||
export async function runProfiles<T, P extends ProfileIdentity>(
|
||||
profiles: P[],
|
||||
attempt: (profile: P) => Promise<T>,
|
||||
failures: ProfileFailure[] = [],
|
||||
): Promise<T> {
|
||||
return attemptFrom(profiles, attempt, failures, 0);
|
||||
for (const [index, profile] of profiles.entries()) {
|
||||
try {
|
||||
return await attempt(profile);
|
||||
} catch (error) {
|
||||
const reason = failureReason(error);
|
||||
failures.push({ type: profile.type, model: profile.model, reason });
|
||||
if (reason === "FAILED" || index === profiles.length - 1) {
|
||||
throw error;
|
||||
}
|
||||
console.error(
|
||||
`${profile.type} ${toMessage(reason)}; trying next profile`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error("profiles must contain at least one profile");
|
||||
}
|
||||
|
||||
export function sanitizedEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const result: NodeJS.ProcessEnv = {};
|
||||
// Environments are maps from variable name to value, converted to the object
|
||||
// form only where a child process is spawned.
|
||||
export function sanitizedEnvironment(
|
||||
environment: ReadonlyMap<string, string>,
|
||||
): Map<string, string> {
|
||||
const result = new Map<string, string>();
|
||||
for (const name of [
|
||||
"PATH",
|
||||
"HOME",
|
||||
@@ -108,27 +141,39 @@ export function sanitizedEnvironment(environment: NodeJS.ProcessEnv): NodeJS.Pro
|
||||
"SSL_CERT_DIR",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
]) {
|
||||
const value = environment[name];
|
||||
if (value !== undefined) result[name] = value;
|
||||
const value = environment.get(name);
|
||||
if (value !== undefined) {
|
||||
result.set(name, value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// The environment variable each agent CLI reads its credential from.
|
||||
function tokenVariable(type: ProfileIdentity["type"]): string {
|
||||
switch (type) {
|
||||
case "CLAUDE":
|
||||
return "CLAUDE_CODE_OAUTH_TOKEN";
|
||||
case "CODEX":
|
||||
return "OPENAI_API_KEY";
|
||||
case "OPENCODE":
|
||||
return "OPENROUTER_API_KEY";
|
||||
}
|
||||
}
|
||||
|
||||
export function agentEnvironment(
|
||||
environment: NodeJS.ProcessEnv,
|
||||
environment: ReadonlyMap<string, string>,
|
||||
profile: ProfileHome,
|
||||
token?: string,
|
||||
): NodeJS.ProcessEnv {
|
||||
): Map<string, string> {
|
||||
const result = sanitizedEnvironment(environment);
|
||||
result["HOME"] = profile.home;
|
||||
if (profile.type === "codex") result["CODEX_HOME"] = `${profile.home}/.codex`;
|
||||
result.set("HOME", profile.home);
|
||||
if (profile.type === "CODEX") {
|
||||
result.set("CODEX_HOME", `${profile.home}/.codex`);
|
||||
}
|
||||
if (token !== undefined && token !== "") {
|
||||
const tokenName = {
|
||||
claude: "CLAUDE_CODE_OAUTH_TOKEN",
|
||||
codex: "OPENAI_API_KEY",
|
||||
opencode: "OPENROUTER_API_KEY",
|
||||
}[profile.type];
|
||||
result[tokenName] = token;
|
||||
const tokenName = tokenVariable(profile.type);
|
||||
result.set(tokenName, token);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user