mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Enhance chat handling.
This commit is contained in:
@@ -63,13 +63,13 @@ function extractUsageFromResponse(responseBody, provider) {
|
|||||||
* @param {function} options.onDisconnect - Callback when client disconnects
|
* @param {function} options.onDisconnect - Callback when client disconnects
|
||||||
* @param {string} options.connectionId - Connection ID for usage tracking
|
* @param {string} options.connectionId - Connection ID for usage tracking
|
||||||
*/
|
*/
|
||||||
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId }) {
|
export async function handleChatCore({ body, modelInfo, credentials, log, onCredentialsRefreshed, onRequestSuccess, onDisconnect, clientRawRequest, connectionId, userAgent }) {
|
||||||
const { provider, model } = modelInfo;
|
const { provider, model } = modelInfo;
|
||||||
|
|
||||||
const sourceFormat = detectFormat(body);
|
const sourceFormat = detectFormat(body);
|
||||||
|
|
||||||
// Check for bypass patterns (warmup, skip) - return fake response
|
// Check for bypass patterns (warmup, skip) - return fake response
|
||||||
const bypassResponse = handleBypassRequest(body, model);
|
const bypassResponse = handleBypassRequest(body, model, userAgent);
|
||||||
if (bypassResponse) {
|
if (bypassResponse) {
|
||||||
return bypassResponse;
|
return bypassResponse;
|
||||||
}
|
}
|
||||||
@@ -173,17 +173,6 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
return createErrorResult(502, errMsg);
|
return createErrorResult(502, errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log headers (mask sensitive values)
|
|
||||||
const safeHeaders = {};
|
|
||||||
for (const [key, value] of Object.entries(providerHeaders || {})) {
|
|
||||||
if (key.toLowerCase().includes("auth") || key.toLowerCase().includes("key") || key.toLowerCase().includes("token")) {
|
|
||||||
safeHeaders[key] = value ? `${value.slice(0, 10)}...` : "";
|
|
||||||
} else {
|
|
||||||
safeHeaders[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log?.debug?.("HEADERS", JSON.stringify(safeHeaders));
|
|
||||||
|
|
||||||
// Handle 401/403 - try token refresh using executor
|
// Handle 401/403 - try token refresh using executor
|
||||||
if (providerResponse.status === 401 || providerResponse.status === 403) {
|
if (providerResponse.status === 401 || providerResponse.status === 403) {
|
||||||
const newCredentials = await refreshWithRetry(
|
const newCredentials = await refreshWithRetry(
|
||||||
|
|||||||
@@ -5,19 +5,14 @@ import { SKIP_PATTERNS } from "../config/constants.js";
|
|||||||
import { formatSSE } from "./stream.js";
|
import { formatSSE } from "./stream.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for bypass patterns (warmup, skip) - return fake response without calling provider
|
* Check for bypass patterns - return fake response without calling provider
|
||||||
* Supports both streaming and non-streaming responses
|
* Only works for Claude CLI requests
|
||||||
* Returns response in the correct sourceFormat using translator
|
|
||||||
*
|
|
||||||
* @param {object} body - Request body
|
|
||||||
* @param {string} model - Model name
|
|
||||||
* @returns {object|null} { success: true, response: Response } or null if not bypass
|
|
||||||
*/
|
*/
|
||||||
export function handleBypassRequest(body, model) {
|
export function handleBypassRequest(body, model, userAgent = "") {
|
||||||
const messages = body.messages;
|
if (!userAgent.includes("claude-cli")) return null;
|
||||||
if (!messages?.length) return null;
|
if (!body.messages?.length) return null;
|
||||||
|
|
||||||
// Helper to extract text from content
|
const messages = body.messages;
|
||||||
const getText = (content) => {
|
const getText = (content) => {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") return content;
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
@@ -28,43 +23,45 @@ export function handleBypassRequest(body, model) {
|
|||||||
|
|
||||||
let shouldBypass = false;
|
let shouldBypass = false;
|
||||||
|
|
||||||
// Check warmup: first message "Warmup"
|
// Pattern 1: Title extraction (assistant message = "{")
|
||||||
|
const lastMsg = messages[messages.length - 1];
|
||||||
|
if (lastMsg?.role === "assistant" && lastMsg.content?.[0]?.text === "{") {
|
||||||
|
shouldBypass = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern 2: Warmup
|
||||||
|
if (!shouldBypass) {
|
||||||
const firstText = getText(messages[0]?.content);
|
const firstText = getText(messages[0]?.content);
|
||||||
if (firstText === "Warmup") {
|
if (firstText === "Warmup") {
|
||||||
shouldBypass = true;
|
shouldBypass = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check count pattern: [{"role":"user","content":"count"}]
|
// Pattern 3: Count
|
||||||
// if (!shouldBypass &&
|
if (!shouldBypass && messages.length === 1 && messages[0]?.role === "user") {
|
||||||
// messages.length === 1 &&
|
const firstText = getText(messages[0]?.content);
|
||||||
// messages[0]?.role === "user" &&
|
if (firstText === "count") {
|
||||||
// firstText === "count") {
|
shouldBypass = true;
|
||||||
// shouldBypass = true;
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Check skip patterns - only check user messages, not system prompt
|
// Pattern 4: Skip patterns
|
||||||
if (!shouldBypass && SKIP_PATTERNS?.length) {
|
if (!shouldBypass && SKIP_PATTERNS?.length) {
|
||||||
// Only check user messages, skip system/assistant messages to avoid matching system prompts
|
|
||||||
const userMessages = messages.filter(m => m.role === "user");
|
const userMessages = messages.filter(m => m.role === "user");
|
||||||
const userText = userMessages.map(m => getText(m.content)).join(" ");
|
const userText = userMessages.map(m => getText(m.content)).join(" ");
|
||||||
const matchedPattern = SKIP_PATTERNS.find(p => userText.includes(p));
|
if (SKIP_PATTERNS.some(p => userText.includes(p))) {
|
||||||
if (matchedPattern) {
|
|
||||||
shouldBypass = true;
|
shouldBypass = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldBypass) return null;
|
if (!shouldBypass) return null;
|
||||||
|
|
||||||
// Detect source format and stream mode
|
|
||||||
const sourceFormat = detectFormat(body);
|
const sourceFormat = detectFormat(body);
|
||||||
const stream = body.stream !== false;
|
const stream = body.stream !== false;
|
||||||
|
|
||||||
// Create bypass response using translator
|
return stream
|
||||||
if (stream) {
|
? createStreamingResponse(sourceFormat, model)
|
||||||
return createStreamingResponse(sourceFormat, model);
|
: createNonStreamingResponse(sourceFormat, model);
|
||||||
} else {
|
|
||||||
return createNonStreamingResponse(sourceFormat, model);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Card, Button, ModelSelectModal } from "@/shared/components";
|
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||||
@@ -27,8 +27,8 @@ export default function ClaudeToolCard({
|
|||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [currentEditingAlias, setCurrentEditingAlias] = useState(null);
|
const [currentEditingAlias, setCurrentEditingAlias] = useState(null);
|
||||||
const [selectedApiKey, setSelectedApiKey] = useState("");
|
const [selectedApiKey, setSelectedApiKey] = useState("");
|
||||||
const [copiedConfig, setCopiedConfig] = useState(false);
|
|
||||||
const [modelAliases, setModelAliases] = useState({});
|
const [modelAliases, setModelAliases] = useState({});
|
||||||
|
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
|
||||||
|
|
||||||
const getConfigStatus = () => {
|
const getConfigStatus = () => {
|
||||||
if (!claudeStatus?.installed) return null;
|
if (!claudeStatus?.installed) return null;
|
||||||
@@ -163,7 +163,7 @@ export default function ClaudeToolCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate settings.json content for manual copy
|
// Generate settings.json content for manual copy
|
||||||
const getSettingsContent = () => {
|
const getManualConfigs = () => {
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||||
@@ -172,17 +172,13 @@ export default function ClaudeToolCard({
|
|||||||
const targetModel = modelMappings[model.alias];
|
const targetModel = modelMappings[model.alias];
|
||||||
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
||||||
});
|
});
|
||||||
return JSON.stringify({ env }, null, 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = async (text) => {
|
return [
|
||||||
try {
|
{
|
||||||
await navigator.clipboard.writeText(text);
|
filename: "~/.claude/settings.json",
|
||||||
setCopiedConfig(true);
|
content: JSON.stringify({ env }, null, 2),
|
||||||
setTimeout(() => setCopiedConfig(false), 2000);
|
},
|
||||||
} catch (err) {
|
];
|
||||||
console.log("Failed to copy:", err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -289,31 +285,23 @@ export default function ClaudeToolCard({
|
|||||||
<Button variant="outline" size="sm" onClick={handleResetSettings} disabled={!claudeStatus?.has9Router} loading={restoring}>
|
<Button variant="outline" size="sm" onClick={handleResetSettings} disabled={!claudeStatus?.has9Router} loading={restoring}>
|
||||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={checkClaudeStatus}>
|
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
|
||||||
<span className="material-symbols-outlined text-[14px]">refresh</span>
|
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>Manual Config
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Manual Config Section */}
|
|
||||||
<div className="pt-4 border-t border-border flex flex-col gap-3">
|
|
||||||
<p className="text-xs text-text-muted">Or copy config manually:</p>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs font-medium text-text-main">~/.claude/settings.json</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(getSettingsContent())}>
|
|
||||||
<span className="material-symbols-outlined text-[14px] mr-1">{copiedConfig ? "check" : "content_copy"}</span>
|
|
||||||
{copiedConfig ? "Copied!" : "Copy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<pre className="px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs overflow-x-auto whitespace-pre-wrap break-all max-h-40 overflow-y-auto">{getSettingsContent()}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ModelSelectModal isOpen={modalOpen} onClose={() => setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} />
|
<ModelSelectModal isOpen={modalOpen} onClose={() => setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} />
|
||||||
|
|
||||||
|
<ManualConfigModal
|
||||||
|
isOpen={showManualConfigModal}
|
||||||
|
onClose={() => setShowManualConfigModal(false)}
|
||||||
|
title="Claude CLI - Manual Configuration"
|
||||||
|
configs={getManualConfigs()}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Card, Button, ModelSelectModal } from "@/shared/components";
|
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled }) {
|
export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled }) {
|
||||||
@@ -13,9 +13,9 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
|||||||
const [showInstallGuide, setShowInstallGuide] = useState(false);
|
const [showInstallGuide, setShowInstallGuide] = useState(false);
|
||||||
const [selectedApiKey, setSelectedApiKey] = useState("");
|
const [selectedApiKey, setSelectedApiKey] = useState("");
|
||||||
const [selectedModel, setSelectedModel] = useState("");
|
const [selectedModel, setSelectedModel] = useState("");
|
||||||
const [copiedConfig, setCopiedConfig] = useState(false);
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [modelAliases, setModelAliases] = useState({});
|
const [modelAliases, setModelAliases] = useState({});
|
||||||
|
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (apiKeys?.length > 0 && !selectedApiKey) {
|
if (apiKeys?.length > 0 && !selectedApiKey) {
|
||||||
@@ -123,6 +123,11 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
|||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getManualConfigs = () => {
|
||||||
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
|
? selectedApiKey
|
||||||
|
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||||
|
|
||||||
const configContent = `# 9Router Configuration for Codex CLI
|
const configContent = `# 9Router Configuration for Codex CLI
|
||||||
model = "${selectedModel}"
|
model = "${selectedModel}"
|
||||||
model_provider = "9router"
|
model_provider = "9router"
|
||||||
@@ -133,22 +138,20 @@ base_url = "${baseUrl}/v1"
|
|||||||
wire_api = "responses"
|
wire_api = "responses"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
|
||||||
? selectedApiKey
|
|
||||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
|
||||||
|
|
||||||
const authContent = JSON.stringify({
|
const authContent = JSON.stringify({
|
||||||
OPENAI_API_KEY: keyToUse
|
OPENAI_API_KEY: keyToUse
|
||||||
}, null, 2);
|
}, null, 2);
|
||||||
|
|
||||||
const copyToClipboard = async (text) => {
|
return [
|
||||||
try {
|
{
|
||||||
await navigator.clipboard.writeText(text);
|
filename: "~/.codex/config.toml",
|
||||||
setCopiedConfig(true);
|
content: configContent,
|
||||||
setTimeout(() => setCopiedConfig(false), 2000);
|
},
|
||||||
} catch (err) {
|
{
|
||||||
console.log("Failed to copy:", err);
|
filename: "~/.codex/auth.json",
|
||||||
}
|
content: authContent,
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -257,39 +260,12 @@ wire_api = "responses"
|
|||||||
<Button variant="outline" size="sm" onClick={handleResetSettings} disabled={!codexStatus.has9Router} loading={restoring}>
|
<Button variant="outline" size="sm" onClick={handleResetSettings} disabled={!codexStatus.has9Router} loading={restoring}>
|
||||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={checkCodexStatus}>
|
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
|
||||||
<span className="material-symbols-outlined text-[14px]">refresh</span>
|
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>Manual Config
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Manual Config Section */}
|
|
||||||
<div className="pt-4 border-t border-border flex flex-col gap-3">
|
|
||||||
<p className="text-xs text-text-muted">Or copy config manually:</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs font-medium text-text-main">~/.codex/config.toml</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(configContent)}>
|
|
||||||
<span className="material-symbols-outlined text-[14px] mr-1">{copiedConfig ? "check" : "content_copy"}</span>
|
|
||||||
{copiedConfig ? "Copied!" : "Copy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<pre className="px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs overflow-x-auto whitespace-pre-wrap break-all max-h-32 overflow-y-auto">{configContent}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs font-medium text-text-main">~/.codex/auth.json</span>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(authContent)}>
|
|
||||||
<span className="material-symbols-outlined text-[14px] mr-1">{copiedConfig ? "check" : "content_copy"}</span>
|
|
||||||
{copiedConfig ? "Copied!" : "Copy"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<pre className="px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs overflow-x-auto whitespace-pre-wrap break-all max-h-32 overflow-y-auto">{authContent}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -302,6 +278,13 @@ wire_api = "responses"
|
|||||||
modelAliases={modelAliases}
|
modelAliases={modelAliases}
|
||||||
title="Select Model for Codex"
|
title="Select Model for Codex"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ManualConfigModal
|
||||||
|
isOpen={showManualConfigModal}
|
||||||
|
onClose={() => setShowManualConfigModal(false)}
|
||||||
|
title="Codex CLI - Manual Configuration"
|
||||||
|
configs={getManualConfigs()}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/shared/components/ManualConfigModal.js
Normal file
46
src/shared/components/ManualConfigModal.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
import Button from "./Button";
|
||||||
|
|
||||||
|
export default function ManualConfigModal({ isOpen, onClose, title = "Manual Configuration", configs = [] }) {
|
||||||
|
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text, index) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopiedIndex(index);
|
||||||
|
setTimeout(() => setCopiedIndex(null), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Failed to copy:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} title={title} size="xl">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{configs.map((config, index) => (
|
||||||
|
<div key={index} className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-text-main">{config.filename}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => copyToClipboard(config.content, index)}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||||
|
{copiedIndex === index ? "check" : "content_copy"}
|
||||||
|
</span>
|
||||||
|
{copiedIndex === index ? "Copied!" : "Copy"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<pre className="px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs overflow-x-auto whitespace-pre-wrap break-all max-h-60 overflow-y-auto border border-border">
|
||||||
|
{config.content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export { default as Header } from "./Header";
|
|||||||
export { default as Footer } from "./Footer";
|
export { default as Footer } from "./Footer";
|
||||||
export { default as OAuthModal } from "./OAuthModal";
|
export { default as OAuthModal } from "./OAuthModal";
|
||||||
export { default as ModelSelectModal } from "./ModelSelectModal";
|
export { default as ModelSelectModal } from "./ModelSelectModal";
|
||||||
|
export { default as ManualConfigModal } from "./ManualConfigModal";
|
||||||
export { default as UsageStats } from "./UsageStats";
|
export { default as UsageStats } from "./UsageStats";
|
||||||
export { default as RequestLogger } from "./RequestLogger";
|
export { default as RequestLogger } from "./RequestLogger";
|
||||||
|
|
||||||
|
|||||||
@@ -61,19 +61,19 @@ export async function handleChat(request, clientRawRequest = null) {
|
|||||||
return handleComboChat({
|
return handleComboChat({
|
||||||
body,
|
body,
|
||||||
models: comboModels,
|
models: comboModels,
|
||||||
handleSingleModel: (b, m) => handleSingleModelChat(b, m, clientRawRequest),
|
handleSingleModel: (b, m) => handleSingleModelChat(b, m, clientRawRequest, request),
|
||||||
log
|
log
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single model request
|
// Single model request
|
||||||
return handleSingleModelChat(body, modelStr, clientRawRequest);
|
return handleSingleModelChat(body, modelStr, clientRawRequest, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle single model chat request
|
* Handle single model chat request
|
||||||
*/
|
*/
|
||||||
async function handleSingleModelChat(body, modelStr, clientRawRequest = null) {
|
async function handleSingleModelChat(body, modelStr, clientRawRequest = null, request = null) {
|
||||||
const modelInfo = await getModelInfo(modelStr);
|
const modelInfo = await getModelInfo(modelStr);
|
||||||
if (!modelInfo.provider) {
|
if (!modelInfo.provider) {
|
||||||
log.warn("CHAT", "Invalid model format", { model: modelStr });
|
log.warn("CHAT", "Invalid model format", { model: modelStr });
|
||||||
@@ -89,6 +89,9 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null) {
|
|||||||
log.info("ROUTING", `Provider: ${provider}, Model: ${model}`);
|
log.info("ROUTING", `Provider: ${provider}, Model: ${model}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract userAgent from request
|
||||||
|
const userAgent = request?.headers?.get("user-agent") || "";
|
||||||
|
|
||||||
// Try with available accounts (fallback on errors)
|
// Try with available accounts (fallback on errors)
|
||||||
let excludeConnectionId = null;
|
let excludeConnectionId = null;
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
@@ -121,6 +124,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null) {
|
|||||||
log,
|
log,
|
||||||
clientRawRequest,
|
clientRawRequest,
|
||||||
connectionId: credentials.connectionId,
|
connectionId: credentials.connectionId,
|
||||||
|
userAgent,
|
||||||
onCredentialsRefreshed: async (newCreds) => {
|
onCredentialsRefreshed: async (newCreds) => {
|
||||||
await updateProviderCredentials(credentials.connectionId, {
|
await updateProviderCredentials(credentials.connectionId, {
|
||||||
accessToken: newCreds.accessToken,
|
accessToken: newCreds.accessToken,
|
||||||
|
|||||||
Reference in New Issue
Block a user