@cat-factory/app 0.7.4 → 0.8.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.
- package/app/components/layout/BoardToolbar.vue +0 -16
- package/app/components/layout/SideBar.vue +23 -0
- package/app/components/settings/OpenRouterCatalogPanel.vue +176 -0
- package/app/composables/api/models.ts +18 -0
- package/app/pages/index.vue +6 -0
- package/app/stores/openrouter.ts +52 -0
- package/app/stores/ui.ts +11 -0
- package/app/types/models.ts +4 -4
- package/app/types/openrouter.ts +45 -0
- package/package.json +2 -2
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useBoardFlow } from '~/composables/useBoardFlow'
|
|
3
3
|
import NotificationsInbox from '~/components/layout/NotificationsInbox.vue'
|
|
4
|
-
import VendorCredentialsModal from '~/components/providers/VendorCredentialsModal.vue'
|
|
5
|
-
import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
|
|
6
4
|
|
|
7
5
|
const ui = useUiStore()
|
|
8
6
|
const board = useBoardStore()
|
|
@@ -127,20 +125,6 @@ const decisionItems = computed(() =>
|
|
|
127
125
|
<!-- human-actionable notifications (merge review, pipeline complete, CI failed) -->
|
|
128
126
|
<NotificationsInbox />
|
|
129
127
|
|
|
130
|
-
<!-- LLM vendor subscriptions (Claude Code / Codex token pool) -->
|
|
131
|
-
<UButton
|
|
132
|
-
color="neutral"
|
|
133
|
-
variant="ghost"
|
|
134
|
-
size="sm"
|
|
135
|
-
icon="i-lucide-key-round"
|
|
136
|
-
title="Connect LLM vendor subscriptions (Claude Code / Codex)"
|
|
137
|
-
@click="ui.openVendorCredentials()"
|
|
138
|
-
>
|
|
139
|
-
Vendors
|
|
140
|
-
</UButton>
|
|
141
|
-
<VendorCredentialsModal />
|
|
142
|
-
<PersonalCredentialModal />
|
|
143
|
-
|
|
144
128
|
<!-- spend safeguard usage -->
|
|
145
129
|
<UButton
|
|
146
130
|
v-if="showSpend"
|
|
@@ -306,6 +306,18 @@ watch(
|
|
|
306
306
|
>
|
|
307
307
|
Default service best practices
|
|
308
308
|
</UButton>
|
|
309
|
+
<UButton
|
|
310
|
+
block
|
|
311
|
+
color="primary"
|
|
312
|
+
variant="soft"
|
|
313
|
+
size="sm"
|
|
314
|
+
icon="i-lucide-key-round"
|
|
315
|
+
class="justify-start"
|
|
316
|
+
title="Connect LLM vendor subscriptions + provider API keys"
|
|
317
|
+
@click="ui.openVendorCredentials()"
|
|
318
|
+
>
|
|
319
|
+
Vendors & keys
|
|
320
|
+
</UButton>
|
|
309
321
|
<UButton
|
|
310
322
|
block
|
|
311
323
|
color="primary"
|
|
@@ -317,6 +329,17 @@ watch(
|
|
|
317
329
|
>
|
|
318
330
|
My local runners
|
|
319
331
|
</UButton>
|
|
332
|
+
<UButton
|
|
333
|
+
block
|
|
334
|
+
color="primary"
|
|
335
|
+
variant="soft"
|
|
336
|
+
size="sm"
|
|
337
|
+
icon="i-lucide-waypoints"
|
|
338
|
+
class="justify-start"
|
|
339
|
+
@click="ui.openOpenRouter()"
|
|
340
|
+
>
|
|
341
|
+
OpenRouter models
|
|
342
|
+
</UButton>
|
|
320
343
|
</div>
|
|
321
344
|
</section>
|
|
322
345
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Workspace settings: "OpenRouter models" — browse OpenRouter's 300+ gateway models and
|
|
3
|
+
// enable a subset for this workspace. OpenRouter is reached via the workspace's API-key pool
|
|
4
|
+
// (connect an OpenRouter key first under "Provider keys"); "Refresh" probes its live catalog
|
|
5
|
+
// server-side, then tick the models to enable. Enabled models — with their context window and
|
|
6
|
+
// price — appear automatically in the model picker and meter against the spend budget.
|
|
7
|
+
import { computed, ref, watch } from 'vue'
|
|
8
|
+
import type { OpenRouterModelMeta } from '~/types/openrouter'
|
|
9
|
+
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
const workspace = useWorkspaceStore()
|
|
12
|
+
const store = useOpenRouterStore()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
|
|
15
|
+
const open = computed({
|
|
16
|
+
get: () => ui.openRouterOpen,
|
|
17
|
+
set: (v: boolean) => (v ? ui.openOpenRouter() : ui.closeOpenRouter()),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// The enabled slugs the user has ticked (seeded from the persisted catalog on open).
|
|
21
|
+
const selected = ref<Set<string>>(new Set())
|
|
22
|
+
const filter = ref('')
|
|
23
|
+
const busy = ref(false)
|
|
24
|
+
|
|
25
|
+
// Load the persisted catalog whenever the panel opens; seed the tick selection from it.
|
|
26
|
+
watch(open, (isOpen) => {
|
|
27
|
+
if (!isOpen || !workspace.workspaceId) return
|
|
28
|
+
void store.load(workspace.workspaceId).then(() => {
|
|
29
|
+
selected.value = new Set(store.enabled.map((m) => m.id))
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// The list to show: the live browse list once refreshed, else the persisted enabled set.
|
|
34
|
+
const source = computed<OpenRouterModelMeta[]>(() =>
|
|
35
|
+
store.browse.length ? store.browse : store.enabled,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const visible = computed(() => {
|
|
39
|
+
const q = filter.value.trim().toLowerCase()
|
|
40
|
+
if (!q) return source.value
|
|
41
|
+
return source.value.filter(
|
|
42
|
+
(m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q),
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const selectedCount = computed(() => selected.value.size)
|
|
47
|
+
|
|
48
|
+
function contextLabel(tokens: number | undefined): string {
|
|
49
|
+
if (!tokens) return ''
|
|
50
|
+
return tokens >= 1000 ? `${Math.round(tokens / 1000)}K ctx` : `${tokens} ctx`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function priceLabel(m: OpenRouterModelMeta): string {
|
|
54
|
+
return `${m.inputPerMillion}/${m.outputPerMillion} per Mtok`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toggle(id: string, on: boolean) {
|
|
58
|
+
const next = new Set(selected.value)
|
|
59
|
+
if (on) next.add(id)
|
|
60
|
+
else next.delete(id)
|
|
61
|
+
selected.value = next
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function refresh() {
|
|
65
|
+
if (!workspace.workspaceId) return
|
|
66
|
+
const result = await store.refresh(workspace.workspaceId)
|
|
67
|
+
if (!result.reachable) {
|
|
68
|
+
toast.add({
|
|
69
|
+
title: 'Could not reach OpenRouter',
|
|
70
|
+
description: store.refreshError ?? 'Connect an OpenRouter key under Provider keys first.',
|
|
71
|
+
icon: 'i-lucide-triangle-alert',
|
|
72
|
+
color: 'error',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function save() {
|
|
78
|
+
if (!workspace.workspaceId) return
|
|
79
|
+
busy.value = true
|
|
80
|
+
try {
|
|
81
|
+
// Persist the ticked models, carrying the metadata from whichever list they came from.
|
|
82
|
+
const byId = new Map(source.value.map((m) => [m.id, m]))
|
|
83
|
+
const models = [...selected.value]
|
|
84
|
+
.map((id) => byId.get(id))
|
|
85
|
+
.filter((m): m is OpenRouterModelMeta => !!m)
|
|
86
|
+
await store.save(workspace.workspaceId, models)
|
|
87
|
+
toast.add({ title: 'OpenRouter catalog saved', icon: 'i-lucide-check', color: 'success' })
|
|
88
|
+
} catch (e) {
|
|
89
|
+
toast.add({
|
|
90
|
+
title: 'Could not save catalog',
|
|
91
|
+
description: e instanceof Error ? e.message : String(e),
|
|
92
|
+
icon: 'i-lucide-triangle-alert',
|
|
93
|
+
color: 'error',
|
|
94
|
+
})
|
|
95
|
+
} finally {
|
|
96
|
+
busy.value = false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<UModal v-model:open="open" title="OpenRouter models" :ui="{ content: 'max-w-2xl' }">
|
|
103
|
+
<template #body>
|
|
104
|
+
<div class="space-y-4">
|
|
105
|
+
<p class="text-xs text-slate-400">
|
|
106
|
+
Reach <strong>300+ models</strong> through one gateway. Connect an OpenRouter key under
|
|
107
|
+
<span class="text-slate-300">Provider keys</span> first, then
|
|
108
|
+
<span class="text-slate-300">Refresh</span> to browse the live catalog and enable the
|
|
109
|
+
models you want. Enabled models appear in the model picker with their context window and
|
|
110
|
+
price, and meter against your spend budget.
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
<div class="flex items-center gap-2">
|
|
114
|
+
<UButton
|
|
115
|
+
color="neutral"
|
|
116
|
+
variant="soft"
|
|
117
|
+
size="sm"
|
|
118
|
+
icon="i-lucide-refresh-cw"
|
|
119
|
+
:loading="store.refreshing"
|
|
120
|
+
@click="refresh()"
|
|
121
|
+
>
|
|
122
|
+
Refresh catalog
|
|
123
|
+
</UButton>
|
|
124
|
+
<UInput
|
|
125
|
+
v-model="filter"
|
|
126
|
+
size="sm"
|
|
127
|
+
class="flex-1"
|
|
128
|
+
icon="i-lucide-search"
|
|
129
|
+
placeholder="Filter by name or slug…"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<p v-if="store.refreshError" class="text-xs text-rose-400">{{ store.refreshError }}</p>
|
|
134
|
+
|
|
135
|
+
<div v-if="visible.length" class="max-h-96 space-y-1 overflow-y-auto pr-1">
|
|
136
|
+
<label
|
|
137
|
+
v-for="m in visible"
|
|
138
|
+
:key="m.id"
|
|
139
|
+
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-900/40 px-2.5 py-1.5 text-sm"
|
|
140
|
+
>
|
|
141
|
+
<UCheckbox
|
|
142
|
+
:model-value="selected.has(m.id)"
|
|
143
|
+
@update:model-value="(v: boolean | 'indeterminate') => toggle(m.id, v === true)"
|
|
144
|
+
/>
|
|
145
|
+
<span class="min-w-0 flex-1">
|
|
146
|
+
<span class="block truncate text-slate-200">{{ m.name }}</span>
|
|
147
|
+
<span class="block truncate font-mono text-[11px] text-slate-500">{{ m.id }}</span>
|
|
148
|
+
</span>
|
|
149
|
+
<span class="shrink-0 text-right text-[11px] text-slate-500">
|
|
150
|
+
<span v-if="m.contextLength" class="block">{{ contextLabel(m.contextLength) }}</span>
|
|
151
|
+
<span class="block">{{ priceLabel(m) }}</span>
|
|
152
|
+
</span>
|
|
153
|
+
</label>
|
|
154
|
+
</div>
|
|
155
|
+
<p v-else class="text-xs text-slate-500">
|
|
156
|
+
No models yet — hit <span class="text-slate-300">Refresh catalog</span> to load
|
|
157
|
+
OpenRouter's live list.
|
|
158
|
+
</p>
|
|
159
|
+
|
|
160
|
+
<div class="flex items-center justify-between">
|
|
161
|
+
<span class="text-xs text-slate-500">{{ selectedCount }} enabled</span>
|
|
162
|
+
<UButton
|
|
163
|
+
color="primary"
|
|
164
|
+
variant="soft"
|
|
165
|
+
size="sm"
|
|
166
|
+
icon="i-lucide-save"
|
|
167
|
+
:loading="busy"
|
|
168
|
+
@click="save()"
|
|
169
|
+
>
|
|
170
|
+
Save
|
|
171
|
+
</UButton>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
</UModal>
|
|
176
|
+
</template>
|
|
@@ -16,6 +16,11 @@ import type {
|
|
|
16
16
|
TestLocalModelEndpointInput,
|
|
17
17
|
UpsertLocalModelEndpointInput,
|
|
18
18
|
} from '~/types/localModels'
|
|
19
|
+
import type {
|
|
20
|
+
OpenRouterCatalog,
|
|
21
|
+
OpenRouterRefreshResult,
|
|
22
|
+
UpsertOpenRouterCatalogInput,
|
|
23
|
+
} from '~/types/openrouter'
|
|
19
24
|
import type { ApiContext } from './context'
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -100,6 +105,19 @@ export function modelsApi({ http, ws }: ApiContext) {
|
|
|
100
105
|
body,
|
|
101
106
|
}),
|
|
102
107
|
|
|
108
|
+
// ---- OpenRouter dynamic catalog (per-workspace gateway models) --------
|
|
109
|
+
// Browse OpenRouter's live catalog (`refresh`, leasing the workspace's pooled
|
|
110
|
+
// OpenRouter key server-side) and enable a subset; enabled models then surface
|
|
111
|
+
// in the per-workspace `/models` catalog with their context + price.
|
|
112
|
+
getOpenRouterCatalog: (workspaceId: string) =>
|
|
113
|
+
http<OpenRouterCatalog>(`${ws(workspaceId)}/openrouter/catalog`),
|
|
114
|
+
|
|
115
|
+
setOpenRouterCatalog: (workspaceId: string, body: UpsertOpenRouterCatalogInput) =>
|
|
116
|
+
http<OpenRouterCatalog>(`${ws(workspaceId)}/openrouter/catalog`, { method: 'PUT', body }),
|
|
117
|
+
|
|
118
|
+
refreshOpenRouterCatalog: (workspaceId: string) =>
|
|
119
|
+
http<OpenRouterRefreshResult>(`${ws(workspaceId)}/openrouter/refresh`, { method: 'POST' }),
|
|
120
|
+
|
|
103
121
|
// ---- per-agent-kind default models (workspace routing overrides) ------
|
|
104
122
|
// The workspace's map of agentKind → model id; a kind absent from the map
|
|
105
123
|
// falls back to the deployment's env routing. `setModelDefaults` replaces the
|
package/app/pages/index.vue
CHANGED
|
@@ -32,6 +32,9 @@ import DatadogPanel from '~/components/settings/DatadogPanel.vue'
|
|
|
32
32
|
import ModelDefaultsPanel from '~/components/settings/ModelDefaultsPanel.vue'
|
|
33
33
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
34
34
|
import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
|
|
35
|
+
import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
|
|
36
|
+
import VendorCredentialsModal from '~/components/providers/VendorCredentialsModal.vue'
|
|
37
|
+
import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
|
|
35
38
|
|
|
36
39
|
const workspace = useWorkspaceStore()
|
|
37
40
|
const github = useGitHubStore()
|
|
@@ -122,6 +125,9 @@ watch(
|
|
|
122
125
|
<ModelDefaultsPanel />
|
|
123
126
|
<ServiceFragmentDefaultsPanel />
|
|
124
127
|
<LocalModelEndpointsPanel />
|
|
128
|
+
<OpenRouterCatalogPanel />
|
|
129
|
+
<VendorCredentialsModal />
|
|
130
|
+
<PersonalCredentialModal />
|
|
125
131
|
</template>
|
|
126
132
|
|
|
127
133
|
<!-- Backend unreachable / bootstrap failed -->
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { OpenRouterModelMeta, OpenRouterRefreshResult } from '~/types/openrouter'
|
|
4
|
+
|
|
5
|
+
// The workspace's OpenRouter dynamic catalog: the enabled subset of OpenRouter's 300+
|
|
6
|
+
// gateway models. `enabled` is what's persisted (and surfaced in the model picker);
|
|
7
|
+
// `browse` is the live catalog from the last `refresh` (not persisted) the user picks from.
|
|
8
|
+
// Scoped to the workspace (its key lives in the workspace API-key pool).
|
|
9
|
+
export const useOpenRouterStore = defineStore('openrouter', () => {
|
|
10
|
+
const api = useApi()
|
|
11
|
+
const enabled = ref<OpenRouterModelMeta[]>([])
|
|
12
|
+
const browse = ref<OpenRouterModelMeta[]>([])
|
|
13
|
+
const loading = ref(false)
|
|
14
|
+
const refreshing = ref(false)
|
|
15
|
+
const refreshError = ref<string | null>(null)
|
|
16
|
+
|
|
17
|
+
async function load(workspaceId: string) {
|
|
18
|
+
loading.value = true
|
|
19
|
+
try {
|
|
20
|
+
const catalog = await api.getOpenRouterCatalog(workspaceId)
|
|
21
|
+
enabled.value = catalog.models
|
|
22
|
+
} catch {
|
|
23
|
+
// Auth disabled / not signed in / feature off → nothing surfaces.
|
|
24
|
+
enabled.value = []
|
|
25
|
+
} finally {
|
|
26
|
+
loading.value = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Probe OpenRouter's live catalog (leases the workspace's pooled key server-side). */
|
|
31
|
+
async function refresh(workspaceId: string): Promise<OpenRouterRefreshResult> {
|
|
32
|
+
refreshing.value = true
|
|
33
|
+
refreshError.value = null
|
|
34
|
+
try {
|
|
35
|
+
const result = await api.refreshOpenRouterCatalog(workspaceId)
|
|
36
|
+
browse.value = result.models
|
|
37
|
+
if (!result.reachable) refreshError.value = result.error ?? 'OpenRouter is unreachable'
|
|
38
|
+
return result
|
|
39
|
+
} finally {
|
|
40
|
+
refreshing.value = false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Persist the enabled subset (the supplied models carry their browse-list metadata). */
|
|
45
|
+
async function save(workspaceId: string, models: OpenRouterModelMeta[]) {
|
|
46
|
+
const catalog = await api.setOpenRouterCatalog(workspaceId, { models })
|
|
47
|
+
enabled.value = catalog.models
|
|
48
|
+
return catalog
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { enabled, browse, loading, refreshing, refreshError, load, refresh, save }
|
|
52
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -81,6 +81,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
81
81
|
const vendorCredentialsOpen = ref(false)
|
|
82
82
|
// Per-user settings panel: the signed-in user's own-machine local model runners.
|
|
83
83
|
const localModelsOpen = ref(false)
|
|
84
|
+
// Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
|
|
85
|
+
const openRouterOpen = ref(false)
|
|
84
86
|
|
|
85
87
|
// Dedicated result-view overlay: a step whose agent kind declares a bespoke
|
|
86
88
|
// visualization (via the archetype's `resultView`) opens here instead of the generic
|
|
@@ -326,6 +328,12 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
326
328
|
function closeLocalModels() {
|
|
327
329
|
localModelsOpen.value = false
|
|
328
330
|
}
|
|
331
|
+
function openOpenRouter() {
|
|
332
|
+
openRouterOpen.value = true
|
|
333
|
+
}
|
|
334
|
+
function closeOpenRouter() {
|
|
335
|
+
openRouterOpen.value = false
|
|
336
|
+
}
|
|
329
337
|
function openRequirementReview(blockId: string) {
|
|
330
338
|
resultView.value = { view: 'requirements-review', blockId, instanceId: null, stepIndex: null }
|
|
331
339
|
}
|
|
@@ -376,6 +384,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
376
384
|
serviceFragmentDefaultsOpen,
|
|
377
385
|
vendorCredentialsOpen,
|
|
378
386
|
localModelsOpen,
|
|
387
|
+
openRouterOpen,
|
|
379
388
|
resultView,
|
|
380
389
|
closeResultView,
|
|
381
390
|
stepDetail,
|
|
@@ -435,6 +444,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
435
444
|
closeVendorCredentials,
|
|
436
445
|
openLocalModels,
|
|
437
446
|
closeLocalModels,
|
|
447
|
+
openOpenRouter,
|
|
448
|
+
closeOpenRouter,
|
|
438
449
|
openRequirementReview,
|
|
439
450
|
openClarityReview,
|
|
440
451
|
closeRequirementReview,
|
package/app/types/models.ts
CHANGED
|
@@ -19,15 +19,15 @@ export interface ModelCost {
|
|
|
19
19
|
* A selectable LLM model, resolved to the flavour in use for this deployment
|
|
20
20
|
* (served by `GET /models`). Mirrors `ModelOption` in `@cat-factory/contracts`.
|
|
21
21
|
* The base `flavor`/`provider`/`model` is the always-available fallback
|
|
22
|
-
* (cloudflare/direct), or the subscription itself for subscription-only
|
|
23
|
-
* `subscription` (when present) is the alternative the picker prefers once
|
|
24
|
-
* workspace has a token for its vendor.
|
|
22
|
+
* (cloudflare/direct/openrouter), or the subscription itself for subscription-only
|
|
23
|
+
* models. `subscription` (when present) is the alternative the picker prefers once
|
|
24
|
+
* the workspace has a token for its vendor.
|
|
25
25
|
*/
|
|
26
26
|
export interface ModelOption {
|
|
27
27
|
id: string
|
|
28
28
|
label: string
|
|
29
29
|
description: string
|
|
30
|
-
flavor: 'cloudflare' | 'direct' | 'subscription'
|
|
30
|
+
flavor: 'cloudflare' | 'direct' | 'openrouter' | 'subscription'
|
|
31
31
|
/**
|
|
32
32
|
* Whether this model is actually selectable for the workspace: a direct API key for
|
|
33
33
|
* its provider, a connected subscription vendor, or Cloudflare AI enabled. Absent on
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Per-workspace OpenRouter dynamic catalog. OpenRouter is a single OpenAI-
|
|
3
|
+
// compatible gateway to 300+ models reached via the workspace's API-key pool. A
|
|
4
|
+
// workspace browses the live catalog and enables a subset; the enabled models
|
|
5
|
+
// surface automatically in the per-workspace model picker (with their context +
|
|
6
|
+
// price) and feed the spend budget.
|
|
7
|
+
//
|
|
8
|
+
// Mirrors the `@cat-factory/contracts` `openrouter` schemas exactly, so a payload
|
|
9
|
+
// returned by the backend drops straight into the Pinia store without translation.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Metadata for one OpenRouter model (prices per 1M tokens, in the spend currency). */
|
|
13
|
+
export interface OpenRouterModelMeta {
|
|
14
|
+
/** OpenRouter `vendor/model` slug, e.g. `anthropic/claude-opus-4.8`. */
|
|
15
|
+
id: string
|
|
16
|
+
/** Human-readable model name from OpenRouter's catalog. */
|
|
17
|
+
name: string
|
|
18
|
+
/** Total context window (input + output tokens), when reported. */
|
|
19
|
+
contextLength?: number
|
|
20
|
+
/** Input price per 1M tokens, in the spend currency. */
|
|
21
|
+
inputPerMillion: number
|
|
22
|
+
/** Output price per 1M tokens, in the spend currency. */
|
|
23
|
+
outputPerMillion: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A workspace's enabled OpenRouter models (the persisted subset). */
|
|
27
|
+
export interface OpenRouterCatalog {
|
|
28
|
+
models: OpenRouterModelMeta[]
|
|
29
|
+
createdAt: number
|
|
30
|
+
updatedAt: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Replace a workspace's enabled OpenRouter models with this subset (+ metadata). */
|
|
34
|
+
export interface UpsertOpenRouterCatalogInput {
|
|
35
|
+
models: OpenRouterModelMeta[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The result of probing OpenRouter's live `/models` for the browse list. */
|
|
39
|
+
export interface OpenRouterRefreshResult {
|
|
40
|
+
reachable: boolean
|
|
41
|
+
/** Every model OpenRouter currently serves (empty when unreachable). */
|
|
42
|
+
models: OpenRouterModelMeta[]
|
|
43
|
+
/** Human-readable failure reason when `reachable` is false. */
|
|
44
|
+
error?: string
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
4
5
|
"repository": {
|
|
5
6
|
"type": "git",
|
|
6
7
|
"url": "git+https://github.com/kibertoad/cat-factory.git",
|
|
7
8
|
"directory": "frontend/app"
|
|
8
9
|
},
|
|
9
|
-
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
10
10
|
"files": [
|
|
11
11
|
"app",
|
|
12
12
|
"nuxt.config.ts"
|