|
|
|
@@ -276,6 +276,52 @@ type CustomMcpTransport = "stdio" | "streamableHttp" | "sse";
|
|
|
|
|
|
|
|
|
|
const CONTEXT_WINDOW_TOKEN_OPTIONS = [65_536, 200_000, 262_144, 500_000, 1_048_576] as const;
|
|
|
|
|
const OAUTH_PROXY_PROVIDERS = new Set(["openai_codex", "xai_grok"]);
|
|
|
|
|
type ProviderRequestOption = {
|
|
|
|
|
kind: "priority" | "hosted_tool";
|
|
|
|
|
titleKey: string;
|
|
|
|
|
title: string;
|
|
|
|
|
helpKey: string;
|
|
|
|
|
help: string;
|
|
|
|
|
toolType?: "web_search" | "x_search";
|
|
|
|
|
defaultEnabled?: boolean;
|
|
|
|
|
forceResponses?: boolean;
|
|
|
|
|
};
|
|
|
|
|
const PROVIDER_REQUEST_OPTIONS: Partial<Record<string, ProviderRequestOption[]>> = {
|
|
|
|
|
openai_codex: [{
|
|
|
|
|
kind: "priority",
|
|
|
|
|
titleKey: "settings.providers.capabilityFastMode",
|
|
|
|
|
title: "Fast mode",
|
|
|
|
|
helpKey: "settings.providers.capabilityFastModeHelp",
|
|
|
|
|
help: "Use OpenAI's priority service tier for faster responses. This consumes credits faster.",
|
|
|
|
|
}],
|
|
|
|
|
openai: [{
|
|
|
|
|
kind: "hosted_tool",
|
|
|
|
|
titleKey: "settings.providers.capabilityOpenAISearch",
|
|
|
|
|
title: "OpenAI web search",
|
|
|
|
|
helpKey: "settings.providers.capabilityOpenAISearchHelp",
|
|
|
|
|
help: "Allow compatible Responses API models to search the web. Search activity appears in chat.",
|
|
|
|
|
toolType: "web_search",
|
|
|
|
|
forceResponses: true,
|
|
|
|
|
}],
|
|
|
|
|
deepseek: [{
|
|
|
|
|
kind: "hosted_tool",
|
|
|
|
|
titleKey: "settings.providers.capabilityDeepSeekSearch",
|
|
|
|
|
title: "DeepSeek web search",
|
|
|
|
|
helpKey: "settings.providers.capabilityDeepSeekSearchHelp",
|
|
|
|
|
help: "Let DeepSeek V4 Flash search the web through its Responses API. Search activity appears in chat.",
|
|
|
|
|
toolType: "web_search",
|
|
|
|
|
defaultEnabled: true,
|
|
|
|
|
}],
|
|
|
|
|
xai_grok: [{
|
|
|
|
|
kind: "hosted_tool",
|
|
|
|
|
titleKey: "settings.providers.capabilityXSearch",
|
|
|
|
|
title: "X Search",
|
|
|
|
|
helpKey: "settings.providers.capabilityXSearchHelp",
|
|
|
|
|
help: "Allow supported Grok models to use xAI-hosted X Search. Search activity appears in chat.",
|
|
|
|
|
toolType: "x_search",
|
|
|
|
|
defaultEnabled: true,
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
const CUSTOM_PROVIDER_CREATION_KEY = "__custom_provider__";
|
|
|
|
|
const CUSTOM_PROVIDER_ADVANCED_FIELDS: ProviderAdvancedField[] = [
|
|
|
|
|
"extra_headers",
|
|
|
|
@@ -306,6 +352,70 @@ function providerJsonValue(value: Record<string, unknown> | null | undefined): s
|
|
|
|
|
return value && Object.keys(value).length > 0 ? JSON.stringify(value, null, 2) : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseProviderExtraBody(value: string): Record<string, unknown> | null {
|
|
|
|
|
if (!value.trim()) return {};
|
|
|
|
|
try {
|
|
|
|
|
const parsed: unknown = JSON.parse(value);
|
|
|
|
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
|
|
|
? parsed as Record<string, unknown>
|
|
|
|
|
: null;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isHostedSearchTool(tool: unknown, toolType: "web_search" | "x_search"): boolean {
|
|
|
|
|
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false;
|
|
|
|
|
const configuredType = (tool as Record<string, unknown>).type;
|
|
|
|
|
if (typeof configuredType !== "string") return false;
|
|
|
|
|
return configuredType === toolType
|
|
|
|
|
|| (toolType === "web_search" && configuredType.startsWith("web_search_"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasHostedSearchTool(value: unknown, toolType: "web_search" | "x_search"): boolean {
|
|
|
|
|
return Array.isArray(value) && value.some((tool) => isHostedSearchTool(tool, toolType));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function providerRequestOptionEnabled(
|
|
|
|
|
option: ProviderRequestOption,
|
|
|
|
|
extraBody: Record<string, unknown>,
|
|
|
|
|
): boolean {
|
|
|
|
|
if (option.kind === "priority") return extraBody.service_tier === "priority";
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(extraBody, "tools")) {
|
|
|
|
|
return hasHostedSearchTool(extraBody.tools, option.toolType!);
|
|
|
|
|
}
|
|
|
|
|
return option.defaultEnabled === true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateProviderRequestOption(
|
|
|
|
|
option: ProviderRequestOption,
|
|
|
|
|
enabled: boolean,
|
|
|
|
|
form: ProviderForm,
|
|
|
|
|
): Partial<ProviderForm> {
|
|
|
|
|
const extraBody = { ...(parseProviderExtraBody(form.extraBody) ?? {}) };
|
|
|
|
|
if (option.kind === "priority") {
|
|
|
|
|
if (enabled) extraBody.service_tier = "priority";
|
|
|
|
|
else if (extraBody.service_tier === "priority") delete extraBody.service_tier;
|
|
|
|
|
} else {
|
|
|
|
|
const toolType = option.toolType!;
|
|
|
|
|
const tools = Array.isArray(extraBody.tools)
|
|
|
|
|
? extraBody.tools.filter((tool) => !isHostedSearchTool(tool, toolType))
|
|
|
|
|
: [];
|
|
|
|
|
if (enabled) tools.push({ type: toolType });
|
|
|
|
|
if (tools.length || option.defaultEnabled) {
|
|
|
|
|
extraBody.tools = tools;
|
|
|
|
|
} else {
|
|
|
|
|
delete extraBody.tools;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
extraBody: providerJsonValue(extraBody),
|
|
|
|
|
...(option.forceResponses && enabled
|
|
|
|
|
? { apiType: "responses" as const }
|
|
|
|
|
: {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function providerFormFromRow(
|
|
|
|
|
provider: SettingsPayload["providers"][number],
|
|
|
|
|
): ProviderForm {
|
|
|
|
@@ -3852,6 +3962,62 @@ function ModelAdvancedFields({
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProviderRequestOptions({
|
|
|
|
|
providerName,
|
|
|
|
|
form,
|
|
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
providerName: string;
|
|
|
|
|
form: ProviderForm;
|
|
|
|
|
onChange: (value: Partial<ProviderForm>) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const options = PROVIDER_REQUEST_OPTIONS[providerName] ?? [];
|
|
|
|
|
if (options.length === 0) return null;
|
|
|
|
|
const extraBody = parseProviderExtraBody(form.extraBody) ?? {};
|
|
|
|
|
|
|
|
|
|
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="overflow-hidden rounded-[18px] border border-border/45 bg-background/75">
|
|
|
|
|
{options.map((option, index) => {
|
|
|
|
|
const title = tx(option.titleKey, option.title);
|
|
|
|
|
const Icon = option.kind === "priority" ? Zap : Globe2;
|
|
|
|
|
const checked = providerRequestOptionEnabled(option, extraBody);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={option.titleKey}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center justify-between gap-4 px-4 py-3",
|
|
|
|
|
index > 0 && "border-t border-border/45",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex min-w-0 items-start gap-3">
|
|
|
|
|
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted/70 text-muted-foreground">
|
|
|
|
|
<Icon className="h-4 w-4" aria-hidden />
|
|
|
|
|
</span>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-[13px] font-semibold text-foreground">{title}</p>
|
|
|
|
|
<p className="mt-0.5 text-[12px] leading-5 text-muted-foreground">
|
|
|
|
|
{tx(option.helpKey, option.help)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ToggleButton
|
|
|
|
|
checked={checked}
|
|
|
|
|
onChange={(enabled) => onChange(
|
|
|
|
|
updateProviderRequestOption(option, enabled, form),
|
|
|
|
|
)}
|
|
|
|
|
ariaLabel={title}
|
|
|
|
|
label={checked ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProviderAdvancedOptions({
|
|
|
|
|
fields,
|
|
|
|
|
form,
|
|
|
|
@@ -4065,11 +4231,11 @@ function ProviderAdvancedOptions({
|
|
|
|
|
</label>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
{footer ? (
|
|
|
|
|
<div className="mt-3 flex items-center justify-end gap-2 border-t border-border/45 pt-3">
|
|
|
|
|
{footer}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
{footer ? (
|
|
|
|
|
<div className="flex items-center justify-end gap-2 border-t border-border/45 py-3">
|
|
|
|
|
{footer}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
@@ -4320,6 +4486,11 @@ function ProvidersSettings({
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ProviderRequestOptions
|
|
|
|
|
providerName={provider.name}
|
|
|
|
|
form={form}
|
|
|
|
|
onChange={(value) => onChangeProviderForm(provider.name, value)}
|
|
|
|
|
/>
|
|
|
|
|
{supportsOauthAdvancedSettings ? (
|
|
|
|
|
<ProviderAdvancedOptions
|
|
|
|
|
fields={advancedFields}
|
|
|
|
@@ -4445,6 +4616,11 @@ function ProvidersSettings({
|
|
|
|
|
className="h-9 rounded-full text-[13px]"
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
<ProviderRequestOptions
|
|
|
|
|
providerName={provider.name}
|
|
|
|
|
form={form}
|
|
|
|
|
onChange={(value) => onChangeProviderForm(provider.name, value)}
|
|
|
|
|
/>
|
|
|
|
|
<ProviderAdvancedOptions
|
|
|
|
|
fields={advancedFields}
|
|
|
|
|
form={form}
|
|
|
|
|