feat: add per-channel HTTP transport controls
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user