refactor(dashboard): extract remaining shared helpers to providerPageHelpers — #3501 Phase 2b (#3658)

#3501 Phase 2b: extract remaining shared helpers to providerPageHelpers (leaf, cycle-safe). Heavy modals unblocked.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-11 11:10:41 -03:00
committed by GitHub
parent 2ec54d06f6
commit d8b9759db6
5 changed files with 593 additions and 337 deletions

View File

@@ -8,6 +8,7 @@
### ♻️ Code Quality
- **Provider-detail god-component decomposition — Phase 2b (remaining shared helpers→leaf)** ([#3501]): extended `providers/[id]/providerPageHelpers.ts` with all remaining pure helpers needed by the heavy modals (`AddApiKeyModal`/`EditConnectionModal`) before they can be extracted. Moved 22 symbols: web-session credential label/hint/check/title helpers; upstream-headers helpers (`upstreamHeadersRecordsEqual`, `headerRowsToRecord`, `effectiveUpstreamHeadersForProtocol`, `anyUpstreamHeadersBadge`, `getProtoSlice`) plus their `HeaderDraftRow`/`CompatModelRow`/`CompatModelMap`/`CompatByProtocolMap` types; Codex consts and helpers (`CODEX_REASONING_STRENGTH_OPTIONS`, `CODEX_ACCOUNT_SERVICE_TIER_VALUES`, `CODEX_GLOBAL_SERVICE_MODE_VALUES`, `getCodexServiceTierLabel`, `normalizeCodexLimitPolicy`, `getCodexRequestDefaults`, `getClaudeCodeCompatibleRequestDefaults`); misc helpers (`compatProtocolLabelKey`, `extractCommandCodeCredentialInput`, `normalizeAndValidateHttpBaseUrl`, `SILICONFLOW_ENDPOINTS`, `CommandCodeAuthFlowState`). New transitive imports wired into the leaf: `MODEL_COMPAT_PROTOCOL_KEYS` (`@/shared/constants/modelCompat`), `CodexServiceTier`/`getCodexRequestDefaults`/`getClaudeCodeCompatibleRequestDefaults` (`@/lib/providers/requestDefaults`), `CodexGlobalServiceMode` (`@/lib/providers/codexFastTier`), `WebSessionCredentialRequirement` (`./webSessionCredentials`). `ProviderDetailPageClient.tsx`: 10,288 → 9,980 LOC. Leaf module: 589 LOC (acyclic). 25-assertion unit test suite passes; smoke test 3/3; no import cycles. Co-authored with @oyi77.
- **Provider-detail god-component decomposition — Phase 2 (helpers→lib)** ([#3501]): extracted the pure shared helpers — `ProviderMessageTranslator`/`LocalProviderMetadata` types, `providerText`/`providerCountText`/`readBooleanToggle`, and the provider base-URL + routing-tag/excluded-model parse/format block — into a new leaf `providers/[id]/providerPageHelpers.ts` (imports only `@/shared`, so the client and modals share them with no import cycle). `ProviderDetailPageClient.tsx`: 10,435 → 10,288 LOC. Unblocks extracting the heavier `AddApiKeyModal`/`EditConnectionModal` (which depend on these helpers) without cycling. The Phase 0 smoke test caught a missing transitive import (`isSelfHostedChatProvider`) at mount — now wired + locked by a new helpers unit test (12 assertions). Co-authored with @oyi77.
- **#3500 fully resolved** — Hard Rule #5 (no raw SQL in route handlers): all 13 internal offenders migrated to `src/lib/db/` modules across slices (call_logs, usage_history/daily_usage_summary, community_servers, usage_logs, semantic_cache, proxy_logs, skills UPDATE, db-backups). The gate's `KNOWN_RAW_SQL` set is renamed to `EXTERNAL_DB_ALLOWED` (with a back-compat alias) and now holds only the **2 external-DB reads** (`oauth/cursor/auto-import`, `oauth/kiro/auto-import`) — these open *another app's* SQLite to import credentials, so by design they cannot live in OmniRoute's `db/` domain. The gate still blocks any NEW raw SQL against OmniRoute's DB.

View File

@@ -48,7 +48,7 @@
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2570,
"src/app/(dashboard)/dashboard/health/page.tsx": 1091,
"src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 10288,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 9981,
"src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx": 906,
"src/app/(dashboard)/dashboard/providers/page.tsx": 1925,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1127,

View File

@@ -72,11 +72,7 @@ import { maskEmail, pickMaskedDisplayValue, pickDisplayValue } from "@/shared/ut
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import EmailPrivacyToggle from "@/shared/components/EmailPrivacyToggle";
import ProviderIcon from "@/shared/components/ProviderIcon";
import {
getClaudeCodeCompatibleRequestDefaults as _getClaudeCodeCompatibleRequestDefaults,
getCodexRequestDefaults as _getCodexRequestDefaults,
type CodexServiceTier,
} from "@/lib/providers/requestDefaults";
import { type CodexServiceTier } from "@/lib/providers/requestDefaults";
import {
CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS,
getCodexEffectiveServiceTier,
@@ -126,20 +122,35 @@ import {
providerText,
providerCountText,
readBooleanToggle,
getWebSessionCredentialLabel,
getWebSessionCredentialHint,
getWebSessionCredentialCheckLabel,
getAddCredentialModalTitle,
upstreamHeadersRecordsEqual,
UPSTREAM_HEADERS_UI_MAX,
headerRowsToRecord,
effectiveUpstreamHeadersForProtocol,
anyUpstreamHeadersBadge,
getProtoSlice,
CODEX_REASONING_STRENGTH_OPTIONS,
CODEX_ACCOUNT_SERVICE_TIER_VALUES,
CODEX_GLOBAL_SERVICE_MODE_VALUES,
getCodexServiceTierLabel,
normalizeCodexLimitPolicy,
getCodexRequestDefaults,
getClaudeCodeCompatibleRequestDefaults,
compatProtocolLabelKey,
extractCommandCodeCredentialInput,
normalizeAndValidateHttpBaseUrl,
SILICONFLOW_ENDPOINTS,
type ProviderMessageTranslator,
type LocalProviderMetadata,
type CommandCodeAuthFlowState,
type HeaderDraftRow,
type CompatByProtocolMap,
type CompatModelRow,
type CompatModelMap,
} from "./providerPageHelpers";
type CompatByProtocolMap = Partial<
Record<
ModelCompatProtocolKey,
{
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
upstreamHeaders?: Record<string, string>;
}
>
>;
/** PATCH fields for provider model compat (matches API + `ModelCompatPerProtocol` shape). */
type ModelCompatSavePatch = {
normalizeToolCallId?: boolean;
@@ -149,35 +160,12 @@ type ModelCompatSavePatch = {
isHidden?: boolean;
};
type CompatModelRow = {
id?: string;
name?: string;
source?: string;
apiFormat?: string;
supportedEndpoints?: string[];
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
isHidden?: boolean;
upstreamHeaders?: Record<string, string>;
compatByProtocol?: CompatByProtocolMap;
};
type CompatModelMap = Map<string, CompatModelRow>;
function buildCompatMap(rows: CompatModelRow[]): CompatModelMap {
const m = new Map<string, CompatModelRow>();
for (const r of rows) if (r.id) m.set(r.id, r);
return m;
}
function getProtoSlice(
c: CompatModelRow | undefined,
o: CompatModelRow | undefined,
protocol: string
) {
return c?.compatByProtocol?.[protocol] ?? o?.compatByProtocol?.[protocol];
}
function isModelHidden(
modelId: string,
customMap: CompatModelMap,
@@ -194,90 +182,6 @@ function isModelHidden(
return false;
}
function getWebSessionCredentialLabel(
t: ProviderMessageTranslator,
requirement: WebSessionCredentialRequirement,
optional: boolean
): string {
if (requirement.kind === "none") {
return providerText(t, "webNoAuthCredentialLabel", "No credential required");
}
const baseLabel =
requirement.kind === "token"
? providerText(t, "webTokenCredentialLabel", "Web session token")
: t("sessionCookieLabel");
return optional ? `${baseLabel} (${t("optional").toLowerCase()})` : baseLabel;
}
function getWebSessionCredentialHint(
t: ProviderMessageTranslator,
requirement: WebSessionCredentialRequirement,
providerName: string,
editing: boolean
): string | undefined {
if (requirement.kind === "none") return undefined;
const values = { provider: providerName, credential: requirement.credentialName };
if (editing) {
return requirement.kind === "token"
? providerText(
t,
"webTokenEditHint",
"Leave blank to keep the current web session token. Credential: {credential}.",
values
)
: providerText(
t,
"webCookieEditHint",
"Leave blank to keep the current session cookie. Required cookie: {credential}.",
values
);
}
return requirement.kind === "token"
? providerText(
t,
"webTokenCredentialHint",
"Credential: {credential}. Paste the token value from your own signed-in {provider} web session, or a DevTools HAR export if the provider supports it.",
values
)
: providerText(
t,
"webCookieCredentialHint",
"Required cookie: {credential}. Paste the Cookie header value from your own signed-in {provider} web session. Do not include the Cookie: prefix.",
values
);
}
function getWebSessionCredentialCheckLabel(
t: ProviderMessageTranslator,
requirement: WebSessionCredentialRequirement
): string {
if (requirement.kind === "token") return providerText(t, "checkWebToken", "Check token");
return providerText(t, "checkCookie", "Check cookie");
}
function getAddCredentialModalTitle(
t: ProviderMessageTranslator,
providerName: string,
requirement: WebSessionCredentialRequirement | null
): string {
if (!requirement) return t("addProviderApiKeyTitle", { provider: providerName });
if (requirement.kind === "none") {
return providerText(t, "addProviderConnectionTitle", "Add {provider} connection", {
provider: providerName,
});
}
if (requirement.kind === "token") {
return providerText(t, "addProviderWebTokenTitle", "Add {provider} web token", {
provider: providerName,
});
}
return providerText(t, "addProviderSessionCookieTitle", "Add {provider} session cookie", {
provider: providerName,
});
}
function WebSessionCredentialGuide({
requirement,
providerName,
@@ -468,36 +372,12 @@ function anyNoPreserveCompatBadge(
return false;
}
function upstreamHeadersRecordsEqual(
a: Record<string, string>,
b: Record<string, string>
): boolean {
const ka = Object.keys(a).sort();
const kb = Object.keys(b).sort();
if (ka.length !== kb.length) return false;
return ka.every((k, i) => k === kb[i] && a[k] === b[k]);
}
type HeaderDraftRow = { id: string; name: string; value: string };
const UPSTREAM_HEADERS_UI_MAX = 16;
function recordToHeaderRows(rec: Record<string, string>, genId: () => string): HeaderDraftRow[] {
const entries = Object.entries(rec).filter(([k]) => k.trim());
if (entries.length === 0) return [{ id: genId(), name: "", value: "" }];
return entries.map(([name, value]) => ({ id: genId(), name, value }));
}
function headerRowsToRecord(rows: HeaderDraftRow[]): Record<string, string> {
const out: Record<string, string> = {};
for (const r of rows) {
const k = r.name.trim();
if (!k) continue;
out[k] = r.value;
}
return out;
}
type ProviderModelsApiErrorBody = {
error?: {
message?: string;
@@ -528,44 +408,6 @@ async function formatProviderModelsErrorResponse(res: Response): Promise<string>
return st || `HTTP ${res.status}`;
}
function effectiveUpstreamHeadersForProtocol(
modelId: string,
protocol: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): Record<string, string> {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const base: Record<string, string> = {};
if (c?.upstreamHeaders && typeof c.upstreamHeaders === "object") {
Object.assign(base, c.upstreamHeaders);
} else if (o?.upstreamHeaders && typeof o.upstreamHeaders === "object") {
Object.assign(base, o.upstreamHeaders);
}
const pc = getProtoSlice(c, o, protocol);
if (pc?.upstreamHeaders && typeof pc.upstreamHeaders === "object") {
Object.assign(base, pc.upstreamHeaders);
}
return base;
}
function anyUpstreamHeadersBadge(
modelId: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const nonempty = (u: unknown) =>
u && typeof u === "object" && !Array.isArray(u) && Object.keys(u as object).length > 0;
if (nonempty(c?.upstreamHeaders) || nonempty(o?.upstreamHeaders)) return true;
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
const pc = getProtoSlice(c, o, p);
if (nonempty(pc?.upstreamHeaders)) return true;
}
return false;
}
interface ModelRowProps {
model: { id: string; name?: string; source?: string; isHidden?: boolean };
fullModel: string;
@@ -829,28 +671,6 @@ interface AddApiKeyModalProps {
onClose: () => void;
}
type CommandCodeAuthFlowState = {
phase:
| "idle"
| "starting"
| "polling"
| "received"
| "applying"
| "applied"
| "expired"
| "error";
state: string;
authUrl: string;
callbackUrl: string;
expiresAt: string | null;
message?: string;
};
const SILICONFLOW_ENDPOINTS = [
{ id: "siliconflow", label: "Global", baseUrl: "https://api.siliconflow.com/v1" },
{ id: "siliconflow-cn", label: "China", baseUrl: "https://api.siliconflow.cn/v1" },
] as const;
interface EditConnectionModalConnection {
id?: string;
name?: string;
@@ -887,74 +707,6 @@ interface EditConnectionModalProps {
onSave: (data: unknown) => Promise<void | unknown>;
onClose: () => void;
}
const CODEX_REASONING_STRENGTH_OPTIONS = [
{ value: "none", label: "None" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "xhigh", label: "XHigh" },
];
const CODEX_ACCOUNT_SERVICE_TIER_VALUES: CodexServiceTier[] = ["default", "priority", "flex"];
const CODEX_GLOBAL_SERVICE_MODE_VALUES: CodexGlobalServiceMode[] = [
"none",
...CODEX_ACCOUNT_SERVICE_TIER_VALUES,
];
function getCodexServiceTierLabel(
t: ProviderMessageTranslator,
value: CodexGlobalServiceMode
): string {
if (value === "none") {
return providerText(t, "codexServiceModeNone", "No global setting");
}
if (value === "default") return providerText(t, "codexServiceTierDefault", "Default");
if (value === "priority") return providerText(t, "codexServiceTierPriority", "Priority");
return providerText(t, "codexServiceTierFlex", "Flex");
}
function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly: boolean } {
const record =
policy && typeof policy === "object" && !Array.isArray(policy)
? (policy as Record<string, unknown>)
: {};
return {
use5h: typeof record.use5h === "boolean" ? record.use5h : true,
useWeekly: typeof record.useWeekly === "boolean" ? record.useWeekly : true,
};
}
/**
* UI adapter around the canonical getCodexRequestDefaults from requestDefaults.ts.
* Adds the "medium" fallback for reasoningEffort required by the connection form.
*/
function getCodexRequestDefaults(providerSpecificData: unknown): {
reasoningEffort: string;
serviceTier?: CodexServiceTier;
} {
const defaults = _getCodexRequestDefaults(providerSpecificData);
return {
reasoningEffort: defaults.reasoningEffort ?? "medium",
...(defaults.serviceTier ? { serviceTier: defaults.serviceTier } : {}),
};
}
function getClaudeCodeCompatibleRequestDefaults(providerSpecificData: unknown): {
context1m: boolean;
} {
const defaults = _getClaudeCodeCompatibleRequestDefaults(providerSpecificData);
return {
context1m: defaults.context1m === true,
};
}
function compatProtocolLabelKey(protocol: string): string {
if (protocol === "openai") return "compatProtocolOpenAI";
if (protocol === "openai-responses") return "compatProtocolOpenAIResponses";
if (protocol === "claude") return "compatProtocolClaude";
return "compatProtocolOpenAI";
}
function ModelCompatPopover({
t,
effectiveModelNormalize,
@@ -8298,52 +8050,6 @@ function ConnectionRow({
);
}
function extractCommandCodeCredentialInput(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
try {
const parsed = JSON.parse(trimmed) as unknown;
if (parsed && typeof parsed === "object") {
const record = parsed as Record<string, unknown>;
const direct = record.apiKey || record.api_key || record.key || record.token;
if (typeof direct === "string" && direct.trim()) return direct.trim();
const nested = record.data;
if (nested && typeof nested === "object") {
const nestedRecord = nested as Record<string, unknown>;
const nestedKey = nestedRecord.apiKey || nestedRecord.api_key || nestedRecord.key;
if (typeof nestedKey === "string" && nestedKey.trim()) return nestedKey.trim();
}
}
} catch {
// Not JSON; continue with URL/raw parsing.
}
try {
const url = new URL(trimmed);
const key =
url.searchParams.get("apiKey") ||
url.searchParams.get("api_key") ||
url.searchParams.get("key") ||
url.searchParams.get("token");
if (key?.trim()) return key.trim();
const hash = url.hash.replace(/^#/, "");
if (hash) {
const hashParams = new URLSearchParams(hash);
const hashKey =
hashParams.get("apiKey") ||
hashParams.get("api_key") ||
hashParams.get("key") ||
hashParams.get("token");
if (hashKey?.trim()) return hashKey.trim();
}
} catch {
// Not a URL; use the raw value.
}
return trimmed;
}
function SiliconFlowEndpointModal({
isOpen,
onSelect,
@@ -9181,19 +8887,6 @@ function AddApiKeyModal({
);
}
function normalizeAndValidateHttpBaseUrl(rawValue, fallbackUrl) {
const value = (typeof rawValue === "string" ? rawValue.trim() : "") || fallbackUrl;
try {
const parsed = new URL(value);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { value: null, error: "Base URL must use http or https" };
}
return { value, error: null };
} catch {
return { value: null, error: "Base URL must be a valid URL" };
}
}
function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnectionModalProps) {
const t = useTranslations("providers");
const notify = useNotificationStore();

View File

@@ -1,9 +1,25 @@
// Pure, shared helpers for the provider-detail page and its extracted modals
// (Issue #3501 strangler-fig decomposition, Phase 2). Leaf module — imports only
// from @/shared, so the page client AND colocated modals can import these without
// a circular dependency. Extracting them here unblocks moving the heavier modals
// from @/shared, @/lib and colocated sibling modules that are themselves acyclic,
// so the page client AND colocated modals can import these without a circular
// dependency. Extracting them here unblocks moving the heavier modals
// (AddApiKeyModal / EditConnectionModal) out of the god-component in later phases.
import { LOCAL_PROVIDERS, isSelfHostedChatProvider } from "@/shared/constants/providers";
import {
MODEL_COMPAT_PROTOCOL_KEYS,
type ModelCompatProtocolKey,
} from "@/shared/constants/modelCompat";
import {
getClaudeCodeCompatibleRequestDefaults as _getClaudeCodeCompatibleRequestDefaults,
getCodexRequestDefaults as _getCodexRequestDefaults,
type CodexServiceTier,
} from "@/lib/providers/requestDefaults";
import { type CodexGlobalServiceMode } from "@/lib/providers/codexFastTier";
import { type WebSessionCredentialRequirement } from "./webSessionCredentials";
// ---------------------------------------------------------------------------
// Types shared between page + modals
// ---------------------------------------------------------------------------
export type ProviderMessageTranslator = ((
key: string,
@@ -18,6 +34,59 @@ export type LocalProviderMetadata = {
[key: string]: unknown;
};
export type CommandCodeAuthFlowState = {
phase:
| "idle"
| "starting"
| "polling"
| "received"
| "applying"
| "applied"
| "expired"
| "error";
state: string;
authUrl: string;
callbackUrl: string;
expiresAt: string | null;
message?: string;
};
// ---------------------------------------------------------------------------
// Compat model map types (shared by upstream-headers helpers and the page)
// ---------------------------------------------------------------------------
export type CompatByProtocolMap = Partial<
Record<
ModelCompatProtocolKey,
{
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
upstreamHeaders?: Record<string, string>;
}
>
>;
export type CompatModelRow = {
id?: string;
name?: string;
source?: string;
apiFormat?: string;
supportedEndpoints?: string[];
normalizeToolCallId?: boolean;
preserveOpenAIDeveloperRole?: boolean;
isHidden?: boolean;
upstreamHeaders?: Record<string, string>;
compatByProtocol?: CompatByProtocolMap;
};
export type CompatModelMap = Map<string, CompatModelRow>;
export type HeaderDraftRow = { id: string; name: string; value: string };
// ---------------------------------------------------------------------------
// Utility — message translation with fallback
// ---------------------------------------------------------------------------
export function providerText(
t: ProviderMessageTranslator,
key: string,
@@ -57,6 +126,10 @@ export function readBooleanToggle(value: unknown, fallback: boolean): boolean {
return fallback;
}
// ---------------------------------------------------------------------------
// Base-URL helpers
// ---------------------------------------------------------------------------
export const CONFIGURABLE_BASE_URL_PROVIDERS = new Set([
"azure-openai",
"azure-ai",
@@ -160,6 +233,10 @@ export function isGlmProvider(providerId?: string | null) {
return providerId === "glm" || providerId === "glm-cn" || providerId === "glmt";
}
// ---------------------------------------------------------------------------
// Routing-tags / excluded-models parse + format
// ---------------------------------------------------------------------------
export function parseRoutingTagsInput(value: string): string[] | undefined {
const tags = Array.from(
new Set(
@@ -199,3 +276,313 @@ export function formatExcludedModelsInput(value: unknown): string {
)
.join(", ");
}
// ---------------------------------------------------------------------------
// Web-session credential label / hint helpers (Phase 2b)
// ---------------------------------------------------------------------------
export function getWebSessionCredentialLabel(
t: ProviderMessageTranslator,
requirement: WebSessionCredentialRequirement,
optional: boolean
): string {
if (requirement.kind === "none") {
return providerText(t, "webNoAuthCredentialLabel", "No credential required");
}
const baseLabel =
requirement.kind === "token"
? providerText(t, "webTokenCredentialLabel", "Web session token")
: t("sessionCookieLabel");
return optional ? `${baseLabel} (${t("optional").toLowerCase()})` : baseLabel;
}
export function getWebSessionCredentialHint(
t: ProviderMessageTranslator,
requirement: WebSessionCredentialRequirement,
providerName: string,
editing: boolean
): string | undefined {
if (requirement.kind === "none") return undefined;
const values = { provider: providerName, credential: requirement.credentialName };
if (editing) {
return requirement.kind === "token"
? providerText(
t,
"webTokenEditHint",
"Leave blank to keep the current web session token. Credential: {credential}.",
values
)
: providerText(
t,
"webCookieEditHint",
"Leave blank to keep the current session cookie. Required cookie: {credential}.",
values
);
}
return requirement.kind === "token"
? providerText(
t,
"webTokenCredentialHint",
"Credential: {credential}. Paste the token value from your own signed-in {provider} web session, or a DevTools HAR export if the provider supports it.",
values
)
: providerText(
t,
"webCookieCredentialHint",
"Required cookie: {credential}. Paste the Cookie header value from your own signed-in {provider} web session. Do not include the Cookie: prefix.",
values
);
}
export function getWebSessionCredentialCheckLabel(
t: ProviderMessageTranslator,
requirement: WebSessionCredentialRequirement
): string {
if (requirement.kind === "token") return providerText(t, "checkWebToken", "Check token");
return providerText(t, "checkCookie", "Check cookie");
}
export function getAddCredentialModalTitle(
t: ProviderMessageTranslator,
providerName: string,
requirement: WebSessionCredentialRequirement | null
): string {
if (!requirement) return t("addProviderApiKeyTitle", { provider: providerName });
if (requirement.kind === "none") {
return providerText(t, "addProviderConnectionTitle", "Add {provider} connection", {
provider: providerName,
});
}
if (requirement.kind === "token") {
return providerText(t, "addProviderWebTokenTitle", "Add {provider} web token", {
provider: providerName,
});
}
return providerText(t, "addProviderSessionCookieTitle", "Add {provider} session cookie", {
provider: providerName,
});
}
// ---------------------------------------------------------------------------
// Upstream-headers helpers (Phase 2b)
// ---------------------------------------------------------------------------
export const UPSTREAM_HEADERS_UI_MAX = 16;
export function upstreamHeadersRecordsEqual(
a: Record<string, string>,
b: Record<string, string>
): boolean {
const ka = Object.keys(a).sort();
const kb = Object.keys(b).sort();
if (ka.length !== kb.length) return false;
return ka.every((k, i) => k === kb[i] && a[k] === b[k]);
}
export function headerRowsToRecord(rows: HeaderDraftRow[]): Record<string, string> {
const out: Record<string, string> = {};
for (const r of rows) {
const k = r.name.trim();
if (!k) continue;
out[k] = r.value;
}
return out;
}
// Internal helper: returns the per-protocol compat slice for a model (custom
// overrides take precedence over overrideMap).
export function getProtoSlice(
c: CompatModelRow | undefined,
o: CompatModelRow | undefined,
protocol: string
) {
return c?.compatByProtocol?.[protocol] ?? o?.compatByProtocol?.[protocol];
}
export function effectiveUpstreamHeadersForProtocol(
modelId: string,
protocol: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): Record<string, string> {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const base: Record<string, string> = {};
if (c?.upstreamHeaders && typeof c.upstreamHeaders === "object") {
Object.assign(base, c.upstreamHeaders);
} else if (o?.upstreamHeaders && typeof o.upstreamHeaders === "object") {
Object.assign(base, o.upstreamHeaders);
}
const pc = getProtoSlice(c, o, protocol);
if (pc?.upstreamHeaders && typeof pc.upstreamHeaders === "object") {
Object.assign(base, pc.upstreamHeaders);
}
return base;
}
export function anyUpstreamHeadersBadge(
modelId: string,
customMap: CompatModelMap,
overrideMap: CompatModelMap
): boolean {
const c = customMap.get(modelId);
const o = overrideMap.get(modelId);
const nonempty = (u: unknown) =>
u && typeof u === "object" && !Array.isArray(u) && Object.keys(u as object).length > 0;
if (nonempty(c?.upstreamHeaders) || nonempty(o?.upstreamHeaders)) return true;
for (const p of MODEL_COMPAT_PROTOCOL_KEYS) {
const pc = getProtoSlice(c, o, p);
if (nonempty(pc?.upstreamHeaders)) return true;
}
return false;
}
// ---------------------------------------------------------------------------
// Codex helpers + consts (Phase 2b)
// ---------------------------------------------------------------------------
export const CODEX_REASONING_STRENGTH_OPTIONS = [
{ value: "none", label: "None" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "xhigh", label: "XHigh" },
];
export const CODEX_ACCOUNT_SERVICE_TIER_VALUES: CodexServiceTier[] = [
"default",
"priority",
"flex",
];
export const CODEX_GLOBAL_SERVICE_MODE_VALUES: CodexGlobalServiceMode[] = [
"none",
...CODEX_ACCOUNT_SERVICE_TIER_VALUES,
];
export function getCodexServiceTierLabel(
t: ProviderMessageTranslator,
value: CodexGlobalServiceMode
): string {
if (value === "none") {
return providerText(t, "codexServiceModeNone", "No global setting");
}
if (value === "default") return providerText(t, "codexServiceTierDefault", "Default");
if (value === "priority") return providerText(t, "codexServiceTierPriority", "Priority");
return providerText(t, "codexServiceTierFlex", "Flex");
}
export function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly: boolean } {
const record =
policy && typeof policy === "object" && !Array.isArray(policy)
? (policy as Record<string, unknown>)
: {};
return {
use5h: typeof record.use5h === "boolean" ? record.use5h : true,
useWeekly: typeof record.useWeekly === "boolean" ? record.useWeekly : true,
};
}
/**
* UI adapter around the canonical getCodexRequestDefaults from requestDefaults.ts.
* Adds the "medium" fallback for reasoningEffort required by the connection form.
*/
export function getCodexRequestDefaults(providerSpecificData: unknown): {
reasoningEffort: string;
serviceTier?: CodexServiceTier;
} {
const defaults = _getCodexRequestDefaults(providerSpecificData);
return {
reasoningEffort: defaults.reasoningEffort ?? "medium",
...(defaults.serviceTier ? { serviceTier: defaults.serviceTier } : {}),
};
}
export function getClaudeCodeCompatibleRequestDefaults(providerSpecificData: unknown): {
context1m: boolean;
} {
const defaults = _getClaudeCodeCompatibleRequestDefaults(providerSpecificData);
return {
context1m: defaults.context1m === true,
};
}
// ---------------------------------------------------------------------------
// Misc pure helpers (Phase 2b)
// ---------------------------------------------------------------------------
export const SILICONFLOW_ENDPOINTS = [
{ id: "siliconflow", label: "Global", baseUrl: "https://api.siliconflow.com/v1" },
{ id: "siliconflow-cn", label: "China", baseUrl: "https://api.siliconflow.cn/v1" },
] as const;
export function compatProtocolLabelKey(protocol: string): string {
if (protocol === "openai") return "compatProtocolOpenAI";
if (protocol === "openai-responses") return "compatProtocolOpenAIResponses";
if (protocol === "claude") return "compatProtocolClaude";
return "compatProtocolOpenAI";
}
export function extractCommandCodeCredentialInput(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
try {
const parsed = JSON.parse(trimmed) as unknown;
if (parsed && typeof parsed === "object") {
const record = parsed as Record<string, unknown>;
const direct = record.apiKey || record.api_key || record.key || record.token;
if (typeof direct === "string" && direct.trim()) return direct.trim();
const nested = record.data;
if (nested && typeof nested === "object") {
const nestedRecord = nested as Record<string, unknown>;
const nestedKey = nestedRecord.apiKey || nestedRecord.api_key || nestedRecord.key;
if (typeof nestedKey === "string" && nestedKey.trim()) return nestedKey.trim();
}
}
} catch {
// Not JSON; continue with URL/raw parsing.
}
try {
const url = new URL(trimmed);
const key =
url.searchParams.get("apiKey") ||
url.searchParams.get("api_key") ||
url.searchParams.get("key") ||
url.searchParams.get("token");
if (key?.trim()) return key.trim();
const hash = url.hash.replace(/^#/, "");
if (hash) {
const hashParams = new URLSearchParams(hash);
const hashKey =
hashParams.get("apiKey") ||
hashParams.get("api_key") ||
hashParams.get("key") ||
hashParams.get("token");
if (hashKey?.trim()) return hashKey.trim();
}
} catch {
// Not a URL; use the raw value.
}
return trimmed;
}
export function normalizeAndValidateHttpBaseUrl(
rawValue: unknown,
fallbackUrl: string
): { value: string | null; error: string | null } {
const value = (typeof rawValue === "string" ? rawValue.trim() : "") || fallbackUrl;
try {
const parsed = new URL(value);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { value: null, error: "Base URL must use http or https" };
}
return { value, error: null };
} catch {
return { value: null, error: "Base URL must be a valid URL" };
}
}

View File

@@ -19,6 +19,31 @@ import {
parseExcludedModelsInput,
formatRoutingTagsInput,
formatExcludedModelsInput,
// Phase 2b additions
getWebSessionCredentialLabel,
getWebSessionCredentialHint,
getWebSessionCredentialCheckLabel,
getAddCredentialModalTitle,
upstreamHeadersRecordsEqual,
UPSTREAM_HEADERS_UI_MAX,
headerRowsToRecord,
effectiveUpstreamHeadersForProtocol,
anyUpstreamHeadersBadge,
getProtoSlice,
CODEX_REASONING_STRENGTH_OPTIONS,
CODEX_ACCOUNT_SERVICE_TIER_VALUES,
CODEX_GLOBAL_SERVICE_MODE_VALUES,
getCodexServiceTierLabel,
normalizeCodexLimitPolicy,
getCodexRequestDefaults,
getClaudeCodeCompatibleRequestDefaults,
compatProtocolLabelKey,
extractCommandCodeCredentialInput,
normalizeAndValidateHttpBaseUrl,
SILICONFLOW_ENDPOINTS,
type HeaderDraftRow,
type CompatModelRow,
type CompatModelMap,
} from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts";
const tStub = Object.assign((key: string) => key, { has: (_k: string) => false });
@@ -62,3 +87,153 @@ test("routing-tags / excluded-models parse + format round-trip", () => {
assert.equal(formatExcludedModelsInput(["a", "b"]), "a, b");
assert.equal(formatRoutingTagsInput(undefined), "");
});
// ---------------------------------------------------------------------------
// Phase 2b — runtime guards for the newly-moved helpers
// ---------------------------------------------------------------------------
const tokenReq = { kind: "token" as const, credentialName: "myToken" };
const cookieReq = { kind: "cookie" as const, credentialName: "SESS" };
const noneReq = { kind: "none" as const, credentialName: "" };
test("getWebSessionCredentialLabel (Phase 2b — transitive import guard)", () => {
assert.ok(getWebSessionCredentialLabel(tStub, tokenReq, false).length > 0);
assert.ok(getWebSessionCredentialLabel(tStub, cookieReq, true).length > 0);
assert.ok(getWebSessionCredentialLabel(tStub, noneReq, false).length > 0);
});
test("getWebSessionCredentialHint returns undefined for none-kind, string otherwise", () => {
assert.equal(getWebSessionCredentialHint(tStub, noneReq, "Acme", false), undefined);
assert.equal(typeof getWebSessionCredentialHint(tStub, tokenReq, "Acme", false), "string");
assert.equal(typeof getWebSessionCredentialHint(tStub, cookieReq, "Acme", true), "string");
});
test("getWebSessionCredentialCheckLabel returns a non-empty string", () => {
assert.ok(getWebSessionCredentialCheckLabel(tStub, tokenReq).length > 0);
assert.ok(getWebSessionCredentialCheckLabel(tStub, cookieReq).length > 0);
});
test("getAddCredentialModalTitle handles null/none/token/cookie requirements", () => {
assert.ok(getAddCredentialModalTitle(tStub, "Acme", null).length > 0);
assert.ok(getAddCredentialModalTitle(tStub, "Acme", noneReq).length > 0);
assert.ok(getAddCredentialModalTitle(tStub, "Acme", tokenReq).length > 0);
assert.ok(getAddCredentialModalTitle(tStub, "Acme", cookieReq).length > 0);
});
test("upstreamHeadersRecordsEqual is order-insensitive and correct", () => {
assert.ok(upstreamHeadersRecordsEqual({ a: "1", b: "2" }, { b: "2", a: "1" }));
assert.ok(!upstreamHeadersRecordsEqual({ a: "1" }, { a: "2" }));
assert.ok(!upstreamHeadersRecordsEqual({ a: "1" }, { a: "1", b: "2" }));
assert.equal(UPSTREAM_HEADERS_UI_MAX, 16);
});
test("headerRowsToRecord filters blank name rows and builds record", () => {
const rows: HeaderDraftRow[] = [
{ id: "1", name: "X-Foo", value: "bar" },
{ id: "2", name: " ", value: "ignored" },
{ id: "3", name: "X-Baz", value: "qux" },
];
assert.deepEqual(headerRowsToRecord(rows), { "X-Foo": "bar", "X-Baz": "qux" });
});
test("effectiveUpstreamHeadersForProtocol merges base and protocol-specific headers", () => {
const row: CompatModelRow = {
id: "m1",
upstreamHeaders: { "X-Base": "base" },
compatByProtocol: { openai: { upstreamHeaders: { "X-Proto": "proto" } } },
};
const customMap: CompatModelMap = new Map([["m1", row]]);
const overrideMap: CompatModelMap = new Map();
const result = effectiveUpstreamHeadersForProtocol("m1", "openai", customMap, overrideMap);
assert.equal(result["X-Base"], "base");
assert.equal(result["X-Proto"], "proto");
});
test("anyUpstreamHeadersBadge detects non-empty upstream headers", () => {
const row: CompatModelRow = { id: "m1", upstreamHeaders: { "X-Foo": "bar" } };
const customMap: CompatModelMap = new Map([["m1", row]]);
const overrideMap: CompatModelMap = new Map();
assert.ok(anyUpstreamHeadersBadge("m1", customMap, overrideMap));
const emptyRow: CompatModelRow = { id: "m2", upstreamHeaders: {} };
const emptyMap: CompatModelMap = new Map([["m2", emptyRow]]);
assert.ok(!anyUpstreamHeadersBadge("m2", emptyMap, new Map()));
});
test("getProtoSlice returns custom compat over override", () => {
const custom: CompatModelRow = {
compatByProtocol: { openai: { normalizeToolCallId: true } },
};
const override: CompatModelRow = {
compatByProtocol: { openai: { normalizeToolCallId: false } },
};
const result = getProtoSlice(custom, override, "openai");
assert.equal(result?.normalizeToolCallId, true);
});
test("CODEX_REASONING_STRENGTH_OPTIONS has expected values", () => {
const values = CODEX_REASONING_STRENGTH_OPTIONS.map((o) => o.value);
assert.ok(values.includes("none"));
assert.ok(values.includes("high"));
});
test("CODEX_ACCOUNT_SERVICE_TIER_VALUES contains expected tiers", () => {
assert.ok(CODEX_ACCOUNT_SERVICE_TIER_VALUES.includes("default"));
assert.ok(CODEX_ACCOUNT_SERVICE_TIER_VALUES.includes("flex"));
});
test("CODEX_GLOBAL_SERVICE_MODE_VALUES starts with none", () => {
assert.equal(CODEX_GLOBAL_SERVICE_MODE_VALUES[0], "none");
assert.ok(CODEX_GLOBAL_SERVICE_MODE_VALUES.includes("priority"));
});
test("getCodexServiceTierLabel returns a string for every mode (transitive import guard)", () => {
for (const mode of CODEX_GLOBAL_SERVICE_MODE_VALUES) {
assert.equal(typeof getCodexServiceTierLabel(tStub, mode), "string");
}
});
test("normalizeCodexLimitPolicy defaults use5h and useWeekly to true", () => {
assert.deepEqual(normalizeCodexLimitPolicy(null), { use5h: true, useWeekly: true });
assert.deepEqual(normalizeCodexLimitPolicy({ use5h: false }), { use5h: false, useWeekly: true });
});
test("getCodexRequestDefaults returns reasoningEffort (transitive import guard)", () => {
const result = getCodexRequestDefaults(null);
assert.equal(typeof result.reasoningEffort, "string");
});
test("getClaudeCodeCompatibleRequestDefaults returns context1m boolean", () => {
const result = getClaudeCodeCompatibleRequestDefaults(null);
assert.equal(typeof result.context1m, "boolean");
});
test("compatProtocolLabelKey maps protocol strings to i18n keys", () => {
assert.equal(compatProtocolLabelKey("openai"), "compatProtocolOpenAI");
assert.equal(compatProtocolLabelKey("claude"), "compatProtocolClaude");
assert.equal(compatProtocolLabelKey("unknown"), "compatProtocolOpenAI");
});
test("extractCommandCodeCredentialInput extracts from JSON/URL/raw (transitive import guard)", () => {
assert.equal(extractCommandCodeCredentialInput(" "), "");
assert.equal(extractCommandCodeCredentialInput("rawtoken"), "rawtoken");
assert.equal(
extractCommandCodeCredentialInput(JSON.stringify({ apiKey: "abc123" })),
"abc123"
);
});
test("normalizeAndValidateHttpBaseUrl validates http/https URLs", () => {
const ok = normalizeAndValidateHttpBaseUrl("https://api.example.com/v1", "");
assert.equal(ok.error, null);
assert.equal(ok.value, "https://api.example.com/v1");
const bad = normalizeAndValidateHttpBaseUrl("ftp://nope.com", "");
assert.ok(bad.error !== null);
const invalid = normalizeAndValidateHttpBaseUrl("not-a-url", "");
assert.ok(invalid.error !== null);
});
test("SILICONFLOW_ENDPOINTS has global and china entries", () => {
const ids = SILICONFLOW_ENDPOINTS.map((e) => e.id);
assert.ok(ids.includes("siliconflow"));
assert.ok(ids.includes("siliconflow-cn"));
});