@shipfox/client-agent 5.0.0 → 6.0.1

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.
Files changed (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +45 -0
  3. package/dist/components/available-provider-card.d.ts +2 -2
  4. package/dist/components/available-provider-card.d.ts.map +1 -1
  5. package/dist/components/available-provider-card.js.map +1 -1
  6. package/dist/components/available-providers-grid.d.ts +2 -2
  7. package/dist/components/available-providers-grid.d.ts.map +1 -1
  8. package/dist/components/available-providers-grid.js +1 -1
  9. package/dist/components/available-providers-grid.js.map +1 -1
  10. package/dist/components/change-default-model-form.d.ts +3 -4
  11. package/dist/components/change-default-model-form.d.ts.map +1 -1
  12. package/dist/components/change-default-model-form.js +2 -4
  13. package/dist/components/change-default-model-form.js.map +1 -1
  14. package/dist/components/custom-model-provider-api-options.d.ts +3 -3
  15. package/dist/components/custom-model-provider-api-options.d.ts.map +1 -1
  16. package/dist/components/custom-model-provider-api-options.js.map +1 -1
  17. package/dist/components/custom-model-provider-form.d.ts +2 -2
  18. package/dist/components/custom-model-provider-form.d.ts.map +1 -1
  19. package/dist/components/custom-model-provider-form.js +12 -10
  20. package/dist/components/custom-model-provider-form.js.map +1 -1
  21. package/dist/components/custom-model-provider-form.stories.d.ts.map +1 -1
  22. package/dist/components/custom-model-provider-form.stories.js +11 -11
  23. package/dist/components/custom-model-provider-form.stories.js.map +1 -1
  24. package/dist/components/custom-model-provider-payload.d.ts +9 -9
  25. package/dist/components/custom-model-provider-payload.d.ts.map +1 -1
  26. package/dist/components/custom-model-provider-payload.js +42 -26
  27. package/dist/components/custom-model-provider-payload.js.map +1 -1
  28. package/dist/components/custom-model-provider-slug.d.ts +1 -1
  29. package/dist/components/custom-model-provider-slug.d.ts.map +1 -1
  30. package/dist/components/custom-model-provider-slug.js +3 -3
  31. package/dist/components/custom-model-provider-slug.js.map +1 -1
  32. package/dist/components/default-model-field.d.ts +2 -2
  33. package/dist/components/default-model-field.d.ts.map +1 -1
  34. package/dist/components/default-model-field.js +1 -1
  35. package/dist/components/default-model-field.js.map +1 -1
  36. package/dist/components/harness-availability.d.ts +4 -7
  37. package/dist/components/harness-availability.d.ts.map +1 -1
  38. package/dist/components/harness-availability.js +2 -11
  39. package/dist/components/harness-availability.js.map +1 -1
  40. package/dist/components/harnesses-section.d.ts.map +1 -1
  41. package/dist/components/harnesses-section.js +4 -6
  42. package/dist/components/harnesses-section.js.map +1 -1
  43. package/dist/components/model-provider-usage-modal.d.ts +2 -2
  44. package/dist/components/model-provider-usage-modal.d.ts.map +1 -1
  45. package/dist/components/model-provider-usage-modal.js +4 -4
  46. package/dist/components/model-provider-usage-modal.js.map +1 -1
  47. package/dist/components/model-provider-usage-modal.stories.d.ts.map +1 -1
  48. package/dist/components/model-provider-usage-modal.stories.js +14 -16
  49. package/dist/components/model-provider-usage-modal.stories.js.map +1 -1
  50. package/dist/components/model-provider-usage-target.d.ts +5 -5
  51. package/dist/components/model-provider-usage-target.d.ts.map +1 -1
  52. package/dist/components/model-provider-usage-target.js +10 -8
  53. package/dist/components/model-provider-usage-target.js.map +1 -1
  54. package/dist/components/model-providers-section.d.ts.map +1 -1
  55. package/dist/components/model-providers-section.js +112 -94
  56. package/dist/components/model-providers-section.js.map +1 -1
  57. package/dist/components/model-providers-section.stories.js +8 -4
  58. package/dist/components/model-providers-section.stories.js.map +1 -1
  59. package/dist/components/test-and-save-form.d.ts +3 -3
  60. package/dist/components/test-and-save-form.d.ts.map +1 -1
  61. package/dist/components/test-and-save-form.js +7 -7
  62. package/dist/components/test-and-save-form.js.map +1 -1
  63. package/dist/core/harness-policy.d.ts +11 -0
  64. package/dist/core/harness-policy.d.ts.map +1 -0
  65. package/dist/core/harness-policy.js +71 -0
  66. package/dist/core/harness-policy.js.map +1 -0
  67. package/dist/core/model-provider-management-reducer.d.ts +51 -0
  68. package/dist/core/model-provider-management-reducer.d.ts.map +1 -0
  69. package/dist/core/model-provider-management-reducer.js +43 -0
  70. package/dist/core/model-provider-management-reducer.js.map +1 -0
  71. package/dist/core/models.d.ts +120 -0
  72. package/dist/core/models.d.ts.map +1 -0
  73. package/dist/core/models.js +3 -0
  74. package/dist/core/models.js.map +1 -0
  75. package/dist/core/onboarding-reducer.d.ts +36 -0
  76. package/dist/core/onboarding-reducer.d.ts.map +1 -0
  77. package/dist/core/onboarding-reducer.js +54 -0
  78. package/dist/core/onboarding-reducer.js.map +1 -0
  79. package/dist/core/provider-policy.d.ts +9 -0
  80. package/dist/core/provider-policy.d.ts.map +1 -0
  81. package/dist/core/provider-policy.js +37 -0
  82. package/dist/core/provider-policy.js.map +1 -0
  83. package/dist/feature.d.ts +7 -0
  84. package/dist/feature.d.ts.map +1 -1
  85. package/dist/feature.js +10 -9
  86. package/dist/feature.js.map +1 -1
  87. package/dist/hooks/api/model-provider-command-mapper.d.ts +11 -0
  88. package/dist/hooks/api/model-provider-command-mapper.d.ts.map +1 -0
  89. package/dist/hooks/api/model-provider-command-mapper.js +144 -0
  90. package/dist/hooks/api/model-provider-command-mapper.js.map +1 -0
  91. package/dist/hooks/api/model-provider-mapper.d.ts +11 -0
  92. package/dist/hooks/api/model-provider-mapper.d.ts.map +1 -0
  93. package/dist/hooks/api/model-provider-mapper.js +94 -0
  94. package/dist/hooks/api/model-provider-mapper.js.map +1 -0
  95. package/dist/hooks/api/model-providers.d.ts +60 -295
  96. package/dist/hooks/api/model-providers.d.ts.map +1 -1
  97. package/dist/hooks/api/model-providers.js +51 -37
  98. package/dist/hooks/api/model-providers.js.map +1 -1
  99. package/dist/pages/model-provider-onboarding-page.d.ts.map +1 -1
  100. package/dist/pages/model-provider-onboarding-page.js +57 -45
  101. package/dist/pages/model-provider-onboarding-page.js.map +1 -1
  102. package/dist/routes/inputs.d.ts +4 -0
  103. package/dist/routes/inputs.d.ts.map +1 -0
  104. package/dist/routes/inputs.js +12 -0
  105. package/dist/routes/inputs.js.map +1 -0
  106. package/dist/routes/model-provider.d.ts.map +1 -1
  107. package/dist/routes/model-provider.js +4 -5
  108. package/dist/routes/model-provider.js.map +1 -1
  109. package/dist/tsconfig.test.tsbuildinfo +1 -1
  110. package/package.json +9 -10
  111. package/src/components/available-provider-card.tsx +2 -2
  112. package/src/components/available-providers-grid.tsx +3 -3
  113. package/src/components/change-default-model-form.tsx +5 -6
  114. package/src/components/custom-model-provider-api-options.ts +3 -3
  115. package/src/components/custom-model-provider-form.stories.tsx +13 -13
  116. package/src/components/custom-model-provider-form.tsx +19 -12
  117. package/src/components/custom-model-provider-payload.test.ts +22 -22
  118. package/src/components/custom-model-provider-payload.ts +69 -54
  119. package/src/components/custom-model-provider-slug.test.ts +1 -1
  120. package/src/components/custom-model-provider-slug.ts +6 -3
  121. package/src/components/default-model-field.tsx +5 -5
  122. package/src/components/harness-availability.test.ts +13 -10
  123. package/src/components/harness-availability.ts +4 -28
  124. package/src/components/harnesses-section.tsx +7 -11
  125. package/src/components/model-provider-usage-modal.stories.tsx +18 -23
  126. package/src/components/model-provider-usage-modal.test.tsx +35 -7
  127. package/src/components/model-provider-usage-modal.tsx +12 -16
  128. package/src/components/model-provider-usage-target.ts +15 -18
  129. package/src/components/model-providers-section.stories.tsx +4 -1
  130. package/src/components/model-providers-section.tsx +112 -125
  131. package/src/components/test-and-save-form.tsx +13 -17
  132. package/src/core/harness-policy.test.ts +15 -0
  133. package/src/core/harness-policy.ts +83 -0
  134. package/src/core/model-provider-management-reducer.test.ts +59 -0
  135. package/src/core/model-provider-management-reducer.ts +72 -0
  136. package/src/core/models.ts +143 -0
  137. package/src/core/onboarding-reducer.test.ts +44 -0
  138. package/src/core/onboarding-reducer.ts +64 -0
  139. package/src/core/provider-policy.ts +53 -0
  140. package/src/feature.ts +12 -10
  141. package/src/hooks/api/model-provider-command-mapper.ts +122 -0
  142. package/src/hooks/api/model-provider-mapper.test.ts +27 -0
  143. package/src/hooks/api/model-provider-mapper.ts +120 -0
  144. package/src/hooks/api/model-providers.test.ts +15 -15
  145. package/src/hooks/api/model-providers.ts +146 -81
  146. package/src/pages/model-provider-onboarding-page.tsx +62 -56
  147. package/src/routes/inputs.test.ts +11 -0
  148. package/src/routes/inputs.ts +9 -0
  149. package/src/routes/model-provider.tsx +4 -3
  150. package/test/fixtures/model-providers.ts +1 -1
  151. package/tsconfig.build.tsbuildinfo +1 -1
  152. package/vercel.json +5 -0
  153. package/dist/components/provider-search.d.ts +0 -4
  154. package/dist/components/provider-search.d.ts.map +0 -1
  155. package/dist/components/provider-search.js +0 -13
  156. package/dist/components/provider-search.js.map +0 -1
  157. package/dist/components/supported-model-provider-catalog-entry.d.ts +0 -8
  158. package/dist/components/supported-model-provider-catalog-entry.d.ts.map +0 -1
  159. package/dist/components/supported-model-provider-catalog-entry.js +0 -9
  160. package/dist/components/supported-model-provider-catalog-entry.js.map +0 -1
  161. package/src/components/provider-search.test.ts +0 -45
  162. package/src/components/provider-search.ts +0 -18
  163. package/src/components/supported-model-provider-catalog-entry.ts +0 -19
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-agent",
3
3
  "license": "MIT",
4
- "version": "5.0.0",
4
+ "version": "6.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -10,10 +10,6 @@
10
10
  "type": "module",
11
11
  "main": "dist/index.js",
12
12
  "types": "dist/index.d.ts",
13
- "imports": {
14
- "#*": "./dist/*",
15
- "#test/*": "./test/*"
16
- },
17
13
  "exports": {
18
14
  ".": {
19
15
  "types": "./dist/index.d.ts",
@@ -32,11 +28,11 @@
32
28
  "@swc/helpers": "^0.5.17",
33
29
  "@tanstack/react-form": "^1.32.0",
34
30
  "@tanstack/react-query": "^5.101.0",
35
- "@shipfox/api-agent-dto": "8.0.0",
36
- "@shipfox/client-shell": "5.0.0",
37
- "@shipfox/client-api": "4.0.0",
38
- "@shipfox/client-ui": "4.0.0",
39
- "@shipfox/react-ui": "0.3.5"
31
+ "@shipfox/api-agent-dto": "9.0.1",
32
+ "@shipfox/client-api": "6.0.1",
33
+ "@shipfox/client-shell": "6.0.1",
34
+ "@shipfox/react-ui": "0.3.6",
35
+ "@shipfox/client-ui": "6.0.1"
40
36
  },
41
37
  "peerDependencies": {
42
38
  "@tanstack/react-query": "^5.101.0",
@@ -44,6 +40,9 @@
44
40
  "react": "^19.0.0",
45
41
  "react-dom": "^19.0.0"
46
42
  },
43
+ "imports": {
44
+ "#*": "./dist/*"
45
+ },
47
46
  "scripts": {
48
47
  "build": "shipfox-swc",
49
48
  "check": "shipfox-biome-check",
@@ -1,7 +1,7 @@
1
- import type {ModelProviderCatalogEntryDto} from '@shipfox/api-agent-dto';
2
1
  import {Icon} from '@shipfox/react-ui/icon';
3
2
  import {Text} from '@shipfox/react-ui/typography';
4
3
  import {cn} from '@shipfox/react-ui/utils';
4
+ import type {SupportedProvider} from '#core/models.js';
5
5
 
6
6
  const SURFACE_CLASS =
7
7
  'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
@@ -10,7 +10,7 @@ export function AvailableProviderCard({
10
10
  entry,
11
11
  onConfigure,
12
12
  }: {
13
- entry: ModelProviderCatalogEntryDto;
13
+ entry: SupportedProvider;
14
14
  onConfigure: () => void;
15
15
  }) {
16
16
  return (
@@ -1,19 +1,19 @@
1
- import type {ModelProviderCatalogEntryDto} from '@shipfox/api-agent-dto';
2
1
  import {Button} from '@shipfox/react-ui/button';
3
2
  import {EmptyState} from '@shipfox/react-ui/empty-state';
4
3
  import {Icon} from '@shipfox/react-ui/icon';
5
4
  import {Input} from '@shipfox/react-ui/input';
6
5
  import type {ReactNode} from 'react';
7
6
  import {useMemo, useRef, useState} from 'react';
7
+ import type {SupportedProvider} from '#core/models.js';
8
+ import {providerMatchesSearch} from '#core/provider-policy.js';
8
9
  import {AvailableProviderCard} from './available-provider-card.js';
9
- import {providerMatchesSearch} from './provider-search.js';
10
10
 
11
11
  export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-12 max-[760px]:grid-cols-1';
12
12
 
13
13
  const SEARCH_VISIBILITY_THRESHOLD = 8;
14
14
  const MAX_ECHOED_QUERY_LENGTH = 40;
15
15
 
16
- export function AvailableProvidersGrid<TEntry extends ModelProviderCatalogEntryDto>({
16
+ export function AvailableProvidersGrid<TEntry extends SupportedProvider>({
17
17
  entries,
18
18
  onSelect,
19
19
  trailingCard,
@@ -1,4 +1,3 @@
1
- import type {ModelProviderConfigDto} from '@shipfox/api-agent-dto';
2
1
  import {Button} from '@shipfox/react-ui/button';
3
2
  import {Callout} from '@shipfox/react-ui/callout';
4
3
  import {fieldError} from '@shipfox/react-ui/form-field';
@@ -6,6 +5,7 @@ import {ModalBody, ModalFooter} from '@shipfox/react-ui/modal';
6
5
  import {Text} from '@shipfox/react-ui/typography';
7
6
  import {useForm} from '@tanstack/react-form';
8
7
  import {useState} from 'react';
8
+ import type {BuiltinProviderConfig, SupportedProvider} from '#core/models.js';
9
9
  import {useUpdateModelProviderDefaultModelMutation} from '#hooks/api/model-providers.js';
10
10
  import {
11
11
  DefaultModelField,
@@ -13,7 +13,6 @@ import {
13
13
  selectedModelForModelPayload,
14
14
  } from './default-model-field.js';
15
15
  import {modelProviderConfigErrorToFormError} from './form-errors.js';
16
- import type {SupportedModelProviderCatalogEntry} from './supported-model-provider-catalog-entry.js';
17
16
 
18
17
  const CHANGE_DEFAULT_MODEL_FORM_ID = 'model-provider-change-default-model-form';
19
18
 
@@ -24,21 +23,21 @@ export function ChangeDefaultModelForm({
24
23
  onSaved,
25
24
  }: {
26
25
  workspaceId: string;
27
- entry: SupportedModelProviderCatalogEntry;
28
- config: ModelProviderConfigDto;
26
+ entry: SupportedProvider;
27
+ config: BuiltinProviderConfig;
29
28
  onSaved: () => void;
30
29
  }) {
31
30
  const updateDefaultModel = useUpdateModelProviderDefaultModelMutation();
32
31
  const [formError, setFormError] = useState<string | undefined>();
33
32
  const form = useForm({
34
- defaultValues: {default_model: defaultModelFormValue(config.default_model)},
33
+ defaultValues: {default_model: defaultModelFormValue(config.defaultModel)},
35
34
  onSubmit: async ({value}) => {
36
35
  setFormError(undefined);
37
36
  try {
38
37
  await updateDefaultModel.mutateAsync({
39
38
  workspaceId,
40
39
  providerId: entry.id,
41
- body: {default_model: selectedModelForModelPayload(value.default_model)},
40
+ defaultModel: selectedModelForModelPayload(value.default_model),
42
41
  });
43
42
  onSaved();
44
43
  } catch (error) {
@@ -1,12 +1,12 @@
1
- import type {ModelProviderApi} from '@shipfox/api-agent-dto';
1
+ import type {ProviderApi} from '#core/models.js';
2
2
 
3
- export const MODEL_PROVIDER_API_OPTIONS: Array<{value: ModelProviderApi; label: string}> = [
3
+ export const MODEL_PROVIDER_API_OPTIONS: Array<{value: ProviderApi; label: string}> = [
4
4
  {value: 'openai-completions', label: 'OpenAI Chat Completions'},
5
5
  {value: 'openai-responses', label: 'OpenAI Responses'},
6
6
  {value: 'anthropic-messages', label: 'Anthropic Messages'},
7
7
  {value: 'google-generative-ai', label: 'Google Generative AI'},
8
8
  ];
9
9
 
10
- export function modelProviderApiLabel(api: ModelProviderApi): string {
10
+ export function modelProviderApiLabel(api: ProviderApi): string {
11
11
  return MODEL_PROVIDER_API_OPTIONS.find((option) => option.value === api)?.label ?? api;
12
12
  }
@@ -1,9 +1,9 @@
1
- import type {CustomModelProviderConfigDto} from '@shipfox/api-agent-dto';
2
1
  import {configureApiClient} from '@shipfox/client-api';
3
2
  import {Modal, ModalContent, ModalHeader, ModalTitle} from '@shipfox/react-ui/modal';
4
3
  import type {Meta, StoryObj} from '@storybook/react';
5
4
  import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
6
5
  import {useMemo, useState} from 'react';
6
+ import type {CustomProviderConfig} from '#core/models.js';
7
7
  import {CustomModelProviderForm} from './custom-model-provider-form.js';
8
8
 
9
9
  interface CustomModelProviderFormStoryProps {
@@ -61,31 +61,31 @@ export const EditStoredSecrets: Story = {
61
61
  args: {mode: 'edit'},
62
62
  };
63
63
 
64
- function customConfig(): CustomModelProviderConfigDto {
64
+ function customConfig(): CustomProviderConfig {
65
65
  return {
66
66
  kind: 'custom',
67
- provider_id: 'local-vllm',
68
- display_name: 'Local vLLM',
67
+ providerId: 'local-vllm',
68
+ displayName: 'Local vLLM',
69
69
  api: 'openai-completions',
70
- base_url: 'http://localhost:8000/v1',
70
+ baseUrl: 'http://localhost:8000/v1',
71
71
  headers: [{name: 'x-region', value: 'us'}],
72
- secret_header_names: ['authorization'],
72
+ secretHeaderNames: ['authorization'],
73
73
  models: [
74
74
  {
75
75
  id: 'llama-3.1',
76
76
  label: 'Llama 3.1',
77
- context_window: 128000,
78
- max_output_tokens: 16384,
77
+ contextWindow: 128000,
78
+ maxOutputTokens: 16384,
79
79
  },
80
80
  {
81
81
  id: 'qwen-coder',
82
82
  label: 'Qwen Coder',
83
- context_window: 32768,
84
- max_output_tokens: 8192,
83
+ contextWindow: 32768,
84
+ maxOutputTokens: 8192,
85
85
  },
86
86
  ],
87
- default_model: 'llama-3.1',
88
- created_at: '2026-05-08T00:00:00.000Z',
89
- updated_at: '2026-05-08T00:00:00.000Z',
87
+ defaultModel: 'llama-3.1',
88
+ createdAt: '2026-05-08T00:00:00.000Z',
89
+ updatedAt: '2026-05-08T00:00:00.000Z',
90
90
  };
91
91
  }
@@ -1,4 +1,3 @@
1
- import type {CustomModelProviderConfigDto, ModelProviderApi} from '@shipfox/api-agent-dto';
2
1
  import {Badge} from '@shipfox/react-ui/badge';
3
2
  import {Button, IconButton} from '@shipfox/react-ui/button';
4
3
  import {Callout} from '@shipfox/react-ui/callout';
@@ -10,10 +9,12 @@ import {Text} from '@shipfox/react-ui/typography';
10
9
  import {useForm} from '@tanstack/react-form';
11
10
  import type {ReactNode, RefObject} from 'react';
12
11
  import {useEffect, useRef, useState} from 'react';
12
+ import type {CustomProviderConfig, ProviderApi} from '#core/models.js';
13
13
  import {
14
14
  useCreateCustomModelProviderMutation,
15
15
  useDiscoverCustomModelProviderModelsBySlugMutation,
16
16
  useDiscoverCustomModelProviderModelsMutation,
17
+ useModelProviderCatalogQuery,
17
18
  useUpdateCustomModelProviderMutation,
18
19
  } from '#hooks/api/model-providers.js';
19
20
  import {MODEL_PROVIDER_API_OPTIONS} from './custom-model-provider-api-options.js';
@@ -44,12 +45,14 @@ export function CustomModelProviderForm({
44
45
  onSaved,
45
46
  }: {
46
47
  workspaceId: string;
47
- existingConfig?: CustomModelProviderConfigDto | undefined;
48
+ existingConfig?: CustomProviderConfig | undefined;
48
49
  onSaved: () => void;
49
50
  }) {
50
51
  const createMutation = useCreateCustomModelProviderMutation();
51
52
  const updateMutation = useUpdateCustomModelProviderMutation();
52
53
  const discoverMutation = useDiscoverCustomModelProviderModelsMutation();
54
+ const catalogQuery = useModelProviderCatalogQuery();
55
+ const reservedProviderIds = catalogQuery.data?.providers.map((provider) => provider.id) ?? [];
53
56
  const discoverBySlugMutation = useDiscoverCustomModelProviderModelsBySlugMutation();
54
57
  const [slugEdited, setSlugEdited] = useState(existingConfig !== undefined);
55
58
  const [formError, setFormError] = useState<string | undefined>();
@@ -67,13 +70,13 @@ export function CustomModelProviderForm({
67
70
  if (existingConfig) {
68
71
  await updateMutation.mutateAsync({
69
72
  workspaceId,
70
- providerId: existingConfig.provider_id,
71
- body: buildUpdateCustomModelProviderBody(existingConfig, value),
73
+ providerId: existingConfig.providerId,
74
+ command: buildUpdateCustomModelProviderBody(existingConfig, value),
72
75
  });
73
76
  } else {
74
77
  await createMutation.mutateAsync({
75
78
  workspaceId,
76
- body: buildCreateCustomModelProviderBody(value),
79
+ command: buildCreateCustomModelProviderBody(value),
77
80
  });
78
81
  }
79
82
  onSaved();
@@ -101,16 +104,16 @@ export function CustomModelProviderForm({
101
104
  const response = existingConfig
102
105
  ? await discoverBySlugMutation.mutateAsync({
103
106
  workspaceId,
104
- providerId: existingConfig.provider_id,
105
- body: buildDiscoverModelsBySlugBody(existingConfig, values),
107
+ providerId: existingConfig.providerId,
108
+ command: buildDiscoverModelsBySlugBody(existingConfig, values),
106
109
  })
107
110
  : await discoverMutation.mutateAsync({
108
111
  workspaceId,
109
- body: buildDiscoverModelsBody(values),
112
+ command: buildDiscoverModelsBody(values),
110
113
  });
111
114
  if (!mountedRef.current) return;
112
115
  const existingIds = new Set(values.models.map((model) => model.id.trim()).filter(Boolean));
113
- const discovered = response.models.filter((model) => !existingIds.has(model.id));
116
+ const discovered = response.filter((model) => !existingIds.has(model.id));
114
117
  form.setFieldValue('models', [
115
118
  ...values.models,
116
119
  ...discovered.map((model) => ({
@@ -200,9 +203,13 @@ export function CustomModelProviderForm({
200
203
  name="slug"
201
204
  validators={{
202
205
  onBlur: ({value}) =>
203
- existingConfig ? undefined : customModelProviderSlugError(value),
206
+ existingConfig
207
+ ? undefined
208
+ : customModelProviderSlugError(value, reservedProviderIds),
204
209
  onSubmit: ({value}) =>
205
- existingConfig ? undefined : customModelProviderSlugError(value),
210
+ existingConfig
211
+ ? undefined
212
+ : customModelProviderSlugError(value, reservedProviderIds),
206
213
  }}
207
214
  >
208
215
  {(field) => (
@@ -234,7 +241,7 @@ export function CustomModelProviderForm({
234
241
  <FormFieldSelect
235
242
  value={field.state.value}
236
243
  onChange={(event) =>
237
- field.handleChange(event.target.value as ModelProviderApi)
244
+ field.handleChange(event.target.value as ProviderApi)
238
245
  }
239
246
  onBlur={field.handleBlur}
240
247
  >
@@ -1,4 +1,4 @@
1
- import type {CustomModelProviderConfigDto} from '@shipfox/api-agent-dto';
1
+ import type {CustomProviderConfig} from '#core/models.js';
2
2
  import {
3
3
  buildCreateCustomModelProviderBody,
4
4
  buildDiscoverModelsBody,
@@ -61,21 +61,21 @@ describe('custom model provider payload mappers', () => {
61
61
 
62
62
  expect(body).toEqual({
63
63
  slug: 'local-vllm',
64
- display_name: 'Local vLLM',
64
+ displayName: 'Local vLLM',
65
65
  api: 'openai-completions',
66
- base_url: 'http://localhost:8000/v1',
67
- api_key: 'sk-local',
66
+ baseUrl: 'http://localhost:8000/v1',
67
+ apiKey: 'sk-local',
68
68
  headers: [{name: 'authorization', value: 'Bearer token', secret: true}],
69
69
  models: [
70
70
  {
71
71
  id: 'llama-3.1',
72
72
  label: 'Llama 3.1',
73
- context_window: 128000,
74
- max_output_tokens: 16384,
75
- input_image: true,
73
+ contextWindow: 128000,
74
+ maxOutputTokens: 16384,
75
+ inputImage: true,
76
76
  },
77
77
  ],
78
- default_model: 'llama-3.1',
78
+ defaultModel: 'llama-3.1',
79
79
  });
80
80
  });
81
81
 
@@ -131,11 +131,11 @@ describe('custom model provider payload mappers', () => {
131
131
 
132
132
  expect(body).toEqual({
133
133
  api: 'openai-responses',
134
- base_url: 'https://llm.example.test/v1',
135
- api_key: 'sk-new',
134
+ baseUrl: 'https://llm.example.test/v1',
135
+ apiKey: 'sk-new',
136
136
  headers: [
137
- {name: 'x-region', value: 'us'},
138
- {name: 'authorization', value: 'Bearer new'},
137
+ {name: 'x-region', value: 'us', secret: false},
138
+ {name: 'authorization', value: 'Bearer new', secret: false},
139
139
  ],
140
140
  });
141
141
  });
@@ -146,25 +146,25 @@ describe('custom model provider payload mappers', () => {
146
146
  });
147
147
  });
148
148
 
149
- function customConfig(): CustomModelProviderConfigDto {
149
+ function customConfig(): CustomProviderConfig {
150
150
  return {
151
151
  kind: 'custom',
152
- provider_id: 'local-vllm',
153
- display_name: 'Local vLLM',
152
+ providerId: 'local-vllm',
153
+ displayName: 'Local vLLM',
154
154
  api: 'openai-responses',
155
- base_url: 'https://llm.example.test/v1',
155
+ baseUrl: 'https://llm.example.test/v1',
156
156
  headers: [{name: 'x-region', value: 'us'}],
157
- secret_header_names: ['authorization'],
157
+ secretHeaderNames: ['authorization'],
158
158
  models: [
159
159
  {
160
160
  id: 'llama-3.1',
161
161
  label: 'Llama 3.1',
162
- context_window: 128000,
163
- max_output_tokens: 16384,
162
+ contextWindow: 128000,
163
+ maxOutputTokens: 16384,
164
164
  },
165
165
  ],
166
- default_model: 'llama-3.1',
167
- created_at: '2026-05-08T00:00:00.000Z',
168
- updated_at: '2026-05-08T00:00:00.000Z',
166
+ defaultModel: 'llama-3.1',
167
+ createdAt: '2026-05-08T00:00:00.000Z',
168
+ updatedAt: '2026-05-08T00:00:00.000Z',
169
169
  };
170
170
  }
@@ -1,15 +1,11 @@
1
1
  import type {
2
- CreateCustomModelProviderBodyDto,
3
- CustomAgentModelDto,
4
- CustomModelProviderConfigDto,
5
- DiscoverCustomModelProviderModelsBodyDto,
6
- DiscoverCustomModelProviderModelsBySlugBodyDto,
7
- ModelProviderApi,
8
- UpdateCustomModelProviderBodyDto,
9
- } from '@shipfox/api-agent-dto';
10
-
11
- type CreateHeaderRequest = NonNullable<CreateCustomModelProviderBodyDto['headers']>[number];
12
- type UpdateHeaderRequest = NonNullable<UpdateCustomModelProviderBodyDto['headers']>[number];
2
+ AgentModel,
3
+ CreateCustomProviderCommand,
4
+ CustomProviderConfig,
5
+ DiscoverProviderModelsCommand,
6
+ ProviderApi,
7
+ UpdateCustomProviderCommand,
8
+ } from '#core/models.js';
13
9
 
14
10
  export interface CustomModelProviderHeaderFormValue {
15
11
  client_id: string;
@@ -33,7 +29,7 @@ export interface CustomModelProviderModelFormValue {
33
29
  export interface CustomModelProviderFormValues {
34
30
  slug: string;
35
31
  display_name: string;
36
- api: ModelProviderApi;
32
+ api: ProviderApi;
37
33
  base_url: string;
38
34
  api_key: string;
39
35
  headers: CustomModelProviderHeaderFormValue[];
@@ -65,13 +61,13 @@ export function createCustomModelProviderFormValues(): CustomModelProviderFormVa
65
61
  }
66
62
 
67
63
  export function editCustomModelProviderFormValues(
68
- config: CustomModelProviderConfigDto,
64
+ config: CustomProviderConfig,
69
65
  ): CustomModelProviderFormValues {
70
66
  return {
71
- slug: config.provider_id,
72
- display_name: config.display_name,
67
+ slug: config.providerId,
68
+ display_name: config.displayName,
73
69
  api: config.api,
74
- base_url: config.base_url,
70
+ base_url: config.baseUrl,
75
71
  api_key: '',
76
72
  headers: [
77
73
  ...config.headers.map((header) => ({
@@ -82,7 +78,7 @@ export function editCustomModelProviderFormValues(
82
78
  hasStoredValue: false,
83
79
  storedName: header.name,
84
80
  })),
85
- ...config.secret_header_names.map((name) => ({
81
+ ...config.secretHeaderNames.map((name) => ({
86
82
  client_id: createFormRowId(),
87
83
  name,
88
84
  value: '',
@@ -92,45 +88,45 @@ export function editCustomModelProviderFormValues(
92
88
  })),
93
89
  ],
94
90
  models: config.models.map(modelFormValue),
95
- default_model: config.default_model ?? '',
91
+ default_model: config.defaultModel ?? '',
96
92
  };
97
93
  }
98
94
 
99
95
  export function buildCreateCustomModelProviderBody(
100
96
  values: CustomModelProviderFormValues,
101
- ): CreateCustomModelProviderBodyDto {
97
+ ): CreateCustomProviderCommand {
102
98
  return {
103
99
  slug: values.slug.trim(),
104
- display_name: values.display_name.trim(),
100
+ displayName: values.display_name.trim(),
105
101
  api: values.api,
106
- base_url: values.base_url.trim(),
107
- ...(values.api_key.trim() ? {api_key: values.api_key.trim()} : {}),
102
+ baseUrl: values.base_url.trim(),
103
+ ...(values.api_key.trim() ? {apiKey: values.api_key.trim()} : {}),
108
104
  ...(requestHeaders(values.headers).length > 0 ? {headers: requestHeaders(values.headers)} : {}),
109
105
  models: requestModels(values.models),
110
- ...(values.default_model.trim() ? {default_model: values.default_model.trim()} : {}),
106
+ ...(values.default_model.trim() ? {defaultModel: values.default_model.trim()} : {}),
111
107
  };
112
108
  }
113
109
 
114
110
  export function buildUpdateCustomModelProviderBody(
115
- config: CustomModelProviderConfigDto,
111
+ config: CustomProviderConfig,
116
112
  values: CustomModelProviderFormValues,
117
- ): UpdateCustomModelProviderBodyDto {
118
- const body: UpdateCustomModelProviderBodyDto = {};
119
- if (values.display_name.trim() !== config.display_name)
120
- body.display_name = values.display_name.trim();
113
+ ): UpdateCustomProviderCommand {
114
+ const body: UpdateCustomProviderCommand = {};
115
+ if (values.display_name.trim() !== config.displayName)
116
+ body.displayName = values.display_name.trim();
121
117
  if (values.api !== config.api) body.api = values.api;
122
- if (values.base_url.trim() !== config.base_url) body.base_url = values.base_url.trim();
123
- if (values.api_key.trim()) body.api_key = values.api_key.trim();
118
+ if (values.base_url.trim() !== config.baseUrl) body.baseUrl = values.base_url.trim();
119
+ if (values.api_key.trim()) body.apiKey = values.api_key.trim();
124
120
  if (customHeadersDirty(config, values.headers)) body.headers = updateHeaders(values.headers);
125
121
  if (customModelsDirty(config, values.models)) body.models = requestModels(values.models);
126
122
 
127
123
  const nextDefault = values.default_model.trim() || null;
128
- if (nextDefault !== config.default_model) body.default_model = nextDefault;
124
+ if (nextDefault !== config.defaultModel) body.defaultModel = nextDefault;
129
125
  return body;
130
126
  }
131
127
 
132
128
  export function customHeadersDirty(
133
- config: CustomModelProviderConfigDto,
129
+ config: CustomProviderConfig,
134
130
  headers: CustomModelProviderHeaderFormValue[],
135
131
  ): boolean {
136
132
  return (
@@ -141,7 +137,7 @@ export function customHeadersDirty(
141
137
  value: header.value,
142
138
  secret: false,
143
139
  })),
144
- ...config.secret_header_names.map((name) => ({
140
+ ...config.secretHeaderNames.map((name) => ({
145
141
  name: normalizeHeaderName(name),
146
142
  secret: true,
147
143
  keep: true,
@@ -151,24 +147,37 @@ export function customHeadersDirty(
151
147
  }
152
148
 
153
149
  export function customModelsDirty(
154
- config: CustomModelProviderConfigDto,
150
+ config: CustomProviderConfig,
155
151
  models: CustomModelProviderModelFormValue[],
156
152
  ): boolean {
157
- return JSON.stringify(requestModels(models)) !== JSON.stringify(config.models);
153
+ return (
154
+ JSON.stringify(requestModels(models)) !==
155
+ JSON.stringify(
156
+ config.models.map((model) => ({
157
+ id: model.id,
158
+ label: model.label,
159
+ ...(model.contextWindow === undefined ? {} : {contextWindow: model.contextWindow}),
160
+ ...(model.maxOutputTokens === undefined ? {} : {maxOutputTokens: model.maxOutputTokens}),
161
+ ...(model.inputImage ? {inputImage: true} : {}),
162
+ ...(model.reasoning ? {reasoning: true} : {}),
163
+ })),
164
+ )
165
+ );
158
166
  }
159
167
 
160
168
  export function buildDiscoverModelsBody(
161
169
  values: CustomModelProviderFormValues,
162
- ): DiscoverCustomModelProviderModelsBodyDto {
170
+ ): DiscoverProviderModelsCommand {
163
171
  return {
164
172
  api: values.api,
165
- base_url: values.base_url.trim(),
166
- ...(values.api_key.trim() ? {api_key: values.api_key.trim()} : {}),
173
+ baseUrl: values.base_url.trim(),
174
+ ...(values.api_key.trim() ? {apiKey: values.api_key.trim()} : {}),
167
175
  ...(requestHeaders(values.headers).length > 0
168
176
  ? {
169
177
  headers: requestHeaders(values.headers).map(({name, value}) => ({
170
178
  name,
171
179
  value,
180
+ secret: false,
172
181
  })),
173
182
  }
174
183
  : {}),
@@ -176,13 +185,13 @@ export function buildDiscoverModelsBody(
176
185
  }
177
186
 
178
187
  export function buildDiscoverModelsBySlugBody(
179
- config: CustomModelProviderConfigDto,
188
+ config: CustomProviderConfig,
180
189
  values: CustomModelProviderFormValues,
181
- ): DiscoverCustomModelProviderModelsBySlugBodyDto {
182
- const body: DiscoverCustomModelProviderModelsBySlugBodyDto = {};
190
+ ): DiscoverProviderModelsCommand {
191
+ const body: DiscoverProviderModelsCommand = {};
183
192
  if (values.api !== config.api) body.api = values.api;
184
- if (values.base_url.trim() !== config.base_url) body.base_url = values.base_url.trim();
185
- if (values.api_key.trim()) body.api_key = values.api_key.trim();
193
+ if (values.base_url.trim() !== config.baseUrl) body.baseUrl = values.base_url.trim();
194
+ if (values.api_key.trim()) body.apiKey = values.api_key.trim();
186
195
  body.headers = updateHeaders(values.headers);
187
196
  return body;
188
197
  }
@@ -200,8 +209,10 @@ export function createFormRowId(): string {
200
209
  return crypto.randomUUID();
201
210
  }
202
211
 
203
- function updateHeaders(headers: CustomModelProviderHeaderFormValue[]): UpdateHeaderRequest[] {
204
- const result: UpdateHeaderRequest[] = [];
212
+ function updateHeaders(
213
+ headers: CustomModelProviderHeaderFormValue[],
214
+ ): NonNullable<UpdateCustomProviderCommand['headers']> {
215
+ const result: NonNullable<UpdateCustomProviderCommand['headers']> = [];
205
216
  for (const header of headers) {
206
217
  const name = normalizeHeaderName(header.name);
207
218
  if (!name) continue;
@@ -219,7 +230,9 @@ function updateHeaders(headers: CustomModelProviderHeaderFormValue[]): UpdateHea
219
230
  return result;
220
231
  }
221
232
 
222
- function requestHeaders(headers: CustomModelProviderHeaderFormValue[]): CreateHeaderRequest[] {
233
+ function requestHeaders(
234
+ headers: CustomModelProviderHeaderFormValue[],
235
+ ): NonNullable<CreateCustomProviderCommand['headers']> {
223
236
  return updateHeaders(headers).flatMap((header) =>
224
237
  'value' in header && header.value
225
238
  ? [{name: header.name, value: header.value, secret: header.secret}]
@@ -227,7 +240,9 @@ function requestHeaders(headers: CustomModelProviderHeaderFormValue[]): CreateHe
227
240
  );
228
241
  }
229
242
 
230
- function requestModels(models: CustomModelProviderModelFormValue[]): CustomAgentModelDto[] {
243
+ function requestModels(
244
+ models: CustomModelProviderModelFormValue[],
245
+ ): CreateCustomProviderCommand['models'] {
231
246
  return models.flatMap((model) => {
232
247
  const id = model.id.trim();
233
248
  const label = model.label.trim();
@@ -236,25 +251,25 @@ function requestModels(models: CustomModelProviderModelFormValue[]): CustomAgent
236
251
  {
237
252
  id,
238
253
  label,
239
- ...(model.context_window.trim() ? {context_window: Number(model.context_window)} : {}),
254
+ ...(model.context_window.trim() ? {contextWindow: Number(model.context_window)} : {}),
240
255
  ...(model.max_output_tokens.trim()
241
- ? {max_output_tokens: Number(model.max_output_tokens)}
256
+ ? {maxOutputTokens: Number(model.max_output_tokens)}
242
257
  : {}),
243
- ...(model.input_image ? {input_image: true} : {}),
258
+ ...(model.input_image ? {inputImage: true} : {}),
244
259
  ...(model.reasoning ? {reasoning: true} : {}),
245
260
  },
246
261
  ];
247
262
  });
248
263
  }
249
264
 
250
- function modelFormValue(model: CustomAgentModelDto): CustomModelProviderModelFormValue {
265
+ function modelFormValue(model: AgentModel): CustomModelProviderModelFormValue {
251
266
  return {
252
267
  client_id: createFormRowId(),
253
268
  id: model.id,
254
269
  label: model.label,
255
- context_window: model.context_window?.toString() ?? '',
256
- max_output_tokens: model.max_output_tokens?.toString() ?? '',
257
- input_image: model.input_image ?? false,
270
+ context_window: model.contextWindow?.toString() ?? '',
271
+ max_output_tokens: model.maxOutputTokens?.toString() ?? '',
272
+ input_image: model.inputImage ?? false,
258
273
  reasoning: model.reasoning ?? false,
259
274
  };
260
275
  }
@@ -25,7 +25,7 @@ describe('customModelProviderSlugError', () => {
25
25
  });
26
26
 
27
27
  test('rejects reserved built-in provider ids', () => {
28
- const result = customModelProviderSlugError('anthropic');
28
+ const result = customModelProviderSlugError('anthropic', ['anthropic']);
29
29
 
30
30
  expect(result).toBe('This id is reserved for a built-in provider.');
31
31
  });