deprecate openrouterApiKey
This commit is contained in:
parent
b4641ad9cb
commit
5a53c0063d
@ -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");
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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 =
|
||||
|
||||
@ -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" }],
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user