mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
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:
committed by
GitHub
parent
2ec54d06f6
commit
d8b9759db6
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user