@cat-factory/app 0.211.0 → 0.213.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.
@@ -12,6 +12,7 @@
12
12
  // base-model picker plus a filterable per-agent override list.
13
13
  import { computed, ref, watch } from 'vue'
14
14
  import { onKeyStroke } from '@vueuse/core'
15
+ import type { ModelFlavor } from '@cat-factory/contracts'
15
16
  import type { AgentKind } from '~/types/domain'
16
17
  import type { ModelPreset } from '~/types/model-presets'
17
18
  import AgentTierSelect from '~/components/palettes/AgentTierSelect.vue'
@@ -19,9 +20,12 @@ import { filterByAgentTierKeeping } from '~/utils/agentTier'
19
20
  import { MODEL_CONFIGURABLE_SYSTEM_KINDS } from '~/utils/catalog'
20
21
  import { cachingLabel, contextLabel, costLabel, displayFlavor, isSelectable } from '~/stores/models'
21
22
  import ConsensusGroupsSection from '~/components/settings/ConsensusGroupsSection.vue'
23
+ import ProviderPreferenceEditor from '~/components/settings/ProviderPreferenceEditor.vue'
24
+ import { showOverrideField } from '~/utils/uiMode'
22
25
 
23
26
  const { t } = useI18n()
24
27
  const ui = useUiStore()
28
+ const uiMode = useUiModeStore()
25
29
  const models = useModelsStore()
26
30
  const presets = useModelPresetsStore()
27
31
  const agents = useAgentsStore()
@@ -43,6 +47,8 @@ interface EditorState {
43
47
  baseModelId: string
44
48
  overrides: Record<string, string>
45
49
  isDefault: boolean
50
+ /** The preset's route order; undefined ⇒ it inherits the deployment's default order. */
51
+ providerPreference: ModelFlavor[] | undefined
46
52
  }
47
53
  const editor = ref<EditorState | null>(null)
48
54
  const busy = ref(false)
@@ -144,6 +150,7 @@ function startCreate() {
144
150
  baseModelId: selectableModels.value[0]?.id ?? 'kimi-k2.7',
145
151
  overrides: {},
146
152
  isDefault: false,
153
+ providerPreference: undefined,
147
154
  }
148
155
  filter.value = ''
149
156
  }
@@ -154,10 +161,19 @@ function startEdit(p: ModelPreset) {
154
161
  baseModelId: p.baseModelId,
155
162
  overrides: { ...p.overrides },
156
163
  isDefault: p.isDefault,
164
+ providerPreference: p.providerPreference ? [...p.providerPreference] : undefined,
157
165
  }
158
166
  filter.value = ''
159
167
  }
160
168
 
169
+ // The route order is an OVERRIDE of the deployment's default, so basic mode hides it — but only
170
+ // while this preset states none. A preset that already carries one (written by a teammate, by the
171
+ // API, or here at the advanced tier) keeps the control, or a basic-mode user would be looking at a
172
+ // preset whose routes they can neither read nor reset.
173
+ const showRouteOrder = computed(() =>
174
+ showOverrideField(uiMode.isAdvanced, editor.value?.providerPreference),
175
+ )
176
+
161
177
  async function setDefault(p: ModelPreset) {
162
178
  if (p.isDefault) return
163
179
  busy.value = true
@@ -249,6 +265,9 @@ async function save() {
249
265
  baseModelId: e.baseModelId,
250
266
  overrides: e.overrides,
251
267
  isDefault: e.isDefault,
268
+ // Always sent on a patch, `[]` included: an absent field means "leave the stored order
269
+ // alone", so a reset has to arrive as the empty list that clears it.
270
+ providerPreference: e.providerPreference ?? [],
252
271
  })
253
272
  } else {
254
273
  await presets.create({
@@ -256,6 +275,7 @@ async function save() {
256
275
  baseModelId: e.baseModelId,
257
276
  overrides: e.overrides,
258
277
  isDefault: e.isDefault,
278
+ ...(e.providerPreference ? { providerPreference: e.providerPreference } : {}),
259
279
  })
260
280
  }
261
281
  editor.value = null
@@ -422,6 +442,12 @@ function fail(title: string, e: unknown) {
422
442
  )
423
443
  }}
424
444
  </span>
445
+ <!-- A custom route order changes which provider the same model runs on, so the
446
+ list says so rather than leaving it visible only inside the editor (which
447
+ basic mode hides). -->
448
+ <span v-if="p.providerPreference?.length" class="text-slate-300">
449
+ · {{ t('settings.modelConfiguration.list.customRouteOrder') }}
450
+ </span>
425
451
  </div>
426
452
  </div>
427
453
  <p
@@ -483,6 +509,17 @@ function fail(title: string, e: unknown) {
483
509
  </label>
484
510
  </div>
485
511
 
512
+ <!-- Which of a model's ROUTES this preset's runs prefer: a compliance preset can put
513
+ AWS Bedrock ahead of a model's own provider API, an everyday preset a flat-rate
514
+ subscription first. An override of the deployment default, so basic mode hides it
515
+ until the preset actually carries one. -->
516
+ <ProviderPreferenceEditor
517
+ v-if="showRouteOrder"
518
+ v-model="editor.providerPreference"
519
+ :has-subscription="creds.configuredVendors.size > 0"
520
+ :is-default-preset="editor.isDefault"
521
+ />
522
+
486
523
  <div>
487
524
  <div class="mb-1 flex items-start justify-between gap-3">
488
525
  <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { DEFAULT_MODEL_FLAVOR_ORDER, type ModelFlavor } from '@cat-factory/contracts'
3
+ import {
4
+ commitFlavorOrder,
5
+ isDefaultFlavorOrder,
6
+ moveFlavor,
7
+ subscriptionOverridesOrder,
8
+ } from '~/components/settings/ProviderPreferenceEditor.logic'
9
+
10
+ const DEFAULT = [...DEFAULT_MODEL_FLAVOR_ORDER]
11
+
12
+ describe('isDefaultFlavorOrder', () => {
13
+ it('is true only for the full shipped order, position for position', () => {
14
+ expect(isDefaultFlavorOrder(DEFAULT)).toBe(true)
15
+ expect(isDefaultFlavorOrder([...DEFAULT].reverse())).toBe(false)
16
+ })
17
+
18
+ it('rejects a PREFIX of the default rather than reading it as "no preference"', () => {
19
+ // `every` is vacuously true for a prefix, so without the length check a partial order that
20
+ // happens to start like the default would be silently cleared — the preset would lose an
21
+ // order the caller meant to store.
22
+ expect(isDefaultFlavorOrder(DEFAULT.slice(0, 2))).toBe(false)
23
+ expect(isDefaultFlavorOrder([])).toBe(false)
24
+ })
25
+ })
26
+
27
+ describe('commitFlavorOrder', () => {
28
+ it('stores an order that equals the default as ABSENT, not as a copy of it', () => {
29
+ // The whole "a preset keeps tracking the shipped order as the product changes it" property
30
+ // rests on this, and the SPA is the only place it happens.
31
+ expect(commitFlavorOrder(DEFAULT)).toBeUndefined()
32
+ })
33
+
34
+ it('stores a genuinely reordered list', () => {
35
+ const reordered: ModelFlavor[] = [
36
+ 'bedrock',
37
+ 'direct',
38
+ 'openrouter',
39
+ 'cloudflare',
40
+ 'subscription',
41
+ ]
42
+ expect(commitFlavorOrder(reordered)).toEqual(reordered)
43
+ })
44
+
45
+ it('copies rather than aliasing the array it was given', () => {
46
+ const next: ModelFlavor[] = ['bedrock', 'direct', 'openrouter', 'cloudflare', 'subscription']
47
+ const stored = commitFlavorOrder(next)
48
+ next[0] = 'cloudflare'
49
+ expect(stored?.[0]).toBe('bedrock')
50
+ })
51
+ })
52
+
53
+ describe('moveFlavor', () => {
54
+ it('swaps with the neighbour in the given direction', () => {
55
+ expect(moveFlavor(DEFAULT, 1, -1)).toEqual([DEFAULT[1], DEFAULT[0], ...DEFAULT.slice(2)])
56
+ })
57
+
58
+ it('is a no-op past either end', () => {
59
+ expect(moveFlavor(DEFAULT, 0, -1)).toBeUndefined()
60
+ expect(moveFlavor(DEFAULT, DEFAULT.length - 1, 1)).toBeUndefined()
61
+ })
62
+
63
+ it('never drops or duplicates a route (a preference reorders, it never filters)', () => {
64
+ const moved = moveFlavor(DEFAULT, 2, 1)!
65
+ expect([...moved].sort()).toEqual([...DEFAULT].sort())
66
+ })
67
+ })
68
+
69
+ describe('subscriptionOverridesOrder', () => {
70
+ const compliance: ModelFlavor[] = ['bedrock', 'direct']
71
+ const subscriptionFirst: ModelFlavor[] = ['subscription', 'direct']
72
+
73
+ it('warns whenever a connected plan can overrule the order (the compliance-preset case)', () => {
74
+ // "Subscriptions always win" is applied by the engine ON TOP of the route this order resolves,
75
+ // so a preset promoting AWS Bedrock is silently overruled for a dual-mode model on a workspace
76
+ // holding a token. The control has to say so rather than promise the route.
77
+ expect(subscriptionOverridesOrder({ preference: compliance, hasSubscription: true })).toBe(true)
78
+ })
79
+
80
+ it('stays quiet when the order already puts the subscription first (override agrees with it)', () => {
81
+ expect(
82
+ subscriptionOverridesOrder({ preference: subscriptionFirst, hasSubscription: true }),
83
+ ).toBe(false)
84
+ })
85
+
86
+ it('stays quiet with no connected subscription — no token, no override', () => {
87
+ expect(subscriptionOverridesOrder({ preference: compliance, hasSubscription: false })).toBe(
88
+ false,
89
+ )
90
+ })
91
+
92
+ it('stays quiet when the preset states no order at all (nothing was promised)', () => {
93
+ expect(subscriptionOverridesOrder({ preference: undefined, hasSubscription: true })).toBe(false)
94
+ expect(subscriptionOverridesOrder({ preference: [], hasSubscription: true })).toBe(false)
95
+ })
96
+
97
+ it('judges the RESOLVED total order, not the caller’s partial list', () => {
98
+ // A one-entry list still resolves to a full order, so "is subscription first" is answerable
99
+ // for it — and `['subscription']` promotes the route even though it names nothing else.
100
+ expect(
101
+ subscriptionOverridesOrder({ preference: ['subscription'], hasSubscription: true }),
102
+ ).toBe(false)
103
+ })
104
+ })
@@ -0,0 +1,81 @@
1
+ // The pure half of `ProviderPreferenceEditor.vue`: what a reordering COMMITS, and what the
2
+ // control has to say about a route order the run path may still override. Extracted so both can
3
+ // be pinned without mounting Vue — the "equals the default is stored as absent" normalisation in
4
+ // particular, which the backend's whole "a preset keeps tracking the shipped order" property rests
5
+ // on and which no backend test can see (the SPA is the only thing that performs it).
6
+
7
+ import {
8
+ DEFAULT_MODEL_FLAVOR_ORDER,
9
+ type ModelFlavor,
10
+ orderedModelFlavorPreference,
11
+ } from '@cat-factory/contracts'
12
+
13
+ /**
14
+ * Whether an order is today's shipped default, position for position.
15
+ *
16
+ * The length check is not redundant: `every` is vacuously true for a PREFIX, so without it a
17
+ * partial order that happens to start like the default would be read as "no preference" and
18
+ * silently cleared. The editor only ever produces full permutations, which is exactly why the
19
+ * guard belongs here rather than in the caller — it is what keeps that a property of this
20
+ * function instead of a property of its one current caller.
21
+ */
22
+ export function isDefaultFlavorOrder(next: readonly ModelFlavor[]): boolean {
23
+ return (
24
+ next.length === DEFAULT_MODEL_FLAVOR_ORDER.length &&
25
+ next.every((flavor, i) => DEFAULT_MODEL_FLAVOR_ORDER[i] === flavor)
26
+ )
27
+ }
28
+
29
+ /**
30
+ * What a reordering stores: the new order, or UNSET when it lands back on the shipped default.
31
+ *
32
+ * "No preference" and "an order that happens to match today's default" are different states, and
33
+ * only the first keeps following the shipped order as the product changes it. Storing a copy would
34
+ * silently pin today's wording of a list that is itself scheduled to be reordered.
35
+ */
36
+ export function commitFlavorOrder(next: readonly ModelFlavor[]): ModelFlavor[] | undefined {
37
+ return isDefaultFlavorOrder(next) ? undefined : [...next]
38
+ }
39
+
40
+ /** Swap the entry at `index` with its neighbour `delta` away; undefined when the move is a no-op. */
41
+ export function moveFlavor(
42
+ order: readonly ModelFlavor[],
43
+ index: number,
44
+ delta: number,
45
+ ): ModelFlavor[] | undefined {
46
+ const next = [...order]
47
+ const target = index + delta
48
+ const moved = next[index]
49
+ const displaced = next[target]
50
+ if (!moved || !displaced) return undefined
51
+ next[index] = displaced
52
+ next[target] = moved
53
+ return next
54
+ }
55
+
56
+ /**
57
+ * Whether to warn that a connected subscription will still win over this order.
58
+ *
59
+ * "Subscriptions always win" is applied by the ENGINE on top of the route this order resolves: a
60
+ * dual-mode model (Kimi/DeepSeek/GLM) switches to its subscription flavour whenever the workspace
61
+ * or the run initiator holds a token, whatever the preset asked for. So the override does not
62
+ * merely re-rank the subscription route within this list — it sits OUTSIDE the list — and the only
63
+ * order it cannot contradict is one that already puts `subscription` first.
64
+ *
65
+ * That is why the test is "is subscription first", not "was subscription demoted": `subscription`
66
+ * is last in today's shipped order, so a demotion test could never fire, and the case that actually
67
+ * bites is the opposite one — a compliance preset promoting `bedrock` and being silently overruled.
68
+ * Until the override moves into this order, the control has to say so, because copy promising a
69
+ * residency-guaranteed route that a connected plan quietly overrules is the one thing a compliance
70
+ * preset must never do.
71
+ *
72
+ * Only when the preset actually states an order (nothing is promised otherwise) AND the workspace
73
+ * has a connected subscription (no token, no override).
74
+ */
75
+ export function subscriptionOverridesOrder(input: {
76
+ preference: readonly ModelFlavor[] | undefined
77
+ hasSubscription: boolean
78
+ }): boolean {
79
+ if (!input.preference?.length || !input.hasSubscription) return false
80
+ return orderedModelFlavorPreference(input.preference)[0] !== 'subscription'
81
+ }
@@ -0,0 +1,180 @@
1
+ <script setup lang="ts">
2
+ // The ROUTE-ORDER editor for one model preset: which of a model's routes (its own provider API,
3
+ // AWS Bedrock, the OpenRouter gateway, Cloudflare Workers AI, a subscription harness) the preset's
4
+ // runs prefer, most preferred first.
5
+ //
6
+ // Two properties of the backend model decide the whole shape of this control:
7
+ //
8
+ // - A preference REORDERS, never filters. So the list always shows EVERY route and the only
9
+ // affordance is moving one. There is deliberately no way to remove a route: a preset that
10
+ // could drop one would make a model whose only route is that one unstartable, which is not
11
+ // what anybody choosing an order means.
12
+ // - "No preference" is a real, distinct state from "an order that happens to match today's
13
+ // default". Only the first keeps tracking the shipped order as the product changes it, so
14
+ // reordering back to the default clears the preference rather than storing a copy of it, and
15
+ // the header says which of the two the preset is in.
16
+ //
17
+ // A third property is the engine's rather than this order's, and it is why the control can warn:
18
+ // "subscriptions always win" is applied on TOP of the resolved route, so on a workspace with a
19
+ // connected plan a dual-mode model ignores an order that ranks `subscription` lower than the
20
+ // shipped default does. The logic module says when; the copy says so plainly.
21
+ import { computed } from 'vue'
22
+ import type { ModelFlavor } from '@cat-factory/contracts'
23
+ import { orderedModelFlavorPreference } from '@cat-factory/contracts'
24
+ import {
25
+ commitFlavorOrder,
26
+ moveFlavor,
27
+ subscriptionOverridesOrder,
28
+ } from '~/components/settings/ProviderPreferenceEditor.logic'
29
+
30
+ const props = defineProps<{
31
+ /** The preset's stored order; empty/absent ⇒ the deployment's default order. */
32
+ modelValue: ModelFlavor[] | undefined
33
+ /**
34
+ * Whether this workspace has ANY subscription vendor connected. Drives the caveat only: the
35
+ * override it warns about is the engine's, so the control cannot prevent it, only name it.
36
+ */
37
+ hasSubscription?: boolean
38
+ /**
39
+ * Whether the preset being edited is the workspace DEFAULT. `GET /models` resolves its flavour
40
+ * badges under the default preset's order, so on any OTHER preset the model list beside this
41
+ * control is showing routes this order does not govern, and the control says so.
42
+ */
43
+ isDefaultPreset?: boolean
44
+ }>()
45
+ const emit = defineEmits<{ 'update:modelValue': [ModelFlavor[] | undefined] }>()
46
+
47
+ const { t } = useI18n()
48
+
49
+ /** Static literal keys, one per route, so the typed-message-key check sees them all. */
50
+ const ROUTE_LABELS: Record<ModelFlavor, string> = {
51
+ direct: 'settings.modelConfiguration.routes.direct',
52
+ bedrock: 'settings.modelConfiguration.routes.bedrock',
53
+ openrouter: 'settings.modelConfiguration.routes.openrouter',
54
+ cloudflare: 'settings.modelConfiguration.routes.cloudflare',
55
+ subscription: 'settings.modelConfiguration.routes.subscription',
56
+ }
57
+ const ROUTE_HINTS: Record<ModelFlavor, string> = {
58
+ direct: 'settings.modelConfiguration.routeHints.direct',
59
+ bedrock: 'settings.modelConfiguration.routeHints.bedrock',
60
+ openrouter: 'settings.modelConfiguration.routeHints.openrouter',
61
+ cloudflare: 'settings.modelConfiguration.routeHints.cloudflare',
62
+ subscription: 'settings.modelConfiguration.routeHints.subscription',
63
+ }
64
+
65
+ /** The order actually in force: the preset's own, else the default. Always all five routes. */
66
+ const order = computed(() => orderedModelFlavorPreference(props.modelValue))
67
+ const isCustom = computed(() => (props.modelValue?.length ?? 0) > 0)
68
+
69
+ /** A connected plan overrules a deprioritised subscription route — see the logic module. */
70
+ const subscriptionWins = computed(() =>
71
+ subscriptionOverridesOrder({
72
+ preference: props.modelValue,
73
+ hasSubscription: props.hasSubscription ?? false,
74
+ }),
75
+ )
76
+
77
+ /** The badges beside this control render under the DEFAULT preset's order, not this one's. */
78
+ const badgesShowAnotherOrder = computed(() => isCustom.value && props.isDefaultPreset === false)
79
+
80
+ function move(index: number, delta: number) {
81
+ const next = moveFlavor(order.value, index, delta)
82
+ if (next) emit('update:modelValue', commitFlavorOrder(next))
83
+ }
84
+
85
+ function reset() {
86
+ emit('update:modelValue', undefined)
87
+ }
88
+ </script>
89
+
90
+ <template>
91
+ <div>
92
+ <div class="mb-1 flex items-start justify-between gap-3">
93
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
94
+ {{ t('settings.modelConfiguration.routeOrder.label') }}
95
+ </span>
96
+ <UButton
97
+ v-if="isCustom"
98
+ size="xs"
99
+ variant="ghost"
100
+ color="neutral"
101
+ icon="i-lucide-rotate-ccw"
102
+ data-testid="preset-route-order-reset"
103
+ @click="reset"
104
+ >
105
+ {{ t('settings.modelConfiguration.routeOrder.reset') }}
106
+ </UButton>
107
+ </div>
108
+ <p class="mb-2 text-[11px] leading-relaxed text-slate-500">
109
+ {{
110
+ isCustom
111
+ ? t('settings.modelConfiguration.routeOrder.customHint')
112
+ : t('settings.modelConfiguration.routeOrder.defaultHint')
113
+ }}
114
+ </p>
115
+ <!-- The engine applies "subscriptions always win" ON TOP of this order, so on a workspace with
116
+ a connected plan a dual-mode model ignores a deprioritised subscription route. Said here
117
+ rather than left to be discovered in a run: copy that promises a residency-guaranteed
118
+ route a connected plan quietly overrules is the one thing this control must not do. -->
119
+ <p
120
+ v-if="subscriptionWins"
121
+ class="mb-2 text-[11px] leading-relaxed text-amber-400/90"
122
+ data-testid="preset-route-order-subscription-warning"
123
+ >
124
+ {{ t('settings.modelConfiguration.routeOrder.subscriptionOverrideHint') }}
125
+ </p>
126
+ <!-- The model list beside this control shows each model's route under the WORKSPACE DEFAULT
127
+ preset (that is what `GET /models` resolves), so on any other preset those badges are
128
+ answering a different question than this order asks. -->
129
+ <p
130
+ v-if="badgesShowAnotherOrder"
131
+ class="mb-2 text-[11px] leading-relaxed text-slate-500"
132
+ data-testid="preset-route-order-badge-hint"
133
+ >
134
+ {{ t('settings.modelConfiguration.routeOrder.badgesUseDefaultPresetHint') }}
135
+ </p>
136
+ <ol
137
+ class="divide-y divide-slate-800 rounded-xl border border-slate-800 bg-slate-900/50"
138
+ data-testid="preset-route-order"
139
+ >
140
+ <li
141
+ v-for="(flavor, index) in order"
142
+ :key="flavor"
143
+ class="flex items-center gap-3 px-4 py-2.5"
144
+ :data-testid="`preset-route-${flavor}`"
145
+ >
146
+ <span
147
+ class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-slate-800 text-[10px] font-semibold text-slate-400"
148
+ >
149
+ {{ index + 1 }}
150
+ </span>
151
+ <div class="min-w-0 flex-1">
152
+ <p class="truncate text-sm text-slate-200">{{ t(ROUTE_LABELS[flavor]) }}</p>
153
+ <p class="truncate text-[11px] text-slate-500">{{ t(ROUTE_HINTS[flavor]) }}</p>
154
+ </div>
155
+ <div class="flex shrink-0 items-center gap-1">
156
+ <UButton
157
+ size="xs"
158
+ variant="ghost"
159
+ color="neutral"
160
+ icon="i-lucide-chevron-up"
161
+ :disabled="index === 0"
162
+ :title="t('settings.modelConfiguration.routeOrder.moveUp')"
163
+ :aria-label="t('settings.modelConfiguration.routeOrder.moveUp')"
164
+ @click="move(index, -1)"
165
+ />
166
+ <UButton
167
+ size="xs"
168
+ variant="ghost"
169
+ color="neutral"
170
+ icon="i-lucide-chevron-down"
171
+ :disabled="index === order.length - 1"
172
+ :title="t('settings.modelConfiguration.routeOrder.moveDown')"
173
+ :aria-label="t('settings.modelConfiguration.routeOrder.moveDown')"
174
+ @click="move(index, 1)"
175
+ />
176
+ </div>
177
+ </li>
178
+ </ol>
179
+ </div>
180
+ </template>
@@ -31,6 +31,12 @@ export type {
31
31
  PlatformOutcomeTotals,
32
32
  PlatformTrendPoint,
33
33
  PlatformFailureSlice,
34
+ PlatformGateStat,
35
+ PlatformTrendSource,
36
+ PlatformAlertSettings,
37
+ PlatformAlertThresholdOverrides,
38
+ PlatformAlertWindow,
39
+ PlatformFailingRun,
34
40
  ReportWindow,
35
41
  ReportSpendDimension,
36
42
  ReportActivityDimension,
@@ -22,7 +22,8 @@
22
22
  "deleteTitle": "Preset löschen",
23
23
  "basePrefix": "Basis:",
24
24
  "overrideCount": "{count} Override | {count} Overrides",
25
- "empty": "Noch keine Presets. Erstelle eines, um Modelle deinen Agenten zuzuordnen."
25
+ "empty": "Noch keine Presets. Erstelle eines, um Modelle deinen Agenten zuzuordnen.",
26
+ "customRouteOrder": "eigene Routen-Reihenfolge"
26
27
  },
27
28
  "editor": {
28
29
  "useBaseModel": "Basismodell verwenden",
@@ -43,6 +44,30 @@
43
44
  "nameRequiredTitle": "Name erforderlich",
44
45
  "nameRequiredBody": "Gib dem Preset einen Namen.",
45
46
  "saveFailed": "Preset konnte nicht gespeichert werden"
47
+ },
48
+ "routes": {
49
+ "direct": "Die eigene Provider-API des Modells",
50
+ "bedrock": "AWS Bedrock",
51
+ "openrouter": "OpenRouter-Gateway",
52
+ "cloudflare": "Cloudflare Workers AI",
53
+ "subscription": "Abo (Claude Code / Codex)"
54
+ },
55
+ "routeHints": {
56
+ "direct": "Benötigt einen API-Schlüssel für diesen Provider.",
57
+ "bedrock": "Läuft in deiner AWS-Region, mit den für dein Konto freigegebenen Modellen.",
58
+ "openrouter": "Ein Schlüssel, viele Anbieter, mit Aufschlag weiterverkauft.",
59
+ "cloudflare": "Immer verfügbar, ohne Prompt-Caching.",
60
+ "subscription": "Pauschales Kontingent eines verbundenen Tarifs."
61
+ },
62
+ "routeOrder": {
63
+ "label": "Routen-Reihenfolge",
64
+ "defaultHint": "Dieses Preset folgt dem Standard des Deployments. Verschiebe eine Route nach oben, um sie zu bevorzugen.",
65
+ "customHint": "Dieses Preset bevorzugt Routen in dieser Reihenfolge. Ein Modell, das nur über eine niedrigere Route erreichbar ist, läuft weiterhin und wird nur später versucht.",
66
+ "reset": "Standardreihenfolge verwenden",
67
+ "moveUp": "Diese Route stärker bevorzugen",
68
+ "moveDown": "Diese Route weniger bevorzugen",
69
+ "subscriptionOverrideHint": "Ein verbundenes Abo gewinnt weiterhin bei Modellen, die eines anbieten: Die Engine leitet diese unabhängig von dieser Reihenfolge auf den Tarif. Routen unterhalb des Abo-Eintrags gelten für alle anderen Modelle.",
70
+ "badgesUseDefaultPresetHint": "Die Route neben jedem Modell oben ist die des Standard-Presets des Workspace, nicht die dieses Presets."
46
71
  }
47
72
  },
48
73
  "account": {
@@ -1085,6 +1110,48 @@
1085
1110
  "offHint": "Nicht gesetzt: Jedes Board entscheidet selbst. Bestehende Boards behalten ihre Auswahl.",
1086
1111
  "saved": "Richtlinie für Ausführungs-Anmeldedaten gespeichert",
1087
1112
  "saveFailed": "Richtlinie für Ausführungs-Anmeldedaten konnte nicht gespeichert werden"
1113
+ },
1114
+ "platformAlerts": {
1115
+ "title": "Plattform-Zustandswarnungen",
1116
+ "description": "Obergrenzen, gegen die der Laufzustand dieses Kontos geprüft wird. Leere Felder übernehmen die Vorgaben der Installation.",
1117
+ "muteLabel": "Warnungen für dieses Konto stummschalten",
1118
+ "muteHint": "Warnungen lassen sich hier nur abschalten. Ob die Prüfung überhaupt läuft, ist eine Einstellung der Installation.",
1119
+ "windowLabel": "Auswertungsfenster",
1120
+ "windowHint": "Wie weit jede Bedingung zurückblickt. Ein längeres Fenster reagiert langsamer und bleibt ruhiger.",
1121
+ "thresholdsLabel": "Schwellenwerte",
1122
+ "inheritHint": "Ein leeres Feld übernimmt die Vorgabe der Installation. Eine Null ist ein echter Wert, kein leeres Feld.",
1123
+ "inheritPlaceholder": "Übernommen",
1124
+ "reset": "Überschreibungen löschen",
1125
+ "saved": "Warneinstellungen gespeichert",
1126
+ "saveFailed": "Warneinstellungen konnten nicht gespeichert werden",
1127
+ "window": {
1128
+ "inherit": "Vorgabe der Installation",
1129
+ "oneHour": "Letzte Stunde",
1130
+ "oneDay": "Letzte 24 Stunden",
1131
+ "sevenDays": "Letzte 7 Tage"
1132
+ },
1133
+ "thresholds": {
1134
+ "minRuns": "Mindestanzahl Läufe",
1135
+ "maxFailureRate": "Obergrenze Fehlerquote",
1136
+ "maxP99DurationMs": "Obergrenze p99-Dauer (Minuten)",
1137
+ "maxBacklog": "Obergrenze Rückstau",
1138
+ "stalledBuckets": "Leere Intervalle bis zum Stillstand",
1139
+ "minStalledPriorRuns": "Läufe, bevor Stillstand zählt",
1140
+ "maxFailureKindShare": "Anteil der dominanten Ursache",
1141
+ "maxSweepFailures": "Fehlschläge der Prüfung in Folge"
1142
+ },
1143
+ "hints": {
1144
+ "minRuns": "Abgeschlossene Läufe, die das Fenster braucht, bevor die Fehlerwarnungen auslösen können.",
1145
+ "maxFailureRate": "Anteil der Läufe, die fehlschlagen dürfen, von 0 bis 1.",
1146
+ "maxP99DurationMs": "Wie lange die langsamsten Läufe dauern dürfen.",
1147
+ "maxBacklog": "Unfertige Läufe, die gleichzeitig unterwegs sein dürfen.",
1148
+ "stalledBuckets": "Abschließende leere Intervalle im Verlauf, die als Stillstand gelten.",
1149
+ "minStalledPriorRuns": "Läufe, die der frühere Teil des Fensters getragen haben muss. Null warnt bei jeder Stille.",
1150
+ "maxFailureKindShare": "Anteil der Fehler, den eine einzelne Ursache ausmachen darf, über 0 und bis 1.",
1151
+ "maxSweepFailures": "Aufeinanderfolgende fehlgeschlagene Durchläufe einer Hintergrundprüfung."
1152
+ },
1153
+ "notLoaded": "Die aktuellen Einstellungen dieses Kontos konnten nicht geladen werden; ein Speichern würde die übrigen überschreiben. Bitte vor dem Bearbeiten neu laden.",
1154
+ "invalidNumbers": "Diese Obergrenzen sind keine Zahlen"
1088
1155
  }
1089
1156
  },
1090
1157
  "inspector": {
@@ -2073,7 +2140,10 @@
2073
2140
  "budget_paused": "Als gelesen markieren",
2074
2141
  "key_drift": "Veraltete Zugangsdaten entfernen",
2075
2142
  "merge_tag_request": "Aufwand erfassen"
2076
- }
2143
+ },
2144
+ "failingRun": "{kind} · {at}",
2145
+ "failingRunsMore": "{count} weitere nicht angezeigt",
2146
+ "failingRunGone": "Dieser Lauf ist nicht mehr geladen und hat keine Aufgabe zum Öffnen."
2077
2147
  },
2078
2148
  "aiProvidersBanner": {
2079
2149
  "setup": {
@@ -3253,7 +3323,9 @@
3253
3323
  "window": {
3254
3324
  "oneHour": "Letzte Stunde",
3255
3325
  "oneDay": "Letzte 24 Stunden",
3256
- "sevenDays": "Letzte 7 Tage"
3326
+ "sevenDays": "Letzte 7 Tage",
3327
+ "thirtyDays": "Letzte 30 Tage",
3328
+ "ninetyDays": "Letzte 90 Tage"
3257
3329
  },
3258
3330
  "outcomes": {
3259
3331
  "title": "Lauf-Ergebnisse",
@@ -3303,6 +3375,22 @@
3303
3375
  },
3304
3376
  "live": {
3305
3377
  "title": "Jetzt aktiv"
3378
+ },
3379
+ "rollup": {
3380
+ "none": "Die tägliche Aggregation hat noch nichts erzeugt. Dieses Zeitfenster ist also mangels Daten leer, nicht mangels Läufen.",
3381
+ "stale": "Die tägliche Aggregation reicht nur bis {date}. Alles danach sind fehlende Daten, keine ruhige Zeit.",
3382
+ "current": "Aus der täglichen Aggregation, vollständig bis {date}."
3383
+ },
3384
+ "gates": {
3385
+ "title": "Gate-Versuche",
3386
+ "empty": "In diesem Zeitfenster wurde kein Gate abgeschlossen.",
3387
+ "gate": "Gate",
3388
+ "settled": "Abgeschlossen",
3389
+ "cleanPasses": "Ohne Fixer bestanden",
3390
+ "attempts": "Fixer-Versuche",
3391
+ "helperFailures": "Fixer-Fehlschläge",
3392
+ "exhausted": "An Menschen übergeben",
3393
+ "hint": "Ohne Fixer bestanden heißt: die Vorprüfung war zufrieden und es wurde gar kein Fixer gestartet. Fixer-Fehlschläge sind Versuche, deren eigener Job abgestürzt ist, im Unterschied zu Versuchen, die liefen und die Prüfung rot ließen."
3306
3394
  }
3307
3395
  },
3308
3396
  "reports": {
@@ -5885,7 +5973,7 @@
5885
5973
  "newDescription": "Neue integrierte Modell-Presets sind verfügbar. Füge sie zur Bibliothek dieses Boards hinzu.",
5886
5974
  "add": "Hinzufügen",
5887
5975
  "updatesHeading": "Updates verfügbar",
5888
- "updatesDescription": "Eine neuere Version dieser integrierten Modell-Presets ist verfügbar. Führe ein erneutes Seeding durch, um sie zu übernehmen (Standard und Reihenfolge bleiben erhalten).",
5976
+ "updatesDescription": "Eine neuere Version dieser integrierten Modell-Presets ist verfügbar. Führe ein erneutes Seeding durch, um sie zu übernehmen: Standard und Reihenfolge bleiben erhalten, eine eigene Routen-Reihenfolge auf dem Preset wird jedoch zurückgesetzt.",
5889
5977
  "versionAvailable": "Version {from} → {to} verfügbar.",
5890
5978
  "reseed": "Erneut seeden",
5891
5979
  "reseedAll": "Alle aktualisieren ({count})",