Merge origin/master at f1402327fa

This commit is contained in:
Tianyi Cui
2026-08-07 23:57:23 +08:00
447 changed files with 20295 additions and 1680 deletions
@@ -13,6 +13,12 @@
* The three fields a hand-declared route cannot default — endpoint, protocol,
* and at least one model — are required here rather than at load, so the
* failure names the field while the user is still looking at it.
*
* There is deliberately no reasoning-effort control, here or on the editor
* card: effort is a per-MODEL capability, and the models under one provider
* disagree about it, so a provider-scoped control can only be set to a value
* some of them reject. The composer's model picker offers each model its own
* levels instead.
*/
import { useState } from 'react'
@@ -30,8 +36,15 @@ import styles from './ModelsSection.module.css'
/** The settings namespace a hand-declared provider is written into. */
const NS = 'llm-pi-ai'
/** A route id usable as a settings key and as the stem of a credential name. */
const ROUTE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
/**
* A route id usable as a settings key AND as the stem of a credential name.
* The leading letter is the second half of that: `deriveKeyRef` uppercases the
* id and replaces every non-alphanumeric run with `_`, and a credential
* reference is a POSIX shell identifier, which cannot start with a digit. A
* digit-leading id passes every check this card makes and then fails at the
* credential seam with a raw regular expression the user cannot act on.
*/
const ROUTE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
/** Props of {@link CustomProviderCard}. */
export interface CustomProviderCardProps {
@@ -73,7 +86,15 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
const [models, setModels] = useState<readonly ModelDraft[]>([])
const [busy, setBusy] = useState(false)
const [failure, setFailure] = useState<string | undefined>(undefined)
/**
* The profile write landed. Only the key write can still be outstanding, so
* the fields that describe the provider are settled and the retry path is
* the credential alone.
*/
const [committed, setCommitted] = useState(false)
const disabled = props.readOnly || busy
/** Everything but the key stops being editable once the provider exists. */
const profileDisabled = disabled || committed
const routeInvalid = route.length > 0 && !ROUTE_PATTERN.test(route)
const routeTaken = taken.includes(route)
@@ -89,14 +110,17 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
const ready = route.length > 0 && !routeInvalid && !routeTaken
&& baseURL.length > 0 && models.length > 0 && modelFailure === undefined
&& keyFailure === undefined
// The one blocked gate worth a line under the form. The route id is omitted
// because its own field already explains itself, and a satisfied card says
// The one blocked gate worth a line under the form. A satisfied card says
// nothing at all rather than printing an empty paragraph.
const hint = failure !== undefined || ready
// The key field prints its own failure directly beneath itself, so a card
// blocked only by the key stays silent here rather than answering with the
// next unmet gate — which is satisfied, and reads as a second, false fault.
|| keyFailure !== undefined
// 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 used to
// fall through to it and contradict the filled-in list right above.
|| route.length === 0 || routeInvalid || routeTaken
? undefined
: baseURL.length === 0
? t('customNeedsBaseUrl')
@@ -107,26 +131,38 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
/** Perform the create, returning a failure message or undefined. */
const createOnce = async (): Promise<string | undefined> => {
const keyRef = deriveKeyRef(route)
const profile = {
...displayName.length === 0 ? {} : { displayName },
apiKeyEnv: keyRef,
api: protocol,
baseURL,
models: models.map(model => ({ ...model })),
const storesKey = keyValue.length > 0
if (!committed) {
const profile = {
...displayName.length === 0 ? {} : { displayName },
// The profile names the conventional reference only when this card is
// about to store a key, matching the editor: a route declared with the
// key left blank keeps its provider-native auth path (a credential
// chain, ADC) instead of resolving a reference nothing ever sets.
...storesKey ? { apiKeyEnv: keyRef } : {},
api: protocol,
baseURL,
models: models.map(model => ({ ...model })),
}
const response = await api.settings.mutate({
ns: NS,
ops: [{ op: 'set', path: ['providers', route], value: profile }],
// `taken` is a snapshot too, so the id check alone cannot see a route
// declared after this card opened; the revision makes that race a
// `settings-conflict` instead of a write over the other profile.
expectedRevision: openedAt,
})
if (!response.result.ok) return response.result.error.message
// The provider now exists. A retry after the key write below fails must
// not re-run this mutate: the revision it holds is the one this write
// just superseded, so the Host would answer `settings-conflict` and the
// key could never be stored from this card at all.
setCommitted(true)
}
const response = await api.settings.mutate({
ns: NS,
ops: [{ op: 'set', path: ['providers', route], value: profile }],
// `taken` is a snapshot too, so the id check alone cannot see a route
// declared after this card opened; the revision makes that race a
// `settings-conflict` instead of a write over the other profile.
expectedRevision: openedAt,
})
if (!response.result.ok) return response.result.error.message
if (keyValue.length > 0) {
if (storesKey) {
const stored = await api.credentials.set({ ref: keyRef, value: keyValue })
// The profile landed; saying the key did not is the only honest report,
// and the row is now editable so the key can be entered again there.
// and the retry above now goes straight back to this write.
if (!stored.result.ok) return stored.result.error.message
}
return undefined
@@ -164,13 +200,15 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
value={route}
placeholder="acme-gateway"
aria-label={t('customRoute')}
disabled={disabled}
disabled={profileDisabled}
onChange={(event) => { setRoute(event.target.value) }}
/>
</div>
<p className={styles['advancedHint']}>
{routeInvalid ? t('customRouteInvalid') : routeTaken ? t('customRouteTaken') : t('customRouteHint')}
</p>
{/* A rejected id reads as a fault, not as guidance — the same split the
key field below already makes between its failure and its hint. */}
{routeInvalid || routeTaken
? <p className={styles['error']}>{t(routeInvalid ? 'customRouteInvalid' : 'customRouteTaken')}</p>
: <p className={styles['advancedHint']}>{t('customRouteHint')}</p>}
<div className={styles['field']}>
<span className={styles['fieldLabel']}>{t('customDisplayName')}</span>
<input
@@ -179,7 +217,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
value={displayName}
placeholder={route.length === 0 ? t('customDisplayName') : route}
aria-label={t('customDisplayName')}
disabled={disabled}
disabled={profileDisabled}
onChange={(event) => { setDisplayName(event.target.value) }}
/>
</div>
@@ -191,7 +229,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
value={baseURL}
placeholder="https://gateway.example/v1"
aria-label={t('baseUrl')}
disabled={disabled}
disabled={profileDisabled}
onChange={(event) => { setBaseURL(event.target.value) }}
/>
</div>
@@ -201,7 +239,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
className={styles['input']}
value={protocol}
aria-label={t('customApi')}
disabled={disabled}
disabled={profileDisabled}
onChange={(event) => { setProtocol(event.target.value) }}
>
{protocols.map(choice => <option key={choice} value={choice}>{choice}</option>)}
@@ -238,7 +276,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
probeBlocked={keyFailure === 'keyBlank' ? 'keyBlankNew' : keyFailure}
api={api}
t={t}
disabled={disabled}
disabled={profileDisabled}
/>
{failure !== undefined ? <p className={styles['error']}>{failure}</p> : null}
{/* Only the gates with something to say render; the route-id gate has its
@@ -250,7 +288,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
submitDisabled={disabled || !ready}
submitLabel="create"
submitBusyLabel="creating"
onCancel={() => { props.onClose(false) }}
onCancel={() => { props.onClose(committed) }}
onSubmit={() => { void create() }}
/>
</div>
@@ -86,6 +86,20 @@
color: var(--dsw-alias-label-primary);
}
/* Reads as an annotation on the name, not as a second name: caption size and
the secondary label tone, so it never competes with the row's own title. It
sits inside `rowIdentity` with the credential dot, which is what keeps it
beside the name rather than drifting toward the actions. */
.rowTag {
flex: none;
padding: 1px 6px;
border: 1px solid var(--dsw-alias-border-l3);
border-radius: 4px;
font-size: 11px;
line-height: 16px;
color: var(--dsw-alias-label-secondary);
}
.credentialDot {
box-sizing: border-box;
display: inline-block;
@@ -271,6 +271,12 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
<div className={styles['rowHead']}>
<span className={styles['rowIdentity']}>
<span className={styles['rowName']}>{row.entry.displayName}</span>
{/* Only the adapter can tell a hand-declared route from a
shipped one it also has a stored profile for, so the tag
follows its answer and stays off when it gives none. */}
{row.entry.declared === true
? <span className={styles['rowTag']}>{t('customTag')}</span>
: null}
{credentialConfigured
? (
<span
@@ -7,8 +7,12 @@
* a key is entered; a blank key materializes a reference-free profile for
* provider-native authentication);
* the collapsed 自定义设置 area carries the per-family extras (`baseURL` for
* both families, `reasoningEffort` for deepseek / `reasoning` for pi-ai, and
* DeepSeek's id/name/context-window model catalog). Everything else stays
* both families and DeepSeek's id/name/context-window model catalog).
* Reasoning effort is deliberately absent: it is a per-MODEL capability, and
* the models under one provider disagree about it, so a provider-scoped
* control can only be set to a value some of them reject. The composer's
* model picker offers each model its own levels; `settings.yaml` keeps the
* profile field for a deployment that knows its route. Everything else stays
* owned by `settings.yaml`. Profile edits land as minimal `settings.mutate`
* path ops against the stored section — the card names only the fields it can
* see instead of rebuilding the whole subtree from a partial descriptor.
@@ -33,18 +37,6 @@ import styles from './ModelsSection.module.css'
/** Per-adapter-family curated field sets (unknown namespaces get the hint alone). */
type EditorLayout = 'deepseek' | 'pi-ai' | 'unknown'
/** Reasoning vocabularies per layout; the empty option means "inherit". */
const EFFORT_CHOICES: Record<'deepseek' | 'pi-ai', readonly string[]> = {
deepseek: ['off', 'high', 'max'],
'pi-ai': ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
}
/** The draft key the effort select edits, per layout. */
const EFFORT_FIELD: Record<'deepseek' | 'pi-ai', string> = {
deepseek: 'reasoningEffort',
'pi-ai': 'reasoning',
}
/** The public DeepSeek endpoint shown as the deepseek base-URL placeholder. */
const DEEPSEEK_PUBLIC_BASE_URL = 'https://api.deepseek.com'
@@ -302,7 +294,6 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
* unknown namespace never reaches this body.
*/
const curatedFields = (family: 'deepseek' | 'pi-ai'): ReactNode => {
const effortField = EFFORT_FIELD[family]
const customModels = getPath(draft, ['models'])
const modelsOverridden = hasPath(draft, ['models'])
const models = modelDrafts(modelsOverridden ? customModels : inheritedModels())
@@ -359,23 +350,6 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
}}
/>
</div>
<div className={styles['field']}>
<span className={styles['fieldLabel']}>{t('effort')}</span>
<select
className={`${styles['input']} ${styles['selectInput']}`}
value={stringAt(draft, effortField) ?? ''}
aria-label={t('effort')}
disabled={disabled}
onChange={(event) => {
setField(effortField, event.target.value === '' ? undefined : event.target.value)
}}
>
<option value="">{t('effortInherit')}</option>
{EFFORT_CHOICES[family].map(choice => (
<option key={choice} value={choice}>{choice}</option>
))}
</select>
</div>
{/* Both families edit the same rows through the same contract; only
the extras differ — DeepSeek's inherited capacities, pi-ai's
endpoint interrogation. */}
@@ -35,8 +35,6 @@ export const en = {
customized: 'Customized settings',
baseUrl: 'Base URL',
baseUrlDefault: 'Provider default',
effort: 'Reasoning effort',
effortInherit: 'Default',
models: 'Models',
modelsInherited: 'Using the adapter defaults',
modelsCustomized: 'Customized model catalog',
@@ -75,9 +73,10 @@ export const en = {
fetchAdopt: 'Add selected',
customAdd: 'Add a custom provider',
customTitle: 'Custom provider',
customTag: 'Custom',
customRoute: 'Provider ID',
customRouteHint: 'Lowercase identifier that uniquely names this provider in requests and as its credential name.',
customRouteInvalid: 'Use lowercase letters, digits, and dashes.',
customRouteHint: 'Lowercase identifier, starting with a letter, that uniquely names this provider in requests and as its credential name.',
customRouteInvalid: 'Start with a lowercase letter; then lowercase letters, digits, and dashes.',
customRouteTaken: 'A provider already uses this ID.',
customDisplayName: 'Display name',
customApi: 'API protocol',
@@ -129,8 +128,6 @@ export const zh: typeof en = {
customized: '自定义设置',
baseUrl: 'API 地址',
baseUrlDefault: '提供方默认',
effort: '推理强度',
effortInherit: '默认',
models: '模型目录',
modelsInherited: '正在使用适配器默认模型',
modelsCustomized: '已自定义模型目录',
@@ -169,9 +166,10 @@ export const zh: typeof en = {
fetchAdopt: '添加所选',
customAdd: '添加自定义提供方',
customTitle: '自定义提供方',
customTag: '自定义',
customRoute: 'Provider ID',
customRouteHint: '小写标识,在请求中唯一标识该提供方,并用于派生凭据名。',
customRouteInvalid: '只能使用小写字母、数字和短横线。',
customRouteHint: '小写字母开头的标识,在请求中唯一标识该提供方,并用于派生凭据名。',
customRouteInvalid: '需以小写字母开头,之后可用小写字母、数字和短横线。',
customRouteTaken: '已有提供方使用了这个 ID。',
customDisplayName: '显示名称',
customApi: 'API 协议',