Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions console/src/api/modules/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
ProviderConfigRequest,
ActiveModelsInfo,
GetActiveModelsRequest,
GetRoutingConfigRequest,
RoutingConfig,
ModelSlotRequest,
CreateCustomProviderRequest,
AddModelRequest,
Expand Down Expand Up @@ -38,6 +40,16 @@ function buildActiveModelQuery(params?: GetActiveModelsRequest): string {
return `/models/active?${searchParams.toString()}`;
}

function buildRoutingQuery(params?: GetRoutingConfigRequest): string {
if (!params?.agent_id) {
return "/config/agents/llm-routing";
}

const searchParams = new URLSearchParams();
searchParams.set("agent_id", params.agent_id);
return `/config/agents/llm-routing?${searchParams.toString()}`;
}

export const providerApi = {
listProviders: () => request<ProviderInfo[]>("/models"),

Expand All @@ -56,6 +68,15 @@ export const providerApi = {
body: JSON.stringify(body),
}),

getRoutingConfig: (params?: GetRoutingConfigRequest) =>
request<RoutingConfig>(buildRoutingQuery(params)),

setRoutingConfig: (body: RoutingConfig, params?: GetRoutingConfigRequest) =>
request<RoutingConfig>(buildRoutingQuery(params), {
method: "PUT",
body: JSON.stringify(body),
}),

/* ---- Custom provider CRUD ---- */

createCustomProvider: (body: CreateCustomProviderRequest) =>
Expand Down
13 changes: 13 additions & 0 deletions console/src/api/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ export interface ModelSlotConfig {
model: string;
}

export type RoutingMode = "local_first" | "cloud_first";

export interface RoutingConfig {
enabled: boolean;
mode: RoutingMode;
local: ModelSlotConfig;
cloud: ModelSlotConfig | null;
}

export interface ActiveModelsInfo {
active_llm?: ModelSlotConfig;
}
Expand All @@ -56,6 +65,10 @@ export interface GetActiveModelsRequest {
agent_id?: string;
}

export interface GetRoutingConfigRequest {
agent_id?: string;
}

export interface ModelSlotRequest {
provider_id: string;
model: string;
Expand Down
30 changes: 30 additions & 0 deletions console/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,21 @@
"noConfiguredModels": "No configured models",
"switchFailed": "Failed to switch model"
},
"chatRoutingSelector": {
"routing": "Routing",
"local": "Local",
"cloud": "Cloud",
"localFirst": "Local First",
"cloudFirst": "Cloud First",
"localProviders": "Local Providers",
"cloudProviders": "Cloud Providers",
"unconfigured": "Unconfigured",
"availableModels": "Available Models",
"configureSlotFirst": "Configure both Local and Cloud in Settings > Models first.",
"configureGlobalFirst": "Set up Local and Cloud in Settings",
"activeRouteSummary": "Local: {{local}} / Cloud: {{cloud}}",
"updateFailed": "Failed to update routing."
},
"tokenUsage": {
"title": "Token Usage",
"description": "View LLM token consumption over time, by date and model.",
Expand Down Expand Up @@ -1018,6 +1033,21 @@
"providerAvailable": "Ready (with models)",
"providerNoModels": "Not Ready (no models)",
"providerNotConfigured": "Not Ready (not configured)",
"routingTitle": "Routing",
"routingEnabled": "Enabled",
"routingMode": "Mode",
"routingModeLocal": "Local",
"routingModeCloud": "Cloud",
"routingLocalProvider": "Local Provider",
"routingLocalModel": "Local Model",
"routingCloudProvider": "Cloud Provider",
"routingCloudModel": "Cloud Model",
"routingSaved": "Routing mode saved.",
"routingDisabled": "Routing mode disabled.",
"routingConfigureSelectedSlot": "Configure the selected mode slot before enabling routing.",
"routingDistinctSlots": "Local and cloud slots must point to different provider/model pairs.",
"routingLocalMustBeLocal": "Local slot must use a local or loopback provider.",
"routingCloudMustBeCloud": "Cloud slot must use a non-local provider.",
"settings": "Settings",
"actions": "Actions",
"search": "Search",
Expand Down
30 changes: 30 additions & 0 deletions console/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,21 @@
"noConfiguredModels": "設定済みモデルがありません",
"switchFailed": "モデルの切り替えに失敗しました"
},
"chatRoutingSelector": {
"routing": "ルーティング",
"local": "ローカル",
"cloud": "クラウド",
"localFirst": "ローカル優先",
"cloudFirst": "クラウド優先",
"localProviders": "ローカルプロバイダー",
"cloudProviders": "クラウドプロバイダー",
"unconfigured": "未設定",
"availableModels": "利用可能なモデル",
"configureSlotFirst": "先に Settings > Models でローカルとクラウドの両方を設定してください。",
"configureGlobalFirst": "先に Settings でローカルとクラウドを設定してください",
"activeRouteSummary": "ローカル: {{local}} / クラウド: {{cloud}}",
"updateFailed": "ルーティングモードの更新に失敗しました。"
},
"models": {
"llmConfiguration": "LLM設定",
"providersTitle": "プロバイダー",
Expand Down Expand Up @@ -835,6 +850,21 @@
"searchPlaceholder": "プロバイダーを検索...",
"searchModelPlaceholder": "モデルを検索...",
"providerNotConfigured": "未準備(未設定)",
"routingTitle": "ルーティング",
"routingEnabled": "有効",
"routingMode": "モード",
"routingModeLocal": "ローカル",
"routingModeCloud": "クラウド",
"routingLocalProvider": "ローカルプロバイダー",
"routingLocalModel": "ローカルモデル",
"routingCloudProvider": "クラウドプロバイダー",
"routingCloudModel": "クラウドモデル",
"routingSaved": "ルーティングモードを保存しました。",
"routingDisabled": "ルーティングモードを無効にしました。",
"routingConfigureSelectedSlot": "ルーティングを有効にする前に、現在のモードのスロットを設定してください。",
"routingDistinctSlots": "ローカルスロットとクラウドスロットは異なる provider/model の組み合わせである必要があります。",
"routingLocalMustBeLocal": "ローカルスロットにはローカルまたはループバックの provider を使用してください。",
"routingCloudMustBeCloud": "クラウドスロットには非ローカル provider を使用してください。",
"settings": "設定",
"actions": "操作",
"custom": "カスタム",
Expand Down
30 changes: 30 additions & 0 deletions console/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,21 @@
"noConfiguredModels": "Нет настроенных моделей",
"switchFailed": "Не удалось переключить модель"
},
"chatRoutingSelector": {
"routing": "Маршрутизация",
"local": "Локально",
"cloud": "Облако",
"localFirst": "Сначала локально",
"cloudFirst": "Сначала облако",
"localProviders": "Локальные провайдеры",
"cloudProviders": "Облачные провайдеры",
"unconfigured": "Не настроено",
"availableModels": "Доступные модели",
"configureSlotFirst": "Сначала настройте и локальный, и облачный слоты в Settings > Models.",
"configureGlobalFirst": "Сначала настройте локальный и облачный слоты в Settings",
"activeRouteSummary": "Локально: {{local}} / Облако: {{cloud}}",
"updateFailed": "Не удалось обновить режим маршрутизации."
},
"models": {
"llmConfiguration": "Конфигурация LLM",
"providersTitle": "Провайдеры",
Expand Down Expand Up @@ -835,6 +850,21 @@
"searchPlaceholder": "Поиск провайдеров...",
"searchModelPlaceholder": "Поиск моделей...",
"providerNotConfigured": "Не готово (не настроено)",
"routingTitle": "Маршрутизация",
"routingEnabled": "Включено",
"routingMode": "Режим",
"routingModeLocal": "Локально",
"routingModeCloud": "Облако",
"routingLocalProvider": "Локальный провайдер",
"routingLocalModel": "Локальная модель",
"routingCloudProvider": "Облачный провайдер",
"routingCloudModel": "Облачная модель",
"routingSaved": "Режим маршрутизации сохранён.",
"routingDisabled": "Режим маршрутизации выключен.",
"routingConfigureSelectedSlot": "Перед включением маршрутизации настройте слот для выбранного режима.",
"routingDistinctSlots": "Локальный и облачный слоты должны указывать на разные пары provider/model.",
"routingLocalMustBeLocal": "Локальный слот должен использовать локального провайдера или loopback-адрес.",
"routingCloudMustBeCloud": "Облачный слот должен использовать нелокального провайдера.",
"settings": "Настройки",
"actions": "Действия",
"custom": "Пользовательский",
Expand Down
30 changes: 30 additions & 0 deletions console/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,21 @@
"noConfiguredModels": "暂无已配置模型",
"switchFailed": "切换模型失败"
},
"chatRoutingSelector": {
"routing": "路由",
"local": "本地",
"cloud": "云端",
"localFirst": "本地优先",
"cloudFirst": "云端优先",
"localProviders": "本地提供商",
"cloudProviders": "云端提供商",
"unconfigured": "未配置",
"availableModels": "可用模型",
"configureSlotFirst": "请先在 设置 > 模型 中同时配置本地和云端槽位。",
"configureGlobalFirst": "请先在设置页配好本地和云端",
"activeRouteSummary": "本地:{{local}} / 云端:{{cloud}}",
"updateFailed": "更新路由模式失败。"
},
"models": {
"llmConfiguration": "LLM 配置",
"providersTitle": "提供商",
Expand Down Expand Up @@ -880,6 +895,21 @@
"providerAvailable": "可用(有模型)",
"providerNoModels": "未就绪(无模型)",
"providerNotConfigured": "未就绪(未配置)",
"routingTitle": "路由",
"routingEnabled": "启用",
"routingMode": "模式",
"routingModeLocal": "本地",
"routingModeCloud": "云端",
"routingLocalProvider": "本地提供商",
"routingLocalModel": "本地模型",
"routingCloudProvider": "云端提供商",
"routingCloudModel": "云端模型",
"routingSaved": "路由模式已保存。",
"routingDisabled": "路由模式已关闭。",
"routingConfigureSelectedSlot": "启用路由前,请先配置当前模式对应的槽位。",
"routingDistinctSlots": "本地和云端槽位必须指向不同的 provider/model 组合。",
"routingLocalMustBeLocal": "本地槽位必须使用本地或回环地址 provider。",
"routingCloudMustBeCloud": "云端槽位必须使用非本地 provider。",
"settings": "设置",
"actions": "操作",
"search": "搜索",
Expand Down
142 changes: 142 additions & 0 deletions console/src/pages/Chat/ModelSelector/RoutingSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { CheckOutlined, RightOutlined } from "@ant-design/icons";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { ModelSlotConfig, RoutingMode } from "../../../api/types";
import styles from "./index.module.less";

export type SlotKind = "local" | "cloud";

interface EligibleProvider {
id: string;
name: string;
base_url?: string;
isLocal: boolean;
models: Array<{ id: string; name: string; is_free?: boolean }>;
}

interface RoutingSectionProps {
routingFeatureReady: boolean;
routingEnabled: boolean;
routingMode: RoutingMode;
effectiveLocalSlot: ModelSlotConfig | null;
effectiveCloudSlot: ModelSlotConfig | null;
localProviders: EligibleProvider[];
cloudProviders: EligibleProvider[];
renderProviderModelMenu: (
providersList: EligibleProvider[],
selectedSlot: ModelSlotConfig | null,
onSelect: (providerId: string, modelId: string) => void,
) => ReactNode;
onActivateRouting: (mode: RoutingMode) => void;
onSetSlot: (kind: SlotKind, providerId: string, modelId: string) => void;
}

export default function RoutingSection({
routingFeatureReady,
routingEnabled,
routingMode,
effectiveLocalSlot,
effectiveCloudSlot,
localProviders,
cloudProviders,
renderProviderModelMenu,
onActivateRouting,
onSetSlot,
}: RoutingSectionProps) {
const { t } = useTranslation();

const renderProviderGroup = (
kind: SlotKind,
providersList: EligibleProvider[],
selectedSlot: ModelSlotConfig | null,
title: string,
) => {
if (providersList.length === 0) {
return null;
}

return (
<>
<div className={styles.sectionLabel}>{title}</div>
{renderProviderModelMenu(
providersList,
selectedSlot,
(providerId, modelId) => {
onSetSlot(kind, providerId, modelId);
},
)}
</>
);
};

const renderRoutingModeItem = (mode: RoutingMode) => {
const isActive = routingEnabled && routingMode === mode;
const label =
mode === "local_first"
? t("chatRoutingSelector.localFirst", { defaultValue: "Local First" })
: t("chatRoutingSelector.cloudFirst", { defaultValue: "Cloud First" });

return (
<div
className={[
styles.providerItem,
isActive ? styles.providerItemActive : "",
].join(" ")}
onClick={(e) => {
e.stopPropagation();
onActivateRouting(mode);
}}
>
<span className={styles.providerName}>{label}</span>
{isActive && <CheckOutlined className={styles.checkIcon} />}
<RightOutlined className={styles.providerArrow} />

<div
className={`${styles.submenu} ${styles.routingSubmenu} modelSubmenu`}
>
{renderProviderGroup(
"local",
localProviders,
effectiveLocalSlot,
t("chatRoutingSelector.localProviders", {
defaultValue: "Local Providers",
}),
)}
{localProviders.length > 0 && cloudProviders.length > 0 ? (
<div className={styles.sectionDivider} />
) : null}
{renderProviderGroup(
"cloud",
cloudProviders,
effectiveCloudSlot,
t("chatRoutingSelector.cloudProviders", {
defaultValue: "Cloud Providers",
}),
)}
</div>
</div>
);
};

return (
<div
className={[
styles.section,
!routingFeatureReady ? styles.sectionDisabled : "",
].join(" ")}
>
<div className={styles.sectionLabel}>
{t("chatRoutingSelector.routing", { defaultValue: "Routing" })}
{!routingFeatureReady ? (
<span className={styles.sectionHint}>
{t("chatRoutingSelector.configureGlobalFirst", {
defaultValue: "Set up Local and Cloud in Settings",
})}
</span>
) : null}
</div>
{renderRoutingModeItem("local_first")}
{renderRoutingModeItem("cloud_first")}
</div>
);
}
Loading
Loading