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:
@@ -69,25 +69,22 @@ function writeJsonFile(sessionPath, filename, data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Mask sensitive data in headers (DISABLED - keep full token for testing)
|
||||
// Mask sensitive headers before writing to disk. ENABLE_REQUEST_LOGS dumps full
|
||||
// request/response bodies; credentials inside must never land in plaintext.
|
||||
function maskSensitiveHeaders(headers) {
|
||||
if (!headers) return {};
|
||||
return { ...headers };
|
||||
|
||||
// Old masking code (disabled):
|
||||
// const masked = { ...headers };
|
||||
// const sensitiveKeys = ["authorization", "x-api-key", "cookie", "token"];
|
||||
//
|
||||
// for (const key of Object.keys(masked)) {
|
||||
// const lowerKey = key.toLowerCase();
|
||||
// if (sensitiveKeys.some(sk => lowerKey.includes(sk))) {
|
||||
// const value = masked[key];
|
||||
// if (value && value.length > 20) {
|
||||
// masked[key] = value.slice(0, 10) + "..." + value.slice(-5);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return masked;
|
||||
const masked = { ...headers };
|
||||
const sensitiveKeys = ["authorization", "x-api-key", "cookie", "token", "api-key"];
|
||||
for (const key of Object.keys(masked)) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (sensitiveKeys.some(sk => lowerKey.includes(sk))) {
|
||||
const value = masked[key];
|
||||
if (typeof value === "string" && value.length > 20) {
|
||||
masked[key] = value.slice(0, 10) + "..." + value.slice(-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
|
||||
// No-op logger when logging is disabled
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { translateResponse, initState } from "../translator/index.js";
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js";
|
||||
import { trackPendingRequest } from "@/lib/usageDb.js";
|
||||
import { extractUsage, mergeUsage, hasValidUsage, estimateUsage, logUsage, addBufferToUsage, filterUsageForFormat, COLORS } from "./usageTracking.js";
|
||||
import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./streamHelpers.js";
|
||||
import { getOpenAIResponsesEventName, isOpenAIResponsesTerminalEvent, formatIncompleteOpenAIResponsesStreamFailure } from "./responsesStreamHelpers.js";
|
||||
@@ -361,10 +361,7 @@ export function createSSEStream(options = {}) {
|
||||
|
||||
if (hasValidUsage(usage)) {
|
||||
logUsage(provider, usage, model, connectionId, apiKey);
|
||||
} else {
|
||||
appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { });
|
||||
}
|
||||
|
||||
// IMPORTANT: In passthrough mode we still must terminate the SSE stream.
|
||||
// Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel:
|
||||
// data: [DONE]\n\n
|
||||
@@ -447,13 +444,9 @@ export function createSSEStream(options = {}) {
|
||||
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
|
||||
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
|
||||
}
|
||||
|
||||
if (hasValidUsage(state?.usage)) {
|
||||
logUsage(state.provider || targetFormat, state.usage, model, connectionId, apiKey);
|
||||
} else {
|
||||
appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { });
|
||||
}
|
||||
|
||||
if (onStreamComplete) {
|
||||
onStreamComplete({
|
||||
content: accumulatedContent,
|
||||
|
||||
Reference in New Issue
Block a user