mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
feat: add OllamaLocalExecutor and update provider handling
- Introduced OllamaLocalExecutor to handle requests for the "ollama-local" provider. - Removed the direct URL construction for "ollama-local" from BaseExecutor. - Updated index.js to include the new OllamaLocalExecutor in the executors mapping. - Enhanced the ProvidersPage component to support dynamic addition of OpenAI/Anthropic compatible providers.
This commit is contained in:
@@ -292,6 +292,58 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Providers (OpenAI/Anthropic Compatible) — dynamic */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
Custom Providers (OpenAI/Anthropic Compatible){" "}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:w-auto">
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => setShowAddAnthropicCompatibleModal(true)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Add Anthropic Compatible
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => setShowAddCompatibleModal(true)}
|
||||
className="w-full !bg-white !text-black hover:!bg-gray-100 sm:w-auto"
|
||||
>
|
||||
Add OpenAI Compatible
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{compatibleProviders.length === 0 &&
|
||||
anthropicCompatibleProviders.length === 0 ? (
|
||||
<div className="flex items-center justify-center gap-2 py-2 border border-dashed border-border rounded-xl text-text-muted text-sm">
|
||||
<span className="material-symbols-outlined text-[18px]">extension</span>
|
||||
<span>No custom providers — use buttons above to add OpenAI/Anthropic compatible endpoints</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{[...compatibleProviders, ...anthropicCompatibleProviders].map(
|
||||
(info) => (
|
||||
<ApiKeyProviderCard
|
||||
key={info.id}
|
||||
providerId={info.id}
|
||||
provider={info}
|
||||
stats={getProviderStats(info.id, "apikey")}
|
||||
authType="compatible"
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(info.id, "apikey", active)
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OAuth Providers */}
|
||||
{oauthEntries.length > 0 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -449,82 +501,6 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* API Key Compatible Providers — dynamic (OpenAI/Anthropic compatible) */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
API Key Compatible Providers{" "}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:w-auto">
|
||||
{/* {(compatibleProviders.length > 0 || anthropicCompatibleProviders.length > 0) && (
|
||||
<button
|
||||
onClick={() => handleBatchTest("compatible")}
|
||||
disabled={!!testingMode}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${testingMode === "compatible"
|
||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||
}`}
|
||||
title="Test all Compatible connections"
|
||||
>
|
||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "compatible" ? " animate-spin" : ""}`}>
|
||||
play_arrow
|
||||
</span>
|
||||
{testingMode === "compatible" ? "Testing..." : "Test All"}
|
||||
</button>
|
||||
)} */}
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => setShowAddAnthropicCompatibleModal(true)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Add Anthropic Compatible
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => setShowAddCompatibleModal(true)}
|
||||
className="w-full !bg-white !text-black hover:!bg-gray-100 sm:w-auto"
|
||||
>
|
||||
Add OpenAI Compatible
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{compatibleProviders.length === 0 &&
|
||||
anthropicCompatibleProviders.length === 0 ? (
|
||||
<div className="text-center py-8 border border-dashed border-border rounded-xl">
|
||||
<span className="material-symbols-outlined text-[32px] text-text-muted mb-2">
|
||||
extension
|
||||
</span>
|
||||
<p className="text-text-muted text-sm">
|
||||
No compatible providers added yet
|
||||
</p>
|
||||
<p className="text-text-muted text-xs mt-1">
|
||||
Use the buttons above to add OpenAI or Anthropic compatible
|
||||
endpoints
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{[...compatibleProviders, ...anthropicCompatibleProviders].map(
|
||||
(info) => (
|
||||
<ApiKeyProviderCard
|
||||
key={info.id}
|
||||
providerId={info.id}
|
||||
provider={info}
|
||||
stats={getProviderStats(info.id, "apikey")}
|
||||
authType="compatible"
|
||||
onToggle={(active) =>
|
||||
handleToggleProvider(info.id, "apikey", active)
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AddOpenAICompatibleModal
|
||||
isOpen={showAddCompatibleModal}
|
||||
onClose={() => setShowAddCompatibleModal(false)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ReactFlow,
|
||||
Handle,
|
||||
Position,
|
||||
Controls,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
@@ -250,13 +251,34 @@ export default function ProviderTopology({ providers = [], activeRequests = [],
|
||||
);
|
||||
|
||||
const rfInstance = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const fitOpts = { padding: 0.2, duration: 200 };
|
||||
const onInit = useCallback((instance) => {
|
||||
rfInstance.current = instance;
|
||||
setTimeout(() => instance.fitView({ padding: 0.3 }), 50);
|
||||
setTimeout(() => instance.fitView(fitOpts), 50);
|
||||
}, []);
|
||||
|
||||
// Re-fit on container resize
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (rfInstance.current) rfInstance.current.fitView(fitOpts);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
// Re-fit when node count/layout changes
|
||||
useEffect(() => {
|
||||
if (rfInstance.current) {
|
||||
const id = setTimeout(() => rfInstance.current.fitView(fitOpts), 50);
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
}, [nodes.length]);
|
||||
|
||||
return (
|
||||
<div className="h-[320px] w-full min-w-0 rounded-lg border border-border bg-bg-subtle/30 sm:h-[480px]">
|
||||
<div ref={containerRef} className="h-[320px] w-full min-w-0 rounded-lg border border-border bg-bg-subtle/30 sm:h-[480px]">
|
||||
{providers.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-text-muted text-sm">
|
||||
No providers connected
|
||||
@@ -268,18 +290,22 @@ export default function ProviderTopology({ providers = [], activeRequests = [],
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.3 }}
|
||||
fitViewOptions={fitOpts}
|
||||
minZoom={0.1}
|
||||
maxZoom={2}
|
||||
onInit={onInit}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
panOnDrag={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnDrag
|
||||
zoomOnScroll
|
||||
zoomOnPinch
|
||||
zoomOnDoubleClick
|
||||
preventScrolling={false}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
elementsSelectable={false}
|
||||
/>
|
||||
>
|
||||
<Controls showInteractive={false} />
|
||||
</ReactFlow>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user