File size: 6,810 Bytes
f9a28cd
96bdf6c
f9a28cd
 
 
b1cfe1b
f9a28cd
 
 
 
 
 
 
 
 
 
 
 
 
 
96bdf6c
 
f9a28cd
 
 
 
 
 
 
 
 
 
 
 
96bdf6c
f9a28cd
 
 
 
 
 
 
 
 
 
96bdf6c
f9a28cd
 
 
6c12d18
96bdf6c
 
 
 
 
 
f9a28cd
 
 
 
96bdf6c
 
 
 
f9a28cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96bdf6c
 
 
 
 
 
 
f9a28cd
 
 
 
 
 
96bdf6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9a28cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b1cfe1b
f9a28cd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import {
    DEFAULT_CHANNEL_REQUEST_MODE_PRIORITY as SHARED_DEFAULT_CHANNEL_REQUEST_MODE_PRIORITY,
    CHANNEL_REQUEST_MODES as SHARED_CHANNEL_REQUEST_MODES,
    CHANNEL_REQUEST_MODE_ADMIN_CONTROL as SHARED_CHANNEL_REQUEST_MODE_ADMIN_CONTROL
} from './channel-request-mode-values.mjs';
import { RequestValidationError } from './image-request-utils';

export const CHANNEL_REQUEST_MODES = SHARED_CHANNEL_REQUEST_MODES as readonly [
    'images-non-stream',
    'images-sse',
    'responses-non-stream',
    'responses-sse'
];

export type ChannelRequestMode = (typeof CHANNEL_REQUEST_MODES)[number];

export type ChannelRequestModeBackend = 'images-api' | 'responses-image-generation';

export type ChannelRequestModeDecision = {
    requested_backend: ChannelRequestModeBackend;
    candidate_channel_request_modes?: readonly ChannelRequestMode[];
    request_mode_priority?: readonly ChannelRequestMode[];
    preferred_channel_request_mode?: ChannelRequestMode;
    fallback_channel_request_mode?: ChannelRequestMode;
    selected_channel_request_mode?: ChannelRequestMode;
    fallback_applied: boolean;
    selected_channel_id?: string;
    upstream_host?: string;
    no_channel_reason?: string;
};

export type ChannelRequestModeHealthSummary = {
    configuredRequestModes: readonly ChannelRequestMode[];
    effectiveRequestModes: readonly ChannelRequestMode[];
    defaultRequestModePriority: readonly ChannelRequestMode[];
    modes: Array<{
        mode: ChannelRequestMode;
        configuredCredentialCount: number;
        healthyCredentialCount: number;
        configuredChannelCount: number;
        healthyChannelCount: number;
    }>;
    effectiveRequestModesByChannel: Array<{
        channelId: string;
        requestModes: readonly ChannelRequestMode[];
        requestModePriority: readonly ChannelRequestMode[];
    }>;
};

export const DEFAULT_CHANNEL_REQUEST_MODES: readonly ChannelRequestMode[] = ['images-non-stream'];
export const DEFAULT_CHANNEL_REQUEST_MODE_PRIORITY = SHARED_DEFAULT_CHANNEL_REQUEST_MODE_PRIORITY as readonly [
    'images-non-stream',
    'images-sse',
    'responses-non-stream',
    'responses-sse'
];
export const CHANNEL_REQUEST_MODE_ADMIN_CONTROL = SHARED_CHANNEL_REQUEST_MODE_ADMIN_CONTROL as {
    readonly source: 'admin_env_whitelist';
    readonly globalEnv: 'OPENAI_UPSTREAM_REQUEST_MODES';
    readonly channelEnvPattern: 'OPENAI_CHANNEL_N_REQUEST_MODES';
    readonly globalPriorityEnv: 'OPENAI_UPSTREAM_REQUEST_MODE_PRIORITY';
    readonly channelPriorityEnvPattern: 'OPENAI_CHANNEL_N_REQUEST_MODE_PRIORITY';
    readonly defaultPriority: readonly ChannelRequestMode[];
    readonly defaultPriorityPolicy: 'lowest_cost_first';
    readonly mutableAtRuntime: false;
    readonly finalGateCommand: string;
    readonly smokeGateCommands: Record<ChannelRequestMode, readonly string[]>;
};

const CHANNEL_REQUEST_MODE_SET = new Set<string>(CHANNEL_REQUEST_MODES);
const CHANNEL_REQUEST_MODE_ALIASES: Record<string, ChannelRequestMode> = {
    'images-api': 'images-non-stream',
    'images-api-non-stream': 'images-non-stream',
    'images-api-json': 'images-non-stream',
    'images-json': 'images-non-stream',
    'images-nonstream': 'images-non-stream',
    'images-api-sse': 'images-sse',
    'images-stream': 'images-sse',
    'images-api-stream': 'images-sse',
    responses: 'responses-non-stream',
    'responses-json': 'responses-non-stream',
    'responses-nonstream': 'responses-non-stream',
    'responses-image-generation': 'responses-non-stream',
    'responses-image-generation-non-stream': 'responses-non-stream',
    'responses-image-generation-sse': 'responses-sse',
    'responses-stream': 'responses-sse'
};

export function parseChannelRequestModes(
    value: string | undefined,
    fieldName: string
): ChannelRequestMode[] | undefined {
    if (!value?.trim()) return undefined;
    const modes: ChannelRequestMode[] = [];
    for (const rawPart of value.split(/[,\s]+/)) {
        const normalized = normalizeChannelRequestMode(rawPart);
        if (!normalized) continue;
        if (!modes.includes(normalized)) {
            modes.push(normalized);
        }
    }
    if (modes.length === 0) {
        throw new RequestValidationError(`${fieldName} 至少需要包含一个请求方式。`, 500);
    }
    return modes;
}

export function parseChannelRequestModePriority(
    value: string | undefined,
    fieldName: string
): ChannelRequestMode[] | undefined {
    return parseChannelRequestModes(value, fieldName);
}

export function getEffectiveChannelRequestModes(input: {
    requestModes?: readonly ChannelRequestMode[];
}): readonly ChannelRequestMode[] {
    return input.requestModes?.length ? input.requestModes : DEFAULT_CHANNEL_REQUEST_MODES;
}

export function orderChannelRequestModesByPriority(input: {
    requestModes: readonly ChannelRequestMode[];
    requestModePriority?: readonly ChannelRequestMode[];
}): ChannelRequestMode[] {
    const allowed = new Set(input.requestModes);
    const ordered: ChannelRequestMode[] = [];
    appendAllowedRequestModes(ordered, allowed, input.requestModePriority ?? []);
    appendAllowedRequestModes(ordered, allowed, DEFAULT_CHANNEL_REQUEST_MODE_PRIORITY);
    appendAllowedRequestModes(ordered, allowed, input.requestModes);
    return ordered;
}

function appendAllowedRequestModes(
    ordered: ChannelRequestMode[],
    allowed: ReadonlySet<ChannelRequestMode>,
    modes: readonly ChannelRequestMode[]
): void {
    for (const mode of modes) {
        if (allowed.has(mode) && !ordered.includes(mode)) ordered.push(mode);
    }
}

export function channelSupportsRequestMode(
    input: { requestModes?: readonly ChannelRequestMode[] },
    mode: ChannelRequestMode
): boolean {
    return getEffectiveChannelRequestModes(input).includes(mode);
}

export function resolveChannelRequestMode(input: {
    imageBackend: ChannelRequestModeBackend;
    streamEnabled: boolean;
}): ChannelRequestMode {
    if (input.imageBackend === 'responses-image-generation') {
        return input.streamEnabled ? 'responses-sse' : 'responses-non-stream';
    }
    return input.streamEnabled ? 'images-sse' : 'images-non-stream';
}

export function isStreamingChannelRequestMode(mode: ChannelRequestMode): boolean {
    return mode.endsWith('-sse');
}

function normalizeChannelRequestMode(value: string): ChannelRequestMode | undefined {
    const normalized = value.trim().toLowerCase().replace(/_/g, '-');
    if (!normalized) return undefined;
    if (CHANNEL_REQUEST_MODE_SET.has(normalized)) return normalized as ChannelRequestMode;
    const aliased = CHANNEL_REQUEST_MODE_ALIASES[normalized];
    if (aliased) return aliased;
    throw new RequestValidationError(`请求方式 ${value} 无效,必须是 ${CHANNEL_REQUEST_MODES.join(', ')} 之一。`, 500);
}