mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-06 11:13:09 +02:00
add no auth and passthrough auth
This commit is contained in:
@@ -1682,12 +1682,23 @@
|
|||||||
"aiProviderApiKeyDescription": "API key used to authenticate requests to this provider",
|
"aiProviderApiKeyDescription": "API key used to authenticate requests to this provider",
|
||||||
"aiProviderApiKeyLastChars": "API Key",
|
"aiProviderApiKeyLastChars": "API Key",
|
||||||
"aiProviderAuthType": "Auth Type",
|
"aiProviderAuthType": "Auth Type",
|
||||||
|
"aiProviderAuthTypeSearch": "Search auth types...",
|
||||||
|
"aiProviderAuthTypeNotFound": "No auth type found",
|
||||||
"aiProviderAuthTypeBearer": "Bearer",
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Authorization: Bearer key. Used by OpenAI and most providers",
|
||||||
"aiProviderAuthTypeXApiKey": "x-api-key",
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key header. Used by Anthropic",
|
||||||
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key header. Used by Google Gemini",
|
||||||
"aiProviderAuthTypeHec": "Splunk HEC",
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Authorization: Splunk key. Used by Splunk HTTP Event Collector",
|
||||||
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer key. Used by Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "No Auth",
|
||||||
|
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||||
"aiProviderAuthTypeDescription": "How the upstream API authenticates requests",
|
"aiProviderAuthTypeDescription": "How the upstream API authenticates requests",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Forward the caller's API key headers to the upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Do not send authentication headers to the upstream",
|
||||||
"aiProviderRoutingMode": "Routing Mode",
|
"aiProviderRoutingMode": "Routing Mode",
|
||||||
"aiProviderRoutingModeDescription": "Send traffic to an upstream URL or to HTTP targets on your sites",
|
"aiProviderRoutingModeDescription": "Send traffic to an upstream URL or to HTTP targets on your sites",
|
||||||
"aiProviderRoutingModeUrl": "Upstream URL",
|
"aiProviderRoutingModeUrl": "Upstream URL",
|
||||||
|
|||||||
@@ -1651,6 +1651,8 @@ export const aiProviders = pgTable("aiProviders", {
|
|||||||
| "x-goog-api-key"
|
| "x-goog-api-key"
|
||||||
| "hec"
|
| "hec"
|
||||||
| "cf-aig-authorization"
|
| "cf-aig-authorization"
|
||||||
|
| "none"
|
||||||
|
| "passthrough"
|
||||||
>()
|
>()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
routingMode: varchar("routingMode")
|
routingMode: varchar("routingMode")
|
||||||
|
|||||||
@@ -1633,6 +1633,8 @@ export const aiProviders = sqliteTable("aiProviders", {
|
|||||||
| "x-goog-api-key"
|
| "x-goog-api-key"
|
||||||
| "hec"
|
| "hec"
|
||||||
| "cf-aig-authorization"
|
| "cf-aig-authorization"
|
||||||
|
| "none"
|
||||||
|
| "passthrough"
|
||||||
>()
|
>()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
routingMode: text("routingMode")
|
routingMode: text("routingMode")
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ export const AI_PROVIDER_AUTH_TYPES = [
|
|||||||
"x-api-key",
|
"x-api-key",
|
||||||
"x-goog-api-key",
|
"x-goog-api-key",
|
||||||
"hec",
|
"hec",
|
||||||
"cf-aig-authorization"
|
"cf-aig-authorization",
|
||||||
|
"none",
|
||||||
|
"passthrough"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type AiProviderAuthType = (typeof AI_PROVIDER_AUTH_TYPES)[number];
|
export type AiProviderAuthType = (typeof AI_PROVIDER_AUTH_TYPES)[number];
|
||||||
@@ -71,6 +73,10 @@ const CONFLICTING_AUTH_HEADERS = [
|
|||||||
"cf-aig-authorization"
|
"cf-aig-authorization"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export function authTypeRequiresApiKey(authType: AiProviderAuthType): boolean {
|
||||||
|
return authType !== "none" && authType !== "passthrough";
|
||||||
|
}
|
||||||
|
|
||||||
export function providerRequiresUpstreamUrl(
|
export function providerRequiresUpstreamUrl(
|
||||||
type: AiProviderType,
|
type: AiProviderType,
|
||||||
routingMode: AiProviderRoutingMode = "url"
|
routingMode: AiProviderRoutingMode = "url"
|
||||||
@@ -116,20 +122,26 @@ export function resolveAiProviderCreateFields(input: {
|
|||||||
const defaults = AI_PROVIDER_DEFAULTS[input.type];
|
const defaults = AI_PROVIDER_DEFAULTS[input.type];
|
||||||
return {
|
return {
|
||||||
upstreamUrl: input.upstreamUrl ?? defaults.upstreamUrl,
|
upstreamUrl: input.upstreamUrl ?? defaults.upstreamUrl,
|
||||||
authType: defaults.authType,
|
authType: input.authType ?? defaults.authType,
|
||||||
routingMode
|
routingMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip inbound client auth headers, then set the provider auth header
|
* Apply provider auth to upstream headers.
|
||||||
* for the given authType.
|
* - Injected modes: strip client auth headers, then set the provider key.
|
||||||
|
* - none: strip client auth headers, send no auth.
|
||||||
|
* - passthrough: leave client auth headers as-is.
|
||||||
*/
|
*/
|
||||||
export function applyAiProviderAuthHeaders(
|
export function applyAiProviderAuthHeaders(
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
authType: AiProviderAuthType,
|
authType: AiProviderAuthType,
|
||||||
apiKey: string
|
apiKey: string | null
|
||||||
): void {
|
): void {
|
||||||
|
if (authType === "passthrough") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const name of CONFLICTING_AUTH_HEADERS) {
|
for (const name of CONFLICTING_AUTH_HEADERS) {
|
||||||
for (const key of Object.keys(headers)) {
|
for (const key of Object.keys(headers)) {
|
||||||
if (key.toLowerCase() === name) {
|
if (key.toLowerCase() === name) {
|
||||||
@@ -138,6 +150,14 @@ export function applyAiProviderAuthHeaders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authType === "none") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error(`API key required for authType ${authType}`);
|
||||||
|
}
|
||||||
|
|
||||||
switch (authType) {
|
switch (authType) {
|
||||||
case "bearer":
|
case "bearer":
|
||||||
headers["Authorization"] = `Bearer ${apiKey}`;
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import config from "@server/lib/config";
|
|||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
import {
|
import {
|
||||||
AiProviderAuthType,
|
AiProviderAuthType,
|
||||||
applyAiProviderAuthHeaders
|
applyAiProviderAuthHeaders,
|
||||||
|
authTypeRequiresApiKey
|
||||||
} from "@server/lib/aiProviderDefaults";
|
} from "@server/lib/aiProviderDefaults";
|
||||||
import {
|
import {
|
||||||
SESSION_COOKIE_NAME,
|
SESSION_COOKIE_NAME,
|
||||||
@@ -488,15 +489,6 @@ export async function chatCompletions(
|
|||||||
|
|
||||||
const { provider } = selection;
|
const { provider } = selection;
|
||||||
|
|
||||||
if (!provider.apiKey) {
|
|
||||||
return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
|
|
||||||
error: { message: "AI provider has no API key configured" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = config.getRawConfig().server.secret!;
|
|
||||||
const apiKey = decrypt(provider.apiKey, secret);
|
|
||||||
|
|
||||||
const upstreamUrl = provider.upstreamUrl;
|
const upstreamUrl = provider.upstreamUrl;
|
||||||
const authType = provider.authType as AiProviderAuthType;
|
const authType = provider.authType as AiProviderAuthType;
|
||||||
|
|
||||||
@@ -508,6 +500,19 @@ export async function chatCompletions(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let apiKey: string | null = null;
|
||||||
|
if (authTypeRequiresApiKey(authType)) {
|
||||||
|
if (!provider.apiKey) {
|
||||||
|
return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
|
||||||
|
error: {
|
||||||
|
message: "AI provider has no API key configured"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const secret = config.getRawConfig().server.secret!;
|
||||||
|
apiKey = decrypt(provider.apiKey, secret);
|
||||||
|
}
|
||||||
|
|
||||||
const targetUrl = `${upstreamUrl.replace(/\/$/, "")}`;
|
const targetUrl = `${upstreamUrl.replace(/\/$/, "")}`;
|
||||||
|
|
||||||
// Drop hop-by-hop / proxy-only headers. Forwarding Host especially
|
// Drop hop-by-hop / proxy-only headers. Forwarding Host especially
|
||||||
|
|||||||
@@ -52,12 +52,4 @@ export function refineProviderUpstreamFields(
|
|||||||
path: ["upstreamUrl"]
|
path: ["upstreamUrl"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === "custom" && !data.authType) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
message: "authType is required for custom providers",
|
|
||||||
path: ["authType"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
|
import { AiProviderAuthTypeSelect } from "@app/components/AiProviderAuthTypeSelect";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -23,13 +24,6 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from "@app/components/ui/select";
|
|
||||||
import { useAiProviderContext } from "@app/hooks/useAiProviderContext";
|
import { useAiProviderContext } from "@app/hooks/useAiProviderContext";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
@@ -40,9 +34,10 @@ import {
|
|||||||
type AiProviderFormValues
|
type AiProviderFormValues
|
||||||
} from "@app/lib/aiProviderFormSchema";
|
} from "@app/lib/aiProviderFormSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import type {
|
import {
|
||||||
AiProviderAuthType,
|
authTypeRequiresApiKey,
|
||||||
AiProviderType
|
type AiProviderAuthType,
|
||||||
|
type AiProviderType
|
||||||
} from "@server/lib/aiProviderDefaults";
|
} from "@server/lib/aiProviderDefaults";
|
||||||
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
@@ -73,7 +68,10 @@ export default function AiProviderAuthenticationPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const showAuthType = provider.type === "custom";
|
const authType = form.watch("authType");
|
||||||
|
const showApiKey = authTypeRequiresApiKey(
|
||||||
|
(authType as AiProviderAuthType | null) ?? "bearer"
|
||||||
|
);
|
||||||
|
|
||||||
async function onSubmit(values: AiProviderFormValues) {
|
async function onSubmit(values: AiProviderFormValues) {
|
||||||
setSaveLoading(true);
|
setSaveLoading(true);
|
||||||
@@ -135,88 +133,22 @@ export default function AiProviderAuthenticationPage() {
|
|||||||
id="ai-provider-auth-form"
|
id="ai-provider-auth-form"
|
||||||
>
|
>
|
||||||
<SettingsFormGrid>
|
<SettingsFormGrid>
|
||||||
{showAuthType && (
|
|
||||||
<SettingsFormCell span="half">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="authType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthType"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
field.value ??
|
|
||||||
"bearer"
|
|
||||||
}
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="bearer">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeBearer"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="x-api-key">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeXApiKey"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="x-goog-api-key">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeXGoogApiKey"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="hec">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeHec"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="cf-aig-authorization">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeCfAigAuthorization"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SettingsFormCell>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SettingsFormCell span="half">
|
<SettingsFormCell span="half">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="apiKey"
|
name="authType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("aiProviderApiKey")}
|
{t(
|
||||||
|
"aiProviderAuthType"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<AiProviderAuthTypeSelect
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
value={
|
value={
|
||||||
field.value ??
|
field.value ??
|
||||||
""
|
"bearer"
|
||||||
}
|
}
|
||||||
onChange={
|
onChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
@@ -225,7 +157,7 @@ export default function AiProviderAuthenticationPage() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"aiProviderApiKeyDescription"
|
"aiProviderAuthTypeDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -233,6 +165,43 @@ export default function AiProviderAuthenticationPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
|
|
||||||
|
{showApiKey && (
|
||||||
|
<SettingsFormCell span="half">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"aiProviderApiKey"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"aiProviderApiKeyDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsFormCell>
|
||||||
|
)}
|
||||||
</SettingsFormGrid>
|
</SettingsFormGrid>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
SettingsSubsectionTitle
|
SettingsSubsectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { AiProviderAuthTypeSelect } from "@app/components/AiProviderAuthTypeSelect";
|
||||||
import { AiProviderTypeSelect } from "@app/components/AiProviderTypeSelect";
|
import { AiProviderTypeSelect } from "@app/components/AiProviderTypeSelect";
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
@@ -33,18 +34,12 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from "@app/components/ui/select";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import {
|
||||||
aiProviderCreateFormSchema,
|
aiProviderCreateFormSchema,
|
||||||
|
defaultAuthTypeForProvider,
|
||||||
emptyUpstreamForType,
|
emptyUpstreamForType,
|
||||||
showsUpstreamUrlField,
|
showsUpstreamUrlField,
|
||||||
toAiProviderCreatePayload,
|
toAiProviderCreatePayload,
|
||||||
@@ -52,6 +47,7 @@ import {
|
|||||||
type AiProviderFormValues
|
type AiProviderFormValues
|
||||||
} from "@app/lib/aiProviderFormSchema";
|
} from "@app/lib/aiProviderFormSchema";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { authTypeRequiresApiKey } from "@server/lib/aiProviderDefaults";
|
||||||
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
import type { CreateOrEditAiProviderResponse } from "@server/routers/aiProvider/types";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -76,7 +72,7 @@ export default function CreateAiProviderPage() {
|
|||||||
type: "openai",
|
type: "openai",
|
||||||
upstreamUrl: emptyUpstreamForType("openai"),
|
upstreamUrl: emptyUpstreamForType("openai"),
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
authType: "bearer",
|
authType: defaultAuthTypeForProvider("openai"),
|
||||||
routingMode: "url",
|
routingMode: "url",
|
||||||
skipTlsVerification: false,
|
skipTlsVerification: false,
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -85,12 +81,13 @@ export default function CreateAiProviderPage() {
|
|||||||
|
|
||||||
const providerType = form.watch("type");
|
const providerType = form.watch("type");
|
||||||
const routingMode = form.watch("routingMode");
|
const routingMode = form.watch("routingMode");
|
||||||
|
const authType = form.watch("authType");
|
||||||
|
|
||||||
const showUpstream = showsUpstreamUrlField(providerType, routingMode);
|
const showUpstream = showsUpstreamUrlField(providerType, routingMode);
|
||||||
const requireUpstream = upstreamUrlRequired(providerType, routingMode);
|
const requireUpstream = upstreamUrlRequired(providerType, routingMode);
|
||||||
const showRoutingMode = providerType === "custom";
|
const showRoutingMode = providerType === "custom";
|
||||||
const showAuthType = providerType === "custom";
|
|
||||||
const showTargets = providerType === "custom" && routingMode === "target";
|
const showTargets = providerType === "custom" && routingMode === "target";
|
||||||
|
const showApiKey = authTypeRequiresApiKey(authType ?? "bearer");
|
||||||
|
|
||||||
async function createTargets(
|
async function createTargets(
|
||||||
providerId: number,
|
providerId: number,
|
||||||
@@ -272,6 +269,12 @@ export default function CreateAiProviderPage() {
|
|||||||
value
|
value
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
form.setValue(
|
||||||
|
"authType",
|
||||||
|
defaultAuthTypeForProvider(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
value !==
|
value !==
|
||||||
"custom"
|
"custom"
|
||||||
@@ -495,88 +498,22 @@ export default function CreateAiProviderPage() {
|
|||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm variant="half">
|
<SettingsSectionForm variant="half">
|
||||||
<SettingsFormGrid>
|
<SettingsFormGrid>
|
||||||
{showAuthType && (
|
|
||||||
<SettingsFormCell span="half">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="authType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthType"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
field.value ??
|
|
||||||
"bearer"
|
|
||||||
}
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="bearer">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeBearer"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="x-api-key">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeXApiKey"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="x-goog-api-key">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeXGoogApiKey"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="hec">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeHec"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="cf-aig-authorization">
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeCfAigAuthorization"
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"aiProviderAuthTypeDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SettingsFormCell>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SettingsFormCell span="half">
|
<SettingsFormCell span="half">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="apiKey"
|
name="authType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("aiProviderApiKey")}
|
{t(
|
||||||
|
"aiProviderAuthType"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<AiProviderAuthTypeSelect
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
value={
|
value={
|
||||||
field.value ??
|
field.value ??
|
||||||
""
|
"bearer"
|
||||||
}
|
}
|
||||||
onChange={
|
onChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
@@ -585,7 +522,7 @@ export default function CreateAiProviderPage() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"aiProviderApiKeyDescription"
|
"aiProviderAuthTypeDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -593,6 +530,43 @@ export default function CreateAiProviderPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
|
|
||||||
|
{showApiKey && (
|
||||||
|
<SettingsFormCell span="half">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"aiProviderApiKey"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"aiProviderApiKeyDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsFormCell>
|
||||||
|
)}
|
||||||
</SettingsFormGrid>
|
</SettingsFormGrid>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from "@app/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import {
|
||||||
|
AI_PROVIDER_AUTH_TYPES,
|
||||||
|
type AiProviderAuthType
|
||||||
|
} from "@server/lib/aiProviderDefaults";
|
||||||
|
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const authLabelMap = {
|
||||||
|
bearer: "aiProviderAuthTypeBearer",
|
||||||
|
"x-api-key": "aiProviderAuthTypeXApiKey",
|
||||||
|
"x-goog-api-key": "aiProviderAuthTypeXGoogApiKey",
|
||||||
|
hec: "aiProviderAuthTypeHec",
|
||||||
|
"cf-aig-authorization": "aiProviderAuthTypeCfAigAuthorization",
|
||||||
|
none: "aiProviderAuthTypeNone",
|
||||||
|
passthrough: "aiProviderAuthTypePassthrough"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const authDescriptionMap = {
|
||||||
|
bearer: "aiProviderAuthTypeBearerDescription",
|
||||||
|
"x-api-key": "aiProviderAuthTypeXApiKeyDescription",
|
||||||
|
"x-goog-api-key": "aiProviderAuthTypeXGoogApiKeyDescription",
|
||||||
|
hec: "aiProviderAuthTypeHecDescription",
|
||||||
|
"cf-aig-authorization": "aiProviderAuthTypeCfAigAuthorizationDescription",
|
||||||
|
none: "aiProviderAuthTypeNoneDescription",
|
||||||
|
passthrough: "aiProviderAuthTypePassthroughDescription"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type AiProviderAuthTypeSelectProps = {
|
||||||
|
value: AiProviderAuthType;
|
||||||
|
onChange: (value: AiProviderAuthType) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AiProviderAuthTypeSelect({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
className
|
||||||
|
}: AiProviderAuthTypeSelectProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
AI_PROVIDER_AUTH_TYPES.map((authType) => ({
|
||||||
|
authType,
|
||||||
|
title: t(authLabelMap[authType]),
|
||||||
|
description: t(authDescriptionMap[authType])
|
||||||
|
})),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selected = options.find((option) => option.authType === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between",
|
||||||
|
!selected && "text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate text-left">
|
||||||
|
{selected?.title ?? t("noneSelected")}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={t("aiProviderAuthTypeSearch")} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
{t("aiProviderAuthTypeNotFound")}
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option.authType}
|
||||||
|
value={`${option.authType} ${option.title} ${option.description}`}
|
||||||
|
onSelect={() => {
|
||||||
|
onChange(option.authType);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4 shrink-0",
|
||||||
|
option.authType === value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
|
<span className="truncate">
|
||||||
|
{option.title}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground text-xs leading-snug">
|
||||||
|
{option.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ import { z } from "zod";
|
|||||||
import {
|
import {
|
||||||
AI_PROVIDER_AUTH_TYPES,
|
AI_PROVIDER_AUTH_TYPES,
|
||||||
AI_PROVIDER_DEFAULTS,
|
AI_PROVIDER_DEFAULTS,
|
||||||
|
authTypeRequiresApiKey,
|
||||||
providerRequiresUpstreamUrl,
|
providerRequiresUpstreamUrl,
|
||||||
|
type AiProviderAuthType,
|
||||||
type AiProviderType
|
type AiProviderType
|
||||||
} from "@server/lib/aiProviderDefaults";
|
} from "@server/lib/aiProviderDefaults";
|
||||||
|
|
||||||
@@ -70,10 +72,10 @@ export const aiProviderFormSchema = z
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === "custom" && !data.authType) {
|
if (!data.authType) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "authType is required for custom providers",
|
message: "authType is required",
|
||||||
path: ["authType"]
|
path: ["authType"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,7 +85,9 @@ export type AiProviderFormValues = z.infer<typeof aiProviderFormSchema>;
|
|||||||
|
|
||||||
export const aiProviderCreateFormSchema = aiProviderFormSchema.superRefine(
|
export const aiProviderCreateFormSchema = aiProviderFormSchema.superRefine(
|
||||||
(data, ctx) => {
|
(data, ctx) => {
|
||||||
if (!data.apiKey?.trim()) {
|
const authType: AiProviderAuthType = data.authType ?? "bearer";
|
||||||
|
|
||||||
|
if (authTypeRequiresApiKey(authType) && !data.apiKey?.trim()) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "API key is required",
|
message: "API key is required",
|
||||||
@@ -93,6 +97,15 @@ export const aiProviderCreateFormSchema = aiProviderFormSchema.superRefine(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function defaultAuthTypeForProvider(
|
||||||
|
type: AiProviderType
|
||||||
|
): AiProviderAuthType {
|
||||||
|
if (type === "custom") {
|
||||||
|
return "bearer";
|
||||||
|
}
|
||||||
|
return AI_PROVIDER_DEFAULTS[type].authType;
|
||||||
|
}
|
||||||
|
|
||||||
export function emptyUpstreamForType(type: AiProviderType): string {
|
export function emptyUpstreamForType(type: AiProviderType): string {
|
||||||
if (type === "custom") {
|
if (type === "custom") {
|
||||||
return "";
|
return "";
|
||||||
@@ -136,10 +149,7 @@ export function toAiProviderCreatePayload(values: AiProviderFormValues) {
|
|||||||
routingMode: values.type === "custom" ? routingMode : undefined,
|
routingMode: values.type === "custom" ? routingMode : undefined,
|
||||||
upstreamUrl,
|
upstreamUrl,
|
||||||
apiKey: values.apiKey?.trim() ? values.apiKey.trim() : undefined,
|
apiKey: values.apiKey?.trim() ? values.apiKey.trim() : undefined,
|
||||||
authType:
|
authType: values.authType ?? "bearer",
|
||||||
values.type === "custom"
|
|
||||||
? (values.authType ?? "bearer")
|
|
||||||
: undefined,
|
|
||||||
skipTlsVerification: values.skipTlsVerification,
|
skipTlsVerification: values.skipTlsVerification,
|
||||||
enabled: values.enabled ?? true
|
enabled: values.enabled ?? true
|
||||||
};
|
};
|
||||||
@@ -160,14 +170,11 @@ export function toAiProviderUpdatePayload(values: AiProviderFormValues) {
|
|||||||
name: values.name.trim(),
|
name: values.name.trim(),
|
||||||
routingMode: values.type === "custom" ? routingMode : "url",
|
routingMode: values.type === "custom" ? routingMode : "url",
|
||||||
upstreamUrl,
|
upstreamUrl,
|
||||||
|
authType: values.authType ?? "bearer",
|
||||||
skipTlsVerification: values.skipTlsVerification ?? false,
|
skipTlsVerification: values.skipTlsVerification ?? false,
|
||||||
enabled: values.enabled ?? true
|
enabled: values.enabled ?? true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (values.type === "custom") {
|
|
||||||
payload.authType = values.authType ?? "bearer";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.apiKey?.trim()) {
|
if (values.apiKey?.trim()) {
|
||||||
payload.apiKey = values.apiKey.trim();
|
payload.apiKey = values.apiKey.trim();
|
||||||
}
|
}
|
||||||
@@ -185,13 +192,10 @@ export function toAiProviderNetworkPayload(values: AiProviderFormValues) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toAiProviderAuthPayload(values: AiProviderFormValues) {
|
export function toAiProviderAuthPayload(values: AiProviderFormValues) {
|
||||||
const payload: Record<string, unknown> = {
|
return {
|
||||||
|
authType: values.authType ?? "bearer",
|
||||||
...(values.apiKey !== undefined ? { apiKey: values.apiKey.trim() } : {})
|
...(values.apiKey !== undefined ? { apiKey: values.apiKey.trim() } : {})
|
||||||
};
|
};
|
||||||
if (values.type === "custom") {
|
|
||||||
payload.authType = values.authType ?? "bearer";
|
|
||||||
}
|
|
||||||
return payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toAiProviderConfigurationPayload(values: AiProviderFormValues) {
|
export function toAiProviderConfigurationPayload(values: AiProviderFormValues) {
|
||||||
|
|||||||
Reference in New Issue
Block a user