feat(proxy): enhance upstream endpoint handling and error summarization

- Added new utility functions for building minimal JSON headers and normalizing responses for compatibility.
- Implemented detailed error handling for unsupported media types and endpoint downgrade scenarios.
- Introduced a new endpoint flow mechanism to manage retries and downgrades between different upstream endpoints.
- Enhanced tests for endpoint flow and error summarization to cover various edge cases and scenarios.
- Updated CSS styles for accounts table to improve layout and responsiveness.
- Refactored ModelTester component to handle incremental text updates more effectively.
This commit is contained in:
cita
2026-03-04 20:59:48 +08:00
parent 82e9f3b601
commit 9fbf434bd6
14 changed files with 1999 additions and 224 deletions
+30 -4
View File
@@ -1308,11 +1308,24 @@ body {
font-size: 12px;
}
.accounts-table {
width: 100%;
table-layout: fixed;
}
.accounts-table th,
.accounts-table td {
white-space: normal;
word-break: break-word;
}
.accounts-actions-col {
min-width: 220px;
width: 24%;
min-width: 0;
}
.accounts-actions-cell {
min-width: 0;
white-space: normal !important;
}
@@ -1320,7 +1333,12 @@ body {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 2px;
gap: 4px 6px;
width: 100%;
}
.accounts-row-actions .btn-link {
white-space: nowrap;
}
.sites-actions-col {
@@ -3125,7 +3143,11 @@ textarea {
}
.accounts-actions-col {
min-width: 180px;
width: 28%;
}
.accounts-actions-cell {
min-width: 0;
}
.accounts-table th,
@@ -3190,7 +3212,11 @@ textarea {
}
.accounts-actions-col {
min-width: 150px;
width: 30%;
}
.accounts-actions-cell {
min-width: 0;
}
.accounts-table th:nth-child(5),
+1 -1
View File
@@ -765,7 +765,7 @@ export default function Accounts() {
)}
{/* Accounts Table */}
<div className="card" style={{ overflowX: 'auto' }}>
<div className="card">
{accounts.length > 0 ? (
<table className="data-table accounts-table">
<thead>
+44 -27
View File
@@ -296,20 +296,51 @@ const applyAssistantDelta = (
status: MESSAGE_STATUS.INCOMPLETE,
};
const toIncrementalText = (existingText: string, incomingText?: string): string => {
if (!incomingText) return '';
if (!existingText) return incomingText;
if (incomingText === existingText) return '';
if (incomingText.startsWith(existingText)) {
return incomingText.slice(existingText.length);
}
if (existingText.endsWith(incomingText)) return '';
const maxOverlap = Math.min(existingText.length, incomingText.length);
const MIN_OVERLAP = 8;
for (let overlap = maxOverlap; overlap >= MIN_OVERLAP; overlap -= 1) {
if (existingText.slice(-overlap) === incomingText.slice(0, overlap)) {
return incomingText.slice(overlap);
}
}
return incomingText;
};
if (delta.reasoningDelta) {
next = {
...next,
reasoningContent: (next.reasoningContent || '') + delta.reasoningDelta,
isThinkingComplete: false,
};
const existingReasoning = next.reasoningContent || '';
const reasoningAppend = toIncrementalText(existingReasoning, delta.reasoningDelta);
if (reasoningAppend) {
next = {
...next,
reasoningContent: existingReasoning + reasoningAppend,
isThinkingComplete: false,
};
}
}
if (delta.contentDelta) {
const existingContent = next.content || '';
const contentAppend = toIncrementalText(existingContent, delta.contentDelta);
if (!contentAppend) {
return replaceMessageAt(messages, targetIndex, next);
}
const hasReasoning = Boolean(next.reasoningContent);
const shouldAutoCollapse = hasReasoning && !next.hasAutoCollapsed;
next = {
...next,
content: (next.content || '') + delta.contentDelta,
content: existingContent + contentAppend,
isReasoningExpanded: shouldAutoCollapse ? false : next.isReasoningExpanded,
hasAutoCollapsed: shouldAutoCollapse || next.hasAutoCollapsed,
};
@@ -383,21 +414,15 @@ const parseAnyStreamDelta = (eventPayload: any): {
}
if (typeof eventPayload.type === 'string') {
// Responses stream emits a full-text summary again in several "done" events
// (output_text.done/content_part.done/output_item.done/response.completed).
// Treat those as structural events only; otherwise UI appends duplicate text.
if (eventPayload.type === 'response.output_item.added' || eventPayload.type === 'response.output_item.done') {
const parsed = extractResponsesContent(eventPayload.item || eventPayload.output_item || eventPayload.response || eventPayload);
return {
contentDelta: parsed.content || undefined,
reasoningDelta: parsed.reasoningContent || undefined,
};
return {};
}
if (eventPayload.type === 'response.content_part.added' || eventPayload.type === 'response.content_part.done') {
const part = eventPayload.part || eventPayload.content_part || eventPayload;
const parsed = extractResponsesContent(part);
return {
contentDelta: parsed.content || undefined,
reasoningDelta: parsed.reasoningContent || undefined,
};
return {};
}
if (eventPayload.type === 'response.content_part.delta') {
@@ -434,18 +459,10 @@ const parseAnyStreamDelta = (eventPayload: any): {
return { reasoningDelta: text || undefined };
}
if (eventPayload.type === 'response.output_text.done') {
const text = typeof eventPayload.text === 'string' ? eventPayload.text : '';
return { contentDelta: text || undefined };
}
if (eventPayload.type === 'response.output_text.done') return {};
if (eventPayload.type === 'response.completed' || eventPayload.type === 'response.failed') {
const parsed = extractResponsesContent(eventPayload.response || eventPayload);
return {
contentDelta: parsed.content || undefined,
reasoningDelta: parsed.reasoningContent || undefined,
done: true,
};
return { done: true };
}
if (eventPayload.type === 'content_block_delta') {