mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chore(release): finalize v3.7.8 stabilization, fix a2a status codes and open issues
This commit is contained in:
@@ -82,7 +82,7 @@ For each bug issue, perform the full analysis:
|
||||
2. **Read ALL comments** — including bot triage comments (Kilo, etc.) and owner/community responses. Pay attention to:
|
||||
- Whether someone already responded with a fix
|
||||
- Whether a community member confirmed the issue is resolved
|
||||
- Whether the issue was marked as duplicate by a bot
|
||||
- Whether the issue was marked as duplicate by a bot. **WARNING: DO NOT blindly trust bot duplicate labels (e.g., kilo-duplicate). Bots make mistakes. You MUST read the full conversation and do your own independent analysis to determine if it is truly a duplicate or a distinct bug.**
|
||||
3. **Identify the claimed error** — extract the exact error message, status code, and provider/model involved
|
||||
|
||||
#### 5b. Check Information Sufficiency
|
||||
@@ -98,14 +98,14 @@ Verify the issue contains enough to act on:
|
||||
|
||||
For each bug, classify into one of 5 actions:
|
||||
|
||||
| Disposition | When to Apply | Action |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
|
||||
| **✅ CLOSE — Duplicate** | Bot flagged >85% similarity + user provides no new info | Close referencing the original issue |
|
||||
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
|
||||
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details | Comment asking for specifics per `/issue-triage` |
|
||||
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
|
||||
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
|
||||
| Disposition | When to Apply | Action |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **✅ CLOSE — Already Fixed** | Owner responded with fix + no user follow-up, OR community confirmed fix | Close with comment citing which version fixed it |
|
||||
| **✅ CLOSE — Duplicate** | You have independently verified the issue is a duplicate (do NOT rely solely on bot flags) + user provides no new info | Close referencing the original issue |
|
||||
| **✅ CLOSE — Stale** | We requested logs/info > 7 days ago with no reply | Close thanking the user, invite to reopen if needed |
|
||||
| **📝 RESPOND — Needs Info** | Issue is real but missing critical reproduction details | Comment asking for specifics per `/issue-triage` |
|
||||
| **📝 RESPOND — User Config** | Error is caused by unsupported env (Node version, wrong model path, missing API enablement) | Comment explaining the user-side fix |
|
||||
| **🔧 FIX — Code Change** | Root cause is confirmed in the codebase | Research, propose solution in report, wait for approval |
|
||||
|
||||
#### 5d. For "FIX — Code Change" Issues
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
- **fix(ui):** hide combo compression controls when the global setting is disabled (#1840)
|
||||
- **fix(db):** tolerate missing request_detail_logs table for legacy deployments (#1848)
|
||||
- **fix(core):** remove unneeded \`store\` payload parameter for providers lacking support (closes #1841)
|
||||
- **fix(core):** ensure safeOutboundFetch and A2A routers return 503 Service Unavailable when security guardrails are triggered
|
||||
- **fix(usage):** correct Unix seconds vs milliseconds parsing logic for Kiro AI quota reset (closes #1849)
|
||||
- **fix(ui):** apply robust NaN handling, ensure 24h consistency, and fix missing hour slots in Compression Analytics (closes #1844)
|
||||
- **fix(ui):** implement short number formatting for token consumption metrics on cache pages to prevent overflow (closes #1842)
|
||||
|
||||
### 🛠️ Maintenance
|
||||
|
||||
|
||||
@@ -2219,6 +2219,11 @@ export async function handleChatCore({
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI's `store` parameter is not supported by most compatible providers and breaks them
|
||||
if (provider !== "openai" && "store" in translatedBody) {
|
||||
delete translatedBody.store;
|
||||
}
|
||||
|
||||
// Provider-specific max_tokens caps (#711)
|
||||
// Some providers reject requests when max_tokens exceeds their API limit.
|
||||
// Cap before sending to avoid upstream HTTP 400 errors.
|
||||
|
||||
@@ -652,7 +652,7 @@ function parseResetTime(resetValue) {
|
||||
if (resetValue instanceof Date) {
|
||||
date = resetValue;
|
||||
} else if (typeof resetValue === "number") {
|
||||
date = new Date(resetValue);
|
||||
date = new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue);
|
||||
} else if (typeof resetValue === "string") {
|
||||
date = new Date(resetValue);
|
||||
} else {
|
||||
|
||||
@@ -240,7 +240,7 @@ export default function CompressionAnalyticsTab() {
|
||||
<div className="card p-5">
|
||||
<h3 className="font-semibold text-text mb-4 flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">show_chart</span>
|
||||
Last 24 Hours
|
||||
Last 24 Hours (Activity)
|
||||
</h3>
|
||||
<div className="flex items-end gap-2 h-48">
|
||||
{stats.last24h.map((entry, idx) => {
|
||||
@@ -258,7 +258,7 @@ export default function CompressionAnalyticsTab() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted rotate-45 origin-left">
|
||||
{entry.hour.substring(0, 2)}
|
||||
{entry.hour.substring(11, 13)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,13 @@ interface CacheMetrics {
|
||||
const REFRESH_INTERVAL_MS = 10_000;
|
||||
const REFRESH_INTERVAL_SECONDS = REFRESH_INTERVAL_MS / 1000;
|
||||
|
||||
function formatNumberCompact(num: number): string {
|
||||
return Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(num);
|
||||
}
|
||||
|
||||
export default function CacheStatsCard() {
|
||||
const [metrics, setMetrics] = useState<CacheMetrics | null>(null);
|
||||
const [resetting, setResetting] = useState(false);
|
||||
@@ -117,19 +124,19 @@ export default function CacheStatsCard() {
|
||||
<div>
|
||||
<p className="text-text-muted">{t("inputTokens")}</p>
|
||||
<p className="font-mono text-lg text-text-main">
|
||||
{metrics.totalInputTokens.toLocaleString()}
|
||||
{formatNumberCompact(metrics.totalInputTokens)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-muted">{t("cachedTokensRead")}</p>
|
||||
<p className="font-mono text-lg text-green-400">
|
||||
{metrics.totalCachedTokens.toLocaleString()}
|
||||
{formatNumberCompact(metrics.totalCachedTokens)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-muted">{t("cacheCreationWrite")}</p>
|
||||
<p className="font-mono text-lg text-blue-400">
|
||||
{metrics.totalCacheCreationTokens.toLocaleString()}
|
||||
{formatNumberCompact(metrics.totalCacheCreationTokens)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +164,7 @@ export default function CacheStatsCard() {
|
||||
<div>
|
||||
<p className="text-text-muted">{t("tokensSaved")}</p>
|
||||
<p className="font-mono text-lg text-green-400">
|
||||
{metrics.tokensSaved.toLocaleString()}
|
||||
{formatNumberCompact(metrics.tokensSaved)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -189,13 +196,13 @@ export default function CacheStatsCard() {
|
||||
</div>
|
||||
<div className="flex items-center gap-4 font-mono">
|
||||
<span className="text-text-muted" title={t("inputTokens")}>
|
||||
{t("inputShort")}: {stats.inputTokens.toLocaleString()}
|
||||
{t("inputShort")}: {formatNumberCompact(stats.inputTokens)}
|
||||
</span>
|
||||
<span className="text-green-400" title={t("cachedTokensRead")}>
|
||||
{t("cachedShort")}: {stats.cachedTokens.toLocaleString()}
|
||||
{t("cachedShort")}: {formatNumberCompact(stats.cachedTokens)}
|
||||
</span>
|
||||
<span className="text-blue-400" title={t("cacheCreationWrite")}>
|
||||
{t("writeShort")}: {stats.cacheCreationTokens.toLocaleString()}
|
||||
{t("writeShort")}: {formatNumberCompact(stats.cacheCreationTokens)}
|
||||
</span>
|
||||
<span className="text-green-400 w-12 text-right">
|
||||
{providerCacheRate.toFixed(0)}%
|
||||
|
||||
@@ -79,7 +79,7 @@ function authenticate(req: NextRequest): boolean {
|
||||
function jsonRpcError(id: string | number | null, code: number, message: string, data?: unknown) {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: "2.0", id, error: { code, message, data } },
|
||||
{ status: code === -32600 ? 400 : code === -32601 ? 404 : code === -32603 ? 500 : 200 }
|
||||
{ status: code === -32600 ? 503 : code === -32601 ? 404 : code === -32603 ? 500 : 200 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ async function rejectIfA2ADisabled(id: string | number | null) {
|
||||
// ============ Route Handler ============
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
console.log("==> HIT A2A ROUTER:", req.url);
|
||||
// Auth check
|
||||
if (!authenticate(req)) {
|
||||
return jsonRpcError(null, -32600, "Unauthorized: missing or invalid API key");
|
||||
@@ -180,6 +181,7 @@ export async function POST(req: NextRequest) {
|
||||
metadata: result.metadata,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("A2A ERROR TRACE:", err);
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
tm.updateTask(task.id, "failed", [{ type: "error", content: msg }], msg);
|
||||
return jsonRpcError(id, -32603, `Skill execution failed: ${msg}`);
|
||||
|
||||
@@ -70,7 +70,7 @@ export function getCompressionAnalyticsSummary(since?: string): CompressionAnaly
|
||||
FROM compression_analytics ${whereClause}
|
||||
`
|
||||
)
|
||||
.get(...params) as ScalarRow;
|
||||
.get(...params) as ScalarRow | undefined;
|
||||
|
||||
const modeRows = db
|
||||
.prepare(
|
||||
@@ -104,6 +104,14 @@ export function getCompressionAnalyticsSummary(since?: string): CompressionAnaly
|
||||
byProvider[key] = { count: r.cnt, tokensSaved: r.saved };
|
||||
}
|
||||
|
||||
const last24hMap = new Map<string, { hour: string; count: number; tokensSaved: number }>();
|
||||
const now = new Date();
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const d = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||
const hourStr = d.toISOString().substring(0, 14) + "00:00Z";
|
||||
last24hMap.set(hourStr, { hour: hourStr, count: 0, tokensSaved: 0 });
|
||||
}
|
||||
|
||||
const hourRows = db
|
||||
.prepare(
|
||||
`
|
||||
@@ -114,19 +122,25 @@ export function getCompressionAnalyticsSummary(since?: string): CompressionAnaly
|
||||
GROUP BY hour ORDER BY hour ASC
|
||||
`
|
||||
)
|
||||
.all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()) as Array<{
|
||||
.all(new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString()) as Array<{
|
||||
hour: string;
|
||||
cnt: number;
|
||||
saved: number;
|
||||
}>;
|
||||
|
||||
const last24h = hourRows.map((r) => ({ hour: r.hour, count: r.cnt, tokensSaved: r.saved }));
|
||||
for (const r of hourRows) {
|
||||
if (last24hMap.has(r.hour)) {
|
||||
last24hMap.set(r.hour, { hour: r.hour, count: r.cnt, tokensSaved: r.saved });
|
||||
}
|
||||
}
|
||||
|
||||
const last24h = Array.from(last24hMap.values());
|
||||
|
||||
return {
|
||||
totalRequests: scalar.total,
|
||||
totalTokensSaved: scalar.totalSaved,
|
||||
avgSavingsPct: Math.round(scalar.avgPct),
|
||||
avgDurationMs: Math.round(scalar.avgDur),
|
||||
totalRequests: scalar?.total ?? 0,
|
||||
totalTokensSaved: scalar?.totalSaved ?? 0,
|
||||
avgSavingsPct: Math.round(scalar?.avgPct ?? 0),
|
||||
avgDurationMs: Math.round(scalar?.avgDur ?? 0),
|
||||
byMode,
|
||||
byProvider,
|
||||
last24h,
|
||||
|
||||
@@ -349,7 +349,7 @@ export function getSafeOutboundFetchErrorStatus(error: unknown) {
|
||||
error.code === "URL_GUARD_BLOCKED" ||
|
||||
error.code === "REDIRECT_BLOCKED"
|
||||
) {
|
||||
return 400;
|
||||
return 503;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -266,7 +266,10 @@ describe("E2E: Security", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
expect([200, 400]).toContain(res.status);
|
||||
if (res.status !== 503) {
|
||||
console.log("SEC-1 FAILED STATUS:", res.status, "BODY:", await res.text());
|
||||
}
|
||||
expect(res.status).toBe(503);
|
||||
});
|
||||
|
||||
itCase("should handle invalid API keys according to server configuration", async () => {
|
||||
@@ -288,7 +291,10 @@ describe("E2E: Security", () => {
|
||||
return;
|
||||
}
|
||||
|
||||
expect([200, 400]).toContain(res.status);
|
||||
if (res.status !== 503) {
|
||||
console.log("SEC-2 FAILED STATUS:", res.status, "BODY:", await res.text());
|
||||
}
|
||||
expect(res.status).toBe(503);
|
||||
});
|
||||
|
||||
itCase("should not expose internal errors in API responses", async () => {
|
||||
|
||||
Reference in New Issue
Block a user