mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
refactor(ai): move the stored-key opener and the Anthropic host check out of provider.ts
Both are self-contained; provider.ts stays within its size baseline.
This commit is contained in:
1 parent
c82ed69549
commit
fb9cd9b965
6 files changed
+49
-46
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
type ModelListing,
|
||||
} from "@/lib/ai/model-availability";
|
||||
import { decrypt } from "@/lib/crypto";
|
||||
import { isAnthropicBaseUrl } from "@/lib/ai/provider";
|
||||
import { isAnthropicBaseUrl } from "@/lib/ai/anthropic-host";
|
||||
import { isCanonicalOpenAIEndpoint } from "@/lib/ai/openai-capabilities";
|
||||
import { loadAiUsageSettings } from "@/lib/ai/capabilities/usage";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* True when a base URL addresses Anthropic's API: the host itself or any
|
||||
* subdomain of `anthropic.com`, case-insensitively. Parsed rather than
|
||||
* substring-matched, so `https://anthropic.com.attacker.example/v1` and
|
||||
* `https://evil.example/?x=api.anthropic.com` both answer false.
|
||||
*
|
||||
* An unparseable value answers false and keeps the OpenAI path, which is the
|
||||
* behaviour that value had before this function existed.
|
||||
*/
|
||||
export function isAnthropicBaseUrl(baseUrl: string): boolean {
|
||||
let host: string;
|
||||
try {
|
||||
host = new URL(baseUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return host === "anthropic.com" || host.endsWith(".anthropic.com");
|
||||
}
|
||||
+7
-43
@@ -3,7 +3,8 @@ import { readOperatorRow } from "@/lib/settings/operator-row";
|
||||
import { bindResponseTimeout } from "./effective-timeout";
|
||||
import { bindReasoningEffort } from "./reasoning-effort";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { decrypt } from "@/lib/crypto";
|
||||
import { openStoredKey } from "./stored-key";
|
||||
import { isAnthropicBaseUrl } from "./anthropic-host";
|
||||
import { CodexClient, resolveCodexVisionSlug } from "./codex-client";
|
||||
import { OpenAIClient } from "./openai-client";
|
||||
import { AnthropicClient } from "./anthropic-client";
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
} from "@/lib/sharing/provider-work-authority";
|
||||
import { memoizePerRequest } from "@/lib/request-cache";
|
||||
import { emitSignal } from "@/lib/logging/signal";
|
||||
import { getEvent } from "@/lib/logging/context";
|
||||
import {
|
||||
acquireProviderTokenRefreshLock,
|
||||
PROVIDER_REFRESH_TRANSACTION_OPTIONS,
|
||||
@@ -371,22 +371,6 @@ function isOperatorOwned(row: { role: string } | null | undefined): boolean {
|
||||
return row?.role === "ADMIN";
|
||||
}
|
||||
|
||||
/**
|
||||
* A stored provider key, or null when it cannot be opened: a key id retired
|
||||
* from `ENCRYPTION_KEYS`, a corrupt row. An unreadable key is an unconfigured
|
||||
* provider, the same as no key at all. Its entry drops out of the chain, so a
|
||||
* generation answers the no-provider refusal (422 `ai.provider.none`)
|
||||
* instead of the decrypt's 500, and the operator sees the warning.
|
||||
*/
|
||||
function openStoredKey(encrypted: string): string | null {
|
||||
try {
|
||||
return decrypt(encrypted);
|
||||
} catch {
|
||||
getEvent()?.addWarning("ai: a stored provider key could not be opened");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.33.1 (#470) — build the OpenAI-compatible gateway client from the user's
|
||||
* three dedicated columns. The signature names every column this provider is
|
||||
@@ -479,8 +463,9 @@ function buildUserProvider(row: UserAIRow): AIProvider | null {
|
||||
// Belt-and-braces: ignore any persisted `aiBaseUrl`. The column is
|
||||
// shared with LOCAL, so a stale LAN URL there would otherwise
|
||||
// redirect the user's OpenAI key to a private host.
|
||||
if (!row.aiOpenaiKeyEncrypted) return null;
|
||||
const apiKey = openStoredKey(row.aiOpenaiKeyEncrypted);
|
||||
const apiKey = row.aiOpenaiKeyEncrypted
|
||||
? openStoredKey(row.aiOpenaiKeyEncrypted)
|
||||
: null;
|
||||
if (apiKey === null) return null;
|
||||
return new OpenAIClient({
|
||||
apiKey,
|
||||
@@ -496,25 +481,6 @@ function buildUserProvider(row: UserAIRow): AIProvider | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a base URL addresses Anthropic's API: the host itself or any
|
||||
* subdomain of `anthropic.com`, case-insensitively. Parsed rather than
|
||||
* substring-matched, so `https://anthropic.com.attacker.example/v1` and
|
||||
* `https://evil.example/?x=api.anthropic.com` both answer false.
|
||||
*
|
||||
* An unparseable value answers false and keeps the OpenAI path, which is the
|
||||
* behaviour that value had before this function existed.
|
||||
*/
|
||||
export function isAnthropicBaseUrl(baseUrl: string): boolean {
|
||||
let host: string;
|
||||
try {
|
||||
host = new URL(baseUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return host === "anthropic.com" || host.endsWith(".anthropic.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* The operator's global provider — one key, one model, one base URL in the
|
||||
* admin settings, serving every user who has configured nothing themselves.
|
||||
@@ -1381,8 +1347,7 @@ async function resolveProviderForType(
|
||||
}
|
||||
case "openai": {
|
||||
const enc = ctx.userRow?.aiOpenaiKeyEncrypted;
|
||||
if (!enc) return null;
|
||||
const apiKey = openStoredKey(enc);
|
||||
const apiKey = enc ? openStoredKey(enc) : null;
|
||||
if (apiKey === null) return null;
|
||||
return new OpenAIClient({
|
||||
apiKey,
|
||||
@@ -1392,8 +1357,7 @@ async function resolveProviderForType(
|
||||
}
|
||||
case "anthropic": {
|
||||
const enc = ctx.userRow?.aiAnthropicKeyEncrypted;
|
||||
if (!enc) return null;
|
||||
const apiKey = openStoredKey(enc);
|
||||
const apiKey = enc ? openStoredKey(enc) : null;
|
||||
if (apiKey === null) return null;
|
||||
return new AnthropicClient({
|
||||
apiKey,
|
||||
|
||||
@@ -9,7 +9,8 @@ import { readOperatorRow } from "@/lib/settings/operator-row";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { memoizePerRequest } from "@/lib/request-cache";
|
||||
import { isAnthropicBaseUrl, probeProviderChain } from "@/lib/ai/provider";
|
||||
import { isAnthropicBaseUrl } from "@/lib/ai/anthropic-host";
|
||||
import { probeProviderChain } from "@/lib/ai/provider";
|
||||
import { isOperatorFundedProvider } from "@/lib/ai/coach/budget";
|
||||
import type { ProviderChainType } from "@/lib/ai/provider-chain";
|
||||
import type { ProviderWorkAuthority } from "@/lib/sharing/provider-work-authority";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Opening a stored AI-provider key.
|
||||
*/
|
||||
import { decrypt } from "@/lib/crypto";
|
||||
import { getEvent } from "@/lib/logging/context";
|
||||
|
||||
/**
|
||||
* A stored provider key, or null when it cannot be opened: a key id retired
|
||||
* from `ENCRYPTION_KEYS`, a corrupt row. An unreadable key is an unconfigured
|
||||
* provider, the same as no key at all. Its entry drops out of the chain, so a
|
||||
* generation answers the no-provider refusal (422 `ai.provider.none`)
|
||||
* instead of the decrypt's 500, and the operator sees the warning.
|
||||
*/
|
||||
export function openStoredKey(encrypted: string): string | null {
|
||||
try {
|
||||
return decrypt(encrypted);
|
||||
} catch {
|
||||
getEvent()?.addWarning("ai: a stored provider key could not be opened");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ vi.mock("@/lib/ai/provider", () => ({
|
||||
resolveProvider: vi.fn(),
|
||||
probeProviderPresence: vi.fn(async () => true),
|
||||
probeProviderChain: vi.fn(),
|
||||
isAnthropicBaseUrl: () => false,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ai/capabilities/gate", () => ({
|
||||
|
||||
Reference in new issue
Block a user