mirror of
https://github.com/Nezumi-2711/9router-config-generate.git
synced 2026-09-22 13:38:32 +00:00
feat: add fetch model function
This commit is contained in:
+61
-16
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||||
import type { ToolId, ConnectionConfig } from './types'
|
import type { ToolId, ConnectionConfig, Model } from './types'
|
||||||
import { mockModels } from './data/mockModels'
|
import { mockModels } from './data/mockModels'
|
||||||
import { tools, toolList } from './data/tools'
|
import { tools, toolList } from './data/tools'
|
||||||
import { Header } from './components/Header'
|
import { Header } from './components/Header'
|
||||||
@@ -7,21 +7,56 @@ import { ConnectionForm } from './components/ConnectionForm'
|
|||||||
import { ModelList } from './components/ModelList'
|
import { ModelList } from './components/ModelList'
|
||||||
import { ToolSelector } from './components/ToolSelector'
|
import { ToolSelector } from './components/ToolSelector'
|
||||||
import { ConfigPreview } from './components/ConfigPreview'
|
import { ConfigPreview } from './components/ConfigPreview'
|
||||||
|
import { fetchRemoteModels } from './services/modelService'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [connection, setConnection] = useState<ConnectionConfig>({
|
const [connection, setConnection] = useState<ConnectionConfig>({
|
||||||
baseUrl: 'http://localhost:20128/v1',
|
baseUrl: 'https://9router.nezumi.pw/v1',
|
||||||
apiKey: '',
|
apiKey: 'sk-caa6204550dbdba8-h1x4oy-4f464660',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Default select 2 popular models
|
const [models, setModels] = useState<Model[]>(mockModels)
|
||||||
const [selectedModelIds, setSelectedModelIds] = useState<string[]>([
|
const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
|
||||||
'cc/claude-sonnet-4.5',
|
|
||||||
'openai/gpt-4o',
|
|
||||||
])
|
|
||||||
|
|
||||||
const [selectedToolId, setSelectedToolId] = useState<ToolId>('copilot')
|
const [selectedToolId, setSelectedToolId] = useState<ToolId>('copilot')
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [isLoadingModels, setIsLoadingModels] = useState(false)
|
||||||
|
const [fetchError, setFetchError] = useState<string | null>(null)
|
||||||
|
const [isLive, setIsLive] = useState(false)
|
||||||
|
|
||||||
|
const handleFetchModels = useCallback(
|
||||||
|
async (overrideConfig?: ConnectionConfig) => {
|
||||||
|
const cfg = overrideConfig || connection
|
||||||
|
if (!cfg.baseUrl) return
|
||||||
|
|
||||||
|
setIsLoadingModels(true)
|
||||||
|
setFetchError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetched = await fetchRemoteModels(cfg)
|
||||||
|
if (fetched.length > 0) {
|
||||||
|
setModels(fetched)
|
||||||
|
setIsLive(true)
|
||||||
|
// Default select the first 2-3 models if nothing selected or preserve existing matching IDs
|
||||||
|
setSelectedModelIds((prev) => {
|
||||||
|
const validPrev = prev.filter((id) => fetched.some((m) => m.id === id))
|
||||||
|
if (validPrev.length > 0) return validPrev
|
||||||
|
return fetched.slice(0, Math.min(3, fetched.length)).map((m) => m.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Unknown error'
|
||||||
|
setFetchError(message)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingModels(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[connection]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-fetch initial models on mount if valid baseUrl exists
|
||||||
|
useEffect(() => {
|
||||||
|
handleFetchModels()
|
||||||
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleToggleModel = (id: string) => {
|
const handleToggleModel = (id: string) => {
|
||||||
setSelectedModelIds((prev) =>
|
setSelectedModelIds((prev) =>
|
||||||
@@ -31,7 +66,7 @@ export function App() {
|
|||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
const q = searchQuery.toLowerCase().trim()
|
const q = searchQuery.toLowerCase().trim()
|
||||||
const visibleIds = mockModels
|
const visibleIds = models
|
||||||
.filter(
|
.filter(
|
||||||
(m) =>
|
(m) =>
|
||||||
!q ||
|
!q ||
|
||||||
@@ -48,7 +83,7 @@ export function App() {
|
|||||||
const handleDeselectAll = () => {
|
const handleDeselectAll = () => {
|
||||||
const q = searchQuery.toLowerCase().trim()
|
const q = searchQuery.toLowerCase().trim()
|
||||||
const visibleIds = new Set(
|
const visibleIds = new Set(
|
||||||
mockModels
|
models
|
||||||
.filter(
|
.filter(
|
||||||
(m) =>
|
(m) =>
|
||||||
!q ||
|
!q ||
|
||||||
@@ -64,8 +99,8 @@ export function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedModels = useMemo(() => {
|
const selectedModels = useMemo(() => {
|
||||||
return mockModels.filter((m) => selectedModelIds.includes(m.id))
|
return models.filter((m) => selectedModelIds.includes(m.id))
|
||||||
}, [selectedModelIds])
|
}, [models, selectedModelIds])
|
||||||
|
|
||||||
const activeTool = tools[selectedToolId] || tools.copilot
|
const activeTool = tools[selectedToolId] || tools.copilot
|
||||||
|
|
||||||
@@ -79,17 +114,27 @@ export function App() {
|
|||||||
<div className="flex flex-col gap-6 min-w-0">
|
<div className="flex flex-col gap-6 min-w-0">
|
||||||
<ConnectionForm
|
<ConnectionForm
|
||||||
connection={connection}
|
connection={connection}
|
||||||
onChange={setConnection}
|
onChange={(newConn) => {
|
||||||
|
setConnection(newConn)
|
||||||
|
setFetchError(null)
|
||||||
|
}}
|
||||||
|
onFetchModels={() => handleFetchModels()}
|
||||||
|
isLoading={isLoadingModels}
|
||||||
|
error={fetchError}
|
||||||
|
modelsCount={models.length}
|
||||||
|
isLive={isLive}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModelList
|
<ModelList
|
||||||
models={mockModels}
|
models={models}
|
||||||
selectedIds={selectedModelIds}
|
selectedIds={selectedModelIds}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={setSearchQuery}
|
onSearchChange={setSearchQuery}
|
||||||
onToggleModel={handleToggleModel}
|
onToggleModel={handleToggleModel}
|
||||||
onSelectAll={handleSelectAll}
|
onSelectAll={handleSelectAll}
|
||||||
onDeselectAll={handleDeselectAll}
|
onDeselectAll={handleDeselectAll}
|
||||||
|
isLoading={isLoadingModels}
|
||||||
|
isLive={isLive}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolSelector
|
<ToolSelector
|
||||||
|
|||||||
@@ -4,16 +4,26 @@ import { Input } from './ui/Input'
|
|||||||
import { Button } from './ui/Button'
|
import { Button } from './ui/Button'
|
||||||
import { Card } from './ui/Card'
|
import { Card } from './ui/Card'
|
||||||
import { Badge } from './ui/Badge'
|
import { Badge } from './ui/Badge'
|
||||||
import { Link2, Key, RefreshCw, Eye, EyeOff } from 'lucide-react'
|
import { Link2, Key, RefreshCw, Eye, EyeOff, AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
interface ConnectionFormProps {
|
interface ConnectionFormProps {
|
||||||
connection: ConnectionConfig
|
connection: ConnectionConfig
|
||||||
onChange: (connection: ConnectionConfig) => void
|
onChange: (connection: ConnectionConfig) => void
|
||||||
|
onFetchModels: () => void
|
||||||
|
isLoading?: boolean
|
||||||
|
error?: string | null
|
||||||
|
modelsCount?: number
|
||||||
|
isLive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionForm: React.FC<ConnectionFormProps> = ({
|
export const ConnectionForm: React.FC<ConnectionFormProps> = ({
|
||||||
connection,
|
connection,
|
||||||
onChange,
|
onChange,
|
||||||
|
onFetchModels,
|
||||||
|
isLoading = false,
|
||||||
|
error = null,
|
||||||
|
modelsCount = 0,
|
||||||
|
isLive = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [showKey, setShowKey] = useState(false)
|
const [showKey, setShowKey] = useState(false)
|
||||||
|
|
||||||
@@ -24,8 +34,8 @@ export const ConnectionForm: React.FC<ConnectionFormProps> = ({
|
|||||||
<h2 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0">
|
<h2 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 m-0">
|
||||||
1. Gateway Connection
|
1. Gateway Connection
|
||||||
</h2>
|
</h2>
|
||||||
<Badge variant="brand" size="sm">
|
<Badge variant={isLive ? 'success' : 'brand'} size="sm">
|
||||||
Local Router
|
{isLive ? 'Live Connected' : 'Local Router'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">OpenAI compatible</span>
|
<span className="text-xs text-zinc-500 dark:text-zinc-400">OpenAI compatible</span>
|
||||||
@@ -62,19 +72,39 @@ export const ConnectionForm: React.FC<ConnectionFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center gap-2 p-2.5 rounded-lg bg-rose-500/10 border border-rose-500/20 text-rose-600 dark:text-rose-400 text-xs">
|
||||||
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
<span className="truncate">{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-1">
|
<div className="flex items-center justify-between pt-1">
|
||||||
<div className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
<div className="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
<div
|
||||||
<span>Local mock catalog loaded</span>
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
isLive ? 'bg-emerald-500 animate-pulse' : 'bg-amber-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{isLive
|
||||||
|
? `Live connected (${modelsCount} models loaded)`
|
||||||
|
: 'Local mock catalog loaded'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled
|
onClick={onFetchModels}
|
||||||
icon={<RefreshCw className="w-3.5 h-3.5" />}
|
disabled={isLoading || !connection.baseUrl}
|
||||||
title="Live endpoint fetching will be available in next release"
|
icon={
|
||||||
|
<RefreshCw
|
||||||
|
className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Fetch models from the specified Base URL and API Key"
|
||||||
>
|
>
|
||||||
Fetch Models (Live)
|
{isLoading ? 'Fetching...' : 'Fetch Models'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Model } from '../types'
|
|||||||
import { Card } from './ui/Card'
|
import { Card } from './ui/Card'
|
||||||
import { Badge } from './ui/Badge'
|
import { Badge } from './ui/Badge'
|
||||||
import { Input } from './ui/Input'
|
import { Input } from './ui/Input'
|
||||||
import { Search, CheckSquare, Square, Eye, Wrench, Layers } from 'lucide-react'
|
import { Search, CheckSquare, Square, Eye, Wrench, Layers, RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
interface ModelListProps {
|
interface ModelListProps {
|
||||||
models: Model[]
|
models: Model[]
|
||||||
@@ -13,6 +13,8 @@ interface ModelListProps {
|
|||||||
onToggleModel: (id: string) => void
|
onToggleModel: (id: string) => void
|
||||||
onSelectAll: () => void
|
onSelectAll: () => void
|
||||||
onDeselectAll: () => void
|
onDeselectAll: () => void
|
||||||
|
isLoading?: boolean
|
||||||
|
isLive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelList: React.FC<ModelListProps> = ({
|
export const ModelList: React.FC<ModelListProps> = ({
|
||||||
@@ -23,6 +25,8 @@ export const ModelList: React.FC<ModelListProps> = ({
|
|||||||
onToggleModel,
|
onToggleModel,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onDeselectAll,
|
onDeselectAll,
|
||||||
|
isLoading = false,
|
||||||
|
isLive = false,
|
||||||
}) => {
|
}) => {
|
||||||
const filteredModels = useMemo(() => {
|
const filteredModels = useMemo(() => {
|
||||||
const q = searchQuery.toLowerCase().trim()
|
const q = searchQuery.toLowerCase().trim()
|
||||||
@@ -48,8 +52,13 @@ export const ModelList: React.FC<ModelListProps> = ({
|
|||||||
2. Select Models
|
2. Select Models
|
||||||
</h2>
|
</h2>
|
||||||
<Badge variant="brand" size="sm">
|
<Badge variant="brand" size="sm">
|
||||||
{selectedIds.length} selected
|
{selectedIds.length} / {models.length} selected
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{isLive && (
|
||||||
|
<Badge variant="success" size="sm">
|
||||||
|
Live
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -71,8 +80,13 @@ export const ModelList: React.FC<ModelListProps> = ({
|
|||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="max-h-72 overflow-y-auto pr-1 flex flex-col gap-2">
|
<div className="max-h-72 overflow-y-auto pr-1 flex flex-col gap-2 relative">
|
||||||
{filteredModels.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<div className="p-8 text-center text-xs text-zinc-500 dark:text-zinc-400 flex flex-col items-center justify-center gap-2 border border-zinc-200 dark:border-zinc-800 rounded-lg">
|
||||||
|
<RefreshCw className="w-5 h-5 animate-spin text-amber-500" />
|
||||||
|
<span>Fetching models from endpoint...</span>
|
||||||
|
</div>
|
||||||
|
) : filteredModels.length === 0 ? (
|
||||||
<div className="p-6 text-center text-xs text-zinc-500 dark:text-zinc-400 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-lg">
|
<div className="p-6 text-center text-xs text-zinc-500 dark:text-zinc-400 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-lg">
|
||||||
No models matching "{searchQuery}"
|
No models matching "{searchQuery}"
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import type { ConnectionConfig, Model } from '../types'
|
||||||
|
|
||||||
|
interface RawModelCapability {
|
||||||
|
vision?: boolean
|
||||||
|
tools?: boolean
|
||||||
|
reasoning?: boolean
|
||||||
|
contextWindow?: number
|
||||||
|
maxOutput?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawModelResponseItem {
|
||||||
|
id: string
|
||||||
|
object?: string
|
||||||
|
owned_by?: string
|
||||||
|
name?: string
|
||||||
|
capabilities?: RawModelCapability
|
||||||
|
context_length?: number
|
||||||
|
max_completion_tokens?: number
|
||||||
|
description?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawModelsResponse {
|
||||||
|
data?: RawModelResponseItem[]
|
||||||
|
object?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProvider(id: string, ownedBy?: string): string {
|
||||||
|
if (ownedBy) {
|
||||||
|
if (ownedBy === 'cx') return 'Codex / OpenAI'
|
||||||
|
if (ownedBy === 'ag') return 'Antigravity / Google'
|
||||||
|
if (ownedBy === 'anthropic' || ownedBy === 'cc') return 'Anthropic'
|
||||||
|
if (ownedBy === 'openai') return 'OpenAI'
|
||||||
|
return ownedBy.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = id.split('/')[0]?.toLowerCase()
|
||||||
|
if (prefix === 'cx') return 'Codex'
|
||||||
|
if (prefix === 'ag') return 'Antigravity'
|
||||||
|
if (prefix === 'cc') return 'Anthropic'
|
||||||
|
if (prefix === 'openai') return 'OpenAI'
|
||||||
|
if (prefix === 'vertex') return 'Google Vertex'
|
||||||
|
if (prefix === 'deepseek') return 'DeepSeek'
|
||||||
|
return prefix ? prefix.toUpperCase() : 'Custom'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatModelName(id: string, rawName?: string): string {
|
||||||
|
if (rawName && rawName !== id) return rawName
|
||||||
|
|
||||||
|
// Remove prefix if present, e.g. "cx/gpt-5.6-sol" -> "gpt-5.6-sol"
|
||||||
|
const cleanId = id.includes('/') ? id.split('/').slice(1).join('/') : id
|
||||||
|
|
||||||
|
return cleanId
|
||||||
|
.split('-')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRemoteModels(connection: ConnectionConfig): Promise<Model[]> {
|
||||||
|
const trimmedBase = connection.baseUrl.replace(/\/+$/, '')
|
||||||
|
const isHttpsOrCustom = trimmedBase.startsWith('http://') || trimmedBase.startsWith('https://')
|
||||||
|
|
||||||
|
let data: RawModelsResponse | null = null
|
||||||
|
|
||||||
|
// 1. Direct fetch with ?key= param if remote requires it or headers
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(`${trimmedBase}/models`)
|
||||||
|
if (connection.apiKey) {
|
||||||
|
urlObj.searchParams.set('key', connection.apiKey.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
const directRes = await fetch(urlObj.toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (directRes.ok) {
|
||||||
|
data = await directRes.json()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If direct CORS/network fails, fallback to standard Authorization header or proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If direct query-param fetch did not succeed, try standard Bearer header
|
||||||
|
if (!data) {
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
}
|
||||||
|
if (connection.apiKey?.trim()) {
|
||||||
|
headers['Authorization'] = `Bearer ${connection.apiKey.trim()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const directRes = await fetch(`${trimmedBase}/models`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (directRes.ok) {
|
||||||
|
data = await directRes.json()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Direct fetch failed (likely CORS on browser), proceed to fallback proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to local Vite / Worker proxy if running in dev or support environment
|
||||||
|
if (!data && isHttpsOrCustom) {
|
||||||
|
try {
|
||||||
|
const proxyRes = await fetch('/api/fetch-models', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
baseUrl: connection.baseUrl,
|
||||||
|
apiKey: connection.apiKey,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (proxyRes.ok) {
|
||||||
|
data = await proxyRes.json()
|
||||||
|
} else {
|
||||||
|
const errJson = await proxyRes.json().catch(() => null)
|
||||||
|
const errMsg = errJson?.error?.message || errJson?.error || proxyRes.statusText
|
||||||
|
throw new Error(`Failed to fetch models: ${errMsg}`)
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('Unable to connect to gateway models endpoint. Check your Base URL and API Key.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawList = Array.isArray(data) ? data : (data.data || [])
|
||||||
|
|
||||||
|
return rawList.map((item) => {
|
||||||
|
const caps = item.capabilities || {}
|
||||||
|
const contextWindow =
|
||||||
|
caps.contextWindow ||
|
||||||
|
item.context_length ||
|
||||||
|
(item.contextWindow as number) ||
|
||||||
|
undefined
|
||||||
|
|
||||||
|
const vision = caps.vision ?? (item.vision as boolean | undefined) ?? false
|
||||||
|
const toolCalling = caps.tools ?? caps.toolCalling ?? (item.toolCalling as boolean | undefined) ?? false
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name || formatModelName(item.id),
|
||||||
|
provider: parseProvider(item.id, item.owned_by),
|
||||||
|
family: item.id.split('/').pop()?.replace(/[^a-zA-Z0-9_-]/g, '-'),
|
||||||
|
contextWindow,
|
||||||
|
vision,
|
||||||
|
toolCalling,
|
||||||
|
description: item.description || (caps.reasoning ? 'Reasoning model with extended thinking capabilities' : undefined),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+55
-1
@@ -2,12 +2,66 @@ import { defineConfig } from 'vite'
|
|||||||
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
|
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
|
||||||
import babel from '@rolldown/plugin-babel'
|
import babel from '@rolldown/plugin-babel'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
react(),
|
react(),
|
||||||
babel({ presets: [reactCompilerPreset()] })
|
babel({ presets: [reactCompilerPreset()] }),
|
||||||
|
{
|
||||||
|
name: 'router-proxy-middleware',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use('/api/fetch-models', async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.statusCode = 405
|
||||||
|
res.end(JSON.stringify({ error: 'Method not allowed' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = ''
|
||||||
|
req.on('data', (chunk: Buffer) => {
|
||||||
|
body += chunk.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const { baseUrl, apiKey } = JSON.parse(body || '{}')
|
||||||
|
if (!baseUrl) {
|
||||||
|
res.statusCode = 400
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ error: 'baseUrl is required' }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUrl = `${baseUrl.replace(/\/+$/, '')}/models`
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey.trim()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteRes = await fetch(targetUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
const remoteData = await remoteRes.json().catch(() => null)
|
||||||
|
|
||||||
|
res.statusCode = remoteRes.status
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify(remoteData || {}))
|
||||||
|
} catch (err: unknown) {
|
||||||
|
res.statusCode = 500
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
const msg = err instanceof Error ? err.message : 'Internal error'
|
||||||
|
res.end(JSON.stringify({ error: msg }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user