This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


The following commit(s) were added to refs/heads/master by this push:
     new cf59a76  feat(channel): create a channel by picking its source
cf59a76 is described below

commit cf59a76b5b8f4f4466835ebad6721cf2fb6944b7
Author: Yang Luo <[email protected]>
AuthorDate: Sat Aug 22 16:37:38 2026 +0800

    feat(channel): create a channel by picking its source
---
 web/src/components/ChannelSourcePicker.tsx |  82 +++++++++
 web/src/lib/channels.ts                    |  62 +++++--
 web/src/locales/en/data.json               |  15 +-
 web/src/locales/zh/data.json               |  15 +-
 web/src/pages/ChannelListPage.tsx          | 279 +++++++++++++++--------------
 5 files changed, 288 insertions(+), 165 deletions(-)

diff --git a/web/src/components/ChannelSourcePicker.tsx 
b/web/src/components/ChannelSourcePicker.tsx
new file mode 100644
index 0000000..54e442c
--- /dev/null
+++ b/web/src/components/ChannelSourcePicker.tsx
@@ -0,0 +1,82 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {KeyRound, LogIn, Settings2} from "lucide-react";
+import i18next from "i18next";
+
+import {channelSources, customSource, subscriptionSource, type ChannelSource} 
from "@/lib/channels";
+
+/**
+ * The title and the line under it. A vendor card is named after the vendor and
+ * says where its requests go; the two cards that are not a vendor say what 
they
+ * are instead, because their base URL would not.
+ */
+export function sourceTitle(source: ChannelSource) {
+  if (source.key === subscriptionSource) {
+    return i18next.t("channel:Claude subscription");
+  }
+  if (source.key === customSource) {
+    return i18next.t("channel:Custom vendor");
+  }
+  return source.label;
+}
+
+function sourceDetail(source: ChannelSource) {
+  if (source.key === subscriptionSource) {
+    return i18next.t("channel:Claude subscription detail");
+  }
+  if (source.key === customSource) {
+    return i18next.t("channel:Custom vendor detail");
+  }
+  return source.channel.baseUrl ?? "";
+}
+
+function sourceIcon(source: ChannelSource) {
+  if (source.key === subscriptionSource) {
+    return LogIn;
+  }
+  if (source.key === customSource) {
+    return Settings2;
+  }
+  return KeyRound;
+}
+
+/**
+ * The first step of creating a channel: where its credentials come from. 
Picking
+ * a card fills in the type, base URL, models and authentication mode, so a
+ * subscription needs nothing typed and a vendor needs only its key.
+ */
+export function ChannelSourcePicker({onPick}: {onPick: (source: ChannelSource) 
=> void}) {
+  return (
+    <div className="grid gap-3 sm:grid-cols-2">
+      {channelSources.map(source => {
+        const Icon = sourceIcon(source);
+        return (
+          <button
+            key={source.key}
+            type="button"
+            onClick={() => onPick(source)}
+            className="hover:border-primary hover:bg-accent/40 flex flex-col 
items-start gap-1 rounded-lg border p-4 text-left transition-colors"
+          >
+            <span className="flex items-center gap-2 text-sm font-medium">
+              <Icon className="size-4 shrink-0" />
+              {sourceTitle(source)}
+            </span>
+            <span className="text-muted-foreground break-all 
text-xs">{sourceDetail(source)}</span>
+          </button>
+        );
+      })}
+    </div>
+  );
+}
diff --git a/web/src/lib/channels.ts b/web/src/lib/channels.ts
index 3fc8b1d..ed9788b 100644
--- a/web/src/lib/channels.ts
+++ b/web/src/lib/channels.ts
@@ -13,13 +13,15 @@
 // limitations under the License.
 
 import * as Setting from "@/Setting";
+import type {Channel} from "@/types";
 
 /** Mirrors object.ChannelProtocol on the server. */
 export function channelProtocol(type: string) {
   return type === "anthropic" ? "anthropic" : "openai";
 }
 
-/** Mirrors object.ChannelAuthClient on the server. */
+/** Mirrors object.ChannelAuthChannel and object.ChannelAuthClient on the 
server. */
+export const authChannel = "channel";
 export const authClient = "client";
 
 /** Mirrors object.UsesClientAuth: whose credentials reach the upstream. */
@@ -122,21 +124,53 @@ export const channelPresets: ChannelPreset[] = [
   },
 ];
 
+/** Where a new channel gets its credentials: one card of the picker. */
+export interface ChannelSource {
+  /** Stable id. The two cards that are not a vendor are titled by the picker. 
*/
+  key: string;
+  /** The vendor's own name, empty when the picker titles the card itself. */
+  label: string;
+  /** What the form starts from once the card is picked. */
+  channel: Partial<Channel>;
+}
+
+export const subscriptionSource = "subscription";
+export const customSource = "custom";
+
 /**
- * What a channel starts from when it is meant to forward the caller's own
- * login. Anthropic is the vendor of the clients that mode exists for; Codex
- * signs in against an API Gateway does not relay.
+ * The sign-in comes first: it is the only source that needs nothing filled in,
+ * and the one people with a subscription and no API key are here for. 
Anthropic
+ * is the vendor of the clients that mode works with; Codex signs in against an
+ * API Gateway does not relay.
  */
-export function clientAuthDefaults() {
-  const preset = channelPresets.find(item => item.type === "anthropic");
-  return {
-    type: "anthropic",
-    baseUrl: preset?.baseUrl ?? "",
-    models: [] as string[],
-    apiKey: "",
-    authMode: authClient,
-  };
-}
+export const channelSources: ChannelSource[] = [
+  {
+    key: subscriptionSource,
+    label: "",
+    channel: {
+      type: "anthropic",
+      baseUrl: channelPresets.find(preset => preset.type === 
"anthropic")?.baseUrl ?? "",
+      models: [],
+      apiKey: "",
+      authMode: authClient,
+    },
+  },
+  ...channelPresets.map(preset => ({
+    key: preset.label.toLowerCase(),
+    label: preset.label,
+    channel: {
+      type: preset.type,
+      baseUrl: preset.baseUrl,
+      models: preset.models,
+      authMode: authChannel,
+    },
+  })),
+  {
+    key: customSource,
+    label: "",
+    channel: {type: "custom", baseUrl: "", models: [], authMode: authChannel},
+  },
+];
 
 /** The base URLs and models offered for a channel type, from the vendors of 
it. */
 export function baseUrlPresets(type: string) {
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 4a88a95..cef1bb3 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -281,18 +281,23 @@
     "Base URL": "Base URL",
     "Base URL hint": "Requests will be sent to",
     "Caller's own login": "The caller's own login",
+    "Change source": "Change",
     "Channel": "Channel",
     "Channel added successfully": "Channel added successfully",
     "Channel deleted successfully": "Channel deleted successfully",
     "Channel not found": "Channel not found",
     "Channel saved": "Channel saved",
     "Channels": "Channels",
+    "Claude subscription": "Claude subscription",
+    "Claude subscription detail": "The sign-in Claude Code already has, 
forwarded upstream. No API key, nothing to fill in. Codex is the exception: it 
signs in against an API Gateway does not relay.",
     "Clear": "Clear",
     "Client auth hint": "Requests are forwarded with the credentials the 
client sent, so an agent already signed in with a subscription keeps its own 
login and no key is stored here. Codex is the exception: its ChatGPT sign-in 
speaks a different API.",
     "Client auth usage hint": "Only the base URL is set: a token here would 
replace the sign-in the client already has.",
     "Connection Failed": "Connection Failed",
     "Connection Successful": "Connection Successful",
     "Cooling down": "Cooling down",
+    "Custom vendor": "Custom",
+    "Custom vendor detail": "Any OpenAI- or Anthropic-compatible API. You give 
it a base URL and a key.",
     "Disabled": "Disabled",
     "Edit Channel": "Edit Channel",
     "Enabled": "Enabled",
@@ -321,18 +326,14 @@
     "Recovering": "Recovering",
     "Retried at": "retried at",
     "Select all": "Select all",
-    "Start from a sign-in": "Use the sign-in my agent has",
-    "Start from a sign-in detail": "No API key. Every request keeps the 
credentials the agent sent, so a ChatGPT or Claude subscription goes on 
working. Codex is the exception: it signs in against an API Gateway does not 
relay.",
-    "Start from an API key": "Paste a vendor API key",
-    "Start from an API key detail": "Pick a vendor, paste its key, and Gateway 
holds it from then on instead of the agent.",
+    "Source": "Source",
+    "Source hint": "Where this channel gets its credentials. Picking one fills 
in the type, base URL and models, so a subscription needs nothing typed and a 
vendor needs only its key.",
     "Status": "Status",
     "Stored API key": "Stored API key",
     "Test Connectivity": "Test Connectivity",
     "Type": "Type",
     "Usage": "Usage",
-    "Usage hint": "Point any client that speaks this channel's API at Gateway 
with these variables.",
-    "Vendor": "Vendor",
-    "Vendor hint": "Fills in the type, base URL and models. Everything stays 
editable."
+    "Usage hint": "Point any client that speaks this channel's API at Gateway 
with these variables."
   },
   "general": {
     "Action": "Action",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index 66955b0..38941f6 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -281,18 +281,23 @@
     "Base URL": "基础 URL",
     "Base URL hint": "实际请求将发往",
     "Caller's own login": "调用方自己的登录",
+    "Change source": "更换",
     "Channel": "渠道",
     "Channel added successfully": "渠道添加成功",
     "Channel deleted successfully": "渠道删除成功",
     "Channel not found": "渠道不存在",
     "Channel saved": "渠道已保存",
     "Channels": "渠道",
+    "Claude subscription": "Claude 订阅登录",
+    "Claude subscription detail": "直接用 Claude Code 已有的登录,原样转发给上游。不需要 API 
Key,什么都不用填。Codex 除外:它的登录所对的 API 不在 Gateway 的转发范围内。",
     "Clear": "清空",
     "Client auth hint": "请求会带着客户端自己的凭据转发上游,已经用订阅登录过的 Agent 可以继续用自己的登录,这里不保存任何 
Key。Codex 除外:它的 ChatGPT 登录走的是另一套 API。",
     "Client auth usage hint": "只设置 base URL:这里再设一个 token 会覆盖客户端已有的登录。",
     "Connection Failed": "连接失败",
     "Connection Successful": "连接成功",
     "Cooling down": "冷却中",
+    "Custom vendor": "自定义",
+    "Custom vendor detail": "任何兼容 OpenAI 或 Anthropic 的 API,由你填写 base URL 和 
Key。",
     "Disabled": "已禁用",
     "Edit Channel": "编辑渠道",
     "Enabled": "已启用",
@@ -321,18 +326,14 @@
     "Recovering": "恢复中",
     "Retried at": "将于",
     "Select all": "全选",
-    "Start from a sign-in": "沿用 Agent 已有的登录",
-    "Start from a sign-in detail": "不需要 API Key。每个请求都带着 Agent 自己发来的凭据,ChatGPT 
或 Claude 订阅照常可用。Codex 除外:它登录的那套 API 不在 Gateway 的转发范围内。",
-    "Start from an API key": "填一个厂商的 API Key",
-    "Start from an API key detail": "选厂商、填 Key,之后这把 Key 由 Gateway 持有,Agent 
拿不到。",
+    "Source": "来源",
+    "Source hint": "这个 Channel 的凭据从哪里来。选一个就会自动填好类型、base URL 
和模型:订阅登录什么都不用填,厂商只用填一个 Key。",
     "Status": "状态",
     "Stored API key": "Gateway 保存的 API Key",
     "Test Connectivity": "测试连接",
     "Type": "类型",
     "Usage": "使用方法",
-    "Usage hint": "任何说这个渠道所用 API 的客户端,配上下面的变量就能走网关。",
-    "Vendor": "厂商",
-    "Vendor hint": "一键填好类型、base URL 和模型,填完还可以改。"
+    "Usage hint": "任何说这个渠道所用 API 的客户端,配上下面的变量就能走网关。"
   },
   "general": {
     "Action": "操作",
diff --git a/web/src/pages/ChannelListPage.tsx 
b/web/src/pages/ChannelListPage.tsx
index eef2e78..8aaf31c 100644
--- a/web/src/pages/ChannelListPage.tsx
+++ b/web/src/pages/ChannelListPage.tsx
@@ -14,12 +14,13 @@
 
 import * as React from "react";
 import {Link, useNavigate} from "react-router-dom";
-import {CircleCheck, CircleX, KeyRound, LogIn, Pencil, Plug, Plus, RefreshCw, 
Trash2} from "lucide-react";
+import {ChevronDown, CircleCheck, CircleX, Pencil, Plug, Plus, RefreshCw, 
Trash2} from "lucide-react";
 import i18next from "i18next";
 
 import * as ChannelBackend from "@/backend/ChannelBackend";
 import * as Setting from "@/Setting";
 import {ChannelModelsField} from "@/components/ChannelModelsField";
+import {ChannelSourcePicker, sourceTitle} from 
"@/components/ChannelSourcePicker";
 import {ConfirmDialog} from "@/components/shared/confirm-dialog";
 import {DataTable, type Column, type SortOrder} from 
"@/components/shared/data-table";
 import {Field, FormDialog} from "@/components/shared/form-dialog";
@@ -33,56 +34,58 @@ import {Card, CardContent, CardDescription, CardHeader, 
CardTitle} from "@/compo
 import {Input} from "@/components/ui/input";
 import {SimpleTooltip} from "@/components/ui/tooltip";
 import {
+  authChannel,
   authClient,
   baseUrlPlaceholder,
   baseUrlPresets,
-  channelPresets,
-  clientAuthDefaults,
+  customSource,
   usesClientAuth,
-  type ChannelPreset,
+  type ChannelSource,
 } from "@/lib/channels";
+import {cn} from "@/lib/utils";
 import type {Account, Channel, ChannelHealth} from "@/types";
 
-function newChannel(owner: string): Channel {
+function newChannel(owner: string, label = "New Channel"): Channel {
   const randomName = Setting.getRandomName();
   return {
     owner: owner,
     name: `channel_${randomName}`,
-    displayName: `New Channel - ${randomName}`,
+    displayName: `${label} - ${randomName}`,
     type: "openai",
     status: "enabled",
     models: [],
     priority: 0,
     baseUrl: "",
     apiKey: "",
-    authMode: "channel",
+    authMode: authChannel,
   };
 }
 
-/** One of the ways to get a first channel, offered while the list is empty. */
-function StartOption({
-  icon: Icon,
-  title,
-  description,
-  onClick,
-}: {
-  icon: React.ComponentType<{className?: string}>;
-  title: string;
-  description: string;
-  onClick: () => void;
-}) {
+/** What a picked source leaves to fill in, which for a subscription is 
nothing. */
+function channelFromSource(owner: string, source: ChannelSource): Channel {
+  return {...newChannel(owner, sourceTitle(source)), ...source.channel};
+}
+
+/**
+ * The fields the source already answered. They stay reachable — a preset is a
+ * starting point, not a lock — but out of the way of the one field, if any,
+ * that is actually left to fill in.
+ */
+function Advanced({defaultOpen, children}: {defaultOpen: boolean; children: 
React.ReactNode}) {
+  const [open, setOpen] = React.useState(defaultOpen);
+
   return (
-    <button
-      type="button"
-      onClick={onClick}
-      className="hover:border-primary hover:bg-accent/40 flex flex-col 
items-start gap-1 rounded-lg border p-4 text-left transition-colors"
-    >
-      <span className="flex items-center gap-2 text-sm font-medium">
-        <Icon className="size-4" />
-        {title}
-      </span>
-      <span className="text-muted-foreground text-xs">{description}</span>
-    </button>
+    <div className="grid gap-4">
+      <button
+        type="button"
+        onClick={() => setOpen(!open)}
+        className="text-muted-foreground hover:text-foreground flex 
items-center gap-1 text-sm"
+      >
+        <ChevronDown className={cn("size-3.5 transition-transform", open && 
"rotate-180")} />
+        {i18next.t("general:Advanced")}
+      </button>
+      {open ? children : null}
+    </div>
   );
 }
 
@@ -101,6 +104,8 @@ export default function ChannelListPage({account}: 
{account: Account}) {
   const [addOpen, setAddOpen] = React.useState(false);
   const [adding, setAdding] = React.useState(false);
   const [form, setForm] = React.useState<Channel>(() => 
newChannel(account.name));
+  // null while the dialog is still asking where the credentials come from.
+  const [source, setSource] = React.useState<ChannelSource | null>(null);
   const [nameError, setNameError] = React.useState("");
   const [health, setHealth] = React.useState<ChannelHealth[]>([]);
 
@@ -155,8 +160,9 @@ export default function ChannelListPage({account}: 
{account: Account}) {
     return () => clearInterval(interval);
   }, []);
 
-  const openAddDialog = (start?: Partial<Channel>) => {
-    setForm({...newChannel(account.name), ...start});
+  const openAddDialog = (start?: ChannelSource) => {
+    setForm(start ? channelFromSource(account.name, start) : 
newChannel(account.name));
+    setSource(start ?? null);
     setNameError("");
     setAddOpen(true);
   };
@@ -165,15 +171,10 @@ export default function ChannelListPage({account}: 
{account: Account}) {
     setForm(prev => ({...prev, [key]: value}));
   };
 
-  const applyPreset = (preset: ChannelPreset) => {
-    setForm(prev => ({
-      ...prev,
-      type: preset.type,
-      baseUrl: preset.baseUrl,
-      // A client-auth channel takes any model, so the vendor's list would only
-      // narrow it.
-      models: usesClientAuth(prev) ? prev.models : preset.models,
-    }));
+  const pickSource = (picked: ChannelSource) => {
+    setForm(channelFromSource(account.name, picked));
+    setNameError("");
+    setSource(picked);
   };
 
   const addChannel = () => {
@@ -394,19 +395,8 @@ export default function ChannelListPage({account}: 
{account: Account}) {
             </CardTitle>
             <CardDescription>{i18next.t("channel:No channels yet 
detail")}</CardDescription>
           </CardHeader>
-          <CardContent className="grid gap-3 sm:grid-cols-2">
-            <StartOption
-              icon={LogIn}
-              title={i18next.t("channel:Start from a sign-in")}
-              description={i18next.t("channel:Start from a sign-in detail")}
-              onClick={() => openAddDialog(clientAuthDefaults())}
-            />
-            <StartOption
-              icon={KeyRound}
-              title={i18next.t("channel:Start from an API key")}
-              description={i18next.t("channel:Start from an API key detail")}
-              onClick={() => openAddDialog()}
-            />
+          <CardContent>
+            <ChannelSourcePicker onPick={openAddDialog} />
           </CardContent>
         </Card>
       ) : (
@@ -439,95 +429,110 @@ export default function ChannelListPage({account}: 
{account: Account}) {
         open={addOpen}
         onOpenChange={setAddOpen}
         title={i18next.t("channel:New Channel")}
+        description={source === null ? i18next.t("channel:Source hint") : 
undefined}
+        size={source === null ? "lg" : "default"}
         submitting={adding}
         onSubmit={addChannel}
+        // Nothing is filled in yet while the source is still being picked, so
+        // there is nothing to submit.
+        footer={
+          source === null ? (
+            <Button type="button" variant="outline" onClick={() => 
setAddOpen(false)}>
+              {i18next.t("general:Cancel")}
+            </Button>
+          ) : undefined
+        }
       >
-        <Field label={i18next.t("general:Name")} htmlFor="channel-name" 
required error={nameError}>
-          <Input
-            id="channel-name"
-            value={form.name}
-            onChange={event => {
-              setFormField("name", event.target.value);
-              setNameError("");
-            }}
-          />
-        </Field>
-        <Field label={i18next.t("general:Display name")} 
htmlFor="channel-display-name">
-          <Input
-            id="channel-display-name"
-            value={form.displayName}
-            onChange={event => setFormField("displayName", event.target.value)}
-          />
-        </Field>
-        <Field label={i18next.t("channel:Vendor")} 
hint={i18next.t("channel:Vendor hint")}>
-          <div className="flex flex-wrap gap-2">
-            {channelPresets.map(preset => (
-              <Button
-                key={preset.label}
-                type="button"
-                size="sm"
-                variant={form.baseUrl === preset.baseUrl ? "default" : 
"outline"}
-                onClick={() => applyPreset(preset)}
+        {source === null ? (
+          <ChannelSourcePicker onPick={pickSource} />
+        ) : (
+          <>
+            <Field label={i18next.t("channel:Source")}>
+              <div className="flex flex-wrap items-center gap-2">
+                <Badge variant="info">{sourceTitle(source)}</Badge>
+                {usesClientAuth(form) ? (
+                  <Badge variant="muted">{i18next.t("channel:Caller's own 
login")}</Badge>
+                ) : null}
+                <Button type="button" size="xs" variant="ghost" onClick={() => 
setSource(null)}>
+                  {i18next.t("channel:Change source")}
+                </Button>
+              </div>
+            </Field>
+            <Field label={i18next.t("general:Name")} htmlFor="channel-name" 
required error={nameError}>
+              <Input
+                id="channel-name"
+                value={form.name}
+                onChange={event => {
+                  setFormField("name", event.target.value);
+                  setNameError("");
+                }}
+              />
+            </Field>
+            <Field label={i18next.t("general:Display name")} 
htmlFor="channel-display-name">
+              <Input
+                id="channel-display-name"
+                value={form.displayName}
+                onChange={event => setFormField("displayName", 
event.target.value)}
+              />
+            </Field>
+            {usesClientAuth(form) ? null : (
+              <Field
+                label={i18next.t("channel:API Key")}
+                htmlFor="channel-api-key"
+                hint={i18next.t("channel:API Key ownership hint")}
               >
-                {preset.label}
-              </Button>
-            ))}
-          </div>
-        </Field>
-        <Field label={i18next.t("channel:Type")}>
-          <SimpleSelect
-            value={form.type}
-            onChange={value => setFormField("type", value)}
-            options={[
-              {label: "OpenAI", value: "openai"},
-              {label: "Anthropic", value: "anthropic"},
-              {label: "Custom", value: "custom"},
-            ]}
-          />
-        </Field>
-        <Field label={i18next.t("channel:Base URL")} 
htmlFor="channel-base-url">
-          <SearchSelect
-            allowCustomValue
-            id="channel-base-url"
-            value={form.baseUrl}
-            placeholder={baseUrlPlaceholder(form.type)}
-            options={baseUrlPresets(form.type)}
-            onChange={value => setFormField("baseUrl", value)}
-          />
-        </Field>
-        <Field
-          label={i18next.t("channel:Authentication")}
-          hint={usesClientAuth(form) ? i18next.t("channel:Client auth hint") : 
undefined}
-        >
-          <SimpleSelect
-            value={form.authMode}
-            // The stored key is meaningless once the caller's own is 
forwarded.
-            onChange={value => setForm(prev => ({...prev, authMode: value, 
apiKey: ""}))}
-            options={[
-              {label: i18next.t("channel:Stored API key"), value: "channel"},
-              {label: i18next.t("channel:Caller's own login"), value: 
authClient},
-            ]}
-          />
-        </Field>
-        {usesClientAuth(form) ? null : (
-          <Field
-            label={i18next.t("channel:API Key")}
-            htmlFor="channel-api-key"
-            hint={i18next.t("channel:API Key ownership hint")}
-          >
-            <PasswordInput
-              id="channel-api-key"
-              placeholder="sk-..."
-              value={form.apiKey}
-              onChange={event => setFormField("apiKey", event.target.value)}
+                <PasswordInput
+                  id="channel-api-key"
+                  placeholder="sk-..."
+                  value={form.apiKey}
+                  onChange={event => setFormField("apiKey", 
event.target.value)}
+                />
+              </Field>
+            )}
+            <ChannelModelsField
+              channel={form}
+              hint={usesClientAuth(form) ? i18next.t("channel:Any model hint") 
: i18next.t("channel:Models hint")}
+              onChange={value => setFormField("models", value)}
             />
-          </Field>
+            <Advanced key={source.key} defaultOpen={source.key === 
customSource}>
+              <Field label={i18next.t("channel:Type")}>
+                <SimpleSelect
+                  value={form.type}
+                  onChange={value => setFormField("type", value)}
+                  options={[
+                    {label: "OpenAI", value: "openai"},
+                    {label: "Anthropic", value: "anthropic"},
+                    {label: "Custom", value: "custom"},
+                  ]}
+                />
+              </Field>
+              <Field label={i18next.t("channel:Base URL")} 
htmlFor="channel-base-url">
+                <SearchSelect
+                  allowCustomValue
+                  id="channel-base-url"
+                  value={form.baseUrl}
+                  placeholder={baseUrlPlaceholder(form.type)}
+                  options={baseUrlPresets(form.type)}
+                  onChange={value => setFormField("baseUrl", value)}
+                />
+              </Field>
+              <Field
+                label={i18next.t("channel:Authentication")}
+                hint={usesClientAuth(form) ? i18next.t("channel:Client auth 
hint") : undefined}
+              >
+                <SimpleSelect
+                  value={form.authMode}
+                  // The stored key is meaningless once the caller's own is 
forwarded.
+                  onChange={value => setForm(prev => ({...prev, authMode: 
value, apiKey: ""}))}
+                  options={[
+                    {label: i18next.t("channel:Stored API key"), value: 
authChannel},
+                    {label: i18next.t("channel:Caller's own login"), value: 
authClient},
+                  ]}
+                />
+              </Field>
+            </Advanced>
+          </>
         )}
-        <ChannelModelsField
-          channel={form}
-          hint={usesClientAuth(form) ? i18next.t("channel:Any model hint") : 
i18next.t("channel:Models hint")}
-          onChange={value => setFormField("models", value)}
-        />
       </FormDialog>
     </PageContainer>
   );


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to