feat: add SSE passthrough stream action

This commit is contained in:
2026-07-20 00:40:14 +08:00
parent ca8935d73e
commit 31e673f777
4 changed files with 282 additions and 4 deletions
+51
View File
@@ -4,6 +4,7 @@ type PageKey = "overview" | "requests" | "profiles" | "rules" | "logs";
type Tone = "" | "success" | "error";
type ReasoningMatchMode = "formula_518n_minus_2" | "manual";
type AuthMode = "passthrough" | "fixed_bearer" | "manual_bearer" | "auth_json";
type StreamAction = "strict_502" | "disconnect" | "passthrough";
type GatewayConfig = {
profile_name?: string;
@@ -30,6 +31,7 @@ type GatewayConfig = {
upstream_fetch_retry_attempts?: number;
upstream_fetch_retry_backoff_ms?: number;
non_stream_status_code?: number;
stream_action?: StreamAction;
log_match?: boolean;
};
@@ -190,6 +192,7 @@ type ProfileFormModel = {
retryable_error_messages?: string[];
upstream_fetch_retry_attempts?: string;
upstream_fetch_retry_backoff_ms?: string;
stream_action?: StreamAction;
endpoints?: string[];
};
@@ -207,6 +210,7 @@ type Profile = {
model_remap?: string;
reasoning_match_mode?: ReasoningMatchMode;
reasoning_equals?: string;
stream_action?: StreamAction;
};
form?: ProfileFormModel;
};
@@ -321,6 +325,7 @@ type ProfileFormState = {
retryable_error_messages: string;
upstream_fetch_retry_attempts: string;
upstream_fetch_retry_backoff_ms: string;
stream_action: StreamAction;
endpoints: string;
};
@@ -346,6 +351,7 @@ type RuleFormState = {
upstream_fetch_retry_backoff_ms: string;
endpoints: string;
non_stream_status_code: string;
stream_action: StreamAction;
log_match: boolean;
};
@@ -454,6 +460,7 @@ const defaultProfileForm: ProfileFormState = {
retryable_error_messages: "Selected model is at capacity. Please try a different model.\nstream disconnected before completion: Concurrency limit exceeded for account, please retry later",
upstream_fetch_retry_attempts: "5",
upstream_fetch_retry_backoff_ms: "350",
stream_action: "strict_502",
endpoints: "/responses\n/chat/completions\n/v1/responses\n/v1/chat/completions",
};
@@ -694,6 +701,24 @@ function normalizeReasoningMode(value: unknown): ReasoningMatchMode {
return value === "manual" ? "manual" : "formula_518n_minus_2";
}
function normalizeStreamAction(value: unknown): StreamAction {
if (value === "disconnect" || value === "passthrough") {
return value;
}
return "strict_502";
}
function formatStreamAction(value: unknown) {
const action = normalizeStreamAction(value);
if (action === "passthrough") {
return "直通";
}
if (action === "disconnect") {
return "透传后断开";
}
return "严格检查";
}
function formatReasoningMode(mode: ReasoningMatchMode) {
return mode === "manual" ? "manual" : "518n-2";
}
@@ -782,6 +807,7 @@ function profileFormFromStatus(status: StatusPayload | null): ProfileFormState {
upstream_fetch_retry_backoff_ms: String(
config.upstream_fetch_retry_backoff_ms ?? defaultProfileForm.upstream_fetch_retry_backoff_ms,
),
stream_action: normalizeStreamAction(config.stream_action),
endpoints: Array.isArray(config.endpoints) ? config.endpoints.join("\n") : defaultProfileForm.endpoints,
};
}
@@ -814,6 +840,7 @@ function profileFormFromProfile(profile: Profile): ProfileFormState {
form.upstream_fetch_retry_attempts || defaultProfileForm.upstream_fetch_retry_attempts,
upstream_fetch_retry_backoff_ms:
form.upstream_fetch_retry_backoff_ms || defaultProfileForm.upstream_fetch_retry_backoff_ms,
stream_action: normalizeStreamAction(form.stream_action),
endpoints: Array.isArray(form.endpoints) ? form.endpoints.join("\n") : "",
};
}
@@ -866,6 +893,7 @@ function ruleFormFromStatus(status: StatusPayload | null): RuleFormState {
),
endpoints: Array.isArray(config.endpoints) ? config.endpoints.join("\n") : "",
non_stream_status_code: String(config.non_stream_status_code || 502),
stream_action: normalizeStreamAction(config.stream_action),
log_match: Boolean(config.log_match),
};
}
@@ -1168,6 +1196,7 @@ export default function App() {
upstream_fetch_retry_backoff_ms: Number.parseInt(ruleForm.upstream_fetch_retry_backoff_ms, 10),
endpoints: splitLines(ruleForm.endpoints),
non_stream_status_code: Number.parseInt(ruleForm.non_stream_status_code, 10),
stream_action: ruleForm.stream_action,
log_match: ruleForm.log_match,
}),
});
@@ -1276,6 +1305,7 @@ export default function App() {
retryable_error_messages: splitLines(profileForm.retryable_error_messages),
upstream_fetch_retry_attempts: Number.parseInt(profileForm.upstream_fetch_retry_attempts, 10),
upstream_fetch_retry_backoff_ms: Number.parseInt(profileForm.upstream_fetch_retry_backoff_ms, 10),
stream_action: profileForm.stream_action,
endpoints: splitLines(profileForm.endpoints),
}),
});
@@ -2202,6 +2232,7 @@ export default function App() {
label="Rule Mode"
value={formatReasoningMode(normalizeReasoningMode(profile.summary?.reasoning_match_mode))}
/>
<MiniStat label="SSE" value={formatStreamAction(profile.summary?.stream_action)} />
</div>
</article>
))
@@ -2388,6 +2419,16 @@ export default function App() {
<input type="number" min={0} step={50} value={profileForm.upstream_fetch_retry_backoff_ms} onChange={(event) => setProfileForm({ ...profileForm, upstream_fetch_retry_backoff_ms: event.target.value })} />
</Field>
</div>
<Field label="stream_action" hint="passthrough 对 stream:true 请求关闭 reasoning retry;若上游返回 text/event-stream,会把原始 chunk 直接转发,不解析、缓存或规范化。">
<select
value={profileForm.stream_action}
onChange={(event) => setProfileForm({ ...profileForm, stream_action: normalizeStreamAction(event.target.value) })}
>
<option value="strict_502">strict_502</option>
<option value="disconnect">disconnect</option>
<option value="passthrough">passthroughSSE </option>
</select>
</Field>
<Field label="endpoints">
<textarea value={profileForm.endpoints} onChange={(event) => setProfileForm({ ...profileForm, endpoints: event.target.value })} />
</Field>
@@ -2717,6 +2758,16 @@ export default function App() {
</label>
</div>
<Field label="stream_action" hint="passthrough 会立即使当前实例的 SSE 原样直通;若要让重启或切换 profile 后仍保持,请在 Profiles 页保存该字段。">
<select
value={ruleForm.stream_action}
onChange={(event) => setRuleForm({ ...ruleForm, stream_action: normalizeStreamAction(event.target.value) })}
>
<option value="strict_502">strict_502</option>
<option value="disconnect">disconnect</option>
<option value="passthrough">passthroughSSE </option>
</select>
</Field>
<div className="toolbar">
<button className="primary" type="submit">