2026-10-08 18:10:07 +09:00
|
|
|
// What the model settings show about providers: the groups of the provider
|
|
|
|
|
// picker, and how far a provider's setup has come
|
|
|
|
|
|
|
|
|
|
import {
|
2026-10-12 21:31:03 +09:00
|
|
|
PROVIDER_NAMES,
|
|
|
|
|
PROVIDERS,
|
|
|
|
|
type ProviderGroup as ProviderGroupId,
|
|
|
|
|
} from "@/lib/providers"
|
|
|
|
|
import type { ProviderConfig, ProviderName } from "@/lib/types/model-config"
|
2026-10-08 18:10:07 +09:00
|
|
|
|
2026-10-12 21:31:03 +09:00
|
|
|
/** The provider picker's groups, each provider in exactly one, in record order */
|
2026-10-08 18:10:07 +09:00
|
|
|
export const PROVIDER_GROUPS: {
|
|
|
|
|
id: ProviderGroupId
|
|
|
|
|
providers: ProviderName[]
|
2026-10-12 21:31:03 +09:00
|
|
|
}[] = (["vendors", "aggregators", "cloud", "selfHosted"] as const).map(
|
|
|
|
|
(id) => ({
|
|
|
|
|
id,
|
|
|
|
|
providers: PROVIDER_NAMES.filter((p) => PROVIDERS[p].group === id),
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-10-08 18:10:07 +09:00
|
|
|
|
|
|
|
|
/** Shown when the user has no provider yet */
|
|
|
|
|
export const POPULAR_PROVIDERS: ProviderName[] = [
|
|
|
|
|
"openai",
|
|
|
|
|
"anthropic",
|
|
|
|
|
"google",
|
|
|
|
|
"deepseek",
|
|
|
|
|
"openrouter",
|
|
|
|
|
"qwen",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
/** The groups with only the providers whose name matches the search */
|
|
|
|
|
export function filterProviderGroups(query: string) {
|
|
|
|
|
const q = query.trim().toLowerCase()
|
|
|
|
|
if (!q) return PROVIDER_GROUPS
|
|
|
|
|
return PROVIDER_GROUPS.map((group) => ({
|
|
|
|
|
...group,
|
|
|
|
|
providers: group.providers.filter(
|
|
|
|
|
(p) =>
|
2026-10-12 21:31:03 +09:00
|
|
|
PROVIDERS[p].label.toLowerCase().includes(q) || p.includes(q),
|
2026-10-08 18:10:07 +09:00
|
|
|
),
|
|
|
|
|
})).filter((group) => group.providers.length > 0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 21:31:03 +09:00
|
|
|
const LEGACY_AZURE_PLACEHOLDER = "https://your-resource.openai.azure.com/openai"
|
|
|
|
|
|
2026-10-08 18:10:07 +09:00
|
|
|
/** The credentials a test or a chat request needs are filled in */
|
|
|
|
|
export function hasCredentials(p: ProviderConfig): boolean {
|
2026-10-12 21:31:03 +09:00
|
|
|
const def = PROVIDERS[p.provider]
|
|
|
|
|
switch (def.credential) {
|
|
|
|
|
case "aws":
|
2026-10-08 18:10:07 +09:00
|
|
|
return (
|
|
|
|
|
(!!p.apiKey ||
|
|
|
|
|
(!!p.awsAccessKeyId && !!p.awsSecretAccessKey)) &&
|
|
|
|
|
!!p.awsRegion
|
|
|
|
|
)
|
2026-10-12 21:31:03 +09:00
|
|
|
case "vertexKey":
|
2026-10-08 18:10:07 +09:00
|
|
|
return !!p.vertexApiKey
|
2026-10-12 21:31:03 +09:00
|
|
|
case "none":
|
|
|
|
|
case "optionalKey":
|
2026-10-08 18:10:07 +09:00
|
|
|
return true
|
2026-10-12 21:31:03 +09:00
|
|
|
case "apiKey": {
|
|
|
|
|
// Without a default URL in the record (Azure, custom) the user's
|
|
|
|
|
// own is part of the credentials. Entries saved while Azure still
|
|
|
|
|
// had a placeholder default hold that placeholder; it names no
|
|
|
|
|
// real resource
|
|
|
|
|
const baseUrl =
|
|
|
|
|
p.baseUrl === LEGACY_AZURE_PLACEHOLDER ? "" : p.baseUrl
|
|
|
|
|
return !!p.apiKey && (!!def.defaultBaseUrl || !!baseUrl)
|
|
|
|
|
}
|
2026-10-08 18:10:07 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ProviderStatus =
|
|
|
|
|
| { kind: "error"; count: number }
|
|
|
|
|
| { kind: "incomplete" }
|
|
|
|
|
| { kind: "untested"; count: number }
|
|
|
|
|
| { kind: "ok"; count: number }
|
|
|
|
|
|
|
|
|
|
/** The one thing about a provider that most needs the user's attention */
|
|
|
|
|
export function providerStatus(p: ProviderConfig): ProviderStatus {
|
|
|
|
|
const failed = p.models.filter((m) => m.validated === false).length
|
|
|
|
|
if (failed > 0) return { kind: "error", count: failed }
|
|
|
|
|
if (!hasCredentials(p) || p.models.length === 0) {
|
|
|
|
|
return { kind: "incomplete" }
|
|
|
|
|
}
|
|
|
|
|
const untested = p.models.filter((m) => m.validated !== true).length
|
|
|
|
|
if (untested > 0) return { kind: "untested", count: untested }
|
|
|
|
|
return { kind: "ok", count: p.models.length }
|
|
|
|
|
}
|