feat(dashboard): per-key model restrictions, pin header routing, combo side-panel picker

- Endpoint: per-API-key model allowlist (schema v3) enforced on chat (403)
  and /v1/models; Full-access toggle + multi-select picker in Keys UI.
- Providers: honor x-connection-id in /v1/chat/completions — pinned requests
  no longer rotate to another account on failure.
- Providers: strategy saves merge into stored enabled:false override;
  Test All groups match grid sections; 1-by-1 skips disabled connections.
- Dashboard: provider-card toggle syncs from server on failure; grid toggles
  always visible; connection rows get clear-✕ for stale error banners.
- Combo editor: on desktop (xl+) the Add-Model picker opens as a floating
  side panel beside the untouched combo popup instead of stacking on top;
  mobile keeps the full-screen overlay.
- Long API-key overflow fixed in key rows + provider model sections.
This commit is contained in:
2026-08-27 09:26:17 +07:00
parent 1d56e2dbc5
commit f0adfb205a
42 changed files with 1028 additions and 339 deletions

View File

@@ -7,7 +7,8 @@ import { createErrorResult } from "../../utils/error.js";
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
import { parseSSEToOpenAIResponse } from "./sseToJsonHandler.js";
import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats, formatDoneLine } from "./requestDetail.js";
import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
import { saveRequestDetail } from "@/lib/usageDb.js";
import { matchStreamErrorPatterns } from "../../utils/streamErrorPatterns.js";
import { decloakToolNames } from "../../utils/claudeCloaking.js";
import { ROLE, RESPONSES_ITEM } from "../../translator/schema/index.js";
@@ -281,7 +282,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
/**
* Handle non-streaming response from provider.
*/
export async function handleNonStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, customToolNames, trackDone, appendLog, pxpipe, reqTag, log }) {
export async function handleNonStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, customToolNames, trackDone, appendLog, pxpipe, reqTag, log, streamErrorPatterns }) {
trackDone();
const contentType = providerResponse.headers.get("content-type") || "";
let responseBody;
@@ -316,6 +317,21 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
// Decloak tool_use names once on raw Claude body, before any translation (INPUT side)
responseBody = decloakToolNames(responseBody, toolNameMap);
// Config-driven in-stream error detection: the HTTP call succeeded but the
// assembled content signals an upstream failure — treat it as an error so
// account/combo fallback and FAILED logging kick in (AGENTS.md hook #3).
const matchedPattern = matchStreamErrorPatterns(
streamErrorPatterns?.[provider],
responseBody?.choices?.[0]?.message?.content || responseBody?.content || "",
);
if (matchedPattern) {
appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` });
if (log?.errorLine) {
log.errorLine(reqTag, "✗", `ERROR 502 · ${provider}/${model} · ${Date.now() - requestStartTime}ms (in-stream)\n Stream error pattern matched: ${matchedPattern}`);
}
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, `Stream error pattern matched: ${matchedPattern}`);
}
const usage = extractUsageFromResponse(responseBody);
appendLog({ tokens: usage, status: "200 OK" });
saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true });