deprecate openrouterApiKey

This commit is contained in:
DaKheera47 2026-01-29 16:42:46 +00:00
parent b4641ad9cb
commit 5a53c0063d
9 changed files with 153 additions and 9 deletions

View File

@ -28,6 +28,7 @@ describe.sequential("Settings API routes", () => {
expect(body.data.defaultModel).toBe("test-model");
expect(Array.isArray(body.data.searchTerms)).toBe(true);
expect(body.data.rxresumeEmail).toBe("resume@example.com");
expect(body.data.llmApiKeyHint).toBe("secr");
expect(body.data.openrouterApiKeyHint).toBe("secr");
expect(body.data.basicAuthActive).toBe(false);
});
@ -54,6 +55,7 @@ describe.sequential("Settings API routes", () => {
expect(patchBody.data.searchTerms).toEqual(["engineer"]);
expect(patchBody.data.overrideSearchTerms).toEqual(["engineer"]);
expect(patchBody.data.rxresumeEmail).toBe("updated@example.com");
expect(patchBody.data.llmApiKeyHint).toBe("upda");
expect(patchBody.data.openrouterApiKeyHint).toBe("upda");
});

View File

@ -229,10 +229,19 @@ settingsRouter.patch("/", async (req: Request, res: Response) => {
}
if ("openrouterApiKey" in input) {
// @deprecated Use llmApiKey. Keep accepting this field for backwards compatibility.
console.warn(
"[DEPRECATED] Received openrouterApiKey update. Storing as llmApiKey and clearing legacy openrouterApiKey.",
);
const value = normalizeEnvInput(input.openrouterApiKey);
promises.push(
settingsRepo.setSetting("openrouterApiKey", value).then(() => {
applyEnvValue("OPENROUTER_API_KEY", value);
settingsRepo.setSetting("llmApiKey", value).then(() => {
applyEnvValue("LLM_API_KEY", value);
}),
);
promises.push(
settingsRepo.setSetting("openrouterApiKey", null).then(() => {
applyEnvValue("OPENROUTER_API_KEY", null);
}),
);
}

View File

@ -82,11 +82,13 @@ const mockProfile = { name: "Test User" };
describe("AI Service Resilience", () => {
beforeEach(() => {
global.fetch = vi.fn();
delete process.env.LLM_API_KEY;
process.env.OPENROUTER_API_KEY = "mock-key"; // Ensure logic tries to call API
});
afterEach(() => {
global.fetch = globalFetch;
delete process.env.LLM_API_KEY;
delete process.env.OPENROUTER_API_KEY;
vi.restoreAllMocks();
});

View File

@ -0,0 +1,63 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const originalEnv = { ...process.env };
describe.sequential("envSettings migration", () => {
let tempDir: string;
let closeDb: (() => void) | null = null;
beforeEach(async () => {
vi.resetModules();
tempDir = await mkdtemp(join(tmpdir(), "job-ops-env-migration-test-"));
process.env = {
...originalEnv,
DATA_DIR: tempDir,
NODE_ENV: "test",
MODEL: "test-model",
};
await import("../db/migrate.js");
const dbMod = await import("../db/index.js");
closeDb = dbMod.closeDb;
});
afterEach(async () => {
if (closeDb) closeDb();
await rm(tempDir, { recursive: true, force: true });
process.env = { ...originalEnv };
});
it("migrates stored openrouterApiKey -> llmApiKey for openrouter provider", async () => {
const settingsRepo = await import("../repositories/settings.js");
const { applyStoredEnvOverrides } = await import("./envSettings.js");
await settingsRepo.setSetting("llmProvider", "openrouter");
await settingsRepo.setSetting("openrouterApiKey", "sk-or-legacy");
await settingsRepo.setSetting("llmApiKey", null);
await applyStoredEnvOverrides();
expect(await settingsRepo.getSetting("llmApiKey")).toBe("sk-or-legacy");
expect(await settingsRepo.getSetting("openrouterApiKey")).toBe(null);
expect(process.env.LLM_API_KEY).toBe("sk-or-legacy");
});
it("does not migrate openrouterApiKey when provider is not openrouter", async () => {
const settingsRepo = await import("../repositories/settings.js");
const { applyStoredEnvOverrides } = await import("./envSettings.js");
await settingsRepo.setSetting("llmProvider", "openai");
await settingsRepo.setSetting("openrouterApiKey", "sk-or-legacy");
await settingsRepo.setSetting("llmApiKey", null);
await applyStoredEnvOverrides();
expect(await settingsRepo.getSetting("llmApiKey")).toBe(null);
expect(await settingsRepo.getSetting("openrouterApiKey")).toBe(
"sk-or-legacy",
);
});
});

View File

@ -90,22 +90,63 @@ export function serializeEnvBoolean(value: boolean | null): string | null {
}
export async function applyStoredEnvOverrides(): Promise<void> {
const overrides = await settingsRepo.getAllSettings();
// Migration: move legacy OpenRouter key to the unified LLM key.
//
// Users only see their API keys once. If we simply switch to LLM_API_KEY without
// copying, they may be unable to recover their existing key.
const effectiveProvider = (overrides.llmProvider ?? process.env.LLM_PROVIDER)
?.trim()
.toLowerCase();
const legacyOpenrouterKey = normalizeEnvInput(overrides.openrouterApiKey);
const unifiedKey = normalizeEnvInput(overrides.llmApiKey);
if (
(effectiveProvider ?? "openrouter") === "openrouter" &&
legacyOpenrouterKey &&
!unifiedKey
) {
console.warn(
"[DEPRECATED] Detected stored OpenRouter API key. Migrating to LLM_API_KEY and clearing legacy storage.",
);
await settingsRepo.setSetting("llmApiKey", legacyOpenrouterKey);
await settingsRepo.setSetting("openrouterApiKey", null);
overrides.llmApiKey = legacyOpenrouterKey;
delete overrides.openrouterApiKey;
}
// Migration helper for env-based users: copy OPENROUTER_API_KEY -> LLM_API_KEY
// at runtime so the app keeps working after removing fallback logic.
if (
(effectiveProvider ?? "openrouter") === "openrouter" &&
!normalizeEnvInput(process.env.LLM_API_KEY) &&
normalizeEnvInput(process.env.OPENROUTER_API_KEY)
) {
console.warn(
"[DEPRECATED] OPENROUTER_API_KEY is deprecated. Copying to LLM_API_KEY for compatibility.",
);
process.env.LLM_API_KEY = normalizeEnvInput(
process.env.OPENROUTER_API_KEY,
)!;
}
await Promise.all([
...readableStringConfig.map(async ({ settingKey, envKey }) => {
const override = await settingsRepo.getSetting(settingKey);
const override = overrides[settingKey] ?? null;
if (override === null) return;
applyEnvValue(envKey, normalizeEnvInput(override));
}),
...readableBooleanConfig.map(
async ({ settingKey, envKey, defaultValue }) => {
const override = await settingsRepo.getSetting(settingKey);
const override = overrides[settingKey] ?? null;
if (override === null) return;
const parsed = parseEnvBoolean(override, defaultValue);
applyEnvValue(envKey, serializeEnvBoolean(parsed));
},
),
...privateStringConfig.map(async ({ settingKey, envKey }) => {
const override = await settingsRepo.getSetting(settingKey);
const override = overrides[settingKey] ?? null;
if (override === null) return;
applyEnvValue(envKey, normalizeEnvInput(override));
}),
@ -144,6 +185,12 @@ export async function getEnvSettingsData(
privateValues[hintKey] = rawValue.slice(0, hintLength);
}
// Backwards-compat: old clients still expect openrouterApiKeyHint.
// Always prefer the unified LLM key hint when present.
if (privateValues.llmApiKeyHint) {
privateValues.openrouterApiKeyHint = privateValues.llmApiKeyHint;
}
const basicAuthUser =
activeOverrides.basicAuthUser ?? process.env.BASIC_AUTH_USER;
const basicAuthPassword =

View File

@ -69,6 +69,10 @@ describe("LlmService", () => {
} as Response);
const llm = new LlmService();
// Backwards-compat: OPENROUTER_API_KEY should be copied to LLM_API_KEY.
expect(process.env.LLM_API_KEY).toBe("test-api-key");
const result = await llm.callJson<{ value: string; count: number }>({
model: "test-model",
messages: [{ role: "user", content: "test" }],

View File

@ -377,12 +377,27 @@ export class LlmService {
const strategy = strategies[resolvedProvider];
const baseUrl = normalizedBaseUrl || strategy.defaultBaseUrl;
const apiKey =
let apiKey =
normalizeEnvInput(options.apiKey) ||
normalizeEnvInput(process.env.LLM_API_KEY) ||
(resolvedProvider === "openrouter"
? normalizeEnvInput(process.env.OPENROUTER_API_KEY)
: null);
null;
// Backwards-compat migration: OPENROUTER_API_KEY -> LLM_API_KEY.
// This prevents users from losing access when upgrading (keys are often only shown once).
if (
!apiKey &&
resolvedProvider === "openrouter" &&
normalizeEnvInput(process.env.OPENROUTER_API_KEY)
) {
console.warn(
"[DEPRECATED] OPENROUTER_API_KEY is deprecated. Copying to LLM_API_KEY; please update your environment.",
);
const migrated = normalizeEnvInput(process.env.OPENROUTER_API_KEY);
if (migrated) {
process.env.LLM_API_KEY = migrated;
apiKey = migrated;
}
}
this.provider = resolvedProvider;
this.baseUrl = baseUrl;

View File

@ -61,6 +61,7 @@ export const updateSettingsSchema = z
.optional(),
jobspyLinkedinFetchDescription: z.boolean().nullable().optional(),
showSponsorInfo: z.boolean().nullable().optional(),
/** @deprecated Use llmApiKey instead. */
openrouterApiKey: z.string().trim().max(2000).nullable().optional(),
rxresumeEmail: z.string().trim().max(200).nullable().optional(),
rxresumePassword: z.string().trim().max(2000).nullable().optional(),

View File

@ -532,6 +532,7 @@ export interface AppSettings {
defaultShowSponsorInfo: boolean;
overrideShowSponsorInfo: boolean | null;
llmApiKeyHint: string | null;
/** @deprecated Use llmApiKeyHint instead. */
openrouterApiKeyHint: string | null;
rxresumeEmail: string | null;
rxresumePasswordHint: string | null;