Fix codex cache session id

This commit is contained in:
decolua
2026-04-13 15:52:01 +07:00
parent b7d85ae463
commit 3b1a608e8d
14 changed files with 1362 additions and 1192 deletions

View File

@@ -1,3 +1,18 @@
# v0.3.86 (2026-04-13)
## Features
- Add provider models and thinking configurations for enhanced chat handling
- Add Vercel relay support to proxy functionality
- Add Vercel deploy endpoint for proxy pools management
## Improvements
- Enhance proxy functionality with new relay capabilities
- Streamline GitHub Actions Docker publish workflow
- Update Docker configuration and package management
## Fixes
- Remove obsolete 9remote installation/management APIs
# v0.3.83 (2026-04-08)
## Fixes

View File

@@ -3,11 +3,16 @@ import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/providers.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
import { getConsistentMachineId } from "../../src/shared/utils/machineId.js";
// In-memory map: hash(first assistant content) → { sessionId, lastUsed }
// In-memory map: hash(machineId + first assistant content) → { sessionId, lastUsed }
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
const assistantSessionMap = new Map();
// Cache machine ID at module level (resolved once)
let cachedMachineId = null;
getConsistentMachineId().then(id => { cachedMachineId = id; });
function hashContent(text) {
return createHash("sha256").update(text).digest("hex").slice(0, 16);
}
@@ -26,17 +31,22 @@ function extractItemText(item) {
return "";
}
// Resolve session_id from first assistant message in conversation history
function resolveConversationSessionId(input) {
if (!Array.isArray(input) || input.length === 0) return generateSessionId();
// Resolve session_id from first assistant message + machineId to avoid cross-user collision
function resolveConversationSessionId(input, machineId) {
const machineSessionId = machineId ? `sess_${hashContent(machineId)}` : generateSessionId();
if (!Array.isArray(input) || input.length === 0) return machineSessionId;
const firstAssistant = input.find(item => item.role === "assistant");
if (!firstAssistant) return generateSessionId(); // Turn 1: no assistant yet
// Find first assistant message that has actual text content
let text = "";
for (const item of input) {
if (item.role === "assistant") {
text = extractItemText(item);
if (text) break;
}
}
if (!text) return machineSessionId;
const text = extractItemText(firstAssistant);
if (!text) return generateSessionId();
const hash = hashContent(text);
const hash = hashContent((machineId || "") + text);
const entry = assistantSessionMap.get(hash);
if (entry) {
entry.lastUsed = Date.now();
@@ -81,8 +91,8 @@ export class CodexExecutor extends BaseExecutor {
* Transform request before sending - inject default instructions if missing
*/
transformRequest(model, body, stream, credentials) {
// Resolve conversation-stable session_id from input history
this._currentSessionId = resolveConversationSessionId(body.input);
// Resolve conversation-stable session_id from input history + machineId
this._currentSessionId = resolveConversationSessionId(body.input, cachedMachineId);
// Convert string input to array format (Codex API requires input as array)
const normalized = normalizeResponsesInput(body.input);
if (normalized) body.input = normalized;

View File

@@ -626,10 +626,19 @@ export default function APIPageClient({ machineId }) {
</button>
</>
) : tunnelLoading ? (
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
{tunnelProgress || "Creating tunnel..."}
</div>
<>
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
{tunnelProgress || "Creating tunnel..."}
</div>
<button
onClick={() => setShowDisableTunnelModal(true)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
title="Disable Tunnel"
>
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
</button>
</>
) : tunnelStatus?.type === "error" ? (
<>
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400">
@@ -639,10 +648,19 @@ export default function APIPageClient({ machineId }) {
<Button size="sm" icon="cloud_upload" onClick={() => setShowEnableTunnelModal(true)}>Enable</Button>
</>
) : tunnelChecking ? (
<div className="flex items-center gap-2 px-3 py-1.5 text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
Checking...
</div>
<>
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
Checking...
</div>
<button
onClick={() => setShowDisableTunnelModal(true)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
title="Disable Tunnel"
>
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
</button>
</>
) : (
<Button
size="sm"
@@ -683,10 +701,19 @@ export default function APIPageClient({ machineId }) {
</button>
</>
) : (tsLoading || tsConnecting) ? (
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
{tsProgress || "Connecting..."}
</div>
<>
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted">
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
{tsProgress || "Connecting..."}
</div>
<button
onClick={() => setShowDisableTsModal(true)}
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
title="Disable Tailscale"
>
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
</button>
</>
) : tsStatus?.type === "error" ? (
<>
<div className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400">

View File

@@ -0,0 +1,164 @@
"use client";
import { useState } from "react";
import PropTypes from "prop-types";
import { Button, Badge, Input, Modal, Select } from "@/shared/components";
export default function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, proxyPools, onSave, onClose }) {
const NONE_PROXY_POOL_VALUE = "__none__";
const [formData, setFormData] = useState({
name: "",
apiKey: "",
priority: 1,
proxyPoolId: NONE_PROXY_POOL_VALUE,
});
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
const [saving, setSaving] = useState(false);
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch("/api/providers/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, apiKey: formData.apiKey }),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
};
const handleSubmit = async () => {
if (!provider || !formData.apiKey) return;
setSaving(true);
try {
let isValid = false;
try {
setValidating(true);
setValidationResult(null);
const res = await fetch("/api/providers/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, apiKey: formData.apiKey }),
});
const data = await res.json();
isValid = !!data.valid;
setValidationResult(isValid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
await onSave({
name: formData.name,
apiKey: formData.apiKey,
priority: formData.priority,
proxyPoolId: formData.proxyPoolId === NONE_PROXY_POOL_VALUE ? null : formData.proxyPoolId,
testStatus: isValid ? "active" : "unknown",
providerSpecificData: undefined
});
} finally {
setSaving(false);
}
};
if (!provider) return null;
return (
<Modal isOpen={isOpen} title={`Add ${providerName || provider} API Key`} onClose={onClose}>
<div className="flex flex-col gap-4">
<Input
label="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Production Key"
/>
<div className="flex gap-2">
<Input
label="API Key"
type="password"
value={formData.apiKey}
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
className="flex-1"
/>
<div className="pt-6">
<Button onClick={handleValidate} disabled={!formData.apiKey || validating || saving} variant="secondary">
{validating ? "Checking..." : "Check"}
</Button>
</div>
</div>
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? "Valid" : "Invalid"}
</Badge>
)}
{isCompatible && (
<p className="text-xs text-text-muted">
{isAnthropic
? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.`
: `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.`
}
</p>
)}
<Input
label="Priority"
type="number"
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
/>
<Select
label="Proxy Pool"
value={formData.proxyPoolId}
onChange={(e) => setFormData({ ...formData, proxyPoolId: e.target.value })}
options={[
{ value: NONE_PROXY_POOL_VALUE, label: "None" },
...(proxyPools || []).map((pool) => ({ value: pool.id, label: pool.name })),
]}
placeholder="None"
/>
{(proxyPools || []).length === 0 && (
<p className="text-xs text-text-muted">
No active proxy pools available. Create one in Proxy Pools page first.
</p>
)}
<p className="text-xs text-text-muted">
Legacy manual proxy fields are still accepted by API for backward compatibility.
</p>
<div className="flex gap-2">
<Button onClick={handleSubmit} fullWidth disabled={!formData.name || !formData.apiKey || saving}>
{saving ? "Saving..." : "Save"}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
Cancel
</Button>
</div>
</div>
</Modal>
);
}
AddApiKeyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
provider: PropTypes.string,
providerName: PropTypes.string,
isCompatible: PropTypes.bool,
isAnthropic: PropTypes.bool,
proxyPools: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
})),
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,117 @@
"use client";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Button, Modal } from "@/shared/components";
export default function AddCustomModelModal({ isOpen, providerAlias, providerDisplayAlias, onSave, onClose }) {
const [modelId, setModelId] = useState("");
const [testStatus, setTestStatus] = useState(null); // null | "testing" | "ok" | "error"
const [testError, setTestError] = useState("");
const [saving, setSaving] = useState(false);
// Reset state when modal opens
useEffect(() => {
if (isOpen) { setModelId(""); setTestStatus(null); setTestError(""); }
}, [isOpen]);
const handleTest = async () => {
if (!modelId.trim()) return;
setTestStatus("testing");
setTestError("");
try {
const res = await fetch("/api/models/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: `${providerAlias}/${modelId.trim()}` }),
});
const data = await res.json();
setTestStatus(data.ok ? "ok" : "error");
setTestError(data.error || "");
} catch (err) {
setTestStatus("error");
setTestError(err.message);
}
};
const handleSave = async () => {
if (!modelId.trim() || saving) return;
setSaving(true);
try {
await onSave(modelId.trim());
} finally {
setSaving(false);
}
};
const handleKeyDown = (e) => {
if (e.key === "Enter") handleTest();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="Add Custom Model">
<div className="flex flex-col gap-4">
<div>
<label className="text-sm font-medium mb-1.5 block">Model ID</label>
<div className="flex gap-2">
<input
type="text"
value={modelId}
onChange={(e) => { setModelId(e.target.value); setTestStatus(null); setTestError(""); }}
onKeyDown={handleKeyDown}
placeholder="e.g. claude-opus-4-5"
className="flex-1 px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
autoFocus
/>
<Button
variant="secondary"
icon="science"
loading={testStatus === "testing"}
onClick={handleTest}
disabled={!modelId.trim() || testStatus === "testing"}
>
{testStatus === "testing" ? "Testing..." : "Test"}
</Button>
</div>
<p className="text-xs text-text-muted mt-1">
Sent to provider as: <code className="font-mono bg-sidebar px-1 rounded">{modelId.trim() || "model-id"}</code>
</p>
</div>
{/* Test result */}
{testStatus === "ok" && (
<div className="flex items-center gap-2 text-sm text-green-600">
<span className="material-symbols-outlined text-base">check_circle</span>
Model is reachable
</div>
)}
{testStatus === "error" && (
<div className="flex items-start gap-2 text-sm text-red-500">
<span className="material-symbols-outlined text-base shrink-0">cancel</span>
<span>{testError || "Model not reachable"}</span>
</div>
)}
<div className="flex gap-2 pt-1">
<Button onClick={onClose} variant="ghost" fullWidth size="sm">Cancel</Button>
<Button
onClick={handleSave}
fullWidth
size="sm"
disabled={!modelId.trim() || saving}
>
{saving ? "Adding..." : "Add Model"}
</Button>
</div>
</div>
</Modal>
);
}
AddCustomModelModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
providerAlias: PropTypes.string.isRequired,
providerDisplayAlias: PropTypes.string.isRequired,
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,250 @@
"use client";
import { useState } from "react";
import PropTypes from "prop-types";
import { Button } from "@/shared/components";
function CompatibleModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias, onTest, testStatus, isTesting }) {
const borderColor = testStatus === "ok"
? "border-green-500/40"
: testStatus === "error"
? "border-red-500/40"
: "border-border";
const iconColor = testStatus === "ok"
? "#22c55e"
: testStatus === "error"
? "#ef4444"
: undefined;
return (
<div className={`flex items-center gap-3 p-3 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
<span
className="material-symbols-outlined text-base text-text-muted"
style={iconColor ? { color: iconColor } : undefined}
>
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{modelId}</p>
<div className="flex items-center gap-1 mt-1">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
<div className="relative group/btn">
<button
onClick={() => onCopy(fullModel, `model-${modelId}`)}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
>
<span className="material-symbols-outlined text-sm">
{copied === `model-${modelId}` ? "check" : "content_copy"}
</span>
</button>
<span className="pointer-events-none absolute top-5 left-1/2 -translate-x-1/2 text-[10px] text-text-muted whitespace-nowrap opacity-0 group-hover/btn:opacity-100 transition-opacity">
{copied === `model-${modelId}` ? "Copied!" : "Copy"}
</span>
</div>
{onTest && (
<div className="relative group/btn">
<button
onClick={onTest}
disabled={isTesting}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-sm" style={isTesting ? { animation: "spin 1s linear infinite" } : undefined}>
{isTesting ? "progress_activity" : "science"}
</span>
</button>
<span className="pointer-events-none absolute top-5 left-1/2 -translate-x-1/2 text-[10px] text-text-muted whitespace-nowrap opacity-0 group-hover/btn:opacity-100 transition-opacity">
{isTesting ? "Testing..." : "Test"}
</span>
</div>
)}
</div>
</div>
<button
onClick={onDeleteAlias}
className="p-1 hover:bg-red-50 rounded text-red-500"
title="Remove model"
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
);
}
export default function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) {
const [newModel, setNewModel] = useState("");
const [adding, setAdding] = useState(false);
const [importing, setImporting] = useState(false);
const [testingModelId, setTestingModelId] = useState(null);
const [modelTestResults, setModelTestResults] = useState({});
const handleTestModel = async (modelId) => {
if (testingModelId) return;
setTestingModelId(modelId);
try {
const res = await fetch("/api/models/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: `${providerStorageAlias}/${modelId}` }),
});
const data = await res.json();
setModelTestResults((prev) => ({ ...prev, [modelId]: data.ok ? "ok" : "error" }));
} catch {
setModelTestResults((prev) => ({ ...prev, [modelId]: "error" }));
} finally {
setTestingModelId(null);
}
};
const providerAliases = Object.entries(modelAliases).filter(
([, model]) => model.startsWith(`${providerStorageAlias}/`)
);
const allModels = providerAliases.map(([alias, fullModel]) => ({
modelId: fullModel.replace(`${providerStorageAlias}/`, ""),
fullModel,
alias,
}));
const generateDefaultAlias = (modelId) => {
const parts = modelId.split("/");
return parts[parts.length - 1];
};
const resolveAlias = (modelId) => {
const fullModel = `${providerStorageAlias}/${modelId}`;
// Skip if this exact model already has an alias
if (Object.values(modelAliases).includes(fullModel)) return null;
const baseAlias = generateDefaultAlias(modelId);
if (!modelAliases[baseAlias]) return baseAlias;
const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`;
if (!modelAliases[prefixedAlias]) return prefixedAlias;
return null;
};
const handleAdd = async () => {
if (!newModel.trim() || adding) return;
const modelId = newModel.trim();
const resolvedAlias = resolveAlias(modelId);
if (!resolvedAlias) {
alert("All suggested aliases already exist. Please choose a different model or remove conflicting aliases.");
return;
}
setAdding(true);
try {
await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
setNewModel("");
} catch (error) {
console.log("Error adding model:", error);
} finally {
setAdding(false);
}
};
const handleImport = async () => {
if (importing) return;
const activeConnection = connections.find((conn) => conn.isActive !== false);
if (!activeConnection) return;
setImporting(true);
try {
const res = await fetch(`/api/providers/${activeConnection.id}/models`);
const data = await res.json();
if (!res.ok) {
alert(data.error || "Failed to import models");
return;
}
const models = data.models || [];
if (models.length === 0) {
alert("No models returned from /models.");
return;
}
let importedCount = 0;
for (const model of models) {
const modelId = model.id || model.name || model.model;
if (!modelId) continue;
const resolvedAlias = resolveAlias(modelId);
if (!resolvedAlias) continue;
await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
importedCount += 1;
}
if (importedCount === 0) {
alert("No new models were added.");
}
} catch (error) {
console.log("Error importing models:", error);
} finally {
setImporting(false);
}
};
const canImport = connections.some((conn) => conn.isActive !== false);
return (
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">
Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint.
</p>
<div className="flex items-end gap-2 flex-wrap">
<div className="flex-1 min-w-[240px]">
<label htmlFor="new-compatible-model-input" className="text-xs text-text-muted mb-1 block">Model ID</label>
<input
id="new-compatible-model-input"
type="text"
value={newModel}
onChange={(e) => setNewModel(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"}
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</div>
<Button size="sm" icon="add" onClick={handleAdd} disabled={!newModel.trim() || adding}>
{adding ? "Adding..." : "Add"}
</Button>
<Button size="sm" variant="secondary" icon="download" onClick={handleImport} disabled={!canImport || importing}>
{importing ? "Importing..." : "Import from /models"}
</Button>
</div>
{!canImport && (
<p className="text-xs text-text-muted">
Add a connection to enable importing models.
</p>
)}
{allModels.length > 0 && (
<div className="flex flex-col gap-3">
{allModels.map(({ modelId, fullModel, alias }) => (
<CompatibleModelRow
key={fullModel}
modelId={modelId}
fullModel={`${providerDisplayAlias}/${modelId}`}
copied={copied}
onCopy={onCopy}
onDeleteAlias={() => onDeleteAlias(alias)}
onTest={connections.length > 0 ? () => handleTestModel(modelId) : undefined}
testStatus={modelTestResults[modelId]}
isTesting={testingModelId === modelId}
/>
))}
</div>
)}
</div>
);
}
CompatibleModelsSection.propTypes = {
providerStorageAlias: PropTypes.string.isRequired,
providerDisplayAlias: PropTypes.string.isRequired,
modelAliases: PropTypes.object.isRequired,
copied: PropTypes.string,
onCopy: PropTypes.func.isRequired,
onSetAlias: PropTypes.func.isRequired,
onDeleteAlias: PropTypes.func.isRequired,
connections: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
isActive: PropTypes.bool,
})).isRequired,
isAnthropic: PropTypes.bool,
};

View File

@@ -0,0 +1,261 @@
"use client";
import { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { Badge, Toggle } from "@/shared/components";
import CooldownTimer from "./CooldownTimer";
export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete }) {
const [showProxyDropdown, setShowProxyDropdown] = useState(false);
const [updatingProxy, setUpdatingProxy] = useState(false);
const proxyDropdownRef = useRef(null);
const proxyPoolMap = new Map((proxyPools || []).map((pool) => [pool.id, pool]));
const boundProxyPoolId = connection.providerSpecificData?.proxyPoolId || null;
const boundProxyPool = boundProxyPoolId ? proxyPoolMap.get(boundProxyPoolId) : null;
const hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl;
const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy;
const proxyDisplayText = boundProxyPool
? `Pool: ${boundProxyPool.name}`
: boundProxyPoolId
? `Pool: ${boundProxyPoolId} (inactive/missing)`
: hasLegacyProxy
? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}`
: "";
let maskedProxyUrl = "";
if (boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl) {
const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl;
try {
const parsed = new URL(rawProxyUrl);
maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
} catch {
maskedProxyUrl = rawProxyUrl;
}
}
const noProxyText = boundProxyPool?.noProxy || connection.providerSpecificData?.connectionNoProxy || "";
let proxyBadgeVariant = "default";
if (boundProxyPool?.isActive === true) {
proxyBadgeVariant = "success";
} else if (boundProxyPoolId || hasLegacyProxy) {
proxyBadgeVariant = "error";
}
// Close dropdown when clicking outside
useEffect(() => {
if (!showProxyDropdown) return;
const handler = (e) => {
if (proxyDropdownRef.current && !proxyDropdownRef.current.contains(e.target)) {
setShowProxyDropdown(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [showProxyDropdown]);
const handleSelectProxy = async (poolId) => {
setUpdatingProxy(true);
try {
await onUpdateProxy(poolId === "__none__" ? null : poolId);
} finally {
setUpdatingProxy(false);
setShowProxyDropdown(false);
}
};
const displayName = isOAuth
? connection.name || connection.email || connection.displayName || "OAuth Account"
: connection.name;
// Use useState + useEffect for impure Date.now() to avoid calling during render
const [isCooldown, setIsCooldown] = useState(false);
// Get earliest model lock timestamp (useEffect handles the Date.now() comparison)
const modelLockUntil = Object.entries(connection)
.filter(([k]) => k.startsWith("modelLock_"))
.map(([, v]) => v)
.filter(v => !!v)
.sort()[0] || null;
useEffect(() => {
const checkCooldown = () => {
const until = Object.entries(connection)
.filter(([k]) => k.startsWith("modelLock_"))
.map(([, v]) => v)
.filter(v => v && new Date(v).getTime() > Date.now())
.sort()[0] || null;
setIsCooldown(!!until);
};
checkCooldown();
const interval = modelLockUntil ? setInterval(checkCooldown, 1000) : null;
return () => {
if (interval) clearInterval(interval);
};
}, [modelLockUntil]);
// Determine effective status (override unavailable if cooldown expired)
const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown)
? "active" // Cooldown expired u2192 treat as active
: connection.testStatus;
const getStatusVariant = () => {
if (connection.isActive === false) return "default";
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
return "default";
};
return (
<div className={`group flex items-center justify-between p-2 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
<div className="flex items-center gap-3 flex-1 min-w-0">
{/* Priority arrows */}
<div className="flex flex-col">
<button
onClick={onMoveUp}
disabled={isFirst}
className={`p-0.5 rounded ${isFirst ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}
>
<span className="material-symbols-outlined text-sm">keyboard_arrow_up</span>
</button>
<button
onClick={onMoveDown}
disabled={isLast}
className={`p-0.5 rounded ${isLast ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}
>
<span className="material-symbols-outlined text-sm">keyboard_arrow_down</span>
</button>
</div>
<span className="material-symbols-outlined text-base text-text-muted">
{isOAuth ? "lock" : "key"}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{displayName}</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant={getStatusVariant()} size="sm" dot>
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
</Badge>
{hasAnyProxy && (
<Badge variant={proxyBadgeVariant} size="sm">
Proxy
</Badge>
)}
{isCooldown && connection.isActive !== false && <CooldownTimer until={modelLockUntil} />}
{connection.lastError && connection.isActive !== false && (
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
{connection.lastError}
</span>
)}
<span className="text-xs text-text-muted">#{connection.priority}</span>
{connection.globalPriority && (
<span className="text-xs text-text-muted">Auto: {connection.globalPriority}</span>
)}
</div>
{hasAnyProxy && (
<div className="mt-1 flex items-center gap-2 flex-wrap">
<span className="text-[11px] text-text-muted truncate max-w-[420px]" title={proxyDisplayText}>
{proxyDisplayText}
</span>
{maskedProxyUrl && (
<code className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded text-text-muted">
{maskedProxyUrl}
</code>
)}
{noProxyText && (
<span className="text-[11px] text-text-muted truncate max-w-[320px]" title={noProxyText}>
no_proxy: {noProxyText}
</span>
)}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
{/* Proxy button with inline dropdown */}
{(proxyPools || []).length > 0 && (
<div className="relative" ref={proxyDropdownRef}>
<button
onClick={() => setShowProxyDropdown((v) => !v)}
className={`flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors ${hasAnyProxy ? "text-primary" : "text-text-muted hover:text-primary"}`}
disabled={updatingProxy}
>
<span className="material-symbols-outlined text-[18px]">
{updatingProxy ? "progress_activity" : "lan"}
</span>
<span className="text-[10px] leading-tight">Proxy</span>
</button>
{showProxyDropdown && (
<div className="absolute right-0 top-full mt-1 z-50 bg-bg border border-border rounded-lg shadow-lg py-1 min-w-[160px]">
<button
onClick={() => handleSelectProxy("__none__")}
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${!boundProxyPoolId ? "text-primary font-medium" : "text-text-main"}`}
>
None
</button>
{(proxyPools || []).map((pool) => (
<button
key={pool.id}
onClick={() => handleSelectProxy(pool.id)}
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${boundProxyPoolId === pool.id ? "text-primary font-medium" : "text-text-main"}`}
>
{pool.name}
</button>
))}
</div>
)}
</div>
)}
<button onClick={onEdit} className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary">
<span className="material-symbols-outlined text-[18px]">edit</span>
<span className="text-[10px] leading-tight">Edit</span>
</button>
<button onClick={onDelete} className="flex flex-col items-center px-2 py-1 rounded hover:bg-red-500/10 text-red-500">
<span className="material-symbols-outlined text-[18px]">delete</span>
<span className="text-[10px] leading-tight">Delete</span>
</button>
</div>
<Toggle
size="sm"
checked={connection.isActive ?? true}
onChange={onToggleActive}
title={(connection.isActive ?? true) ? "Disable connection" : "Enable connection"}
/>
</div>
</div>
);
}
ConnectionRow.propTypes = {
connection: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
email: PropTypes.string,
displayName: PropTypes.string,
modelLockUntil: PropTypes.string,
testStatus: PropTypes.string,
isActive: PropTypes.bool,
lastError: PropTypes.string,
priority: PropTypes.number,
globalPriority: PropTypes.number,
}).isRequired,
proxyPools: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
proxyUrl: PropTypes.string,
noProxy: PropTypes.string,
isActive: PropTypes.bool,
})),
isOAuth: PropTypes.bool.isRequired,
isFirst: PropTypes.bool.isRequired,
isLast: PropTypes.bool.isRequired,
onMoveUp: PropTypes.func.isRequired,
onMoveDown: PropTypes.func.isRequired,
onToggleActive: PropTypes.func.isRequired,
onUpdateProxy: PropTypes.func,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,42 @@
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
export default function CooldownTimer({ until }) {
const [remaining, setRemaining] = useState("");
useEffect(() => {
const updateRemaining = () => {
const diff = new Date(until).getTime() - Date.now();
if (diff <= 0) {
setRemaining("");
return;
}
const secs = Math.floor(diff / 1000);
if (secs < 60) {
setRemaining(`${secs}s`);
} else if (secs < 3600) {
setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`);
} else {
const hrs = Math.floor(secs / 3600);
const mins = Math.floor((secs % 3600) / 60);
setRemaining(`${hrs}h ${mins}m`);
}
};
updateRemaining();
const interval = setInterval(updateRemaining, 1000);
return () => clearInterval(interval);
}, [until]);
if (!remaining) return null;
return (
<span className="text-xs text-orange-500 font-mono">
{remaining}
</span>
);
}
CooldownTimer.propTypes = {
until: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,161 @@
"use client";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Button, Badge, Input, Modal, Select } from "@/shared/components";
export default function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) {
const [formData, setFormData] = useState({
name: "",
prefix: "",
apiType: "chat",
baseUrl: "https://api.openai.com/v1",
});
const [saving, setSaving] = useState(false);
const [checkKey, setCheckKey] = useState("");
const [checkModelId, setCheckModelId] = useState("");
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
useEffect(() => {
if (node) {
setFormData({
name: node.name || "",
prefix: node.prefix || "",
apiType: node.apiType || "chat",
baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"),
});
}
}, [node, isAnthropic]);
const apiTypeOptions = [
{ value: "chat", label: "Chat Completions" },
{ value: "responses", label: "Responses API" },
];
const handleSubmit = async () => {
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
setSaving(true);
try {
const payload = {
name: formData.name,
prefix: formData.prefix,
baseUrl: formData.baseUrl,
};
if (!isAnthropic) {
payload.apiType = formData.apiType;
}
await onSave(payload);
} finally {
setSaving(false);
}
};
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch("/api/provider-nodes/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: isAnthropic ? "anthropic-compatible" : "openai-compatible",
modelId: checkModelId.trim() || undefined
}),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
};
if (!node) return null;
return (
<Modal isOpen={isOpen} title={`Edit ${isAnthropic ? "Anthropic" : "OpenAI"} Compatible`} onClose={onClose}>
<div className="flex flex-col gap-4">
<Input
label="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`}
hint="Required. A friendly label for this node."
/>
<Input
label="Prefix"
value={formData.prefix}
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
placeholder={isAnthropic ? "ac-prod" : "oc-prod"}
hint="Required. Used as the provider prefix for model IDs."
/>
{!isAnthropic && (
<Select
label="API Type"
options={apiTypeOptions}
value={formData.apiType}
onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
/>
)}
<Input
label="Base URL"
value={formData.baseUrl}
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"}
hint={`Use the base URL (ending in /v1) for your ${isAnthropic ? "Anthropic" : "OpenAI"}-compatible API.`}
/>
<div className="flex gap-2">
<Input
label="API Key (for Check)"
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
className="flex-1"
/>
<div className="pt-6">
<Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
{validating ? "Checking..." : "Check"}
</Button>
</div>
</div>
<Input
label="Model ID (optional)"
value={checkModelId}
onChange={(e) => setCheckModelId(e.target.value)}
placeholder="e.g. my-model-id"
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
/>
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? "Valid" : "Invalid"}
</Badge>
)}
<div className="flex gap-2">
<Button onClick={handleSubmit} fullWidth disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || saving}>
{saving ? "Saving..." : "Save"}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
Cancel
</Button>
</div>
</div>
</Modal>
);
}
EditCompatibleNodeModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
node: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
prefix: PropTypes.string,
apiType: PropTypes.string,
baseUrl: PropTypes.string,
}),
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
isAnthropic: PropTypes.bool,
};

View File

@@ -0,0 +1,86 @@
import PropTypes from "prop-types";
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting }) {
const borderColor = testStatus === "ok"
? "border-green-500/40"
: testStatus === "error"
? "border-red-500/40"
: "border-border";
const iconColor = testStatus === "ok"
? "#22c55e"
: testStatus === "error"
? "#ef4444"
: undefined;
return (
<div className={`group px-3 py-2 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
<div className="flex items-center gap-2">
<span
className="material-symbols-outlined text-base"
style={iconColor ? { color: iconColor } : undefined}
>
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
</span>
<div className="flex flex-col gap-1">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
{model.name && <span className="text-[9px] text-text-muted/70 italic pl-1">{model.name}</span>}
</div>
{onTest && (
<div className="relative group/btn">
<button
onClick={onTest}
disabled={isTesting}
className={`p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary transition-opacity ${isTesting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`}
>
<span className="material-symbols-outlined text-sm" style={isTesting ? { animation: "spin 1s linear infinite" } : undefined}>
{isTesting ? "progress_activity" : "science"}
</span>
</button>
<span className="pointer-events-none absolute mt-1 top-5 left-1/2 -translate-x-1/2 text-[10px] text-text-muted whitespace-nowrap opacity-0 group-hover/btn:opacity-100 transition-opacity">
{isTesting ? "Testing..." : "Test"}
</span>
</div>
)}
<div className="relative group/btn">
<button
onClick={() => onCopy(fullModel, `model-${model.id}`)}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
>
<span className="material-symbols-outlined text-sm">
{copied === `model-${model.id}` ? "check" : "content_copy"}
</span>
</button>
<span className="pointer-events-none absolute mt-1 top-5 left-1/2 -translate-x-1/2 text-[10px] text-text-muted whitespace-nowrap opacity-0 group-hover/btn:opacity-100 transition-opacity">
{copied === `model-${model.id}` ? "Copied!" : "Copy"}
</span>
</div>
{isCustom && (
<button
onClick={onDeleteAlias}
className="p-0.5 hover:bg-red-500/10 rounded text-text-muted hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
title="Remove custom model"
>
<span className="material-symbols-outlined text-sm">close</span>
</button>
)}
</div>
</div>
);
}
ModelRow.propTypes = {
model: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
fullModel: PropTypes.string.isRequired,
alias: PropTypes.string,
copied: PropTypes.string,
onCopy: PropTypes.func.isRequired,
testStatus: PropTypes.oneOf(["ok", "error"]),
isCustom: PropTypes.bool,
isFree: PropTypes.bool,
onDeleteAlias: PropTypes.func,
onTest: PropTypes.func,
isTesting: PropTypes.bool,
};

View File

@@ -0,0 +1,183 @@
"use client";
import { useState } from "react";
import PropTypes from "prop-types";
import { Button } from "@/shared/components";
function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias, onTest, testStatus, isTesting }) {
const borderColor = testStatus === "ok"
? "border-green-500/40"
: testStatus === "error"
? "border-red-500/40"
: "border-border";
const iconColor = testStatus === "ok"
? "#22c55e"
: testStatus === "error"
? "#ef4444"
: undefined;
return (
<div className={`flex items-center gap-3 p-3 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
<span
className="material-symbols-outlined text-base text-text-muted"
style={iconColor ? { color: iconColor } : undefined}
>
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{modelId}</p>
<div className="flex items-center gap-1 mt-1">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
<div className="relative group/btn">
<button
onClick={() => onCopy(fullModel, `model-${modelId}`)}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
>
<span className="material-symbols-outlined text-sm">
{copied === `model-${modelId}` ? "check" : "content_copy"}
</span>
</button>
<span className="pointer-events-none absolute top-5 left-1/2 -translate-x-1/2 text-[10px] text-text-muted whitespace-nowrap opacity-0 group-hover/btn:opacity-100 transition-opacity">
{copied === `model-${modelId}` ? "Copied!" : "Copy"}
</span>
</div>
{onTest && (
<div className="relative group/btn">
<button
onClick={onTest}
disabled={isTesting}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-sm" style={isTesting ? { animation: "spin 1s linear infinite" } : undefined}>
{isTesting ? "progress_activity" : "science"}
</span>
</button>
<span className="pointer-events-none absolute top-5 left-1/2 -translate-x-1/2 text-[10px] text-text-muted whitespace-nowrap opacity-0 group-hover/btn:opacity-100 transition-opacity">
{isTesting ? "Testing..." : "Test"}
</span>
</div>
)}
</div>
</div>
{/* Delete button */}
<button
onClick={onDeleteAlias}
className="p-1 hover:bg-red-50 rounded text-red-500"
title="Remove model"
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
);
}
PassthroughModelRow.propTypes = {
modelId: PropTypes.string.isRequired,
fullModel: PropTypes.string.isRequired,
copied: PropTypes.string,
onCopy: PropTypes.func.isRequired,
onDeleteAlias: PropTypes.func.isRequired,
onTest: PropTypes.func,
testStatus: PropTypes.oneOf(["ok", "error"]),
isTesting: PropTypes.bool,
};
export default function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) {
const [newModel, setNewModel] = useState("");
const [adding, setAdding] = useState(false);
// Filter aliases for this provider - models are persisted via alias
const providerAliases = Object.entries(modelAliases).filter(
([, model]) => model.startsWith(`${providerAlias}/`)
);
const allModels = providerAliases.map(([alias, fullModel]) => ({
modelId: fullModel.replace(`${providerAlias}/`, ""),
fullModel,
alias,
}));
// Generate default alias from modelId (last part after /)
const generateDefaultAlias = (modelId) => {
const parts = modelId.split("/");
return parts[parts.length - 1];
};
const handleAdd = async () => {
if (!newModel.trim() || adding) return;
const modelId = newModel.trim();
const defaultAlias = generateDefaultAlias(modelId);
// Check if alias already exists
if (modelAliases[defaultAlias]) {
alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`);
return;
}
setAdding(true);
try {
await onSetAlias(modelId, defaultAlias);
setNewModel("");
} catch (error) {
console.log("Error adding model:", error);
} finally {
setAdding(false);
}
};
return (
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">
OpenRouter supports any model. Add models and create aliases for quick access.
</p>
{/* Add new model */}
<div className="flex items-end gap-2">
<div className="flex-1">
<label htmlFor="new-model-input" className="text-xs text-text-muted mb-1 block">Model ID (from OpenRouter)</label>
<input
id="new-model-input"
type="text"
value={newModel}
onChange={(e) => setNewModel(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
placeholder="anthropic/claude-3-opus"
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
/>
</div>
<Button size="sm" icon="add" onClick={handleAdd} disabled={!newModel.trim() || adding}>
{adding ? "Adding..." : "Add"}
</Button>
</div>
{/* Models list */}
{allModels.length > 0 && (
<div className="flex flex-col gap-3">
{allModels.map(({ modelId, fullModel, alias }) => (
<PassthroughModelRow
key={fullModel}
modelId={modelId}
fullModel={fullModel}
copied={copied}
onCopy={onCopy}
onDeleteAlias={() => onDeleteAlias(alias)}
/>
))}
</div>
)}
</div>
);
}
PassthroughModelsSection.propTypes = {
providerAlias: PropTypes.string.isRequired,
modelAliases: PropTypes.object.isRequired,
copied: PropTypes.string,
onCopy: PropTypes.func.isRequired,
onSetAlias: PropTypes.func.isRequired,
onDeleteAlias: PropTypes.func.isRequired,
};

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,12 @@ function CollapsibleSection({ title, children, defaultOpen = false, icon = null
);
}
function getInputTokens(tokens) {
const prompt = tokens?.prompt_tokens || tokens?.input_tokens || 0;
const cache = tokens?.cached_tokens || tokens?.cache_read_input_tokens || 0;
return prompt < cache ? cache : prompt;
}
export default function RequestDetailsTab() {
const [details, setDetails] = useState([]);
const [pagination, setPagination] = useState({
@@ -276,7 +282,7 @@ export default function RequestDetailsTab() {
</span>
</td>
<td className="p-4 text-sm text-text-main text-right font-mono">
{detail.tokens?.prompt_tokens?.toLocaleString() || 0}
{getInputTokens(detail.tokens).toLocaleString()}
</td>
<td className="p-4 text-sm text-text-main text-right font-mono">
{detail.tokens?.completion_tokens?.toLocaleString() || 0}
@@ -359,7 +365,7 @@ export default function RequestDetailsTab() {
<div>
<span className="text-text-muted">Input Tokens:</span>{" "}
<span className="text-text-main font-mono">
{selectedDetail.tokens?.prompt_tokens?.toLocaleString() || 0}
{getInputTokens(selectedDetail.tokens).toLocaleString()}
</span>
</div>
<div>

View File

@@ -13,6 +13,7 @@ import { getModelInfo, getComboModels } from "../services/model.js";
import { handleChatCore } from "open-sse/handlers/chatCore.js";
import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
import { handleComboChat } from "open-sse/services/combo.js";
import { handleBypassRequest } from "open-sse/utils/bypassHandler.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import { detectFormatByEndpoint } from "open-sse/translator/formats.js";
import * as log from "../utils/logger.js";
@@ -83,6 +84,11 @@ export async function handleChat(request, clientRawRequest = null) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}
// Bypass naming/warmup requests before combo rotation to avoid wasting rotation slots
const userAgent = request?.headers?.get("user-agent") || "";
const bypassResponse = handleBypassRequest(body, modelStr, userAgent, !!settings.ccFilterNaming);
if (bypassResponse) return bypassResponse.response || bypassResponse;
// Check if model is a combo (has multiple models with fallback)
const comboModels = await getComboModels(modelStr);
if (comboModels) {