🐈

VPSで「認証つきDify環境」を作る⑤:Difyアプリを「Customモデル」としてUIに出す

に公開

💡 この記事はシリーズ⑤です。
ここでは、Difyで作ったフロー(=アプリ)をChatbotUIの「モデル選択」に表示できるようにします。
もともとChatbotUIにはCustomルートの仕組みがあるので、そこへDifyを載せるイメージです。


⑤Difyアプリを「Customモデル」としてUIに出す

ここでは、Difyで作ったフロー(=アプリ)を、ChatbotUIの「モデル選択」に出せるようにします。もともと、ChatbotUIにはCustomルートを設定する用意がありますので、それを使えるようにしていくイメージです。

このあたりからは、ゴリゴリとコードを書き直していきますのでややこしくなります。頑張ってついてきてくださいね。

では、順番にいってみましょう!


1.Supabase のスキーマを拡張(最小識別子)

どのサービスに送るのか(OpenAI/Custom など)と、Difyの会話ID(続きの会話に使う)を保存するため、チャットの履歴テーブル public.chats に、送信先と会話IDを入れる欄を追加します。

Supabaseを開いて、ダッシュボード → 左メニュー SQL editor を開き、以下をコピペして実行(Run/Ctrl+Enter)します。

-- どのプロバイダに送るか(既存は openai 等)を保持
alter table public.chats
  add column if not exists provider text not null default 'openai';

-- Dify の会話継続に使う conversation_id を保持(初回は null)
alter table public.chats
  add column if not exists provider_thread_id text;

-- provider='dify' のレコードを 'custom' に更新
update public.chats
set provider = 'custom'
where provider = 'dify';

2.「Custom」を使える状態(有効化)にする~サーバからUIへお知らせ~

サーバ側で「Customを使いますよ」とUIに伝えるフラグを立てます。

app/api/keys/route.tsに一行追記します。

cd /opt/chatbot-ui
sudo nano /opt/chatbot-ui/app/api/keys/route.ts

赤字部分を一行追記します。

// app/api/keys/route.ts

import { isUsingEnvironmentKey } from "@/lib/envs"
import { createResponse } from "@/lib/server/server-utils"
import { EnvKey } from "@/types/key-type"
import { VALID_ENV_KEYS } from "@/types/valid-keys"

export async function GET() {
  const envKeyMap: Record<string, VALID_ENV_KEYS> = {
    azure: VALID_ENV_KEYS.AZURE_OPENAI_API_KEY,
    openai: VALID_ENV_KEYS.OPENAI_API_KEY,
    google: VALID_ENV_KEYS.GOOGLE_GEMINI_API_KEY,
    anthropic: VALID_ENV_KEYS.ANTHROPIC_API_KEY,
    mistral: VALID_ENV_KEYS.MISTRAL_API_KEY,
    groq: VALID_ENV_KEYS.GROQ_API_KEY,
    perplexity: VALID_ENV_KEYS.PERPLEXITY_API_KEY,
    openrouter: VALID_ENV_KEYS.OPENROUTER_API_KEY,

    openai_organization_id: VALID_ENV_KEYS.OPENAI_ORGANIZATION_ID,

    azure_openai_endpoint: VALID_ENV_KEYS.AZURE_OPENAI_ENDPOINT,
    azure_gpt_35_turbo_name: VALID_ENV_KEYS.AZURE_GPT_35_TURBO_NAME,
    azure_gpt_45_vision_name: VALID_ENV_KEYS.AZURE_GPT_45_VISION_NAME,
    azure_gpt_45_turbo_name: VALID_ENV_KEYS.AZURE_GPT_45_TURBO_NAME,
    azure_embeddings_name: VALID_ENV_KEYS.AZURE_EMBEDDINGS_NAME
  }

  const isUsingEnvKeyMap = Object.keys(envKeyMap).reduce<Record<string, boolean>>(
    (acc, provider) => {
      const key = envKeyMap[provider]
      if (key) acc[provider] = isUsingEnvironmentKey(key as EnvKey)
      return acc
    },
    {}
  )

+   isUsingEnvKeyMap.custom = true

  return createResponse({ isUsingEnvKeyMap }, 200)
}

3.「Customモデル一覧」を返すAPIを作る

新規ファイルを作成します。このファイルが、UIがDifyアプリ(=dify_appsテーブルの内容)を読んで一覧表示するための中核です。

ディレクトリ&ファイルを作成します。

sudo mkdir -p /opt/chatbot-ui/app/api/models/custom
sudo nano /opt/chatbot-ui/app/api/models/custom/route.ts

以下の内容を貼り付けしてください。

// app/api/models/custom/route.ts
import { NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabaseAdmin'

export const dynamic = 'force-dynamic'  // キャッシュ抑止(一覧が変わる可能性があるため)
export const runtime = 'nodejs'  //  Edge 実行を避ける

export async function GET() {
  const { data, error } = await supabaseAdmin
    .from('dify_apps')
    .select('slug, display_name')
    .eq('is_active', true)
    .order('display_name', { ascending: true })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  const models = (data ?? []).map((r) => ({
    id: r.slug,           // chats.model に入る値
    name: r.display_name, // UI表示名
  }))

  return NextResponse.json(models, { status: 200 })
}

4. 画面(UI)に「Customモデル」を出す

APIで取ってきた Customモデルの一覧を、既存のモデルリストに混ぜて、「モデル選択」に表示できるようにします。未登録のIDでも壊れないよう予防も入れます。8つのファイルを作成・編集して、最後にテストします。

①空のCustomリストを用意

【新規作成ファイル】lib/models/custom-llm-list.ts

custom 用の空リストを新規作成します。

cd /opt/chatbot-ui
sudo nano /opt/chatbot-ui/lib/models/custom-llm-list.ts

以下の内容をコピペしてください。

// /lib/models/custom-llm-list.ts
import type { LLM } from "@/types";
export const CUSTOM_LLM_LIST: LLM[] = [];

②総合リストにCustomを足す

【編集するファイル】lib/models/llm/llm-list.ts

CUSTOM_LLM_LIST を import → LLM_LIST と LLM_LIST_MAP に custom を追加します。

cd /opt/chatbot-ui
sudo nano /opt/chatbot-ui/lib/models/llm/llm-list.ts

「+」部分を3行追記します。

import { LLM } from "@/types"
import { ANTHROPIC_LLM_LIST } from "./anthropic-llm-list"
import { GOOGLE_LLM_LIST } from "./google-llm-list"
import { MISTRAL_LLM_LIST } from "./mistral-llm-list"
import { GROQ_LLM_LIST } from "./groq-llm-list"
import { OPENAI_LLM_LIST } from "./openai-llm-list"
import { PERPLEXITY_LLM_LIST } from "./perplexity-llm-list"
+ import { CUSTOM_LLM_LIST } from "../custom-llm-list"

export const LLM_LIST: LLM[] = [
  ...OPENAI_LLM_LIST,
  ...GOOGLE_LLM_LIST,
  ...MISTRAL_LLM_LIST,
  ...GROQ_LLM_LIST,
  ...PERPLEXITY_LLM_LIST,
+  ...CUSTOM_LLM_LIST,
  ...ANTHROPIC_LLM_LIST
]

export const LLM_LIST_MAP: Record<string, LLM[]> = {
  openai: OPENAI_LLM_LIST,
  azure: OPENAI_LLM_LIST,
  google: GOOGLE_LLM_LIST,
  mistral: MISTRAL_LLM_LIST,
  groq: GROQ_LLM_LIST,
  perplexity: PERPLEXITY_LLM_LIST,
+   custom: CUSTOM_LLM_LIST,
  anthropic: ANTHROPIC_LLM_LIST
}

③モデル取得の流れにCustomを組み込む

【編集するファイル】lib/models/fetch-models.ts

/api/keys の返り値で custom:true の時だけ /api/models/custom を呼ぶようにします。また、取得した {id, name} を LLM 型に直してリストへ追加します。あと、失敗してもアプリが落ちないように try/catch しておきます。

cd /opt/chatbot-ui
sudo nano /opt/chatbot-ui/lib/models/fetch-models.ts

「+」部分を追記します。

import { Tables } from "@/supabase/types"
import { LLM, LLMID, OpenRouterLLM } from "@/types"
import { toast } from "sonner"
import { LLM_LIST_MAP } from "./llm/llm-list"

export const fetchHostedModels = async (profile: Tables<"profiles">) => {
  try {
    const providers = ["google", "anthropic", "mistral", "groq", "perplexity"]

    if (profile.use_azure_openai) {
      providers.push("azure")
    } else {
      providers.push("openai")
    }

    const response = await fetch("/api/keys")

    if (!response.ok) {
      throw new Error(`Server is not responding.`)
    }

    const data = await response.json()

    let modelsToAdd: LLM[] = []

    for (const provider of providers) {
      let providerKey: keyof typeof profile

      if (provider === "google") {
        providerKey = "google_gemini_api_key"
      } else if (provider === "azure") {
        providerKey = "azure_openai_api_key"
      } else {
        providerKey = `${provider}_api_key` as keyof typeof profile
      }

      if (profile?.[providerKey] || data.isUsingEnvKeyMap[provider]) {
        const models = LLM_LIST_MAP[provider]

        if (Array.isArray(models)) {
          modelsToAdd.push(...models)
        }
      }
    }

+     if (data?.isUsingEnvKeyMap?.custom) {
+       try {
+         const res = await fetch("/api/models/custom", { cache: "no-store" })
+         if (res.ok) {
+           const customList: Array<{ id: string; name: string }> = await res.json()
+           const mapped: LLM[] = customList.map((m) => ({
+             modelId: m.id as LLMID,           // chats.model に入る値(= slug)
+             modelName: m.name || m.id,        // UI表示名
+             provider: "custom",               // ← ここが送信先の判定キー
+             hostedId: m.id,                   // 任意。IDをそのまま
+             platformLink: "https://www.example.com", // 表示用リンク(任意)
+             imageInput: false                 // Dify側の画像入力を使わない前提(使うなら true に)
+           }))
+           modelsToAdd.push(...mapped)
+         } else {
+           console.warn("Failed to fetch custom models: " + (await res.text()))
+         }
+       } catch (e) {
+         console.warn("Error fetching custom models: " + e)
+       }
+     }

    return {
      envKeyMap: data.isUsingEnvKeyMap,
      hostedModels: modelsToAdd
    }
  } catch (error) {
    console.warn("Error fetching hosted models: " + error)
  }
}

// --- 以下略 ---

④型(LLMID)をゆるくしてslugを受け入れる

【編集するファイル】types/llms.ts

export type CustomLLMID = string; を足し、LLMID に含めます。

cd /opt/chatbot-ui
sudo nano /opt/chatbot-ui/types/llms.ts

「+」部分を2行追記します。

import { ModelProvider } from "."

export type LLMID =
  | OpenAILLMID
  | GoogleLLMID
  | AnthropicLLMID
  | MistralLLMID
  | GroqLLMID
  | PerplexityLLMID
+   | CustomLLMID

// OpenAI Models (UPDATED 5/13/24)
export type OpenAILLMID =
  | "gpt-4o" // GPT-4o
  | "gpt-4-turbo-preview" // GPT-4 Turbo
  | "gpt-4-vision-preview" // GPT-4 Vision
  | "gpt-4" // GPT-4
  | "gpt-3.5-turbo" // Updated GPT-3.5 Turbo

// Google Models
export type GoogleLLMID =
  | "gemini-pro" // Gemini Pro
  | "gemini-pro-vision" // Gemini Pro Vision
  | "gemini-1.5-pro-latest" // Gemini 1.5 Pro
  | "gemini-1.5-flash" // Gemini 1.5 Flash

// Anthropic Models
export type AnthropicLLMID =
  | "claude-2.1" // Claude 2
  | "claude-instant-1.2" // Claude Instant
  | "claude-3-haiku-20240307" // Claude 3 Haiku
  | "claude-3-sonnet-20240229" // Claude 3 Sonnet
  | "claude-3-opus-20240229" // Claude 3 Opus
  | "claude-3-5-sonnet-20240620" // Claude 3.5 Sonnet

// Mistral Models
export type MistralLLMID =
  | "mistral-tiny" // Mistral Tiny
  | "mistral-small-latest" // Mistral Small
  | "mistral-medium-latest" // Mistral Medium
  | "mistral-large-latest" // Mistral Large

export type GroqLLMID =
  | "llama3-8b-8192" // LLaMA3-8b
  | "llama3-70b-8192" // LLaMA3-70b
  | "mixtral-8x7b-32768" // Mixtral-8x7b
  | "gemma-7b-it" // Gemma-7b IT

// Perplexity Models (UPDATED 1/31/24)
export type PerplexityLLMID =
  | "pplx-7b-online" // Perplexity Online 7B
  | "pplx-70b-online" // Perplexity Online 70B
  | "pplx-7b-chat" // Perplexity Chat 7B
  | "pplx-70b-chat" // Perplexity Chat 70B
  | "mixtral-8x7b-instruct" // Mixtral 8x7B Instruct
  | "mistral-7b-instruct" // Mistral 7B Instruct
  | "llama-2-70b-chat" // Llama2 70B Chat
  | "codellama-34b-instruct" // CodeLlama 34B Instruct
  | "codellama-70b-instruct" // CodeLlama 70B Instruct
  | "sonar-small-chat" // Sonar Small Chat
  | "sonar-small-online" // Sonar Small Online
  | "sonar-medium-chat" // Sonar Medium Chat
  | "sonar-medium-online" // Sonar Medium Online

+ // Dify(Custom)のモデルID(= slug)
+ export type CustomLLMID = string;


export interface LLM {
  modelId: LLMID
  modelName: string
  provider: ModelProvider
  hostedId: string
  platformLink: string
  imageInput: boolean
  pricing?: {
    currency: string
    unit: string
    inputCost: number
    outputCost?: number
  }
}

export interface OpenRouterLLM extends LLM {
  maxContext: number
}

⑤未登録IDの安全弁(上限値の既定)

【編集するファイル】lib/chat-setting-limits.ts

DEFAULT_LIMITS を用意して、未知のIDでもUIが壊れないようにします。代表例として support-bot を1件登録しておくのもOK。getChatSettingLimits(id) を作って、無ければ既定値を返します。

cd /opt/chatbot-ui
sudo nano /opt/chatbot-ui/lib/chat-setting-limits.ts

「+」部分を2ブロック追記します。

import { LLMID } from "@/types"

type ChatSettingLimits = {
  MIN_TEMPERATURE: number
  MAX_TEMPERATURE: number
  MAX_TOKEN_OUTPUT_LENGTH: number
  MAX_CONTEXT_LENGTH: number
}

+ // ★追加: 未登録モデル用の既定値(UIのスライダー等が未定義で壊れないように)
+ export const DEFAULT_LIMITS: ChatSettingLimits = {
+   MIN_TEMPERATURE: 0.0,
+   MAX_TEMPERATURE: 2.0,
+   MAX_TOKEN_OUTPUT_LENGTH: 4096,
+   MAX_CONTEXT_LENGTH: 128000
+ }

export const CHAT_SETTING_LIMITS: Record<LLMID, ChatSettingLimits> = {
  // ANTHROPIC MODELS

// --- 中略 ---

  "sonar-medium-online": {
    MIN_TEMPERATURE: 0.0,
    MAX_TEMPERATURE: 1.0,
    MAX_TOKEN_OUTPUT_LENGTH: 12000,
    MAX_CONTEXT_LENGTH: 12000
  },

+   // ★任意: Dify(Custom)用に代表の slug を1件だけ登録しておくと安心
+   //      (新しい slug は登録しなくても、下のフォールバックで動作します)
+   "support-bot": {
+     MIN_TEMPERATURE: 0.0,
+     MAX_TEMPERATURE: 2.0,
+     MAX_TOKEN_OUTPUT_LENGTH: 4096,
+     MAX_CONTEXT_LENGTH: 128000
+   }
+ }

+ // ★追加: 未登録モデルIDのときに既定値を返すヘルパ
+ export const getChatSettingLimits = (id: string): ChatSettingLimits => {
+   return CHAT_SETTING_LIMITS[id as keyof typeof CHAT_SETTING_LIMITS] ?? DEFAULT_LIMITS
+ }

⑥モデル選択で重複を消す

【編集するファイル】components/models/model-select.tsx

useMemo で同じIDが複数来た時に1つにまとめます。
components/models/model-select.tsxを修正

cd /opt/chatbot-ui
sudo nano components/models/model-select.tsx

「-」部分は削除し、「+」部分を追記します。

import { ChatbotUIContext } from "@/context/context"
import { LLM, LLMID, ModelProvider } from "@/types"
import { IconCheck, IconChevronDown } from "@tabler/icons-react"
- import { FC, useContext, useEffect, useRef, useState } from "react"
+ import { FC, useContext, useEffect, useMemo, useRef, useState } from "react"
import { Button } from "../ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuTrigger
} from "../ui/dropdown-menu"
import { Input } from "../ui/input"
import { Tabs, TabsList, TabsTrigger } from "../ui/tabs"
import { ModelIcon } from "./model-icon"
import { ModelOption } from "./model-option"

interface ModelSelectProps {
  selectedModelId: string
  onSelectModel: (modelId: LLMID) => void
}

export const ModelSelect: FC<ModelSelectProps> = ({
  selectedModelId,
  onSelectModel
}) => {
  const {
    profile,
    models,
    availableHostedModels,
    availableLocalModels,
    availableOpenRouterModels
  } = useContext(ChatbotUIContext)

  const inputRef = useRef<HTMLInputElement>(null)
  const triggerRef = useRef<HTMLButtonElement>(null)

  const [isOpen, setIsOpen] = useState(false)
  const [search, setSearch] = useState("")
  const [tab, setTab] = useState<"hosted" | "local">("hosted")

  useEffect(() => {
    if (isOpen) {
      setTimeout(() => {
        inputRef.current?.focus()
      }, 100) // FIX: hacky
    }
  }, [isOpen])

  const handleSelectModel = (modelId: LLMID) => {
    onSelectModel(modelId)
    setIsOpen(false)
  }

-   const allModels = [
-     ...models.map(model => ({
-       modelId: model.model_id as LLMID,
-       modelName: model.name,
-       provider: "custom" as ModelProvider,
-       hostedId: model.id,
-       platformLink: "",
-       imageInput: false
-     })),
-     ...availableHostedModels,
-     ...availableLocalModels,
-     ...availableOpenRouterModels
-   ]

+   const allModels = useMemo(() => {
+     const merged: LLM[] = [
+       ...availableHostedModels,
+       ...availableLocalModels,
+       ...availableOpenRouterModels
+     ]
+     const seen = new Set<string>()
+     return merged.filter(m => {
+       if (seen.has(m.modelId)) return false
+       seen.add(m.modelId)
+       return true
+     })
+   }, [availableHostedModels, availableLocalModels, availableOpenRouterModels])

  const groupedModels = allModels.reduce<Record<string, LLM[]>>(
    (groups, model) => {
 
// --- 以下略 ---

⑦プロフィール保存後、Customモデルも読み直す

【編集するファイル】components/utility/profile-settings.tsx

既存プロバイダと同じ流れで custom も扱い、/api/models/custom の結果を重複なく availableHostedModels に足すようにします。

cd /opt/chatbot-ui
sudo nano components/utility/profile-settings.tsx

「-」部分は削除し、「+」部分を追記します。

import { ChatbotUIContext } from "@/context/context"
import {
  PROFILE_CONTEXT_MAX,
  PROFILE_DISPLAY_NAME_MAX,
  PROFILE_USERNAME_MAX,
  PROFILE_USERNAME_MIN
} from "@/db/limits"
import { updateProfile } from "@/db/profile"
import { uploadProfileImage } from "@/db/storage/profile-images"
import { exportLocalStorageAsJSON } from "@/lib/export-old-data"
import { fetchOpenRouterModels } from "@/lib/models/fetch-models"
import { LLM_LIST_MAP } from "@/lib/models/llm/llm-list"
import { supabase } from "@/lib/supabase/browser-client"
import { cn } from "@/lib/utils"
- import { OpenRouterLLM, } from "@/types"
+ import { OpenRouterLLM, LLM } from "@/types"// ★追加: LLM を使うので import
import {
  IconCircleCheckFilled,
  IconCircleXFilled,
  IconFileDownload,
  IconLoader2,
  IconLogout,
  IconUser
} from "@tabler/icons-react"

// --- 中略 ---

    setProfile(updatedProfile)

    toast.success("Profile updated!")

    const providers = [
      "openai",
      "google",
      "azure",
      "anthropic",
      "mistral",
      "groq",
      "perplexity",
      "openrouter",
+       "custom" // ★追加
    ]

    providers.forEach(async provider => {
+       // ★追加: Custom(Dify) は envKeyMap.custom が true なら /api/models/custom を取得して反映
+       if (provider === "custom") {
+         if (envKeyMap["custom"]) {
+           try {
+             const res = await fetch("/api/models/custom", { cache: "no-store" })
+             if (res.ok) {
+               const customList: Array<{ id: string; name: string }> =
+                 await res.json()
+               const mapped: LLM[] = customList.map(m => ({
+                 modelId: m.id as any,
+                 modelName: m.name || m.id,
+                 provider: "custom",
+                 hostedId: m.id,
+                 platformLink: "https://www.example.com",
+                 imageInput: false
+               }))
+               setAvailableHostedModels(prev => {
+                 const seen = new Set(prev.map(p => p.modelId))
+                 const merged = [...prev]
+                 for (const m of mapped) {
+                   if (!seen.has(m.modelId)) {
+                     merged.push(m)
+                     seen.add(m.modelId)
+                   }
+                 }
+                 return merged
+               })
+             } else {
+               console.warn("Failed to fetch custom models", await res.text())
+             }
+           } catch (e) {
+             console.warn("Error fetching custom models:", e)
+           }
+         } else {
+           // env で custom が無効なら、custom を一覧から取り除く
+           setAvailableHostedModels(prev =>
+             prev.filter(m => m.provider !== "custom")
+           )
+         }
+         return // ★Custom の処理はここで終了。他プロバイダの既存ロジックには触れない
+       }


      let providerKey: keyof typeof profile

      if (provider === "google") {
        providerKey = "google_gemini_api_key"
      } else if (provider === "azure") {
        providerKey = "azure_openai_api_key"
      } else {
        providerKey = `${provider}_api_key` as keyof typeof profile
      }

// --- 以下略 ---

⑧Customでも壊れないように調整

【編集するファイル】components/ui/chat-settings-form.tsx

Custom(= Dify の slug)を選んだときも、温度や最大トークンなどの上限値スライダーが正常表示・保存できるようにします。

cd /opt/chatbot-ui
sudo nano components/ui/chat-settings-form.tsx

「-」部分は削除し、「+」部分を追記します。

"use client"

import { ChatbotUIContext } from "@/context/context"
- import { CHAT_SETTING_LIMITS } from "@/lib/chat-setting-limits"
+ import { getChatSettingLimits } from "@/lib/chat-setting-limits"
import { ChatSettings } from "@/types"
import { IconInfoCircle } from "@tabler/icons-react"
import { FC, useContext } from "react"
import { ModelSelect } from "../models/model-select"
import { AdvancedSettings } from "./advanced-settings"
import { Checkbox } from "./checkbox"
import { Label } from "./label"


// --- 中略 ---


const AdvancedContent: FC<AdvancedContentProps> = ({
  chatSettings,
  onChangeChatSettings,
  showTooltip
}) => {
  const { profile, selectedWorkspace, availableOpenRouterModels, models } =
    useContext(ChatbotUIContext)

  const isCustomModel = models.some(
    model => model.model_id === chatSettings.model
  )

  function findOpenRouterModel(modelId: string) {
    return availableOpenRouterModels.find(model => model.modelId === modelId)
  }

-   const MODEL_LIMITS = CHAT_SETTING_LIMITS[chatSettings.model] || {
-     MIN_TEMPERATURE: 0,
-     MAX_TEMPERATURE: 1,
-     MAX_CONTEXT_LENGTH:
-       findOpenRouterModel(chatSettings.model)?.maxContext || 4096
-   }

+   const _limits = getChatSettingLimits(chatSettings.model)
+   const MODEL_LIMITS = {
+     ..._limits,
+     MAX_CONTEXT_LENGTH:
+       findOpenRouterModel(chatSettings.model)?.maxContext ||
+       _limits.MAX_CONTEXT_LENGTH
+   }

  return (
    <div className="mt-5">
      <div className="space-y-3">

// --- 以下略 ---

⑨ビルド&テスト

ここまででいったん、ビルド&テストしてみましょう。

cd /opt/chatbot-ui
rm -rf .next
npm run build
npm run start -- -H 0.0.0.0 -p 3000

★チェックリスト

curl -s https://chat.example.com/api/keys | python3 -m json.tool

→ "custom": true を確認します。

curl -s https://chat.example.com/api/models/custom | python3 -m json.tool

→ [{ "id": "support-bot", "name": "..." }] のように出るはずです。

chat.example.com にブラウザでアクセスしてChatbotUIにログインします。
右上のモデル選択のところをクリックすると「テスト_チャットフロー」と表示されていてモデル選択できるようになっています。

ただ、この時点でモデルを選択しても会話はできません。
画面の「モデル選択」に Difyのdisplay_name が出て、選べる状態になっているところまでを確認して、次章で“送る先”をつなぎ、会話ができるようにします。


🚀 まとめ

これで、ChatbotUIのモデル選択画面にDifyのdisplay_nameが表示され、選択できる状態になりました。
次章では、実際に選択したモデルへメッセージを送信し、Dify経由で会話を成立させる部分を実装していきます。


🪄 次の記事:
[VPSで「認証つきDify環境」を作る⑥:SSEでDifyに中継して“会話できるUI”にする(予定)]

Discussion