|
|
@@ -47,6 +47,15 @@ const NS = 'llm-pi-ai'
|
|
|
*/
|
|
|
const ROUTE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
|
|
|
|
+function isHttpUrl(value: string): boolean {
|
|
|
+ try {
|
|
|
+ const protocol = new URL(value).protocol
|
|
|
+ return protocol === 'http:' || protocol === 'https:'
|
|
|
+ } catch {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/** Props of {@link CustomProviderCard}. */
|
|
|
export interface CustomProviderCardProps {
|
|
|
/** Route ids already declared, so the card refuses to shadow one. */
|
|
|
@@ -98,6 +107,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
|
|
|
|
|
const routeInvalid = route.length > 0 && !ROUTE_PATTERN.test(route)
|
|
|
const routeTaken = taken.includes(route)
|
|
|
+ const baseUrlInvalid = baseURL.length > 0 && !isHttpUrl(baseURL)
|
|
|
// Rows are checked by the same per-row validator the editor cards use, so a
|
|
|
// bad row is named by its position here too. Capacities have route-level
|
|
|
// fallbacks; what a route cannot default is at least one model.
|
|
|
@@ -108,7 +118,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
|
|
// legitimately authenticate through the provider's own ambient discovery.
|
|
|
const keyValue = keyDraft.trim()
|
|
|
const ready = route.length > 0 && !routeInvalid && !routeTaken
|
|
|
- && baseURL.length > 0 && models.length > 0 && modelFailure === undefined
|
|
|
+ && baseURL.length > 0 && !baseUrlInvalid && models.length > 0 && modelFailure === undefined
|
|
|
&& keyFailure === undefined
|
|
|
// The one blocked gate worth a line under the form. A satisfied card says
|
|
|
// nothing at all rather than printing an empty paragraph.
|
|
|
@@ -120,7 +130,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
|
|
// Same for the route id, and it must be tested rather than assumed: the
|
|
|
// fallback arm below reads "no models yet", so an unmet route gate would
|
|
|
// fall through to it and contradict the filled-in list right above.
|
|
|
- || route.length === 0 || routeInvalid || routeTaken
|
|
|
+ || route.length === 0 || routeInvalid || routeTaken || baseUrlInvalid
|
|
|
? undefined
|
|
|
: baseURL.length === 0
|
|
|
? t('customNeedsBaseUrl')
|
|
|
@@ -227,10 +237,12 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
|
|
value={baseURL}
|
|
|
placeholder={t('customBaseUrlPlaceholder')}
|
|
|
aria-label={t('baseUrl')}
|
|
|
+ aria-invalid={baseUrlInvalid}
|
|
|
disabled={profileDisabled}
|
|
|
onChange={(event) => { setBaseURL(event.target.value) }}
|
|
|
/>
|
|
|
</div>
|
|
|
+ {baseUrlInvalid ? <p className={styles['error']}>{t('customBaseUrlInvalid')}</p> : null}
|
|
|
<div className={styles['field']}>
|
|
|
<span className={styles['fieldLabel']}>{t('customApi')}</span>
|
|
|
<select
|
|
|
@@ -271,7 +283,9 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
|
|
api: protocol,
|
|
|
...keyValue.length === 0 ? {} : { apiKey: keyValue },
|
|
|
}}
|
|
|
- probeBlocked={keyFailure === 'keyBlank' ? 'keyBlankNew' : keyFailure}
|
|
|
+ probeBlocked={baseUrlInvalid
|
|
|
+ ? 'customBaseUrlInvalid'
|
|
|
+ : keyFailure === 'keyBlank' ? 'keyBlankNew' : keyFailure}
|
|
|
operations={operations}
|
|
|
t={t}
|
|
|
disabled={profileDisabled}
|