Skip to content

Commit 14b1b2d

Browse files
authored
feat: support muxing fim and chat (#343)
* chore: update deps * chore: manually update openapi and generate new types * feat: add muxing fim, chat support * refactor: rename provider delete text * refactor: muxing based on new enum * chore: update enum
1 parent 3c8b909 commit 14b1b2d

10 files changed

+239
-59
lines changed

package-lock.json

+37-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@radix-ui/react-dialog": "^1.1.4",
3232
"@radix-ui/react-separator": "^1.1.0",
3333
"@radix-ui/react-slot": "^1.1.0",
34-
"@stacklok/ui-kit": "^1.0.1-4",
34+
"@stacklok/ui-kit": "^1.0.1-9",
3535
"@tanstack/react-query": "^5.64.1",
3636
"@tanstack/react-query-devtools": "^5.66.0",
3737
"@types/lodash": "^4.17.15",

src/api/generated/types.gen.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,23 @@ export type ModelByProvider = {
144144

145145
/**
146146
* Represents the different types of matchers we support.
147+
*
148+
* The 3 rules present match filenames and request types. They're used in conjunction with the
149+
* matcher field in the MuxRule model.
150+
* E.g.
151+
* - catch_all-> Always match
152+
* - filename_match and match: requests.py -> Match the request if the filename is requests.py
153+
* - fim_filename and match: main.py -> Match the request if the request type is fim
154+
* and the filename is main.py
155+
*
156+
* NOTE: Removing or updating fields from this enum will require a migration.
157+
* Adding new fields is safe.
147158
*/
148159
export enum MuxMatcherType {
149160
CATCH_ALL = 'catch_all',
150161
FILENAME_MATCH = 'filename_match',
151-
REQUEST_TYPE_MATCH = 'request_type_match',
162+
FIM_FILENAME = 'fim_filename',
163+
CHAT_FILENAME = 'chat_filename',
152164
}
153165

154166
/**

src/api/openapi.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1642,10 +1642,11 @@
16421642
"enum": [
16431643
"catch_all",
16441644
"filename_match",
1645-
"request_type_match"
1645+
"fim_filename",
1646+
"chat_filename"
16461647
],
16471648
"title": "MuxMatcherType",
1648-
"description": "Represents the different types of matchers we support."
1649+
"description": "Represents the different types of matchers we support.\n\nThe 3 rules present match filenames and request types. They're used in conjunction with the\nmatcher field in the MuxRule model.\nE.g.\n- catch_all-> Always match\n- filename_match and match: requests.py -> Match the request if the filename is requests.py\n- fim_filename and match: main.py -> Match the request if the request type is fim\nand the filename is main.py\n\nNOTE: Removing or updating fields from this enum will require a migration.\nAdding new fields is safe."
16491650
},
16501651
"MuxRule": {
16511652
"properties": {

src/features/providers/components/workspaces-by-provider.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export function WorkspacesByProvider({
1010
if (workspaces.length === 0) return null
1111
return (
1212
<div className="mb-6 flex flex-col gap-1">
13-
<p>The following workspaces will be impacted by this action</p>
13+
<p>
14+
The following workspaces are currently using this provider and will need
15+
to be updated:
16+
</p>
1417
<div className="flex flex-wrap gap-1">
1518
{uniqBy(workspaces, 'name').map((item, index) => {
1619
return (

src/features/workspace/components/__tests__/workspace-muxing-model.test.tsx

+58-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ test('renders muxing model', async () => {
77
render(
88
<WorkspaceMuxingModel isArchived={false} workspaceName="fake-workspace" />
99
)
10+
11+
expect(
12+
screen.getByRole('button', {
13+
name: /all types/i,
14+
})
15+
).toBeVisible()
1016
expect(screen.getByText(/model muxing/i)).toBeVisible()
1117
expect(
1218
screen.getByText(
@@ -46,6 +52,11 @@ test('disabled muxing fields and buttons for archived workspace', async () => {
4652

4753
expect(await screen.findByRole('button', { name: /save/i })).toBeDisabled()
4854
expect(screen.getByTestId(/workspace-models-dropdown/i)).toBeDisabled()
55+
expect(
56+
screen.getByRole('button', {
57+
name: /all types/i,
58+
})
59+
).toBeDisabled()
4960
expect(
5061
await screen.findByRole('button', { name: /add filter/i })
5162
).toBeDisabled()
@@ -75,12 +86,27 @@ test('submit additional model overrides', async () => {
7586
name: /filter by/i,
7687
})
7788
expect(textFields.length).toEqual(2)
89+
90+
const requestTypeSelect = screen.getAllByRole('button', {
91+
name: /fim & chat/i,
92+
})[0]
93+
await userEvent.click(requestTypeSelect as HTMLFormElement)
94+
await userEvent.click(
95+
screen.getByRole('option', {
96+
name: 'FIM',
97+
})
98+
)
99+
expect(
100+
screen.getByRole('button', {
101+
name: 'FIM',
102+
})
103+
).toBeVisible()
78104
const modelsButton = await screen.findAllByTestId(
79105
/workspace-models-dropdown/i
80106
)
81107
expect(modelsButton.length).toEqual(2)
82108

83-
await userEvent.type(textFields[1] as HTMLFormElement, '.ts')
109+
await userEvent.type(textFields[0] as HTMLFormElement, '.tsx')
84110

85111
await userEvent.click(
86112
(await screen.findByRole('button', {
@@ -94,6 +120,37 @@ test('submit additional model overrides', async () => {
94120
})
95121
)
96122

123+
await userEvent.click(screen.getByRole('button', { name: /add filter/i }))
124+
await userEvent.click(
125+
screen.getAllByRole('button', {
126+
name: /chat/i,
127+
})[1] as HTMLFormElement
128+
)
129+
130+
await userEvent.click(
131+
screen.getByRole('option', {
132+
name: 'Chat',
133+
})
134+
)
135+
136+
await userEvent.type(
137+
screen.getAllByRole('textbox', {
138+
name: /filter by/i,
139+
})[1] as HTMLFormElement,
140+
'.ts'
141+
)
142+
143+
await userEvent.click(
144+
(await screen.findByRole('button', {
145+
name: /select a model/i,
146+
})) as HTMLFormElement
147+
)
148+
149+
await userEvent.click(
150+
screen.getByRole('option', {
151+
name: /chatgpt-4o/i,
152+
})
153+
)
97154
await userEvent.click(screen.getByRole('button', { name: /save/i }))
98155

99156
await waitFor(() => {

src/features/workspace/components/workspace-models-dropdown.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function groupModelsByProviderName(
3737
id: providerName,
3838
textValue: providerName,
3939
items: items.map((item) => ({
40-
id: `${item.provider_id}/${item.name}`,
40+
id: `${item.provider_id}:${item.name}`,
4141
textValue: item.name,
4242
})),
4343
}))
@@ -116,7 +116,7 @@ export function WorkspaceModelsDropdown({
116116
const selectedValue = v.values().next().value
117117
if (!selectedValue && typeof selectedValue !== 'string') return
118118
if (typeof selectedValue === 'string') {
119-
const [provider_id, modelName] = selectedValue.split('/')
119+
const [provider_id, modelName] = selectedValue.split(':')
120120
if (!provider_id || !modelName) return
121121
onChange({
122122
model: modelName,

src/features/workspace/components/workspace-muxing-model.tsx

+30-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
Label,
1010
Link,
1111
LinkButton,
12+
Select,
13+
SelectButton,
1214
Text,
1315
TextField,
1416
Tooltip,
@@ -17,10 +19,7 @@ import {
1719
} from '@stacklok/ui-kit'
1820
import { twMerge } from 'tailwind-merge'
1921
import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace'
20-
import {
21-
MuxMatcherType,
22-
V1ListAllModelsForAllProvidersResponse,
23-
} from '@/api/generated'
22+
import { V1ListAllModelsForAllProvidersResponse } from '@/api/generated'
2423
import { FormEvent } from 'react'
2524
import {
2625
LayersThree01,
@@ -37,6 +36,7 @@ import {
3736
useMuxingRulesFormState,
3837
} from '../hooks/use-muxing-rules-form-workspace'
3938
import { FormButtons } from '@/components/FormButtons'
39+
import { getRuleData, isRequestType } from '../lib/utils'
4040

4141
function MissingProviderBanner() {
4242
return (
@@ -77,9 +77,31 @@ function SortableItem({
7777
isArchived,
7878
isDefaultRule,
7979
}: SortableItemProps) {
80-
const placeholder = isDefaultRule ? 'Catch-all' : 'e.g. file type, file name'
80+
const { selectedKey, placeholder, items } = getRuleData({
81+
isDefaultRule,
82+
matcher_type: rule.matcher_type,
83+
})
84+
8185
return (
8286
<div className="flex items-center gap-2" key={rule.id}>
87+
<div className="flex w-2/5 justify-between">
88+
<Select
89+
aria-labelledby="request type"
90+
selectedKey={selectedKey}
91+
name="request_type"
92+
isRequired
93+
isDisabled={isDefaultRule}
94+
className="w-full"
95+
items={items}
96+
onSelectionChange={(matcher_type) => {
97+
if (isRequestType(matcher_type)) {
98+
setRuleItem({ ...rule, matcher_type })
99+
}
100+
}}
101+
>
102+
<SelectButton />
103+
</Select>
104+
</div>
83105
<div className="flex w-full justify-between">
84106
<TextField
85107
aria-labelledby="filter-by-label-id"
@@ -149,9 +171,7 @@ export function WorkspaceMuxingModel({
149171
body: rules.map(({ id, ...rest }) => {
150172
void id
151173

152-
return rest.matcher
153-
? { ...rest, matcher_type: MuxMatcherType.FILENAME_MATCH }
154-
: { ...rest }
174+
return rest
155175
}),
156176
},
157177
{
@@ -200,6 +220,7 @@ export function WorkspaceMuxingModel({
200220
<div className="flex w-full flex-col gap-2">
201221
<div className="flex gap-2">
202222
<div className="w-12">&nbsp;</div>
223+
<div className="w-2/5">Request Type</div>
203224
<div className="w-full">
204225
<Label id="filter-by-label-id" className="flex items-center">
205226
Filter by
@@ -214,7 +235,7 @@ export function WorkspaceMuxingModel({
214235
</Label>
215236
</div>
216237
<div className="w-3/5">
217-
<Label id="preferred-model-id">Preferred Model</Label>
238+
<Label id="preferred-model-id">Model</Label>
218239
</div>
219240
</div>
220241
<SortableArea

0 commit comments

Comments
 (0)