feat: add per-channel HTTP transport controls

This commit is contained in:
CaIon
2026-07-27 21:41:13 +08:00
parent b27b2b1d6f
commit e99a9bd86f
24 changed files with 1330 additions and 81 deletions
@@ -284,6 +284,8 @@ const SENSITIVE_FORM_FIELDS = [
'force_format',
'thinking_to_content',
'proxy',
'http_protocol',
'http2_connection_shards',
'pass_through_body_enabled',
'system_prompt',
'system_prompt_override',
@@ -339,6 +341,9 @@ function hasAdvancedSettingsValues(values: ChannelFormValues): boolean {
values.thinking_to_content ||
values.pass_through_body_enabled ||
values.system_prompt_override ||
(values.http_protocol && values.http_protocol !== 'auto') ||
(values.http2_connection_shards != null &&
values.http2_connection_shards > 1) ||
values.claude_beta_query ||
values.upstream_model_update_check_enabled ||
values.upstream_model_update_auto_sync_enabled ||
@@ -745,6 +750,8 @@ export function ChannelMutateDrawer({
'disable_task_polling_sleep'
)
const currentProxy = form.watch('proxy')
const currentHttpProtocol = form.watch('http_protocol')
const currentHttp2ConnectionShards = form.watch('http2_connection_shards')
const currentSystemPrompt = form.watch('system_prompt')
const currentSystemPromptOverride = form.watch('system_prompt_override')
const currentAllowServiceTier = form.watch('allow_service_tier')
@@ -1014,7 +1021,9 @@ export function ChannelMutateDrawer({
currentDisableTaskPollingSleep ||
currentProxy?.trim() ||
currentSystemPrompt?.trim() ||
currentSystemPromptOverride
currentSystemPromptOverride ||
(currentHttpProtocol && currentHttpProtocol !== 'auto') ||
(currentHttp2ConnectionShards != null && currentHttp2ConnectionShards > 1)
)
let fieldPassthroughConfigured = false
if (currentType === 1 || currentType === 57) {
@@ -4185,6 +4194,129 @@ export function ChannelMutateDrawer({
)}
/>
<FormField
control={form.control}
name='http_protocol'
render={({ field }) => (
<FormItem>
<FormLabel>{t('HTTP Protocol')}</FormLabel>
<Select
items={[
{
value: 'auto',
label: t('Auto'),
},
{
value: 'http1',
label: t('HTTP/1.1'),
},
]}
value={field.value || 'auto'}
onValueChange={(value) => {
const nextProtocol =
value === 'http1' ? 'http1' : 'auto'
field.onChange(nextProtocol)
if (nextProtocol === 'http1') {
form.setValue(
'http2_connection_shards',
1,
{
shouldDirty: true,
shouldValidate: true,
}
)
}
}}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent
alignItemWithTrigger={false}
>
<SelectGroup>
<SelectItem value='auto'>
{t('Auto')}
</SelectItem>
<SelectItem value='http1'>
{t('HTTP/1.1')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
{t(
'Auto negotiates HTTP/2 when available. HTTP/1.1 forces multiple keep-alive connections under concurrency.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='http2_connection_shards'
render={({ field }) => {
const http1Selected =
currentHttpProtocol === 'http1'
const shardItems = Array.from(
{ length: 8 },
(_, index) => {
const value = String(index + 1)
return { value, label: value }
}
)
return (
<FormItem>
<FormLabel>
{t('HTTP/2 Connection Shards')}
</FormLabel>
<Select
items={shardItems}
value={String(field.value || 1)}
disabled={http1Selected}
onValueChange={(value) => {
field.onChange(Number(value))
}}
>
<FormControl>
<SelectTrigger disabled={http1Selected}>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent
alignItemWithTrigger={false}
>
<SelectGroup>
{shardItems.map((item) => (
<SelectItem
key={item.value}
value={item.value}
>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
{http1Selected
? t(
'HTTP/2 connection shards are unavailable when HTTP/1.1 is selected.'
)
: t(
'Spread HTTP/2 traffic across multiple reusable connections to the same upstream origin (1-8).'
)}
</FormDescription>
<FormMessage />
</FormItem>
)
}}
/>
<FormField
control={form.control}
name='system_prompt'
+5
View File
@@ -245,6 +245,11 @@ export const ERROR_MESSAGES = {
INVALID_MODEL_MAPPING: 'Invalid model mapping format',
INVALID_PROXY:
'Proxy address must use HTTP, HTTPS, SOCKS5, or SOCKS5H and include a valid host',
INVALID_HTTP_PROTOCOL: 'HTTP protocol must be Auto or HTTP/1.1',
INVALID_HTTP2_CONNECTION_SHARDS:
'HTTP/2 connection shards must be between 1 and 8',
INVALID_HTTP1_WITH_SHARDS:
'HTTP/2 connection shards must be 1 when HTTP/1.1 is selected',
CREATE_FAILED: 'Failed to create channel',
UPDATE_FAILED: 'Failed to update channel',
DELETE_FAILED: 'Failed to delete channel',
@@ -39,6 +39,8 @@ const ADVANCED_SETTINGS_FIELDS = new Set<FieldPath<ChannelFormValues>>([
'thinking_to_content',
'pass_through_body_enabled',
'proxy',
'http_protocol',
'http2_connection_shards',
'system_prompt',
'system_prompt_override',
'allow_service_tier',
+77 -2
View File
@@ -70,6 +70,37 @@ function isOptionalProxyURL(value: string | undefined): boolean {
}
}
export const HTTP_PROTOCOL_AUTO = 'auto'
export const HTTP_PROTOCOL_HTTP1 = 'http1'
export const MAX_HTTP2_CONNECTION_SHARDS = 8
export function normalizeHttpProtocol(
value: string | undefined | null
): 'auto' | 'http1' {
const normalized = String(value || '')
.trim()
.toLowerCase()
if (normalized === HTTP_PROTOCOL_HTTP1) {
return HTTP_PROTOCOL_HTTP1
}
return HTTP_PROTOCOL_AUTO
}
export function normalizeHttp2ConnectionShards(
value: number | undefined | null
): number {
if (value == null || Number.isNaN(value) || value === 0) {
return 1
}
if (value < 1) {
return 1
}
if (value > MAX_HTTP2_CONNECTION_SHARDS) {
return MAX_HTTP2_CONNECTION_SHARDS
}
return value
}
function parseOptionalJson(value: string | undefined): unknown {
if (!value?.trim()) return undefined
return JSON.parse(value)
@@ -225,6 +256,8 @@ export const channelFormSchema = z
.string()
.optional()
.refine(isOptionalProxyURL, ERROR_MESSAGES.INVALID_PROXY),
http_protocol: z.enum(['auto', 'http1']).optional(),
http2_connection_shards: z.number().int().optional(),
pass_through_body_enabled: z.boolean().optional(),
system_prompt: z.string().optional(),
system_prompt_override: z.boolean().optional(),
@@ -340,6 +373,23 @@ export const channelFormSchema = z
'Vertex AI API Key mode does not support batch creation'
)
}
const protocol = normalizeHttpProtocol(data.http_protocol)
const shards = data.http2_connection_shards ?? 1
if (shards < 1 || shards > MAX_HTTP2_CONNECTION_SHARDS) {
addRequiredIssue(
ctx,
'http2_connection_shards',
ERROR_MESSAGES.INVALID_HTTP2_CONNECTION_SHARDS
)
}
if (protocol === HTTP_PROTOCOL_HTTP1 && shards > 1) {
addRequiredIssue(
ctx,
'http2_connection_shards',
ERROR_MESSAGES.INVALID_HTTP1_WITH_SHARDS
)
}
})
export type ChannelFormValues = z.infer<typeof channelFormSchema>
@@ -378,6 +428,8 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = {
force_format: false,
thinking_to_content: false,
proxy: '',
http_protocol: HTTP_PROTOCOL_AUTO,
http2_connection_shards: 1,
pass_through_body_enabled: false,
system_prompt: '',
system_prompt_override: false,
@@ -416,6 +468,8 @@ export function transformChannelToFormDefaults(
force_format: false,
thinking_to_content: false,
proxy: '',
http_protocol: HTTP_PROTOCOL_AUTO as 'auto' | 'http1',
http2_connection_shards: 1,
pass_through_body_enabled: false,
system_prompt: '',
system_prompt_override: false,
@@ -424,10 +478,17 @@ export function transformChannelToFormDefaults(
if (channel.setting) {
try {
const parsed = JSON.parse(channel.setting)
const protocol = normalizeHttpProtocol(parsed.http_protocol)
const shards = normalizeHttp2ConnectionShards(
parsed.http2_connection_shards
)
extraSettings = {
force_format: parsed.force_format || false,
thinking_to_content: parsed.thinking_to_content || false,
proxy: parsed.proxy || '',
http_protocol: protocol,
http2_connection_shards:
protocol === HTTP_PROTOCOL_HTTP1 ? 1 : shards,
pass_through_body_enabled: parsed.pass_through_body_enabled || false,
system_prompt: parsed.system_prompt || '',
system_prompt_override: parsed.system_prompt_override || false,
@@ -540,8 +601,8 @@ export function transformChannelToFormDefaults(
/**
* Build the setting JSON string from form extra settings
*/
function buildSettingJSON(formData: ChannelFormValues): string {
const settingObj = {
export function buildSettingJSON(formData: ChannelFormValues): string {
const settingObj: Record<string, unknown> = {
force_format: formData.force_format || false,
thinking_to_content: formData.thinking_to_content || false,
proxy: formData.proxy?.trim() || '',
@@ -549,6 +610,20 @@ function buildSettingJSON(formData: ChannelFormValues): string {
system_prompt: formData.system_prompt || '',
system_prompt_override: formData.system_prompt_override || false,
}
const protocol = normalizeHttpProtocol(formData.http_protocol)
const shards =
protocol === HTTP_PROTOCOL_HTTP1
? 1
: normalizeHttp2ConnectionShards(formData.http2_connection_shards)
// Omit defaults so unchanged channels keep equivalent JSON.
if (protocol === HTTP_PROTOCOL_HTTP1) {
settingObj.http_protocol = HTTP_PROTOCOL_HTTP1
} else if (shards > 1) {
settingObj.http2_connection_shards = shards
}
return JSON.stringify(settingObj)
}
+2
View File
@@ -86,6 +86,8 @@ export interface ChannelSettings {
pass_through_body_enabled?: boolean
system_prompt?: string
system_prompt_override?: boolean
http_protocol?: 'auto' | 'http1' | string
http2_connection_shards?: number
}
export interface ChannelOtherSettings {