Skip to content

Commit e69653c

Browse files
Gregory-Pereirajeff-phillips-18
authored andcommitted
Custom model endpoint redesign
Signed-off-by: Jeffrey Phillips <[email protected]>
1 parent 411ce3c commit e69653c

File tree

12 files changed

+697
-189
lines changed

12 files changed

+697
-189
lines changed

api-server/rhelai-install/rhelai-install.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ if [ -d "/tmp/api-server" ]; then
2222
fi
2323

2424
mkdir -p /tmp/api-server
25-
cd /tmp/ui/api-server
26-
wget https://instructlab-ui.s3.us-east-1.amazonaws.com/apiserver/apiserver-linux-amd64.tar.gz
27-
tar -xzf apiserver-linux-amd64.tar.gz
28-
mv apiserver-linux-amd64/ilab-apiserver /usr/local/sbin
29-
rm -rf apiserver-linux-amd64 apiserver-linux-amd64.tar.gz
25+
cd /tmp/api-server
26+
curl -sLO https://instructlab-ui.s3.us-east-1.amazonaws.com/apiserver/apiserver-linux-amd64.tar.gz
27+
tar -xzf apiserver-linux-amd64.tar.gz
28+
mv apiserver-linux-amd64/ilab-apiserver "{$HOME}/.local/bin"
29+
rm -rf apiserver-linux-amd64 apiserver-linux-amd64.tar.gz /tmp/api-server
3030

3131
CUDA_FLAG=""
3232

src/app/api/playground/chat/route.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export async function POST(req: NextRequest) {
1010
const { question, systemRole } = await req.json();
1111
const apiURL = req.nextUrl.searchParams.get('apiURL');
1212
const modelName = req.nextUrl.searchParams.get('modelName');
13+
const apiKey = req.nextUrl.searchParams.get('apiKey');
1314

1415
if (!apiURL || !modelName) {
1516
return new NextResponse('Missing API URL or Model Name', { status: 400 });
@@ -26,16 +27,34 @@ export async function POST(req: NextRequest) {
2627
stream: true
2728
};
2829

29-
const agent = new https.Agent({
30-
rejectUnauthorized: false
31-
});
30+
let agent: https.Agent;
31+
if (apiKey && apiKey != '') {
32+
agent = new https.Agent({
33+
rejectUnauthorized: true
34+
});
35+
} else {
36+
agent = new https.Agent({
37+
rejectUnauthorized: false
38+
});
39+
}
40+
41+
let headers: HeadersInit;
42+
if (apiKey && apiKey != '') {
43+
headers = {
44+
'Content-Type': 'application/json',
45+
Accept: 'text/event-stream',
46+
Authorization: `Bearer: ${apiKey}`
47+
};
48+
} else {
49+
headers = {
50+
'Content-Type': 'application/json',
51+
Accept: 'text/event-stream'
52+
};
53+
}
3254

3355
const chatResponse = await fetch(`${apiURL}/v1/chat/completions`, {
3456
method: 'POST',
35-
headers: {
36-
'Content-Type': 'application/json',
37-
accept: 'application/json'
38-
},
57+
headers: headers,
3958
body: JSON.stringify(requestData),
4059
agent: apiURL.startsWith('https') ? agent : undefined
4160
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant, TextInput } from '@patternfly/react-core';
5+
import { Endpoint } from '@/types';
6+
7+
interface Props {
8+
endpoint: Endpoint;
9+
onClose: (deleteEndpoint: boolean) => void;
10+
}
11+
12+
const DeleteEndpointModal: React.FC<Props> = ({ endpoint, onClose }) => {
13+
const [deleteEndpointName, setDeleteEndpointName] = React.useState<string>('');
14+
15+
return (
16+
<Modal
17+
variant={ModalVariant.medium}
18+
isOpen
19+
onClose={() => onClose(false)}
20+
aria-labelledby="confirm-delete-custom-model-endpoint"
21+
aria-describedby="show-yaml-body-variant"
22+
>
23+
<ModalHeader
24+
titleIconVariant="warning"
25+
title="Delete custom model endpoint?"
26+
labelId="confirm-delete-custom-model-endpoint-title"
27+
description={
28+
<>
29+
The <strong>{endpoint.name}</strong> custom model endpoint will be deleted.
30+
</>
31+
}
32+
/>
33+
<ModalBody id="delete-custom-model-endpoint">
34+
<Content component="p">
35+
Type <strong>{endpoint.name}</strong> to confirm deletion:
36+
<span style={{ color: 'var(--pf-t--global--color--status--danger--default' }}> *</span>
37+
</Content>
38+
<TextInput
39+
isRequired
40+
type="text"
41+
id="deleteEndpointByName"
42+
name="deleteEndpointByName"
43+
title="type {endpoint.name} to confirm."
44+
value={deleteEndpointName}
45+
onChange={(_, value) => setDeleteEndpointName(value)}
46+
/>
47+
</ModalBody>
48+
<ModalFooter>
49+
<Button
50+
key="confirm"
51+
variant="danger"
52+
isDisabled={deleteEndpointName !== endpoint.name}
53+
onClick={() => {
54+
if (deleteEndpointName === endpoint.name) {
55+
onClose(true);
56+
}
57+
}}
58+
>
59+
Delete
60+
</Button>
61+
<Button key="cancel" variant="secondary" onClick={() => onClose(false)}>
62+
Cancel
63+
</Button>
64+
</ModalFooter>
65+
</Modal>
66+
);
67+
};
68+
69+
export default DeleteEndpointModal;
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Endpoint, ModelEndpointStatus } from '@/types';
5+
import {
6+
Button,
7+
Form,
8+
FormGroup,
9+
FormHelperText,
10+
HelperText,
11+
HelperTextItem,
12+
Modal,
13+
ModalBody,
14+
ModalFooter,
15+
ModalHeader,
16+
ModalVariant,
17+
Popover,
18+
TextInput,
19+
ValidatedOptions
20+
} from '@patternfly/react-core';
21+
import { ExclamationCircleIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
22+
import { fetchEndpointStatus } from '@/components/Chat/modelService';
23+
24+
const removeTrailingSlash = (inputUrl: string): string => {
25+
if (inputUrl.slice(-1) === '/') {
26+
return inputUrl.slice(0, -1);
27+
}
28+
return inputUrl;
29+
};
30+
31+
const validateUrl = (link: string): boolean => {
32+
try {
33+
new URL(link);
34+
return true;
35+
} catch (e) {
36+
return false;
37+
}
38+
};
39+
40+
interface Props {
41+
endpoint: Endpoint;
42+
onClose: (endpoint?: Endpoint) => void;
43+
}
44+
45+
const EditEndpointModal: React.FC<Props> = ({ endpoint, onClose }) => {
46+
const [endpointName, setEndpointName] = React.useState<string>(endpoint.name || '');
47+
const [endpointDescription, setEndpointDescription] = React.useState<string>(endpoint.description || '');
48+
const [url, setUrl] = React.useState<string>(endpoint.url || '');
49+
const [modelName, setModelName] = React.useState<string>(endpoint.modelName || '');
50+
const [modelDescription, setModelDescription] = React.useState<string>(endpoint.modelDescription || '');
51+
const [apiKey, setApiKey] = React.useState<string>(endpoint.apiKey || '');
52+
const [nameTouched, setNameTouched] = React.useState<boolean>();
53+
const [urlTouched, setUrlTouched] = React.useState<boolean>();
54+
const [modelNameTouched, setModelNameTouched] = React.useState<boolean>();
55+
const [apiKeyTouched, setApiKeyTouched] = React.useState<boolean>();
56+
57+
const validName = endpointName.trim().length > 0;
58+
const validUrl = validateUrl(url);
59+
const validModelName = modelName.trim().length > 0;
60+
const validApiKey = apiKey.trim().length > 0;
61+
62+
const isValid = validName && validUrl && validModelName && validApiKey;
63+
64+
return (
65+
<Modal
66+
variant={ModalVariant.medium}
67+
isOpen
68+
onClose={() => onClose()}
69+
aria-labelledby="endpoint-modal-title"
70+
aria-describedby="endpoint-body-variant"
71+
>
72+
<ModalHeader
73+
title={endpoint?.id ? `Edit ${endpoint.name}` : 'Add a custom model endpoint'}
74+
labelId="endpoint-modal-title"
75+
description={
76+
endpoint?.id
77+
? 'Update the model endpoint details below.'
78+
: 'Add a custom model endpoint to interact with and test a fine-tuned model using the chat interface.'
79+
}
80+
/>
81+
<ModalBody>
82+
<Form>
83+
<FormGroup label="Endpoint name" isRequired fieldId="endpointName">
84+
<TextInput
85+
isRequired
86+
type="text"
87+
id="endpointName"
88+
name="endpointName"
89+
value={endpointName}
90+
onChange={(_, value) => setEndpointName(value)}
91+
placeholder="Enter name"
92+
onBlur={() => setNameTouched(true)}
93+
/>
94+
{nameTouched && !validName ? (
95+
<FormHelperText>
96+
<HelperText>
97+
<HelperTextItem icon={<ExclamationCircleIcon />} variant={ValidatedOptions.error}>
98+
Required field
99+
</HelperTextItem>
100+
</HelperText>
101+
</FormHelperText>
102+
) : null}
103+
</FormGroup>
104+
<FormGroup label="Endpoint description" fieldId="endpointDescription">
105+
<TextInput
106+
type="text"
107+
id="endpointDescription"
108+
name="endpointDescription"
109+
value={endpointDescription}
110+
onChange={(_, value) => setEndpointDescription(value)}
111+
placeholder="Enter description"
112+
/>
113+
</FormGroup>
114+
<FormGroup
115+
label="URL"
116+
isRequired
117+
fieldId="url"
118+
labelHelp={
119+
<Popover
120+
headerContent="Which URL do I use?"
121+
bodyContent="This should be the full endpoint of what you want to use for chat inference. For example, with OpenAI this would be: `https://api.openai.com/v1/chat/completions` (IE. it should include the path)."
122+
>
123+
<Button variant="link" isInline aria-label="More info">
124+
<OutlinedQuestionCircleIcon />
125+
</Button>
126+
</Popover>
127+
}
128+
>
129+
<TextInput
130+
isRequired
131+
type="text"
132+
id="url"
133+
name="url"
134+
value={url}
135+
onChange={(_, value) => setUrl(value)}
136+
placeholder="Enter URL"
137+
onBlur={() => setUrlTouched(true)}
138+
/>
139+
{urlTouched && !validUrl ? (
140+
<FormHelperText>
141+
<HelperText>
142+
<HelperTextItem icon={<ExclamationCircleIcon />} variant={ValidatedOptions.error}>
143+
Please enter a valid URL.
144+
</HelperTextItem>
145+
</HelperText>
146+
</FormHelperText>
147+
) : null}
148+
</FormGroup>
149+
<FormGroup label="Model name" isRequired fieldId="modelName">
150+
<TextInput
151+
isRequired
152+
type="text"
153+
id="modelName"
154+
name="modelName"
155+
value={modelName}
156+
onChange={(_, value) => setModelName(value)}
157+
placeholder="Enter model name"
158+
onBlur={() => setModelNameTouched(true)}
159+
/>
160+
{modelNameTouched && !validModelName ? (
161+
<FormHelperText>
162+
<HelperText>
163+
<HelperTextItem icon={<ExclamationCircleIcon />} variant={ValidatedOptions.error}>
164+
Required field
165+
</HelperTextItem>
166+
</HelperText>
167+
</FormHelperText>
168+
) : null}
169+
</FormGroup>
170+
<FormGroup label="Model description" fieldId="modelDescription">
171+
<TextInput
172+
type="text"
173+
id="modelDescription"
174+
name="modelDescription"
175+
value={modelDescription}
176+
onChange={(_, value) => setModelDescription(value)}
177+
placeholder="Enter description"
178+
/>
179+
</FormGroup>
180+
<FormGroup
181+
label="API Key"
182+
isRequired
183+
fieldId="apiKey"
184+
labelHelp={
185+
<Popover headerContent="What is an API Key?" bodyContent="An API key is a unique identifier used to authenticate requests to an API.">
186+
<Button variant="link" isInline aria-label="More info">
187+
<OutlinedQuestionCircleIcon />
188+
</Button>
189+
</Popover>
190+
}
191+
>
192+
<TextInput
193+
isRequired
194+
type="password"
195+
id="apiKey"
196+
name="apiKey"
197+
value={apiKey}
198+
onChange={(_, value) => setApiKey(value)}
199+
placeholder="Enter API key"
200+
onBlur={() => setApiKeyTouched(true)}
201+
/>
202+
{apiKeyTouched && !validApiKey ? (
203+
<FormHelperText>
204+
<HelperText>
205+
<HelperTextItem icon={<ExclamationCircleIcon />} variant={ValidatedOptions.error}>
206+
Required field
207+
</HelperTextItem>
208+
</HelperText>
209+
</FormHelperText>
210+
) : null}
211+
</FormGroup>
212+
</Form>
213+
</ModalBody>
214+
<ModalFooter>
215+
<Button
216+
key="save"
217+
variant="primary"
218+
isDisabled={!isValid}
219+
onClick={async () => {
220+
const updatedUrl = removeTrailingSlash(url);
221+
const newEndpoint = {
222+
id: endpoint.id,
223+
name: endpointName,
224+
description: endpointDescription,
225+
url: updatedUrl,
226+
modelName: modelName,
227+
modelDescription: modelDescription,
228+
apiKey: apiKey,
229+
status: ModelEndpointStatus.unknown,
230+
enabled: true
231+
};
232+
newEndpoint.status = await fetchEndpointStatus(newEndpoint);
233+
onClose(newEndpoint);
234+
}}
235+
>
236+
{endpoint.id ? 'Save' : 'Add'}
237+
</Button>
238+
<Button key="cancel" variant="link" onClick={() => onClose()}>
239+
Cancel
240+
</Button>
241+
</ModalFooter>
242+
</Modal>
243+
);
244+
};
245+
246+
export default EditEndpointModal;

0 commit comments

Comments
 (0)