@dsh-plus/llm-pi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat),
3
+ * extends 输入框带 datalist 候选——候选来自 /catalog 端点:选定 source
4
+ * (builtin / models-dev)与 provider 后拉取其 models 列表。
5
+ * @module llm-pi/client/views/models
6
+ */
7
+ import { useEffect, useState, type ReactElement } from 'react'
8
+
9
+ import { fetchCatalog, type WireModelsDevStatus } from '../api.ts'
10
+ import { MODALITIES, THINKING_LEVELS } from '../constants.ts'
11
+ import { emptyModelDraft, type ModelDraft, type ReasoningDraft } from '../draft.ts'
12
+ import { CheckRow, TextField } from '../fields.tsx'
13
+ import { CompatEditor } from './compat.tsx'
14
+
15
+ type CatalogSource = 'builtin' | 'models-dev'
16
+
17
+ function catalogNote(status: WireModelsDevStatus | undefined, t: (key: string) => string): string {
18
+ if (status === undefined) return ''
19
+ if (status.error !== null) return `${t('modelsDevError')}${status.error}`
20
+ return `${t('modelsDevStatusLine')}:${status.providers} 个 provider,快照 ${status.fetchedAt ?? '-'}`
21
+ }
22
+
23
+ export interface ModelsTableProps {
24
+ route: string
25
+ api: string
26
+ /** 默认候选 provider(route 级 extends)。 */
27
+ defaultProvider: string
28
+ models: ModelDraft[]
29
+ epoch: number
30
+ disabled?: boolean
31
+ t(key: string): string
32
+ onModels(next: ModelDraft[]): void
33
+ }
34
+
35
+ export function ModelsTable(props: ModelsTableProps): ReactElement {
36
+ const { t } = props
37
+ const [source, setSource] = useState<CatalogSource>('builtin')
38
+ const [providerIds, setProviderIds] = useState<string[]>([])
39
+ const [provider, setProvider] = useState('')
40
+ const [candidateModels, setCandidateModels] = useState<string[]>([])
41
+ const [note, setNote] = useState('')
42
+ const listId = `lpc-models-${props.route.replace(/[^a-zA-Z0-9_-]/g, '_')}`
43
+
44
+ useEffect(() => {
45
+ let alive = true
46
+ setNote(t('catalogLoading'))
47
+ void (async () => {
48
+ try {
49
+ const list = await fetchCatalog('', source)
50
+ if (!alive) return
51
+ setProviderIds(list.providers)
52
+ const preferred = list.providers.includes(props.defaultProvider)
53
+ ? props.defaultProvider
54
+ : (list.providers[0] ?? '')
55
+ setProvider(preferred)
56
+ setNote(catalogNote(list.status, t))
57
+ if (preferred === '') return
58
+ const result = await fetchCatalog(preferred, source)
59
+ if (alive) setCandidateModels(result.models)
60
+ } catch {
61
+ if (alive) setNote(t('catalogFailed'))
62
+ }
63
+ })()
64
+ return () => {
65
+ alive = false
66
+ }
67
+ }, [source])
68
+
69
+ const onProviderChange = (value: string): void => {
70
+ setProvider(value)
71
+ if (value === '') {
72
+ setCandidateModels([])
73
+ return
74
+ }
75
+ void fetchCatalog(value, source)
76
+ .then((result) => setCandidateModels(result.models))
77
+ .catch(() => setNote(t('catalogFailed')))
78
+ }
79
+
80
+ return (
81
+ <div className="lpc-models">
82
+ <div className="lpc-modelHead">
83
+ <span className="lpc-modelTitle">{t('modelsGroup')}</span>
84
+ <button
85
+ type="button"
86
+ className="lpc-btn lpc-btnGhost lpc-btnSmall"
87
+ disabled={props.disabled === true}
88
+ onClick={() => props.onModels([...props.models, emptyModelDraft()])}
89
+ >
90
+ {t('addModel')}
91
+ </button>
92
+ </div>
93
+ <div className="lpc-catalogBar">
94
+ <label className="lpc-catalogLabel" htmlFor={`${listId}-source`}>{t('catalogSource')}</label>
95
+ <select
96
+ id={`${listId}-source`}
97
+ className="lpc-input lpc-select lpc-catalogSelect"
98
+ value={source}
99
+ disabled={props.disabled === true}
100
+ onChange={(event) => setSource(event.target.value as CatalogSource)}
101
+ >
102
+ <option value="builtin">builtin</option>
103
+ <option value="models-dev">models-dev</option>
104
+ </select>
105
+ <label className="lpc-catalogLabel" htmlFor={`${listId}-provider`}>{t('catalogProvider')}</label>
106
+ <select
107
+ id={`${listId}-provider`}
108
+ className="lpc-input lpc-select lpc-catalogSelect"
109
+ value={provider}
110
+ disabled={props.disabled === true || providerIds.length === 0}
111
+ onChange={(event) => onProviderChange(event.target.value)}
112
+ >
113
+ <option value="">-</option>
114
+ {providerIds.map((id) => (
115
+ <option key={id} value={id}>{id}</option>
116
+ ))}
117
+ </select>
118
+ </div>
119
+ {note !== '' ? <p className="lpc-hint">{note}</p> : null}
120
+ <datalist id={listId}>
121
+ {candidateModels.map((id) => (
122
+ <option key={id} value={id} />
123
+ ))}
124
+ </datalist>
125
+ {props.models.map((model, index) => (
126
+ <ModelRow
127
+ key={`${index}:${model.id}`}
128
+ index={index}
129
+ model={model}
130
+ api={props.api}
131
+ listId={listId}
132
+ epoch={props.epoch}
133
+ disabled={props.disabled === true}
134
+ t={t}
135
+ onPatch={(patch) =>
136
+ props.onModels(props.models.map((m, i) => (i === index ? { ...m, ...patch } : m)))
137
+ }
138
+ onRemove={() => props.onModels(props.models.filter((_, i) => i !== index))}
139
+ />
140
+ ))}
141
+ </div>
142
+ )
143
+ }
144
+
145
+ export interface ModelRowProps {
146
+ index: number
147
+ model: ModelDraft
148
+ api: string
149
+ listId: string
150
+ epoch: number
151
+ disabled?: boolean
152
+ t(key: string): string
153
+ onPatch(patch: Partial<ModelDraft>): void
154
+ onRemove(): void
155
+ }
156
+
157
+ export function ModelRow(props: ModelRowProps): ReactElement {
158
+ const { model, t } = props
159
+ const id = `${props.listId}-m${props.index}`
160
+ return (
161
+ <div className="lpc-modelRow">
162
+ <div className="lpc-modelHead">
163
+ <span className="lpc-modelTitle">{t('modelRow')} {props.index + 1}</span>
164
+ <button
165
+ type="button"
166
+ className="lpc-btn lpc-btnGhost lpc-btnSmall"
167
+ disabled={props.disabled === true}
168
+ onClick={props.onRemove}
169
+ >
170
+ {t('deleteModel')}
171
+ </button>
172
+ </div>
173
+ <div className="lpc-grid">
174
+ <TextField
175
+ id={`${id}-id`}
176
+ label={t('modelId')}
177
+ hint={t('modelIdHint')}
178
+ value={model.id}
179
+ disabled={props.disabled === true}
180
+ invalid={model.id.trim() === ''}
181
+ invalidLabel={t('modelIdRequired')}
182
+ onEdit={(value) => props.onPatch({ id: value })}
183
+ />
184
+ <TextField
185
+ id={`${id}-extends`}
186
+ label={t('modelExtends')}
187
+ hint={t('modelExtendsHint')}
188
+ value={model.extends}
189
+ list={props.listId}
190
+ disabled={props.disabled === true}
191
+ onEdit={(value) => props.onPatch({ extends: value })}
192
+ />
193
+ <TextField
194
+ id={`${id}-name`}
195
+ label={t('modelName')}
196
+ value={model.name}
197
+ disabled={props.disabled === true}
198
+ onEdit={(value) => props.onPatch({ name: value })}
199
+ />
200
+ <TextField
201
+ id={`${id}-ctx`}
202
+ label={t('contextWindow')}
203
+ numeric
204
+ value={model.contextWindow}
205
+ disabled={props.disabled === true}
206
+ onEdit={(value) => props.onPatch({ contextWindow: value })}
207
+ />
208
+ <TextField
209
+ id={`${id}-max`}
210
+ label={t('maxTokens')}
211
+ numeric
212
+ value={model.maxTokens}
213
+ disabled={props.disabled === true}
214
+ onEdit={(value) => props.onPatch({ maxTokens: value })}
215
+ />
216
+ <div className="lpc-field">
217
+ <span className="lpc-label">{t('input')}</span>
218
+ {MODALITIES.map((modality) => (
219
+ <CheckRow
220
+ key={modality}
221
+ id={`${id}-input-${modality}`}
222
+ label={modality}
223
+ checked={model.input[modality]}
224
+ disabled={props.disabled === true}
225
+ onEdit={(checked) => props.onPatch({ input: { ...model.input, [modality]: checked } })}
226
+ />
227
+ ))}
228
+ </div>
229
+ </div>
230
+ <ReasoningEditor
231
+ idPrefix={`${id}-re`}
232
+ value={model.reasoningEfforts}
233
+ disabled={props.disabled === true}
234
+ t={t}
235
+ onEdit={(reasoningEfforts) => props.onPatch({ reasoningEfforts })}
236
+ />
237
+ <CompatEditor
238
+ idPrefix={`${id}-compat`}
239
+ api={props.api}
240
+ compat={model.compat}
241
+ epoch={props.epoch}
242
+ disabled={props.disabled === true}
243
+ wide
244
+ t={t}
245
+ onEdit={(compat) => props.onPatch({ compat })}
246
+ />
247
+ </div>
248
+ )
249
+ }
250
+
251
+ export interface ReasoningEditorProps {
252
+ idPrefix: string
253
+ value: ReasoningDraft
254
+ disabled?: boolean
255
+ t(key: string): string
256
+ onEdit(next: ReasoningDraft): void
257
+ }
258
+
259
+ export function ReasoningEditor(props: ReasoningEditorProps): ReactElement {
260
+ const { value } = props
261
+ return (
262
+ <div className="lpc-field lpc-wide">
263
+ <div className="lpc-head">
264
+ <span className="lpc-label">{props.t('reasoningEfforts')}</span>
265
+ </div>
266
+ <div className="lpc-checkRow">
267
+ <input
268
+ id={`${props.idPrefix}-nonreasoning`}
269
+ type="checkbox"
270
+ checked={value.nonReasoning}
271
+ disabled={props.disabled === true}
272
+ onChange={(event) => props.onEdit({ ...value, nonReasoning: event.target.checked })}
273
+ />
274
+ <label htmlFor={`${props.idPrefix}-nonreasoning`}>{props.t('nonReasoning')}</label>
275
+ </div>
276
+ {value.nonReasoning ? null : (
277
+ <div className="lpc-grid">
278
+ {THINKING_LEVELS.map((level) => (
279
+ <TextField
280
+ key={level}
281
+ id={`${props.idPrefix}-${level}`}
282
+ label={level}
283
+ value={value.levels[level] ?? ''}
284
+ disabled={props.disabled === true}
285
+ onEdit={(text) => props.onEdit({ ...value, levels: { ...value.levels, [level]: text } })}
286
+ />
287
+ ))}
288
+ </div>
289
+ )}
290
+ </div>
291
+ )
292
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Provider route 的标量字段组:基础字段网格(继承/显示名/端点/凭据/默认容量/
3
+ * 超时/默认模态/预算)与协议/档位/缓存/传输下拉组。compat、headers、
4
+ * retryPolicy 与模型目录在 views/providers.tsx 的 route 小节内另行渲染。
5
+ * @module llm-pi/client/views/provider-fields
6
+ */
7
+ import type { ReactElement } from 'react'
8
+
9
+ import {
10
+ BUDGET_KEYS, CACHE_RETENTION_OPTIONS, MODALITIES, PROTOCOL_IDS, THINKING_LEVELS, TRANSPORT_OPTIONS,
11
+ } from '../constants.ts'
12
+ import type { ProviderDraft } from '../draft.ts'
13
+ import { CheckRow, SelectField, TextField } from '../fields.tsx'
14
+ import { pruneCompatForApi } from './compat.tsx'
15
+
16
+ export interface ProviderScalarFieldsProps {
17
+ id: string
18
+ draft: ProviderDraft
19
+ disabled?: boolean
20
+ t(key: string): string
21
+ onPatch(patch: Partial<ProviderDraft>): void
22
+ }
23
+
24
+ export function ProviderScalarFields(props: ProviderScalarFieldsProps): ReactElement {
25
+ const { draft, t } = props
26
+ return (
27
+ <div className="lpc-grid">
28
+ <TextField
29
+ id={`${props.id}-extends`}
30
+ label={t('extends')}
31
+ hint={t('extendsHint')}
32
+ value={draft.extends}
33
+ disabled={props.disabled === true}
34
+ onEdit={(value) => props.onPatch({ extends: value })}
35
+ />
36
+ <TextField
37
+ id={`${props.id}-displayName`}
38
+ label={t('displayName')}
39
+ value={draft.displayName}
40
+ disabled={props.disabled === true}
41
+ onEdit={(value) => props.onPatch({ displayName: value })}
42
+ />
43
+ <TextField
44
+ id={`${props.id}-baseURL`}
45
+ label={t('baseURL')}
46
+ hint={t('baseURLHint')}
47
+ value={draft.baseURL}
48
+ disabled={props.disabled === true}
49
+ onEdit={(value) => props.onPatch({ baseURL: value })}
50
+ />
51
+ <TextField
52
+ id={`${props.id}-apiKeyEnv`}
53
+ label={t('apiKeyEnv')}
54
+ hint={t('apiKeyEnvHint')}
55
+ value={draft.apiKeyEnv}
56
+ disabled={props.disabled === true}
57
+ onEdit={(value) => props.onPatch({ apiKeyEnv: value })}
58
+ />
59
+ <TextField
60
+ id={`${props.id}-defaultCtx`}
61
+ label={t('defaultContextWindow')}
62
+ numeric
63
+ value={draft.defaultContextWindow}
64
+ disabled={props.disabled === true}
65
+ onEdit={(value) => props.onPatch({ defaultContextWindow: value })}
66
+ />
67
+ <TextField
68
+ id={`${props.id}-defaultMax`}
69
+ label={t('defaultMaxTokens')}
70
+ numeric
71
+ value={draft.defaultMaxTokens}
72
+ disabled={props.disabled === true}
73
+ onEdit={(value) => props.onPatch({ defaultMaxTokens: value })}
74
+ />
75
+ <TextField
76
+ id={`${props.id}-timeout`}
77
+ label={t('timeoutMs')}
78
+ numeric
79
+ value={draft.timeoutMs}
80
+ disabled={props.disabled === true}
81
+ onEdit={(value) => props.onPatch({ timeoutMs: value })}
82
+ />
83
+ <TextField
84
+ id={`${props.id}-wsTimeout`}
85
+ label={t('websocketConnectTimeoutMs')}
86
+ numeric
87
+ value={draft.websocketConnectTimeoutMs}
88
+ disabled={props.disabled === true}
89
+ onEdit={(value) => props.onPatch({ websocketConnectTimeoutMs: value })}
90
+ />
91
+ <TextField
92
+ id={`${props.id}-streamIdle`}
93
+ label={t('streamIdleTimeoutMs')}
94
+ numeric
95
+ value={draft.streamIdleTimeoutMs}
96
+ disabled={props.disabled === true}
97
+ onEdit={(value) => props.onPatch({ streamIdleTimeoutMs: value })}
98
+ />
99
+ <div className="lpc-field">
100
+ <span className="lpc-label">{t('defaultInput')}</span>
101
+ {MODALITIES.map((modality) => (
102
+ <CheckRow
103
+ key={modality}
104
+ id={`${props.id}-input-${modality}`}
105
+ label={modality}
106
+ checked={draft.input[modality]}
107
+ disabled={props.disabled === true}
108
+ onEdit={(checked) => props.onPatch({ input: { ...draft.input, [modality]: checked } })}
109
+ />
110
+ ))}
111
+ </div>
112
+ <div className="lpc-field">
113
+ <span className="lpc-label">{t('thinkingBudgets')}</span>
114
+ <div className="lpc-grid lpc-gridNested">
115
+ {BUDGET_KEYS.map((key) => (
116
+ <TextField
117
+ key={key}
118
+ id={`${props.id}-budget-${key}`}
119
+ label={key}
120
+ numeric
121
+ value={draft.thinkingBudgets[key]}
122
+ disabled={props.disabled === true}
123
+ onEdit={(value) =>
124
+ props.onPatch({ thinkingBudgets: { ...draft.thinkingBudgets, [key]: value } })
125
+ }
126
+ />
127
+ ))}
128
+ </div>
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ /** 协议/档位/缓存/传输四个下拉组。 */
135
+ export function ProviderSelectFields(props: ProviderScalarFieldsProps): ReactElement {
136
+ const { draft, t } = props
137
+ return (
138
+ <div className="lpc-grid">
139
+ <SelectField
140
+ id={`${props.id}-api`}
141
+ label={t('api')}
142
+ value={draft.api}
143
+ options={PROTOCOL_IDS}
144
+ unsetLabel={t('compatUnset')}
145
+ disabled={props.disabled === true}
146
+ onEdit={(value) => props.onPatch({ api: value, compat: pruneCompatForApi(draft.compat, value) })}
147
+ />
148
+ <SelectField
149
+ id={`${props.id}-reasoning`}
150
+ label={t('reasoning')}
151
+ value={draft.reasoning}
152
+ options={THINKING_LEVELS}
153
+ unsetLabel={t('compatUnset')}
154
+ disabled={props.disabled === true}
155
+ onEdit={(value) => props.onPatch({ reasoning: value })}
156
+ />
157
+ <SelectField
158
+ id={`${props.id}-cache`}
159
+ label={t('cacheRetention')}
160
+ value={draft.cacheRetention}
161
+ options={CACHE_RETENTION_OPTIONS}
162
+ unsetLabel={t('compatUnset')}
163
+ disabled={props.disabled === true}
164
+ onEdit={(value) => props.onPatch({ cacheRetention: value })}
165
+ />
166
+ <SelectField
167
+ id={`${props.id}-transport`}
168
+ label={t('transport')}
169
+ value={draft.transport}
170
+ options={TRANSPORT_OPTIONS}
171
+ unsetLabel={t('compatUnset')}
172
+ disabled={props.disabled === true}
173
+ onEdit={(value) => props.onPatch({ transport: value })}
174
+ />
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Provider 路由编辑:每个 route 一张可折叠小节(键名 + 标量字段 +
3
+ * headers 键值对 + retryPolicy JSON + compat + 模型目录)。
4
+ * 支持新增 route(输入键名)与删除 route。
5
+ * @module llm-pi/client/views/providers
6
+ */
7
+ import { useState, type ReactElement } from 'react'
8
+
9
+ import type { ProviderDraft } from '../draft.ts'
10
+ import { CollapseSection, JsonField, KeyValueEditor } from '../fields.tsx'
11
+ import { CompatEditor } from './compat.tsx'
12
+ import { ModelsTable } from './models.tsx'
13
+ import { ProviderScalarFields, ProviderSelectFields } from './provider-fields.tsx'
14
+
15
+ export interface ProvidersSectionProps {
16
+ providers: Record<string, ProviderDraft>
17
+ epoch: number
18
+ disabled?: boolean
19
+ t(key: string): string
20
+ onAddRoute(key: string): void
21
+ onRemoveRoute(route: string): void
22
+ onPatchProvider(route: string, patch: Partial<ProviderDraft>): void
23
+ }
24
+
25
+ export function ProvidersSection(props: ProvidersSectionProps): ReactElement {
26
+ const [newRoute, setNewRoute] = useState('')
27
+ const [routeError, setRouteError] = useState('')
28
+ const submitAdd = (): void => {
29
+ const key = newRoute.trim()
30
+ if (key === '') {
31
+ setRouteError(props.t('routeEmpty'))
32
+ return
33
+ }
34
+ if (props.providers[key] !== undefined) {
35
+ setRouteError(props.t('routeDuplicate'))
36
+ return
37
+ }
38
+ props.onAddRoute(key)
39
+ setNewRoute('')
40
+ setRouteError('')
41
+ }
42
+ return (
43
+ <div className="lpc-section">
44
+ <p className="lpc-groupLabel">{props.t('providersGroup')}</p>
45
+ <div className="lpc-addRoute">
46
+ <input
47
+ className={`lpc-input${routeError !== '' ? ' lpc-inputInvalid' : ''}`}
48
+ value={newRoute}
49
+ disabled={props.disabled === true}
50
+ placeholder={props.t('addRoutePlaceholder')}
51
+ onChange={(event) => {
52
+ setNewRoute(event.target.value)
53
+ setRouteError('')
54
+ }}
55
+ />
56
+ <button
57
+ type="button"
58
+ className="lpc-btn lpc-btnGhost"
59
+ disabled={props.disabled === true}
60
+ onClick={submitAdd}
61
+ >
62
+ {props.t('addRoute')}
63
+ </button>
64
+ </div>
65
+ {routeError !== '' ? <p className="lpc-invalid">{routeError}</p> : null}
66
+ {Object.entries(props.providers).map(([route, draft]) => (
67
+ <ProviderSection
68
+ key={route}
69
+ route={route}
70
+ draft={draft}
71
+ epoch={props.epoch}
72
+ disabled={props.disabled === true}
73
+ t={props.t}
74
+ onRemove={() => props.onRemoveRoute(route)}
75
+ onPatch={(patch) => props.onPatchProvider(route, patch)}
76
+ />
77
+ ))}
78
+ </div>
79
+ )
80
+ }
81
+
82
+ export interface ProviderSectionProps {
83
+ route: string
84
+ draft: ProviderDraft
85
+ epoch: number
86
+ disabled?: boolean
87
+ t(key: string): string
88
+ onRemove(): void
89
+ onPatch(patch: Partial<ProviderDraft>): void
90
+ }
91
+
92
+ export function ProviderSection(props: ProviderSectionProps): ReactElement {
93
+ const [open, setOpen] = useState(false)
94
+ const { route, draft, t } = props
95
+ const id = route.replace(/[^a-zA-Z0-9_-]/g, '_')
96
+ const summary = draft.api !== '' ? draft.api : draft.extends !== '' ? `extends ${draft.extends}` : ''
97
+ const fieldProps = { id, draft, disabled: props.disabled === true, t, onPatch: props.onPatch }
98
+ return (
99
+ <div className="lpc-route">
100
+ <div className="lpc-routeHead">
101
+ <button
102
+ type="button"
103
+ className="lpc-routeToggle"
104
+ aria-expanded={open}
105
+ onClick={() => setOpen(!open)}
106
+ >
107
+ <span className={`lpc-chevron${open ? ' lpc-chevronOpen' : ''}`}>▾</span>
108
+ <span className="lpc-routeKey">{route}</span>
109
+ {summary !== '' ? <span className="lpc-routeApi">{summary}</span> : null}
110
+ </button>
111
+ <button
112
+ type="button"
113
+ className="lpc-btn lpc-btnGhost lpc-btnSmall"
114
+ disabled={props.disabled === true}
115
+ onClick={props.onRemove}
116
+ >
117
+ {t('deleteRoute')}
118
+ </button>
119
+ </div>
120
+ {open ? (
121
+ <div className="lpc-routeBody">
122
+ <p className="lpc-groupLabel">{t('providerFields')}</p>
123
+ <ProviderScalarFields {...fieldProps} />
124
+ <ProviderSelectFields {...fieldProps} />
125
+ <KeyValueEditor
126
+ id={`${id}-headers`}
127
+ label={t('headers')}
128
+ hint={t('headersHint')}
129
+ pairs={draft.headers}
130
+ disabled={props.disabled === true}
131
+ keyPlaceholder={t('key')}
132
+ valuePlaceholder={t('value')}
133
+ addLabel={t('add')}
134
+ removeLabel={t('remove')}
135
+ onEdit={(headers) => props.onPatch({ headers })}
136
+ />
137
+ <CollapseSection id={`${id}-advanced`} title={t('advancedGroup')} defaultOpen={false}>
138
+ <>
139
+ <JsonField
140
+ id={`${id}-retry`}
141
+ label={t('retryPolicy')}
142
+ hint={t('retryPolicyHint')}
143
+ invalidText={t('invalidJson')}
144
+ value={draft.retryPolicy}
145
+ epoch={props.epoch}
146
+ disabled={props.disabled === true}
147
+ wide
148
+ onEdit={(retryPolicy) => props.onPatch({ retryPolicy })}
149
+ />
150
+ <CompatEditor
151
+ idPrefix={`${id}-compat`}
152
+ api={draft.api}
153
+ compat={draft.compat}
154
+ epoch={props.epoch}
155
+ disabled={props.disabled === true}
156
+ wide
157
+ t={t}
158
+ onEdit={(compat) => props.onPatch({ compat })}
159
+ />
160
+ </>
161
+ </CollapseSection>
162
+ <ModelsTable
163
+ route={route}
164
+ api={draft.api}
165
+ defaultProvider={draft.extends}
166
+ models={draft.models}
167
+ epoch={props.epoch}
168
+ disabled={props.disabled === true}
169
+ t={t}
170
+ onModels={(models) => props.onPatch({ models })}
171
+ />
172
+ </div>
173
+ ) : null}
174
+ </div>
175
+ )
176
+ }