@truedat/ai 8.13.3 → 8.13.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/ai",
3
- "version": "8.13.3",
3
+ "version": "8.13.4",
4
4
  "description": "Truedat Web Artificial Intelligence package",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -53,7 +53,7 @@
53
53
  "@testing-library/jest-dom": "^6.6.3",
54
54
  "@testing-library/react": "^16.3.0",
55
55
  "@testing-library/user-event": "^14.6.1",
56
- "@truedat/test": "8.13.3",
56
+ "@truedat/test": "8.13.4",
57
57
  "identity-obj-proxy": "^3.0.0",
58
58
  "jest": "^29.7.0",
59
59
  "redux-saga-test-plan": "^4.0.6"
@@ -83,5 +83,5 @@
83
83
  "semantic-ui-react": "^3.0.0-beta.2",
84
84
  "swr": "^2.3.3"
85
85
  },
86
- "gitHead": "f02b46c12aeac48b129806b2c6a23cb0c87dcc94"
86
+ "gitHead": "4ec142f1c856aa669b232b12250c800a19ab9ace"
87
87
  }
@@ -9,6 +9,7 @@ export const providerTypes = [
9
9
  "bedrock_claude",
10
10
  "gemini",
11
11
  "anthropic",
12
+ "llm_gateway",
12
13
  ];
13
14
 
14
15
  export const agentTypes = ["fast", "high", "thinking"];
@@ -19,6 +19,7 @@ import {
19
19
  BedrockClaude,
20
20
  Gemini,
21
21
  Anthropic,
22
+ LlmGateway,
22
23
  } from "./providerProperties";
23
24
 
24
25
  const providerPropertiesComponents = {
@@ -27,6 +28,7 @@ const providerPropertiesComponents = {
27
28
  bedrock_claude: <BedrockClaude />,
28
29
  gemini: <Gemini />,
29
30
  anthropic: <Anthropic />,
31
+ llm_gateway: <LlmGateway />,
30
32
  };
31
33
 
32
34
  export default function ProviderEditor({
@@ -170,3 +170,195 @@ describe("<ProviderEditor />", () => {
170
170
  });
171
171
  });
172
172
  });
173
+
174
+ describe("<ProviderEditor /> LLM gateway", () => {
175
+ const gatewayRenderOpts = {
176
+ ...renderOpts,
177
+ messages: {
178
+ en: {
179
+ "providers.type.llm_gateway": "Company LLM gateway",
180
+ "form.validation.required": "{prop} is required",
181
+ },
182
+ },
183
+ };
184
+ const gatewayProvider = {
185
+ id: 2,
186
+ name: "Company models",
187
+ type: "llm_gateway",
188
+ properties: {
189
+ base_url: "https://llm.example.com/v1",
190
+ model: "meta-llama/llama-3-3-70b-instruct",
191
+ temperature: "0.2",
192
+ top_p: "0.9",
193
+ },
194
+ };
195
+ const propertyLabels = ["baseUrl", "model", "temperature", "topP", "apiKey"];
196
+ const getProperty = (rendered, label) =>
197
+ rendered.getByPlaceholderText(`providerProperties.form.${label}`);
198
+ const renderGateway = async (selectedProvider = gatewayProvider) => {
199
+ const onSubmit = jest.fn();
200
+ const rendered = render(
201
+ <ProviderEditor {...props} {...{ selectedProvider, onSubmit }} />,
202
+ gatewayRenderOpts,
203
+ );
204
+ await waitForLoad(rendered);
205
+ return { ...rendered, onSubmit };
206
+ };
207
+
208
+ beforeEach(() => jest.useRealTimers());
209
+
210
+ it("renders the saved LLM gateway properties in order with a password input", async () => {
211
+ const rendered = await renderGateway();
212
+ const inputs = rendered.container.querySelectorAll(
213
+ 'input[placeholder^="providerProperties.form."]',
214
+ );
215
+
216
+ expect(Array.from(inputs, (input) => input.placeholder)).toEqual(
217
+ propertyLabels.map((label) => `providerProperties.form.${label}`),
218
+ );
219
+ expect(
220
+ Array.from(
221
+ inputs,
222
+ (input) => input.closest(".field").querySelector("label").textContent,
223
+ ),
224
+ ).toEqual(
225
+ propertyLabels.map((label) => `providerProperties.form.${label}`),
226
+ );
227
+ expect(getProperty(rendered, "baseUrl")).toHaveValue(
228
+ gatewayProvider.properties.base_url,
229
+ );
230
+ expect(getProperty(rendered, "model")).toHaveValue(
231
+ gatewayProvider.properties.model,
232
+ );
233
+ expect(getProperty(rendered, "temperature")).toHaveValue("0.2");
234
+ expect(getProperty(rendered, "topP")).toHaveValue("0.9");
235
+ expect(getProperty(rendered, "apiKey")).toHaveAttribute("type", "password");
236
+ expect(getProperty(rendered, "apiKey")).toHaveValue("");
237
+ expect(rendered.container).toMatchSnapshot();
238
+ expect(getProperty(rendered, "baseUrl").closest(".field")).toHaveClass(
239
+ "required",
240
+ );
241
+ expect(getProperty(rendered, "model").closest(".field")).toHaveClass(
242
+ "required",
243
+ );
244
+ for (const label of ["temperature", "topP", "apiKey"]) {
245
+ expect(getProperty(rendered, label).closest(".field")).not.toHaveClass(
246
+ "required",
247
+ );
248
+ }
249
+ });
250
+
251
+ it("offers the translated LLM gateway option and submits a complete new provider", async () => {
252
+ const rendered = await renderGateway({
253
+ name: "",
254
+ type: "",
255
+ properties: {},
256
+ });
257
+ const user = userEvent.setup({ delay: null });
258
+
259
+ await user.type(
260
+ rendered.getByPlaceholderText("providers.form.name"),
261
+ "Company models",
262
+ );
263
+ await user.click(rendered.getAllByRole("listbox")[0]);
264
+ await user.click(
265
+ rendered.getByRole("option", { name: "Company LLM gateway" }),
266
+ );
267
+
268
+ expect(rendered.container).toMatchSnapshot(
269
+ "new provider after selecting LLM gateway",
270
+ );
271
+
272
+ const values = [
273
+ "https://llm.example.com/v1",
274
+ "meta-llama/llama-3-3-70b-instruct",
275
+ "0.2",
276
+ "0.9",
277
+ "test-token",
278
+ ];
279
+ for (const [index, label] of propertyLabels.entries()) {
280
+ await user.type(getProperty(rendered, label), values[index]);
281
+ }
282
+ await user.tab();
283
+ const save = rendered.getByRole("button", { name: /save/i });
284
+ await waitFor(() => expect(save).toBeEnabled());
285
+ await user.click(save);
286
+
287
+ await waitFor(() => expect(rendered.onSubmit).toHaveBeenCalledTimes(1));
288
+ expect(rendered.onSubmit.mock.calls[0][0]).toEqual({
289
+ name: "Company models",
290
+ type: "llm_gateway",
291
+ agents: undefined,
292
+ properties: {
293
+ base_url: values[0],
294
+ model: values[1],
295
+ temperature: values[2],
296
+ top_p: values[3],
297
+ api_key: values[4],
298
+ },
299
+ });
300
+ });
301
+
302
+ it.each(["baseUrl", "model"])(
303
+ "shows the required message and prevents submission without %s",
304
+ async (label) => {
305
+ const rendered = await renderGateway();
306
+ const user = userEvent.setup({ delay: null });
307
+ const input = getProperty(rendered, label);
308
+
309
+ await user.clear(input);
310
+ await user.tab();
311
+
312
+ expect(
313
+ await rendered.findByText(
314
+ `providerProperties.form.${label} is required`,
315
+ ),
316
+ ).toBeVisible();
317
+ const save = rendered.getByRole("button", { name: /save/i });
318
+ expect(save).toBeDisabled();
319
+ await user.click(save);
320
+ expect(rendered.onSubmit).not.toHaveBeenCalled();
321
+
322
+ await user.type(
323
+ input,
324
+ label === "baseUrl" ? "https://llm.example.com" : "gpt-5.1",
325
+ );
326
+ await user.tab();
327
+ await waitFor(() => expect(save).toBeEnabled());
328
+ expect(
329
+ rendered.queryByText(`providerProperties.form.${label} is required`),
330
+ ).not.toBeInTheDocument();
331
+ },
332
+ );
333
+
334
+ it("allows saving with only the required properties and keeps the URL and model unchanged", async () => {
335
+ const rendered = await renderGateway({
336
+ ...gatewayProvider,
337
+ properties: {
338
+ base_url: "https://llm.example.com",
339
+ model: "gpt-5.1",
340
+ },
341
+ });
342
+ const user = userEvent.setup({ delay: null });
343
+
344
+ expect(getProperty(rendered, "temperature")).toHaveValue("");
345
+ expect(getProperty(rendered, "topP")).toHaveValue("");
346
+ expect(getProperty(rendered, "apiKey")).toHaveValue("");
347
+ await user.type(
348
+ rendered.getByPlaceholderText("providers.form.name"),
349
+ " updated",
350
+ );
351
+ const save = rendered.getByRole("button", { name: /save/i });
352
+ await waitFor(() => expect(save).toBeEnabled());
353
+ await user.click(save);
354
+
355
+ await waitFor(() => expect(rendered.onSubmit).toHaveBeenCalledTimes(1));
356
+ expect(rendered.onSubmit.mock.calls[0][0].properties).toEqual({
357
+ base_url: "https://llm.example.com",
358
+ model: "gpt-5.1",
359
+ temperature: undefined,
360
+ top_p: undefined,
361
+ api_key: undefined,
362
+ });
363
+ });
364
+ });
@@ -1,5 +1,647 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
+ exports[`<ProviderEditor /> LLM gateway offers the translated LLM gateway option and submits a complete new provider: new provider after selecting LLM gateway 1`] = `
4
+ <div>
5
+ <form
6
+ class="ui form"
7
+ >
8
+ <h3
9
+ class="ui dividing header"
10
+ >
11
+ Company models
12
+ </h3>
13
+ <div
14
+ class="required field"
15
+ >
16
+ <label>
17
+ providers.form.name
18
+ </label>
19
+ <div
20
+ class="ui input"
21
+ >
22
+ <input
23
+ autocomplete="off"
24
+ placeholder="providers.form.name"
25
+ required=""
26
+ type="text"
27
+ value="Company models"
28
+ />
29
+ </div>
30
+ </div>
31
+ <div
32
+ class="required field"
33
+ >
34
+ <label>
35
+ providers.form.type
36
+ </label>
37
+ <div
38
+ aria-expanded="false"
39
+ class="ui selection dropdown"
40
+ role="listbox"
41
+ tabindex="0"
42
+ >
43
+ <div
44
+ aria-atomic="true"
45
+ aria-live="polite"
46
+ class="divider text"
47
+ role="alert"
48
+ >
49
+ Company LLM gateway
50
+ </div>
51
+ <i
52
+ aria-hidden="true"
53
+ class="dropdown icon"
54
+ />
55
+ <div
56
+ class="menu transition"
57
+ >
58
+ <div
59
+ aria-checked="false"
60
+ aria-selected="false"
61
+ class="item"
62
+ role="option"
63
+ style="pointer-events: all;"
64
+ >
65
+ <span
66
+ class="text"
67
+ >
68
+ providers.type.openai
69
+ </span>
70
+ </div>
71
+ <div
72
+ aria-checked="false"
73
+ aria-selected="false"
74
+ class="item"
75
+ role="option"
76
+ style="pointer-events: all;"
77
+ >
78
+ <span
79
+ class="text"
80
+ >
81
+ providers.type.azure_openai
82
+ </span>
83
+ </div>
84
+ <div
85
+ aria-checked="false"
86
+ aria-selected="false"
87
+ class="item"
88
+ role="option"
89
+ style="pointer-events: all;"
90
+ >
91
+ <span
92
+ class="text"
93
+ >
94
+ providers.type.bedrock_claude
95
+ </span>
96
+ </div>
97
+ <div
98
+ aria-checked="false"
99
+ aria-selected="false"
100
+ class="item"
101
+ role="option"
102
+ style="pointer-events: all;"
103
+ >
104
+ <span
105
+ class="text"
106
+ >
107
+ providers.type.gemini
108
+ </span>
109
+ </div>
110
+ <div
111
+ aria-checked="false"
112
+ aria-selected="false"
113
+ class="item"
114
+ role="option"
115
+ style="pointer-events: all;"
116
+ >
117
+ <span
118
+ class="text"
119
+ >
120
+ providers.type.anthropic
121
+ </span>
122
+ </div>
123
+ <div
124
+ aria-checked="true"
125
+ aria-selected="true"
126
+ class="active selected item"
127
+ role="option"
128
+ style="pointer-events: all;"
129
+ >
130
+ <span
131
+ class="text"
132
+ >
133
+ Company LLM gateway
134
+ </span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <div
140
+ class="field"
141
+ >
142
+ <label>
143
+ providers.form.agents
144
+ </label>
145
+ <div
146
+ aria-disabled="false"
147
+ aria-expanded="false"
148
+ class="ui selection dropdown"
149
+ role="listbox"
150
+ tabindex="0"
151
+ >
152
+ <div
153
+ aria-atomic="true"
154
+ aria-live="polite"
155
+ class="divider default text"
156
+ role="alert"
157
+ >
158
+ providers.form.agents.placeholder
159
+ </div>
160
+ <i
161
+ aria-hidden="true"
162
+ class="dropdown icon"
163
+ />
164
+ <div
165
+ class="menu transition"
166
+ >
167
+ <div
168
+ aria-checked="false"
169
+ aria-selected="true"
170
+ class="selected item"
171
+ role="option"
172
+ style="pointer-events: all;"
173
+ >
174
+ <span
175
+ class="text"
176
+ >
177
+ providers.agent.fast
178
+ </span>
179
+ </div>
180
+ <div
181
+ aria-checked="false"
182
+ aria-selected="false"
183
+ class="item"
184
+ role="option"
185
+ style="pointer-events: all;"
186
+ >
187
+ <span
188
+ class="text"
189
+ >
190
+ providers.agent.high
191
+ </span>
192
+ </div>
193
+ <div
194
+ aria-checked="false"
195
+ aria-selected="false"
196
+ class="item"
197
+ role="option"
198
+ style="pointer-events: all;"
199
+ >
200
+ <span
201
+ class="text"
202
+ >
203
+ providers.agent.thinking
204
+ </span>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ <div
210
+ class="required field"
211
+ >
212
+ <label>
213
+ providerProperties.form.baseUrl
214
+ </label>
215
+ <div
216
+ class="ui input"
217
+ >
218
+ <input
219
+ autocomplete="off"
220
+ placeholder="providerProperties.form.baseUrl"
221
+ required=""
222
+ type="text"
223
+ value=""
224
+ />
225
+ </div>
226
+ </div>
227
+ <div
228
+ class="required field"
229
+ >
230
+ <label>
231
+ providerProperties.form.model
232
+ </label>
233
+ <div
234
+ class="ui input"
235
+ >
236
+ <input
237
+ autocomplete="off"
238
+ placeholder="providerProperties.form.model"
239
+ required=""
240
+ type="text"
241
+ value=""
242
+ />
243
+ </div>
244
+ </div>
245
+ <div
246
+ class="field"
247
+ >
248
+ <label>
249
+ providerProperties.form.temperature
250
+ </label>
251
+ <div
252
+ class="ui input"
253
+ >
254
+ <input
255
+ autocomplete="off"
256
+ placeholder="providerProperties.form.temperature"
257
+ type="text"
258
+ value=""
259
+ />
260
+ </div>
261
+ </div>
262
+ <div
263
+ class="field"
264
+ >
265
+ <label>
266
+ providerProperties.form.topP
267
+ </label>
268
+ <div
269
+ class="ui input"
270
+ >
271
+ <input
272
+ autocomplete="off"
273
+ placeholder="providerProperties.form.topP"
274
+ type="text"
275
+ value=""
276
+ />
277
+ </div>
278
+ </div>
279
+ <div
280
+ class="field"
281
+ >
282
+ <label>
283
+ providerProperties.form.apiKey
284
+ </label>
285
+ <div
286
+ class="ui input"
287
+ >
288
+ <input
289
+ autocomplete="off"
290
+ placeholder="providerProperties.form.apiKey"
291
+ type="password"
292
+ value=""
293
+ />
294
+ </div>
295
+ </div>
296
+ <div
297
+ class="ui hidden divider"
298
+ />
299
+ <div
300
+ class="ui right aligned container"
301
+ >
302
+ <button
303
+ class="ui primary disabled button"
304
+ disabled=""
305
+ tabindex="-1"
306
+ >
307
+ actions.save
308
+ </button>
309
+ <button
310
+ class="ui button"
311
+ >
312
+ actions.cancel
313
+ </button>
314
+ <button
315
+ class="ui red button"
316
+ >
317
+ actions.delete
318
+ </button>
319
+ </div>
320
+ </form>
321
+ </div>
322
+ `;
323
+
324
+ exports[`<ProviderEditor /> LLM gateway renders the saved LLM gateway properties in order with a password input 1`] = `
325
+ <div>
326
+ <form
327
+ class="ui form"
328
+ >
329
+ <h3
330
+ class="ui dividing header"
331
+ >
332
+ Company models
333
+ </h3>
334
+ <div
335
+ class="required field"
336
+ >
337
+ <label>
338
+ providers.form.name
339
+ </label>
340
+ <div
341
+ class="ui input"
342
+ >
343
+ <input
344
+ autocomplete="off"
345
+ placeholder="providers.form.name"
346
+ required=""
347
+ type="text"
348
+ value="Company models"
349
+ />
350
+ </div>
351
+ </div>
352
+ <div
353
+ class="required field"
354
+ >
355
+ <label>
356
+ providers.form.type
357
+ </label>
358
+ <div
359
+ aria-expanded="false"
360
+ class="ui selection dropdown"
361
+ role="listbox"
362
+ tabindex="0"
363
+ >
364
+ <div
365
+ aria-atomic="true"
366
+ aria-live="polite"
367
+ class="divider text"
368
+ role="alert"
369
+ >
370
+ Company LLM gateway
371
+ </div>
372
+ <i
373
+ aria-hidden="true"
374
+ class="dropdown icon"
375
+ />
376
+ <div
377
+ class="menu transition"
378
+ >
379
+ <div
380
+ aria-checked="false"
381
+ aria-selected="false"
382
+ class="item"
383
+ role="option"
384
+ style="pointer-events: all;"
385
+ >
386
+ <span
387
+ class="text"
388
+ >
389
+ providers.type.openai
390
+ </span>
391
+ </div>
392
+ <div
393
+ aria-checked="false"
394
+ aria-selected="false"
395
+ class="item"
396
+ role="option"
397
+ style="pointer-events: all;"
398
+ >
399
+ <span
400
+ class="text"
401
+ >
402
+ providers.type.azure_openai
403
+ </span>
404
+ </div>
405
+ <div
406
+ aria-checked="false"
407
+ aria-selected="false"
408
+ class="item"
409
+ role="option"
410
+ style="pointer-events: all;"
411
+ >
412
+ <span
413
+ class="text"
414
+ >
415
+ providers.type.bedrock_claude
416
+ </span>
417
+ </div>
418
+ <div
419
+ aria-checked="false"
420
+ aria-selected="false"
421
+ class="item"
422
+ role="option"
423
+ style="pointer-events: all;"
424
+ >
425
+ <span
426
+ class="text"
427
+ >
428
+ providers.type.gemini
429
+ </span>
430
+ </div>
431
+ <div
432
+ aria-checked="false"
433
+ aria-selected="false"
434
+ class="item"
435
+ role="option"
436
+ style="pointer-events: all;"
437
+ >
438
+ <span
439
+ class="text"
440
+ >
441
+ providers.type.anthropic
442
+ </span>
443
+ </div>
444
+ <div
445
+ aria-checked="true"
446
+ aria-selected="true"
447
+ class="active selected item"
448
+ role="option"
449
+ style="pointer-events: all;"
450
+ >
451
+ <span
452
+ class="text"
453
+ >
454
+ Company LLM gateway
455
+ </span>
456
+ </div>
457
+ </div>
458
+ </div>
459
+ </div>
460
+ <div
461
+ class="field"
462
+ >
463
+ <label>
464
+ providers.form.agents
465
+ </label>
466
+ <div
467
+ aria-disabled="false"
468
+ aria-expanded="false"
469
+ class="ui selection dropdown"
470
+ role="listbox"
471
+ tabindex="0"
472
+ >
473
+ <div
474
+ aria-atomic="true"
475
+ aria-live="polite"
476
+ class="divider default text"
477
+ role="alert"
478
+ >
479
+ providers.form.agents.placeholder
480
+ </div>
481
+ <i
482
+ aria-hidden="true"
483
+ class="dropdown icon"
484
+ />
485
+ <div
486
+ class="menu transition"
487
+ >
488
+ <div
489
+ aria-checked="false"
490
+ aria-selected="true"
491
+ class="selected item"
492
+ role="option"
493
+ style="pointer-events: all;"
494
+ >
495
+ <span
496
+ class="text"
497
+ >
498
+ providers.agent.fast
499
+ </span>
500
+ </div>
501
+ <div
502
+ aria-checked="false"
503
+ aria-selected="false"
504
+ class="item"
505
+ role="option"
506
+ style="pointer-events: all;"
507
+ >
508
+ <span
509
+ class="text"
510
+ >
511
+ providers.agent.high
512
+ </span>
513
+ </div>
514
+ <div
515
+ aria-checked="false"
516
+ aria-selected="false"
517
+ class="item"
518
+ role="option"
519
+ style="pointer-events: all;"
520
+ >
521
+ <span
522
+ class="text"
523
+ >
524
+ providers.agent.thinking
525
+ </span>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ <div
531
+ class="required field"
532
+ >
533
+ <label>
534
+ providerProperties.form.baseUrl
535
+ </label>
536
+ <div
537
+ class="ui input"
538
+ >
539
+ <input
540
+ autocomplete="off"
541
+ placeholder="providerProperties.form.baseUrl"
542
+ required=""
543
+ type="text"
544
+ value="https://llm.example.com/v1"
545
+ />
546
+ </div>
547
+ </div>
548
+ <div
549
+ class="required field"
550
+ >
551
+ <label>
552
+ providerProperties.form.model
553
+ </label>
554
+ <div
555
+ class="ui input"
556
+ >
557
+ <input
558
+ autocomplete="off"
559
+ placeholder="providerProperties.form.model"
560
+ required=""
561
+ type="text"
562
+ value="meta-llama/llama-3-3-70b-instruct"
563
+ />
564
+ </div>
565
+ </div>
566
+ <div
567
+ class="field"
568
+ >
569
+ <label>
570
+ providerProperties.form.temperature
571
+ </label>
572
+ <div
573
+ class="ui input"
574
+ >
575
+ <input
576
+ autocomplete="off"
577
+ placeholder="providerProperties.form.temperature"
578
+ type="text"
579
+ value="0.2"
580
+ />
581
+ </div>
582
+ </div>
583
+ <div
584
+ class="field"
585
+ >
586
+ <label>
587
+ providerProperties.form.topP
588
+ </label>
589
+ <div
590
+ class="ui input"
591
+ >
592
+ <input
593
+ autocomplete="off"
594
+ placeholder="providerProperties.form.topP"
595
+ type="text"
596
+ value="0.9"
597
+ />
598
+ </div>
599
+ </div>
600
+ <div
601
+ class="field"
602
+ >
603
+ <label>
604
+ providerProperties.form.apiKey
605
+ </label>
606
+ <div
607
+ class="ui input"
608
+ >
609
+ <input
610
+ autocomplete="off"
611
+ placeholder="providerProperties.form.apiKey"
612
+ type="password"
613
+ value=""
614
+ />
615
+ </div>
616
+ </div>
617
+ <div
618
+ class="ui hidden divider"
619
+ />
620
+ <div
621
+ class="ui right aligned container"
622
+ >
623
+ <button
624
+ class="ui primary disabled button"
625
+ disabled=""
626
+ tabindex="-1"
627
+ >
628
+ actions.save
629
+ </button>
630
+ <button
631
+ class="ui button"
632
+ >
633
+ actions.cancel
634
+ </button>
635
+ <button
636
+ class="ui red button"
637
+ >
638
+ actions.delete
639
+ </button>
640
+ </div>
641
+ </form>
642
+ </div>
643
+ `;
644
+
3
645
  exports[`<ProviderEditor /> matches snapshot without onDelete 1`] = `
4
646
  <div>
5
647
  <form
@@ -120,6 +762,19 @@ exports[`<ProviderEditor /> matches snapshot without onDelete 1`] = `
120
762
  providers.type.anthropic
121
763
  </span>
122
764
  </div>
765
+ <div
766
+ aria-checked="false"
767
+ aria-selected="false"
768
+ class="item"
769
+ role="option"
770
+ style="pointer-events: all;"
771
+ >
772
+ <span
773
+ class="text"
774
+ >
775
+ providers.type.llm_gateway
776
+ </span>
777
+ </div>
123
778
  </div>
124
779
  </div>
125
780
  </div>
@@ -390,6 +1045,19 @@ exports[`<ProviderEditor /> matches the latest snapshot 1`] = `
390
1045
  providers.type.anthropic
391
1046
  </span>
392
1047
  </div>
1048
+ <div
1049
+ aria-checked="false"
1050
+ aria-selected="false"
1051
+ class="item"
1052
+ role="option"
1053
+ style="pointer-events: all;"
1054
+ >
1055
+ <span
1056
+ class="text"
1057
+ >
1058
+ providers.type.llm_gateway
1059
+ </span>
1060
+ </div>
393
1061
  </div>
394
1062
  </div>
395
1063
  </div>
@@ -663,6 +1331,19 @@ exports[`<ProviderEditor /> test cancel button with confirm 1`] = `
663
1331
  providers.type.anthropic
664
1332
  </span>
665
1333
  </div>
1334
+ <div
1335
+ aria-checked="false"
1336
+ aria-selected="false"
1337
+ class="item"
1338
+ role="option"
1339
+ style="pointer-events: all;"
1340
+ >
1341
+ <span
1342
+ class="text"
1343
+ >
1344
+ providers.type.llm_gateway
1345
+ </span>
1346
+ </div>
666
1347
  </div>
667
1348
  </div>
668
1349
  </div>
@@ -934,6 +1615,19 @@ exports[`<ProviderEditor /> test delete button 1`] = `
934
1615
  providers.type.anthropic
935
1616
  </span>
936
1617
  </div>
1618
+ <div
1619
+ aria-checked="false"
1620
+ aria-selected="false"
1621
+ class="item"
1622
+ role="option"
1623
+ style="pointer-events: all;"
1624
+ >
1625
+ <span
1626
+ class="text"
1627
+ >
1628
+ providers.type.llm_gateway
1629
+ </span>
1630
+ </div>
937
1631
  </div>
938
1632
  </div>
939
1633
  </div>
@@ -0,0 +1,146 @@
1
+ import { useIntl } from "react-intl";
2
+ import { Controller, useFormContext } from "react-hook-form";
3
+ import { Form } from "semantic-ui-react";
4
+
5
+ export default function LlmGateway() {
6
+ const { formatMessage } = useIntl();
7
+ const { control } = useFormContext();
8
+
9
+ return (
10
+ <>
11
+ <Controller
12
+ control={control}
13
+ name="properties.base_url"
14
+ rules={{
15
+ required: formatMessage(
16
+ { id: "form.validation.required" },
17
+ {
18
+ prop: formatMessage({
19
+ id: "providerProperties.form.baseUrl",
20
+ }),
21
+ },
22
+ ),
23
+ }}
24
+ render={({
25
+ field: { onBlur, onChange, value },
26
+ fieldState: { error },
27
+ }) => (
28
+ <Form.Input
29
+ autoComplete="off"
30
+ placeholder={formatMessage({
31
+ id: "providerProperties.form.baseUrl",
32
+ })}
33
+ error={error?.message}
34
+ label={formatMessage({
35
+ id: "providerProperties.form.baseUrl",
36
+ })}
37
+ onBlur={onBlur}
38
+ onChange={(_e, { value }) => onChange(value)}
39
+ value={value ?? ""}
40
+ required
41
+ />
42
+ )}
43
+ />
44
+ <Controller
45
+ control={control}
46
+ name="properties.model"
47
+ rules={{
48
+ required: formatMessage(
49
+ { id: "form.validation.required" },
50
+ {
51
+ prop: formatMessage({
52
+ id: "providerProperties.form.model",
53
+ }),
54
+ },
55
+ ),
56
+ }}
57
+ render={({
58
+ field: { onBlur, onChange, value },
59
+ fieldState: { error },
60
+ }) => (
61
+ <Form.Input
62
+ autoComplete="off"
63
+ placeholder={formatMessage({
64
+ id: "providerProperties.form.model",
65
+ })}
66
+ error={error?.message}
67
+ label={formatMessage({
68
+ id: "providerProperties.form.model",
69
+ })}
70
+ onBlur={onBlur}
71
+ onChange={(_e, { value }) => onChange(value)}
72
+ value={value ?? ""}
73
+ required
74
+ />
75
+ )}
76
+ />
77
+ <Controller
78
+ control={control}
79
+ name="properties.temperature"
80
+ render={({
81
+ field: { onBlur, onChange, value },
82
+ fieldState: { error },
83
+ }) => (
84
+ <Form.Input
85
+ autoComplete="off"
86
+ placeholder={formatMessage({
87
+ id: "providerProperties.form.temperature",
88
+ })}
89
+ error={error?.message}
90
+ label={formatMessage({
91
+ id: "providerProperties.form.temperature",
92
+ })}
93
+ onBlur={onBlur}
94
+ onChange={(_e, { value }) => onChange(value)}
95
+ value={value ?? ""}
96
+ />
97
+ )}
98
+ />
99
+ <Controller
100
+ control={control}
101
+ name="properties.top_p"
102
+ render={({
103
+ field: { onBlur, onChange, value },
104
+ fieldState: { error },
105
+ }) => (
106
+ <Form.Input
107
+ autoComplete="off"
108
+ placeholder={formatMessage({
109
+ id: "providerProperties.form.topP",
110
+ })}
111
+ error={error?.message}
112
+ label={formatMessage({
113
+ id: "providerProperties.form.topP",
114
+ })}
115
+ onBlur={onBlur}
116
+ onChange={(_e, { value }) => onChange(value)}
117
+ value={value ?? ""}
118
+ />
119
+ )}
120
+ />
121
+ <Controller
122
+ control={control}
123
+ name="properties.api_key"
124
+ render={({
125
+ field: { onBlur, onChange, value },
126
+ fieldState: { error },
127
+ }) => (
128
+ <Form.Input
129
+ type="password"
130
+ autoComplete="off"
131
+ placeholder={formatMessage({
132
+ id: "providerProperties.form.apiKey",
133
+ })}
134
+ error={error?.message}
135
+ label={formatMessage({
136
+ id: "providerProperties.form.apiKey",
137
+ })}
138
+ onBlur={onBlur}
139
+ onChange={(_e, { value }) => onChange(value)}
140
+ value={value ?? ""}
141
+ />
142
+ )}
143
+ />
144
+ </>
145
+ );
146
+ }
@@ -3,5 +3,6 @@ import AzureOpenai from "./AzureOpenai";
3
3
  import BedrockClaude from "./BedrockClaude";
4
4
  import Gemini from "./Gemini";
5
5
  import Anthropic from "./Anthropic";
6
+ import LlmGateway from "./LlmGateway";
6
7
 
7
- export { Openai, AzureOpenai, BedrockClaude, Gemini, Anthropic };
8
+ export { Openai, AzureOpenai, BedrockClaude, Gemini, Anthropic, LlmGateway };