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:
+30
-4
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user