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(body.data.defaultModel).toBe("test-model");
|
||||||
expect(Array.isArray(body.data.searchTerms)).toBe(true);
|
expect(Array.isArray(body.data.searchTerms)).toBe(true);
|
||||||
expect(body.data.rxresumeEmail).toBe("resume@example.com");
|
expect(body.data.rxresumeEmail).toBe("resume@example.com");
|
||||||
|
expect(body.data.llmApiKeyHint).toBe("secr");
|
||||||
expect(body.data.openrouterApiKeyHint).toBe("secr");
|
expect(body.data.openrouterApiKeyHint).toBe("secr");
|
||||||
expect(body.data.basicAuthActive).toBe(false);
|
expect(body.data.basicAuthActive).toBe(false);
|
||||||
});
|
});
|
||||||
@ -54,6 +55,7 @@ describe.sequential("Settings API routes", () => {
|
|||||||
expect(patchBody.data.searchTerms).toEqual(["engineer"]);
|
expect(patchBody.data.searchTerms).toEqual(["engineer"]);
|
||||||
expect(patchBody.data.overrideSearchTerms).toEqual(["engineer"]);
|
expect(patchBody.data.overrideSearchTerms).toEqual(["engineer"]);
|
||||||
expect(patchBody.data.rxresumeEmail).toBe("updated@example.com");
|
expect(patchBody.data.rxresumeEmail).toBe("updated@example.com");
|
||||||
|
expect(patchBody.data.llmApiKeyHint).toBe("upda");
|
||||||
expect(patchBody.data.openrouterApiKeyHint).toBe("upda");
|
expect(patchBody.data.openrouterApiKeyHint).toBe("upda");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -229,10 +229,19 @@ settingsRouter.patch("/", async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("openrouterApiKey" in input) {
|
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);
|
const value = normalizeEnvInput(input.openrouterApiKey);
|
||||||
promises.push(
|
promises.push(
|
||||||
settingsRepo.setSetting("openrouterApiKey", value).then(() => {
|
settingsRepo.setSetting("llmApiKey", value).then(() => {
|
||||||
applyEnvValue("OPENROUTER_API_KEY", value);
|
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", () => {
|
describe("AI Service Resilience", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
global.fetch = vi.fn();
|
global.fetch = vi.fn();
|
||||||
|
delete process.env.LLM_API_KEY;
|
||||||
process.env.OPENROUTER_API_KEY = "mock-key"; // Ensure logic tries to call API
|
process.env.OPENROUTER_API_KEY = "mock-key"; // Ensure logic tries to call API
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
global.fetch = globalFetch;
|
global.fetch = globalFetch;
|
||||||
|
delete process.env.LLM_API_KEY;
|
||||||
delete process.env.OPENROUTER_API_KEY;
|
delete process.env.OPENROUTER_API_KEY;
|
||||||
vi.restoreAllMocks();
|
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> {
|
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([
|
await Promise.all([
|
||||||
...readableStringConfig.map(async ({ settingKey, envKey }) => {
|
...readableStringConfig.map(async ({ settingKey, envKey }) => {
|
||||||
const override = await settingsRepo.getSetting(settingKey);
|
const override = overrides[settingKey] ?? null;
|
||||||
if (override === null) return;
|
if (override === null) return;
|
||||||
applyEnvValue(envKey, normalizeEnvInput(override));
|
applyEnvValue(envKey, normalizeEnvInput(override));
|
||||||
}),
|
}),
|
||||||
...readableBooleanConfig.map(
|
...readableBooleanConfig.map(
|
||||||
async ({ settingKey, envKey, defaultValue }) => {
|
async ({ settingKey, envKey, defaultValue }) => {
|
||||||
const override = await settingsRepo.getSetting(settingKey);
|
const override = overrides[settingKey] ?? null;
|
||||||
if (override === null) return;
|
if (override === null) return;
|
||||||
const parsed = parseEnvBoolean(override, defaultValue);
|
const parsed = parseEnvBoolean(override, defaultValue);
|
||||||
applyEnvValue(envKey, serializeEnvBoolean(parsed));
|
applyEnvValue(envKey, serializeEnvBoolean(parsed));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
...privateStringConfig.map(async ({ settingKey, envKey }) => {
|
...privateStringConfig.map(async ({ settingKey, envKey }) => {
|
||||||
const override = await settingsRepo.getSetting(settingKey);
|
const override = overrides[settingKey] ?? null;
|
||||||
if (override === null) return;
|
if (override === null) return;
|
||||||
applyEnvValue(envKey, normalizeEnvInput(override));
|
applyEnvValue(envKey, normalizeEnvInput(override));
|
||||||
}),
|
}),
|
||||||
@ -144,6 +185,12 @@ export async function getEnvSettingsData(
|
|||||||
privateValues[hintKey] = rawValue.slice(0, hintLength);
|
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 =
|
const basicAuthUser =
|
||||||
activeOverrides.basicAuthUser ?? process.env.BASIC_AUTH_USER;
|
activeOverrides.basicAuthUser ?? process.env.BASIC_AUTH_USER;
|
||||||
const basicAuthPassword =
|
const basicAuthPassword =
|
||||||
|
|||||||
@ -69,6 +69,10 @@ describe("LlmService", () => {
|
|||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const llm = new LlmService();
|
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 }>({
|
const result = await llm.callJson<{ value: string; count: number }>({
|
||||||
model: "test-model",
|
model: "test-model",
|
||||||
messages: [{ role: "user", content: "test" }],
|
messages: [{ role: "user", content: "test" }],
|
||||||
|
|||||||
@ -377,12 +377,27 @@ export class LlmService {
|
|||||||
const strategy = strategies[resolvedProvider];
|
const strategy = strategies[resolvedProvider];
|
||||||
const baseUrl = normalizedBaseUrl || strategy.defaultBaseUrl;
|
const baseUrl = normalizedBaseUrl || strategy.defaultBaseUrl;
|
||||||
|
|
||||||
const apiKey =
|
let apiKey =
|
||||||
normalizeEnvInput(options.apiKey) ||
|
normalizeEnvInput(options.apiKey) ||
|
||||||
normalizeEnvInput(process.env.LLM_API_KEY) ||
|
normalizeEnvInput(process.env.LLM_API_KEY) ||
|
||||||
(resolvedProvider === "openrouter"
|
null;
|
||||||
? normalizeEnvInput(process.env.OPENROUTER_API_KEY)
|
|
||||||
: 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.provider = resolvedProvider;
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
|
|||||||
@ -61,6 +61,7 @@ export const updateSettingsSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
jobspyLinkedinFetchDescription: z.boolean().nullable().optional(),
|
jobspyLinkedinFetchDescription: z.boolean().nullable().optional(),
|
||||||
showSponsorInfo: z.boolean().nullable().optional(),
|
showSponsorInfo: z.boolean().nullable().optional(),
|
||||||
|
/** @deprecated Use llmApiKey instead. */
|
||||||
openrouterApiKey: z.string().trim().max(2000).nullable().optional(),
|
openrouterApiKey: z.string().trim().max(2000).nullable().optional(),
|
||||||
rxresumeEmail: z.string().trim().max(200).nullable().optional(),
|
rxresumeEmail: z.string().trim().max(200).nullable().optional(),
|
||||||
rxresumePassword: z.string().trim().max(2000).nullable().optional(),
|
rxresumePassword: z.string().trim().max(2000).nullable().optional(),
|
||||||
|
|||||||
@ -532,6 +532,7 @@ export interface AppSettings {
|
|||||||
defaultShowSponsorInfo: boolean;
|
defaultShowSponsorInfo: boolean;
|
||||||
overrideShowSponsorInfo: boolean | null;
|
overrideShowSponsorInfo: boolean | null;
|
||||||
llmApiKeyHint: string | null;
|
llmApiKeyHint: string | null;
|
||||||
|
/** @deprecated Use llmApiKeyHint instead. */
|
||||||
openrouterApiKeyHint: string | null;
|
openrouterApiKeyHint: string | null;
|
||||||
rxresumeEmail: string | null;
|
rxresumeEmail: string | null;
|
||||||
rxresumePasswordHint: string | null;
|
rxresumePasswordHint: string | null;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user