@ouraihub/aether-ui 0.0.0-stage → 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.
Files changed (179) hide show
  1. package/LICENSE +789 -0
  2. package/fixtures/model-classify.json +42 -0
  3. package/fixtures/pricing-cost.json +56 -0
  4. package/fixtures/reasoning-infer.json +44 -0
  5. package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
  6. package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
  7. package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
  8. package/fixtures/sse/anthropic-messages/text.sse +27 -0
  9. package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
  10. package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
  11. package/fixtures/sse/expect.json +20 -0
  12. package/fixtures/sse/openai-chat/chinese.sse +14 -0
  13. package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
  14. package/fixtures/sse/openai-chat/single-tool.sse +22 -0
  15. package/fixtures/sse/openai-chat/text.sse +14 -0
  16. package/fixtures/sse/openai-chat/truncated.sse +12 -0
  17. package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
  18. package/fixtures/sse/openai-responses/chinese.sse +33 -0
  19. package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
  20. package/fixtures/sse/openai-responses/single-tool.sse +27 -0
  21. package/fixtures/sse/openai-responses/text.sse +33 -0
  22. package/fixtures/sse/openai-responses/truncated.sse +30 -0
  23. package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
  24. package/package.json +71 -3
  25. package/src/CopyInput.svelte +33 -0
  26. package/src/KitNotifier.svelte +33 -0
  27. package/src/chat/ApprovalCard.svelte +23 -0
  28. package/src/chat/ChatPanel.svelte +2097 -0
  29. package/src/chat/MessageItem.svelte +583 -0
  30. package/src/chat/SkillPicker.svelte +126 -0
  31. package/src/chat/ToolCallItem.svelte +23 -0
  32. package/src/chat/UsageDetails.svelte +138 -0
  33. package/src/chat/index.ts +2 -0
  34. package/src/chat/run-session.svelte.ts +556 -0
  35. package/src/components/ui/alert/alert-action.svelte +20 -0
  36. package/src/components/ui/alert/alert-description.svelte +23 -0
  37. package/src/components/ui/alert/alert-title.svelte +23 -0
  38. package/src/components/ui/alert/alert.svelte +43 -0
  39. package/src/components/ui/alert/index.ts +17 -0
  40. package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  41. package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  42. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  43. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
  44. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  45. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
  46. package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
  47. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
  48. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  49. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
  50. package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  51. package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  52. package/src/components/ui/alert-dialog/index.ts +40 -0
  53. package/src/components/ui/button/button.svelte +81 -0
  54. package/src/components/ui/button/index.ts +17 -0
  55. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  56. package/src/components/ui/dialog/dialog-content.svelte +48 -0
  57. package/src/components/ui/dialog/dialog-description.svelte +17 -0
  58. package/src/components/ui/dialog/dialog-footer.svelte +32 -0
  59. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-portal.svelte +7 -0
  62. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  63. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  64. package/src/components/ui/dialog/dialog.svelte +7 -0
  65. package/src/components/ui/dialog/index.ts +34 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  69. package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  70. package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  71. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  72. package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  73. package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  74. package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
  75. package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
  76. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  77. package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  78. package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
  79. package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  80. package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  81. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  82. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  83. package/src/components/ui/dropdown-menu/index.ts +54 -0
  84. package/src/components/ui/empty/empty-content.svelte +23 -0
  85. package/src/components/ui/empty/empty-description.svelte +23 -0
  86. package/src/components/ui/empty/empty-header.svelte +20 -0
  87. package/src/components/ui/empty/empty-media.svelte +41 -0
  88. package/src/components/ui/empty/empty-title.svelte +20 -0
  89. package/src/components/ui/empty/empty.svelte +23 -0
  90. package/src/components/ui/empty/index.ts +22 -0
  91. package/src/components/ui/field/field-content.svelte +20 -0
  92. package/src/components/ui/field/field-description.svelte +25 -0
  93. package/src/components/ui/field/field-error.svelte +58 -0
  94. package/src/components/ui/field/field-group.svelte +23 -0
  95. package/src/components/ui/field/field-label.svelte +25 -0
  96. package/src/components/ui/field/field-legend.svelte +24 -0
  97. package/src/components/ui/field/field-separator.svelte +35 -0
  98. package/src/components/ui/field/field-set.svelte +20 -0
  99. package/src/components/ui/field/field-title.svelte +20 -0
  100. package/src/components/ui/field/field.svelte +47 -0
  101. package/src/components/ui/field/index.ts +33 -0
  102. package/src/components/ui/input/index.ts +7 -0
  103. package/src/components/ui/input/input.svelte +48 -0
  104. package/src/components/ui/label/index.ts +7 -0
  105. package/src/components/ui/label/label.svelte +20 -0
  106. package/src/components/ui/select/index.ts +40 -0
  107. package/src/components/ui/select/select-content.svelte +45 -0
  108. package/src/components/ui/select/select-group-heading.svelte +21 -0
  109. package/src/components/ui/select/select-group.svelte +17 -0
  110. package/src/components/ui/select/select-item.svelte +43 -0
  111. package/src/components/ui/select/select-label.svelte +20 -0
  112. package/src/components/ui/select/select-portal.svelte +7 -0
  113. package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
  114. package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
  115. package/src/components/ui/select/select-separator.svelte +18 -0
  116. package/src/components/ui/select/select-trigger.svelte +29 -0
  117. package/src/components/ui/select/select-value.svelte +7 -0
  118. package/src/components/ui/select/select.svelte +11 -0
  119. package/src/components/ui/separator/index.ts +7 -0
  120. package/src/components/ui/separator/separator.svelte +21 -0
  121. package/src/components/ui/sonner/index.ts +1 -0
  122. package/src/components/ui/sonner/sonner.svelte +40 -0
  123. package/src/components/ui/switch/index.ts +7 -0
  124. package/src/components/ui/switch/switch.svelte +34 -0
  125. package/src/components/ui/textarea/index.ts +7 -0
  126. package/src/components/ui/textarea/textarea.svelte +22 -0
  127. package/src/context.ts +34 -0
  128. package/src/dev/Demo.svelte +45 -0
  129. package/src/dev/main.ts +7 -0
  130. package/src/i18n/en.ts +617 -0
  131. package/src/i18n/index.ts +23 -0
  132. package/src/i18n/zh-CN.ts +599 -0
  133. package/src/index.ts +10 -0
  134. package/src/logs/LogPanel.svelte +97 -0
  135. package/src/logs/LogSettings.svelte +56 -0
  136. package/src/logs/index.ts +5 -0
  137. package/src/logs/logger.ts +96 -0
  138. package/src/markdown.ts +8 -0
  139. package/src/mcp/McpClientSettings.svelte +491 -0
  140. package/src/mcp/McpServerSettings.svelte +327 -0
  141. package/src/mcp/config.ts +26 -0
  142. package/src/mcp/index.ts +3 -0
  143. package/src/mcp/input.ts +73 -0
  144. package/src/notify.svelte.ts +43 -0
  145. package/src/ports/contract.ts +165 -0
  146. package/src/ports/fake.ts +484 -0
  147. package/src/ports/index.ts +7 -0
  148. package/src/ports/search.ts +47 -0
  149. package/src/ports/skill-sources.ts +45 -0
  150. package/src/providers/EndpointKeysDialog.svelte +343 -0
  151. package/src/providers/EndpointPanel.svelte +491 -0
  152. package/src/providers/ModelPickerDialog.svelte +187 -0
  153. package/src/providers/ModelSelect.svelte +434 -0
  154. package/src/providers/ModelSettingsDialog.svelte +453 -0
  155. package/src/providers/ProvidersSettings.svelte +631 -0
  156. package/src/providers/RequestConfigDialog.svelte +250 -0
  157. package/src/providers/SecretInput.svelte +276 -0
  158. package/src/providers/draft.ts +329 -0
  159. package/src/providers/edit-session.ts +146 -0
  160. package/src/providers/index.ts +5 -0
  161. package/src/providers/reasoning.ts +41 -0
  162. package/src/providers/revision.svelte.ts +4 -0
  163. package/src/search/SearchSettings.svelte +703 -0
  164. package/src/search/index.ts +13 -0
  165. package/src/search/settings-flow.ts +62 -0
  166. package/src/session.ts +20 -0
  167. package/src/skills/SkillAdd.svelte +347 -0
  168. package/src/skills/SkillAuthoring.svelte +381 -0
  169. package/src/skills/SkillEditor.svelte +94 -0
  170. package/src/skills/SkillPreviewDialog.svelte +122 -0
  171. package/src/skills/SkillsSettings.svelte +140 -0
  172. package/src/skills/error.ts +32 -0
  173. package/src/skills/index.ts +4 -0
  174. package/src/styles/kit.css +137 -0
  175. package/src/styles/tokens.css +22 -0
  176. package/src/tauri/index.ts +282 -0
  177. package/src/utils.ts +22 -0
  178. package/src/web/index.ts +376 -0
  179. package/README.md +0 -3
@@ -0,0 +1,453 @@
1
+ <script lang="ts">
2
+ // Per-model settings (docs/41 §3–§4). Edits a copy; nothing changes until Save.
3
+ import { untrack } from 'svelte'
4
+ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
5
+ import MinusIcon from '@lucide/svelte/icons/minus'
6
+ import PlusIcon from '@lucide/svelte/icons/plus'
7
+ import { Button } from '#lib/components/ui/button/index.js'
8
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
9
+ import * as Field from '#lib/components/ui/field/index.js'
10
+ import { Input } from '#lib/components/ui/input/index.js'
11
+ import * as Select from '#lib/components/ui/select/index.js'
12
+ import CopyInput from '../CopyInput.svelte'
13
+ import { getKitContext } from '../context'
14
+ import type { Model, ModelType, PricingTier, Protocol, ReasoningDialect, ReasoningEffort } from '../ports'
15
+ import { cn } from '../utils'
16
+ import { idPrefix, purposeOf, validateModel, withPurpose, withType, type ModelField, type Purpose } from './draft'
17
+ import { REASONING_DIALECTS, REASONING_EFFORTS, allowsAuto, dialectFits, emptyReasoning } from './reasoning'
18
+
19
+ let {
20
+ open = $bindable(false),
21
+ model,
22
+ protocol,
23
+ usedBy = [],
24
+ onsave,
25
+ }: {
26
+ open: boolean
27
+ model: Model | null
28
+ protocol: Protocol
29
+ /** Localized role names bound to this model (contract §6). */
30
+ usedBy?: string[]
31
+ onsave: (model: Model) => void
32
+ } = $props()
33
+
34
+ const MAIN_TYPES = ['chat', 'image', 'embedding', 'rerank'] as const
35
+ const OTHER_TYPES: ModelType[] = ['video', 'tts', 'stt', 'unknown']
36
+ // Presets mix decimal and binary sizes, so labels are explicit.
37
+ const CONTEXT_PRESETS: [string, number][] = [
38
+ ['128K', 128000],
39
+ ['200K', 200000],
40
+ ['256K', 262144],
41
+ ['400K', 400000],
42
+ ['512K', 524288],
43
+ ['1M', 1000000],
44
+ ]
45
+ const OUTPUT_PRESETS: [string, number][] = [
46
+ ['16K', 16384],
47
+ ['32K', 32768],
48
+ ['64K', 65536],
49
+ ['128K', 131072],
50
+ ]
51
+ const PRICES = ['input', 'output', 'cacheRead', 'cacheWrite'] as const
52
+ const PURPOSES: Purpose[] = ['chat', 'image', 'edit']
53
+ const CURRENCIES = [
54
+ ['USD', '$'],
55
+ ['CNY', '¥'],
56
+ ] as const
57
+ const CHIP = 'aria-pressed:border-primary aria-pressed:bg-primary/10 aria-pressed:text-primary'
58
+
59
+ const { t } = getKitContext().i18n
60
+ let edit = $state<Model | null>(null)
61
+ let more = $state(true)
62
+ $effect.pre(() => {
63
+ if (!open) return
64
+ edit = untrack(() => (model ? $state.snapshot(model) : null))
65
+ more = true
66
+ })
67
+ const dialects = $derived(REASONING_DIALECTS.filter((dialect) => dialectFits(dialect, protocol)))
68
+ const errors = $derived(edit ? validateModel(edit, protocol) : [])
69
+ const purpose = $derived(edit && purposeOf(edit))
70
+ const count = (value: number) => (Number.isNaN(value) ? null : value)
71
+
72
+ function errorFor(field: ModelField) {
73
+ return errors
74
+ .filter((item) => item.field === field)
75
+ .map((item) => ({ message: t(`providers.error.${field}.${item.reason}`) }))
76
+ }
77
+ function toggleEffort(effort: ReasoningEffort) {
78
+ const config = edit?.reasoning
79
+ if (!edit || !config) return
80
+ const efforts = config.efforts.includes(effort)
81
+ ? config.efforts.filter((item) => item !== effort)
82
+ : [...config.efforts, effort]
83
+ edit.reasoning = { ...config, efforts }
84
+ }
85
+ function setBudget(value: number) {
86
+ if (!edit?.reasoning) return
87
+ const rest = { ...edit.reasoning }
88
+ delete rest.budgetTokens
89
+ edit.reasoning = Number.isNaN(value) ? rest : { ...rest, budgetTokens: value }
90
+ }
91
+ /** Tri-state: not probed → supported → unsupported → not probed. */
92
+ function cycleTools() {
93
+ if (edit) edit.supportsTools = edit.supportsTools === null ? true : edit.supportsTools ? false : null
94
+ }
95
+ const tier = (minInputTokens: number): PricingTier => ({
96
+ minInputTokens,
97
+ input: 0,
98
+ output: 0,
99
+ cacheRead: null,
100
+ cacheWrite: null,
101
+ })
102
+ function setPrice(index: number, key: keyof PricingTier, value: number) {
103
+ const target = edit?.pricing?.tiers[index]
104
+ if (!target) return
105
+ // Required prices stay NaN when cleared so validation flags them; cache prices fall back to the input price.
106
+ if (key === 'cacheRead' || key === 'cacheWrite') target[key] = count(value)
107
+ else target[key] = value
108
+ }
109
+ function removeTier(index: number) {
110
+ const pricing = edit?.pricing
111
+ if (!edit || !pricing) return
112
+ pricing.tiers.splice(index, 1)
113
+ if (pricing.tiers[0]) pricing.tiers[0].minInputTokens = 0
114
+ else edit.pricing = null
115
+ }
116
+ function save() {
117
+ if (!edit || errors.length > 0) return
118
+ const name = edit.name?.trim() || null
119
+ const groupName = edit.groupName?.trim() || null
120
+ onsave({ ...$state.snapshot(edit), name, groupName })
121
+ open = false
122
+ }
123
+ </script>
124
+
125
+ <Dialog.Root bind:open>
126
+ <Dialog.Content class="flex max-h-[85dvh] min-w-0 flex-col sm:max-w-lg">
127
+ <Dialog.Header class="min-w-0 text-left">
128
+ <Dialog.Title>{t('providers.edit_model')}</Dialog.Title>
129
+ </Dialog.Header>
130
+ {#if edit}
131
+ <Field.Group class="min-h-0 overflow-y-auto pr-1">
132
+ <Field.Field>
133
+ <Field.Label for="kit-model-id">{t('providers.model_id')}</Field.Label>
134
+ <CopyInput id="kit-model-id" class="font-mono" readonly value={edit.id} />
135
+ </Field.Field>
136
+ <Field.Field>
137
+ <Field.Label for="kit-model-name">{t('providers.model_name')}</Field.Label>
138
+ <Input
139
+ id="kit-model-name"
140
+ placeholder={edit.id}
141
+ autocomplete="off"
142
+ aria-invalid={errorFor('name').length > 0}
143
+ bind:value={() => edit?.name ?? '', (value) => edit && (edit.name = value)}
144
+ />
145
+ <Field.Error errors={errorFor('name')} />
146
+ </Field.Field>
147
+ <Field.Field>
148
+ <Field.Label for="kit-model-group">{t('providers.group_name')}</Field.Label>
149
+ <Input
150
+ id="kit-model-group"
151
+ placeholder={idPrefix(edit.id)}
152
+ autocomplete="off"
153
+ aria-invalid={errorFor('groupName').length > 0}
154
+ bind:value={() => edit?.groupName ?? '', (value) => edit && (edit.groupName = value)}
155
+ />
156
+ <Field.Error errors={errorFor('groupName')} />
157
+ </Field.Field>
158
+ {#if purpose}
159
+ <Field.Set>
160
+ <Field.Legend variant="label">{t('providers.purpose')}</Field.Legend>
161
+ <div class="flex flex-wrap gap-4">
162
+ {#each PURPOSES as item (item)}
163
+ <label class="flex items-center gap-1.5 text-kit-sm"
164
+ ><input
165
+ type="radio"
166
+ name="kit-model-purpose"
167
+ checked={purpose === item}
168
+ onchange={() => edit && (edit = withPurpose(edit, item))}
169
+ />{t(`providers.purpose.${item}`)}</label
170
+ >
171
+ {/each}
172
+ </div>
173
+ </Field.Set>
174
+ {/if}
175
+ <Button
176
+ variant="ghost"
177
+ size="sm"
178
+ class="justify-start self-start px-1"
179
+ aria-expanded={more}
180
+ onclick={() => (more = !more)}
181
+ ><ChevronRightIcon class={cn('transition-transform', more && 'rotate-90')} />{t(
182
+ 'providers.more_settings',
183
+ )}</Button
184
+ >
185
+ {#if more}
186
+ <Field.Set>
187
+ <Field.Legend variant="label">{t('providers.model_type')}</Field.Legend>
188
+ <div class="flex flex-wrap gap-1.5">
189
+ {#each MAIN_TYPES as type (type)}
190
+ <Button
191
+ variant="outline"
192
+ size="sm"
193
+ class={CHIP}
194
+ aria-pressed={edit.modelType === type}
195
+ onclick={() => edit && (edit = withType(edit, type))}>{t(`providers.type.${type}`)}</Button
196
+ >
197
+ {/each}
198
+ <Select.Root
199
+ type="single"
200
+ value={OTHER_TYPES.includes(edit.modelType) ? edit.modelType : ''}
201
+ onValueChange={(value) => edit && value && (edit = withType(edit, value as ModelType))}
202
+ >
203
+ <Select.Trigger
204
+ size="sm"
205
+ class={cn(OTHER_TYPES.includes(edit.modelType) && 'border-primary bg-primary/10 text-primary')}
206
+ aria-label={t('providers.type_other')}
207
+ >{OTHER_TYPES.includes(edit.modelType)
208
+ ? t(`providers.type.${edit.modelType}`)
209
+ : t('providers.type_other')}</Select.Trigger
210
+ >
211
+ <Select.Content>
212
+ <Select.Group>
213
+ {#each OTHER_TYPES as type (type)}<Select.Item
214
+ value={type}
215
+ label={t(`providers.type.${type}`)}
216
+ />{/each}
217
+ </Select.Group>
218
+ </Select.Content>
219
+ </Select.Root>
220
+ </div>
221
+ {#if usedBy.length > 0 && edit.modelType !== model?.modelType}
222
+ <Field.Description class="text-warning text-kit-xs" role="status"
223
+ >{t('providers.type_in_use', { roles: usedBy.join(t('providers.list_separator')) })}</Field.Description
224
+ >
225
+ {/if}
226
+ </Field.Set>
227
+ {#if edit.modelType === 'chat'}
228
+ <Field.Set>
229
+ <Field.Legend variant="label">{t('providers.capabilities')}</Field.Legend>
230
+ <div class="flex flex-wrap gap-1.5">
231
+ <Button
232
+ variant="outline"
233
+ size="sm"
234
+ class={CHIP}
235
+ aria-pressed={edit.reasoning !== null}
236
+ onclick={() =>
237
+ edit && (edit.reasoning = edit.reasoning ? null : emptyReasoning(dialects[0] ?? 'openai'))}
238
+ >{t('providers.cap_reasoning')}</Button
239
+ >
240
+ <Button
241
+ variant="outline"
242
+ size="sm"
243
+ class={cn(CHIP, edit.supportsTools === false && 'text-muted-foreground line-through')}
244
+ aria-pressed={edit.supportsTools === true}
245
+ title={t(`providers.tools_state.${edit.supportsTools ?? 'unknown'}`)}
246
+ onclick={cycleTools}
247
+ >{t('providers.cap_tools')}{#if edit.supportsTools === null}<span
248
+ class="text-muted-foreground text-kit-xs">{t('providers.tools_state.unknown')}</span
249
+ >{/if}</Button
250
+ >
251
+ {#if protocol !== 'openai-chat'}
252
+ <Button
253
+ variant="outline"
254
+ size="sm"
255
+ class={CHIP}
256
+ aria-pressed={edit.nativeWebSearch}
257
+ title={t('providers.nativeWebSearch')}
258
+ onclick={() => edit && (edit.nativeWebSearch = !edit.nativeWebSearch)}
259
+ >{t('providers.cap_web')}</Button
260
+ >
261
+ {/if}
262
+ </div>
263
+ </Field.Set>
264
+ {#if edit.reasoning}
265
+ {@render reasoningControls(edit.reasoning.dialect)}
266
+ {/if}
267
+ <Field.Set>
268
+ <Field.Legend variant="label">{t('providers.input_modalities')}</Field.Legend>
269
+ <div class="flex flex-wrap gap-1.5">
270
+ <Button
271
+ variant="outline"
272
+ size="sm"
273
+ class={CHIP}
274
+ aria-pressed={edit.canVision}
275
+ onclick={() => edit && (edit.canVision = !edit.canVision)}>{t('providers.cap_vision')}</Button
276
+ >
277
+ </div>
278
+ </Field.Set>
279
+ {/if}
280
+ {@render tokens('contextWindow', CONTEXT_PRESETS, 32000)}
281
+ {@render tokens('maxOutput', OUTPUT_PRESETS, 4096)}
282
+ {#if edit.modelType === 'chat'}
283
+ {@render pricing()}
284
+ {/if}
285
+ {/if}
286
+ </Field.Group>
287
+ {/if}
288
+ <Dialog.Footer class="items-center">
289
+ <p class="text-muted-foreground mr-auto text-kit-xs">{t('providers.applies_next')}</p>
290
+ <Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
291
+ <Button disabled={errors.length > 0} onclick={save}>{t('providers.save')}</Button>
292
+ </Dialog.Footer>
293
+ </Dialog.Content>
294
+ </Dialog.Root>
295
+
296
+ {#snippet reasoningControls(dialect: ReasoningDialect)}
297
+ <Field.Field>
298
+ <Field.Label for="kit-model-dialect">{t('providers.reasoning_dialect')}</Field.Label>
299
+ <Select.Root
300
+ type="single"
301
+ value={dialect}
302
+ onValueChange={(value) => edit && (edit.reasoning = emptyReasoning(value as ReasoningDialect))}
303
+ >
304
+ <Select.Trigger id="kit-model-dialect" class="w-full">{t(`providers.dialect.${dialect}`)}</Select.Trigger>
305
+ <Select.Content>
306
+ <Select.Group>
307
+ {#each dialects as item (item)}<Select.Item value={item} label={t(`providers.dialect.${item}`)} />{/each}
308
+ </Select.Group>
309
+ </Select.Content>
310
+ </Select.Root>
311
+ </Field.Field>
312
+ <Field.Set>
313
+ <Field.Legend variant="label">{t('providers.reasoning_efforts')}</Field.Legend>
314
+ <div class="flex flex-wrap gap-3">
315
+ {#each REASONING_EFFORTS.filter((effort) => effort !== 'auto' || allowsAuto(dialect)) as effort (effort)}
316
+ <label class="flex items-center gap-1 text-kit-sm"
317
+ ><input
318
+ type="checkbox"
319
+ checked={edit?.reasoning?.efforts.includes(effort)}
320
+ onchange={() => toggleEffort(effort)}
321
+ />{t(`chat.effort.${effort}`)}</label
322
+ >
323
+ {/each}
324
+ </div>
325
+ {#if dialect !== 'anthropic-budget' && dialect !== 'qwen'}<Field.Error errors={errorFor('reasoning')} />{/if}
326
+ </Field.Set>
327
+ {#if dialect === 'anthropic-budget' || dialect === 'qwen'}
328
+ <Field.Field>
329
+ <Field.Label for="kit-model-budget">{t('providers.reasoning_budget')}</Field.Label>
330
+ <Input
331
+ id="kit-model-budget"
332
+ type="number"
333
+ min={dialect === 'qwen' ? '1' : '1024'}
334
+ step="1"
335
+ aria-invalid={errorFor('reasoning').length > 0}
336
+ value={edit?.reasoning?.budgetTokens ?? ''}
337
+ oninput={(event) => setBudget(event.currentTarget.valueAsNumber)}
338
+ />
339
+ <Field.Error errors={errorFor('reasoning')} />
340
+ </Field.Field>
341
+ {/if}
342
+ {/snippet}
343
+
344
+ {#snippet tokens(field: 'contextWindow' | 'maxOutput', presets: [string, number][], fallback: number)}
345
+ {@const value = edit?.[field] ?? null}
346
+ <Field.Field>
347
+ <Field.Label for="kit-model-{field}"
348
+ >{t(`providers.${field === 'contextWindow' ? 'context_window' : 'max_output'}`)}</Field.Label
349
+ >
350
+ <div class="flex flex-wrap gap-1.5">
351
+ {#each presets as [label, preset] (preset)}
352
+ <Button
353
+ variant="outline"
354
+ size="sm"
355
+ class={CHIP}
356
+ aria-pressed={value === preset}
357
+ onclick={() => edit && (edit[field] = preset)}>{label}</Button
358
+ >
359
+ {/each}
360
+ </div>
361
+ <Input
362
+ id="kit-model-{field}"
363
+ type="number"
364
+ min="1"
365
+ step="1"
366
+ placeholder={String(fallback)}
367
+ aria-invalid={errorFor(field).length > 0}
368
+ value={value ?? ''}
369
+ oninput={(event) => edit && (edit[field] = count(event.currentTarget.valueAsNumber))}
370
+ />
371
+ <Field.Error errors={errorFor(field)} />
372
+ </Field.Field>
373
+ {/snippet}
374
+
375
+ {#snippet pricing()}
376
+ <Field.Set>
377
+ <Field.Legend variant="label">{t('providers.pricing')}</Field.Legend>
378
+ {#if edit?.pricing}
379
+ {@const current = edit.pricing}
380
+ <div class="flex flex-wrap gap-1.5" role="group" aria-label={t('providers.currency')}>
381
+ {#each CURRENCIES as [currency, symbol] (currency)}
382
+ <Button
383
+ variant="outline"
384
+ size="sm"
385
+ class={CHIP}
386
+ aria-pressed={current.currency === currency}
387
+ aria-label={t(`providers.currency.${currency}`)}
388
+ onclick={() => (current.currency = currency)}>{symbol}</Button
389
+ >
390
+ {/each}
391
+ </div>
392
+ <p class="text-muted-foreground text-kit-xs">{t('providers.pricing_unit')}</p>
393
+ {#each current.tiers as item, index (index)}
394
+ <fieldset class="flex flex-col gap-2 rounded-kit border p-2">
395
+ <legend class="flex w-full items-center gap-2 px-1 text-kit-sm"
396
+ >{t('providers.tier', { n: index + 1 })}{#if current.tiers.length > 1}<Button
397
+ variant="ghost"
398
+ size="icon-xs"
399
+ class="ml-auto"
400
+ aria-label={t('providers.tier_remove', { n: index + 1 })}
401
+ title={t('providers.tier_remove', { n: index + 1 })}
402
+ onclick={() => removeTier(index)}><MinusIcon /></Button
403
+ >{/if}</legend
404
+ >
405
+ <div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
406
+ <label class="flex flex-col gap-1 text-kit-xs"
407
+ >{t('providers.tier_start')}<Input
408
+ type="number"
409
+ min="0"
410
+ step="1"
411
+ disabled={index === 0}
412
+ value={item.minInputTokens}
413
+ oninput={(event) => setPrice(index, 'minInputTokens', event.currentTarget.valueAsNumber)}
414
+ /></label
415
+ >
416
+ {#each PRICES as key (key)}
417
+ <label class="flex flex-col gap-1 text-kit-xs"
418
+ >{t(`providers.price.${key}`)}<Input
419
+ type="number"
420
+ min="0"
421
+ step="any"
422
+ placeholder={key === 'cacheRead' || key === 'cacheWrite' ? t('providers.price_same_as_input') : ''}
423
+ value={item[key] ?? ''}
424
+ oninput={(event) => setPrice(index, key, event.currentTarget.valueAsNumber)}
425
+ /></label
426
+ >
427
+ {/each}
428
+ </div>
429
+ </fieldset>
430
+ {/each}
431
+ <Field.Error errors={errorFor('pricing')} />
432
+ <div class="flex flex-wrap gap-1.5">
433
+ {#if current.tiers.length < 8}<Button
434
+ variant="outline"
435
+ size="sm"
436
+ onclick={() => current.tiers.push(tier((current.tiers.at(-1)?.minInputTokens ?? 0) + 200000))}
437
+ ><PlusIcon />{t('providers.tier_add')}</Button
438
+ >{/if}
439
+ <Button variant="ghost" size="sm" onclick={() => edit && (edit.pricing = null)}
440
+ >{t('providers.pricing_clear')}</Button
441
+ >
442
+ </div>
443
+ {:else}
444
+ <Button
445
+ variant="outline"
446
+ size="sm"
447
+ class="self-start"
448
+ onclick={() => edit && (edit.pricing = { currency: 'USD', tiers: [tier(0)] })}
449
+ ><PlusIcon />{t('providers.pricing_add')}</Button
450
+ >
451
+ {/if}
452
+ </Field.Set>
453
+ {/snippet}