Fix
This commit is contained in:
@@ -43,47 +43,54 @@ export async function handleComboChat({ body, models, handleSingleModel, log })
|
|||||||
const modelStr = models[i];
|
const modelStr = models[i];
|
||||||
log.info("COMBO", `Trying model ${i + 1}/${models.length}: ${modelStr}`);
|
log.info("COMBO", `Trying model ${i + 1}/${models.length}: ${modelStr}`);
|
||||||
|
|
||||||
const result = await handleSingleModel(body, modelStr);
|
|
||||||
|
|
||||||
// Success (2xx) - return response
|
|
||||||
if (result.ok) {
|
|
||||||
log.info("COMBO", `Model ${modelStr} succeeded`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract error info from response
|
|
||||||
let errorText = result.statusText || "";
|
|
||||||
let retryAfter = null;
|
|
||||||
try {
|
try {
|
||||||
const errorBody = await result.clone().json();
|
const result = await handleSingleModel(body, modelStr);
|
||||||
errorText = errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
|
|
||||||
retryAfter = errorBody?.retryAfter || null;
|
// Success (2xx) - return response
|
||||||
} catch {
|
if (result.ok) {
|
||||||
// Ignore JSON parse errors
|
log.info("COMBO", `Model ${modelStr} succeeded`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract error info from response
|
||||||
|
let errorText = result.statusText || "";
|
||||||
|
let retryAfter = null;
|
||||||
|
try {
|
||||||
|
const errorBody = await result.clone().json();
|
||||||
|
errorText = errorBody?.error?.message || errorBody?.error || errorBody?.message || errorText;
|
||||||
|
retryAfter = errorBody?.retryAfter || null;
|
||||||
|
} catch {
|
||||||
|
// Ignore JSON parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track earliest retryAfter across all combo models
|
||||||
|
if (retryAfter && (!earliestRetryAfter || new Date(retryAfter) < new Date(earliestRetryAfter))) {
|
||||||
|
earliestRetryAfter = retryAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize error text to string (Worker-safe)
|
||||||
|
if (typeof errorText !== "string") {
|
||||||
|
try { errorText = JSON.stringify(errorText); } catch { errorText = String(errorText); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if should fallback to next model
|
||||||
|
const { shouldFallback } = checkFallbackError(result.status, errorText);
|
||||||
|
|
||||||
|
if (!shouldFallback) {
|
||||||
|
log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to next model
|
||||||
|
lastError = errorText || String(result.status);
|
||||||
|
if (!lastStatus) lastStatus = result.status;
|
||||||
|
log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status });
|
||||||
|
} catch (error) {
|
||||||
|
// Catch unexpected exceptions to ensure fallback continues
|
||||||
|
lastError = error.message || String(error);
|
||||||
|
if (!lastStatus) lastStatus = 500;
|
||||||
|
log.warn("COMBO", `Model ${modelStr} threw error, trying next`, { error: lastError });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track earliest retryAfter across all combo models
|
|
||||||
if (retryAfter && (!earliestRetryAfter || new Date(retryAfter) < new Date(earliestRetryAfter))) {
|
|
||||||
earliestRetryAfter = retryAfter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize error text to string (Worker-safe)
|
|
||||||
if (typeof errorText !== "string") {
|
|
||||||
try { errorText = JSON.stringify(errorText); } catch { errorText = String(errorText); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if should fallback to next model
|
|
||||||
const { shouldFallback } = checkFallbackError(result.status, errorText);
|
|
||||||
|
|
||||||
if (!shouldFallback) {
|
|
||||||
log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to next model
|
|
||||||
lastError = errorText || String(result.status);
|
|
||||||
if (!lastStatus) lastStatus = result.status;
|
|
||||||
log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All models failed
|
// All models failed
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export default function OverviewCards({ stats }) {
|
|||||||
<span className="text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
|
<span className="text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="px-4 py-3 flex flex-col gap-1">
|
<Card className="px-4 py-3 flex flex-col gap-1">
|
||||||
<span className="text-text-muted text-sm uppercase font-semibold">Total Cost</span>
|
<span className="text-text-muted text-sm uppercase font-semibold">Est. Cost</span>
|
||||||
<span className="text-2xl font-bold text-warning">{fmtCost(stats.totalCost)}</span>
|
<span className="text-2xl font-bold text-warning">~{fmtCost(stats.totalCost)}</span>
|
||||||
|
<span className="text-[10px] text-text-muted">Estimated, not actual billing</span>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, useCallback, useRef, useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Handle,
|
Handle,
|
||||||
Position,
|
Position,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||||
@@ -206,11 +208,36 @@ export default function ProviderTopology({ providers = [], activeRequests = [],
|
|||||||
[errorProvider]
|
[errorProvider]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
// Stable key for providers list — only changes when provider set changes
|
||||||
|
const providersKey = useMemo(
|
||||||
|
() => providers.map((p) => p.provider).sort().join(","),
|
||||||
|
[providers]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { nodes: layoutNodes, edges: layoutEdges } = useMemo(
|
||||||
() => buildLayout(providers, activeSet, lastSet, errorSet),
|
() => buildLayout(providers, activeSet, lastSet, errorSet),
|
||||||
[providers, activeSet, lastSet, errorSet]
|
[providers, activeSet, lastSet, errorSet]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(layoutNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutEdges);
|
||||||
|
const rfInstance = useRef(null);
|
||||||
|
|
||||||
|
// Sync nodes/edges when data changes
|
||||||
|
useEffect(() => {
|
||||||
|
setNodes(layoutNodes);
|
||||||
|
setEdges(layoutEdges);
|
||||||
|
// Re-fit view after update
|
||||||
|
if (rfInstance.current) {
|
||||||
|
setTimeout(() => rfInstance.current.fitView({ padding: 0.3 }), 50);
|
||||||
|
}
|
||||||
|
}, [layoutNodes, layoutEdges, setNodes, setEdges]);
|
||||||
|
|
||||||
|
const onInit = useCallback((instance) => {
|
||||||
|
rfInstance.current = instance;
|
||||||
|
setTimeout(() => instance.fitView({ padding: 0.3 }), 50);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-lg border border-border bg-bg-subtle/30" style={{ height: 480 }}>
|
<div className="w-full rounded-lg border border-border bg-bg-subtle/30" style={{ height: 480 }}>
|
||||||
{providers.length === 0 ? (
|
{providers.length === 0 ? (
|
||||||
@@ -219,12 +246,15 @@ export default function ProviderTopology({ providers = [], activeRequests = [],
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={initialNodes}
|
key={providersKey}
|
||||||
edges={initialEdges}
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={{ padding: 0.3 }}
|
fitViewOptions={{ padding: 0.3 }}
|
||||||
onInit={(instance) => setTimeout(() => instance.fitView({ padding: 0.3 }), 50)}
|
onInit={onInit}
|
||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
panOnDrag={false}
|
panOnDrag={false}
|
||||||
zoomOnScroll={false}
|
zoomOnScroll={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user