mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 04:59:51 +08:00
refactor(providers): the UI, the connection test and the routes read the record's fields
Step 5 of the provider refactor plan. Provider names gave way to the record's credential and sdk fields in: hasCredentials (one rule per credential kind), the connection test (an Object.hasOwn check first, then a credential switch; EdgeOne's site endpoint and cookie forwarding follow credential none), the chat route (the on-site function and the single-system rule for the OpenAI SDK), the credentials form (which fields to show, the optional-key label, the base URL hint from the record), the picker (the note from the record), the logo (icon and logo fields), the settings dialog (test blocker, error hints, EdgeOne origin), the admin panel (fixedCredentials), chatRequestUrl (sdk), the model list (sdk aihubmix) and the zod schema (Object.hasOwn instead of 'in'). The eight provider-specific dictionary entries moved into the record as four-language objects; needAzure became a generic needUrl. Two intended behavior changes: the connection test answers 400 'Invalid provider' for an unknown name whether or not a key is sent (before: 400 'API key is required' without a key, 200 with an error text with one); and Azure has no placeholder default URL any more, so a new Azure entry starts with an empty URL and the generic 'needs a URL' rule applies. The e2e Azure test seeds no URL and expects the new wording. Out-of-layer provider-name checks: 44 before, 6 after (the admin panel's fixed-variable branches for Bedrock, Vertex AI and Ollama, kept on purpose). Behavior snapshot unchanged.
This commit is contained in:
@@ -55,13 +55,14 @@ import { getApiEndpoint } from "@/lib/base-path"
|
||||
import { formatMessage } from "@/lib/i18n/utils"
|
||||
import type { ListedModel } from "@/lib/provider-models"
|
||||
import { hasCredentials } from "@/lib/provider-setup"
|
||||
import { PROVIDERS } from "@/lib/providers"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
import type {
|
||||
ModelConfig,
|
||||
ProviderConfig,
|
||||
ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config"
|
||||
import { SUGGESTED_MODELS } from "@/lib/types/model-config"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useUiStore } from "@/stores/ui-store"
|
||||
|
||||
@@ -300,7 +301,7 @@ export function ModelConfigDialog({
|
||||
const errorHints = (p: ProviderConfig): Record<string, string> => ({
|
||||
...(dict.errors.llm as Record<string, string>),
|
||||
invalid_api_key:
|
||||
p.provider === "bedrock" && !p.apiKey
|
||||
PROVIDERS[p.provider].credential === "aws" && !p.apiKey
|
||||
? t.awsKeysRejected
|
||||
: t.keyRejected,
|
||||
model_not_found: t.modelNotFound,
|
||||
@@ -530,9 +531,9 @@ export function ModelConfigDialog({
|
||||
const run = (latestTestRun.get(selectedProviderId) ?? 0) + 1
|
||||
latestTestRun.set(selectedProviderId, run)
|
||||
|
||||
// For EdgeOne, construct baseUrl from current origin
|
||||
// The site's own function (EdgeOne): its URL is this origin's
|
||||
const baseUrl =
|
||||
selectedProvider.provider === "edgeone"
|
||||
PROVIDERS[selectedProvider.provider].credential === "none"
|
||||
? `${window.location.origin}/api/edgeai`
|
||||
: selectedProvider.baseUrl
|
||||
const hints = errorHints(selectedProvider)
|
||||
@@ -741,7 +742,7 @@ export function ModelConfigDialog({
|
||||
}
|
||||
|
||||
const providerName = (p: ProviderConfig) =>
|
||||
p.name || PROVIDER_INFO[p.provider].label
|
||||
p.name || PROVIDERS[p.provider].label
|
||||
|
||||
const renderModelRow = (model: ModelConfig) => {
|
||||
if (!selectedProvider) return null
|
||||
@@ -922,11 +923,13 @@ export function ModelConfigDialog({
|
||||
: undefined
|
||||
const canTest = credentialsDone && models.length > 0
|
||||
const testing = validationStatus === "validating"
|
||||
const def = PROVIDERS[provider.provider]
|
||||
// A key-only provider without a default URL (Azure) needs its URL too
|
||||
const testBlocker = !credentialsDone
|
||||
? provider.provider === "bedrock"
|
||||
? def.credential === "aws"
|
||||
? t.needAws
|
||||
: provider.provider === "azure"
|
||||
? t.needAzure
|
||||
: def.credential === "apiKey" && !def.defaultBaseUrl
|
||||
? t.needUrl
|
||||
: t.needKey
|
||||
: models.length === 0
|
||||
? t.needModel
|
||||
@@ -984,7 +987,7 @@ export function ModelConfigDialog({
|
||||
done={credentialsDone && !keyRejected}
|
||||
title={t.stepConnect}
|
||||
>
|
||||
{provider.provider === "edgeone" ? (
|
||||
{def.credential === "none" ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t.edgeoneNoKey}
|
||||
</p>
|
||||
@@ -1027,7 +1030,7 @@ export function ModelConfigDialog({
|
||||
done={models.length > 0}
|
||||
title={t.stepModels}
|
||||
action={
|
||||
PROVIDER_INFO[provider.provider].modelList && (
|
||||
def.modelList && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFetchModels}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ChevronRight, Key, Link2, Tag } from "lucide-react"
|
||||
import { type ReactNode, useState } from "react"
|
||||
import { useLocale } from "@/components/canvas/locale-context"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
@@ -13,13 +14,8 @@ import {
|
||||
} from "@/components/ui/select"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { formatMessage } from "@/lib/i18n/utils"
|
||||
import { OPTIONS_OPEN } from "@/lib/provider-setup"
|
||||
import {
|
||||
chatRequestUrl,
|
||||
normalizeBaseUrl,
|
||||
PROVIDER_INFO,
|
||||
type ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
import { PROVIDERS, type ProviderName } from "@/lib/providers"
|
||||
import { chatRequestUrl, normalizeBaseUrl } from "@/lib/types/model-config"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Logical secret field. The caller owns the actual input — plaintext for the
|
||||
@@ -94,7 +90,8 @@ export function ProviderCredentialsFields({
|
||||
sessionTokenInput,
|
||||
}: ProviderCredentialsFieldsProps) {
|
||||
const dict = useDictionary()
|
||||
const info = PROVIDER_INFO[provider]
|
||||
const locale = useLocale()
|
||||
const info = PROVIDERS[provider]
|
||||
const baseUrlLabel = formatMessage(dict.modelConfig.baseUrlWithExample, {
|
||||
example: info.defaultBaseUrl || "https://api.example.com/v1",
|
||||
})
|
||||
@@ -105,7 +102,7 @@ export function ProviderCredentialsFields({
|
||||
() =>
|
||||
!!name ||
|
||||
(!!baseUrl && baseUrl !== info.defaultBaseUrl) ||
|
||||
OPTIONS_OPEN.includes(provider),
|
||||
!!info.optionsOpen,
|
||||
)
|
||||
// Bedrock: the way that is filled in (an API key is used first when both
|
||||
// are, as the server does), else the one shown last, so emptying the
|
||||
@@ -199,7 +196,8 @@ export function ProviderCredentialsFields({
|
||||
>
|
||||
<Key className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{dict.modelConfig.apiKey}
|
||||
{provider === "ollama" && ` ${dict.modelConfig.optional}`}
|
||||
{info.credential === "optionalKey" &&
|
||||
` ${dict.modelConfig.optional}`}
|
||||
</Label>
|
||||
{info.apiKeyUrl && (
|
||||
<a
|
||||
@@ -248,26 +246,21 @@ export function ProviderCredentialsFields({
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{provider === "minimax" && (
|
||||
{info.baseUrlHint && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{dict.modelConfig.minimaxBaseUrlHint}
|
||||
</p>
|
||||
)}
|
||||
{provider === "mimo" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{dict.modelConfig.mimoBaseUrlHint}
|
||||
{info.baseUrlHint[locale]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
// EdgeOne needs no credentials
|
||||
if (provider === "edgeone") return null
|
||||
// A provider without credentials (EdgeOne) has no fields
|
||||
if (info.credential === "none") return null
|
||||
|
||||
if (settingsLayout) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{provider === "bedrock" ? (
|
||||
{info.credential === "aws" ? (
|
||||
<>
|
||||
{/* Buttons, not radios: arrow keys would empty
|
||||
secrets without a deliberate press */}
|
||||
@@ -393,7 +386,7 @@ export function ProviderCredentialsFields({
|
||||
)}
|
||||
{regionField}
|
||||
</>
|
||||
) : provider === "vertexai" ? (
|
||||
) : info.credential === "vertexKey" ? (
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="vertex-api-key"
|
||||
@@ -423,14 +416,14 @@ export function ProviderCredentialsFields({
|
||||
optionsOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
{provider === "bedrock"
|
||||
{info.credential === "aws"
|
||||
? dict.modelConfig.moreOptionsName
|
||||
: dict.modelConfig.moreOptions}
|
||||
</button>
|
||||
{optionsOpen && (
|
||||
<>
|
||||
{provider !== "bedrock" &&
|
||||
(provider === "vertexai"
|
||||
{info.credential !== "aws" &&
|
||||
(info.credential === "vertexKey"
|
||||
? vertexBaseUrlField
|
||||
: baseUrlField)}
|
||||
{nameField}
|
||||
@@ -445,7 +438,7 @@ export function ProviderCredentialsFields({
|
||||
<div className="space-y-5">
|
||||
{nameField}
|
||||
|
||||
{provider === "bedrock" ? (
|
||||
{info.credential === "aws" ? (
|
||||
<>
|
||||
{/* AWS Access Key ID */}
|
||||
<div className="space-y-2">
|
||||
@@ -479,7 +472,7 @@ export function ProviderCredentialsFields({
|
||||
|
||||
{regionField}
|
||||
</>
|
||||
) : provider === "vertexai" ? (
|
||||
) : info.credential === "vertexKey" ? (
|
||||
<>
|
||||
{/* Vertex AI API Key (Express Mode) */}
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Cloud, Server, Sparkles } from "lucide-react"
|
||||
import { PROVIDER_LOGO_MAP, type ProviderName } from "@/lib/types/model-config"
|
||||
import { PROVIDERS, type ProviderName } from "@/lib/providers"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Provider logo from models.dev, with Lucide fallbacks for providers
|
||||
const ICONS = { cloud: Cloud, server: Server, sparkles: Sparkles }
|
||||
|
||||
// Provider logo from models.dev, or the record's Lucide icon for providers
|
||||
// that have no logo there
|
||||
export function ProviderLogo({
|
||||
provider,
|
||||
@@ -11,17 +13,11 @@ export function ProviderLogo({
|
||||
provider: ProviderName
|
||||
className?: string
|
||||
}) {
|
||||
if (provider === "bedrock") {
|
||||
return <Cloud className={cn("size-4", className)} />
|
||||
const { icon, logo } = PROVIDERS[provider]
|
||||
if (icon) {
|
||||
const Icon = ICONS[icon]
|
||||
return <Icon className={cn("size-4", className)} />
|
||||
}
|
||||
if (provider === "sglang") {
|
||||
return <Server className={cn("size-4", className)} />
|
||||
}
|
||||
if (provider === "doubao") {
|
||||
return <Sparkles className={cn("size-4", className)} />
|
||||
}
|
||||
|
||||
const logoName = PROVIDER_LOGO_MAP[provider] || provider
|
||||
return (
|
||||
// biome-ignore lint/performance/noImgElement: External URL from models.dev
|
||||
<img
|
||||
@@ -29,7 +25,7 @@ export function ProviderLogo({
|
||||
aria-hidden="true"
|
||||
className={cn("size-4 dark:invert", className)}
|
||||
height={16}
|
||||
src={`https://models.dev/logos/${logoName}.svg`}
|
||||
src={`https://models.dev/logos/${logo ?? provider}.svg`}
|
||||
width={16}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ChevronRight, Search } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useLocale } from "@/components/canvas/locale-context"
|
||||
import { ProviderLogo } from "@/components/provider-logo"
|
||||
import { SettingsHeader } from "@/components/settings/settings-header"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { useDictionary } from "@/hooks/use-dictionary"
|
||||
import { filterProviderGroups, PROVIDER_NOTE_KEYS } from "@/lib/provider-setup"
|
||||
import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
|
||||
import { filterProviderGroups } from "@/lib/provider-setup"
|
||||
import { PROVIDERS, type ProviderName } from "@/lib/providers"
|
||||
|
||||
/** Every provider in groups, with a search; picking one adds it */
|
||||
export function ProviderPicker({
|
||||
@@ -20,6 +21,7 @@ export function ProviderPicker({
|
||||
}) {
|
||||
const dict = useDictionary()
|
||||
const t = dict.modelConfig
|
||||
const locale = useLocale()
|
||||
const [query, setQuery] = useState("")
|
||||
const groups = filterProviderGroups(query)
|
||||
const groupLabels: Record<string, string> = {
|
||||
@@ -28,8 +30,6 @@ export function ProviderPicker({
|
||||
cloud: t.groupCloud,
|
||||
selfHosted: t.groupSelfHosted,
|
||||
}
|
||||
const notes = t as Record<string, string>
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<SettingsHeader>
|
||||
@@ -93,9 +93,8 @@ export function ProviderPicker({
|
||||
</h3>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(148px,1fr))] gap-2">
|
||||
{group.providers.map((provider) => {
|
||||
const noteKey = PROVIDER_NOTE_KEYS[provider]
|
||||
const note = [
|
||||
noteKey && notes[noteKey],
|
||||
PROVIDERS[provider].note?.[locale],
|
||||
added.includes(provider) && t.added,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -116,14 +115,11 @@ export function ProviderPicker({
|
||||
<span
|
||||
className="block truncate text-[12.5px] font-medium"
|
||||
title={
|
||||
PROVIDER_INFO[provider]
|
||||
PROVIDERS[provider]
|
||||
.label
|
||||
}
|
||||
>
|
||||
{
|
||||
PROVIDER_INFO[provider]
|
||||
.label
|
||||
}
|
||||
{PROVIDERS[provider].label}
|
||||
</span>
|
||||
{note && (
|
||||
<span className="block truncate text-[11px] text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user