@shipfox/client-agent 5.0.0 → 6.0.2

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 (168) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +57 -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/state/model-provider-onboarding.d.ts.map +1 -1
  110. package/dist/state/model-provider-onboarding.js +9 -18
  111. package/dist/state/model-provider-onboarding.js.map +1 -1
  112. package/dist/tsconfig.test.tsbuildinfo +1 -1
  113. package/package.json +9 -10
  114. package/src/components/available-provider-card.tsx +2 -2
  115. package/src/components/available-providers-grid.tsx +3 -3
  116. package/src/components/change-default-model-form.tsx +5 -6
  117. package/src/components/custom-model-provider-api-options.ts +3 -3
  118. package/src/components/custom-model-provider-form.stories.tsx +13 -13
  119. package/src/components/custom-model-provider-form.tsx +19 -12
  120. package/src/components/custom-model-provider-payload.test.ts +22 -22
  121. package/src/components/custom-model-provider-payload.ts +69 -54
  122. package/src/components/custom-model-provider-slug.test.ts +1 -1
  123. package/src/components/custom-model-provider-slug.ts +6 -3
  124. package/src/components/default-model-field.tsx +5 -5
  125. package/src/components/harness-availability.test.ts +13 -10
  126. package/src/components/harness-availability.ts +4 -28
  127. package/src/components/harnesses-section.tsx +7 -11
  128. package/src/components/model-provider-usage-modal.stories.tsx +18 -23
  129. package/src/components/model-provider-usage-modal.test.tsx +35 -7
  130. package/src/components/model-provider-usage-modal.tsx +12 -16
  131. package/src/components/model-provider-usage-target.ts +15 -18
  132. package/src/components/model-providers-section.stories.tsx +4 -1
  133. package/src/components/model-providers-section.tsx +112 -125
  134. package/src/components/test-and-save-form.tsx +13 -17
  135. package/src/core/harness-policy.test.ts +15 -0
  136. package/src/core/harness-policy.ts +83 -0
  137. package/src/core/model-provider-management-reducer.test.ts +59 -0
  138. package/src/core/model-provider-management-reducer.ts +72 -0
  139. package/src/core/models.ts +143 -0
  140. package/src/core/onboarding-reducer.test.ts +44 -0
  141. package/src/core/onboarding-reducer.ts +64 -0
  142. package/src/core/provider-policy.ts +53 -0
  143. package/src/feature.ts +12 -10
  144. package/src/hooks/api/model-provider-command-mapper.ts +122 -0
  145. package/src/hooks/api/model-provider-mapper.test.ts +27 -0
  146. package/src/hooks/api/model-provider-mapper.ts +120 -0
  147. package/src/hooks/api/model-providers.test.ts +15 -15
  148. package/src/hooks/api/model-providers.ts +146 -81
  149. package/src/pages/model-provider-onboarding-page.tsx +62 -56
  150. package/src/routes/inputs.test.ts +11 -0
  151. package/src/routes/inputs.ts +9 -0
  152. package/src/routes/model-provider.tsx +4 -3
  153. package/src/state/model-provider-onboarding.test.ts +4 -4
  154. package/src/state/model-provider-onboarding.ts +13 -23
  155. package/test/fixtures/model-providers.ts +1 -1
  156. package/tsconfig.build.tsbuildinfo +1 -1
  157. package/vercel.json +5 -0
  158. package/dist/components/provider-search.d.ts +0 -4
  159. package/dist/components/provider-search.d.ts.map +0 -1
  160. package/dist/components/provider-search.js +0 -13
  161. package/dist/components/provider-search.js.map +0 -1
  162. package/dist/components/supported-model-provider-catalog-entry.d.ts +0 -8
  163. package/dist/components/supported-model-provider-catalog-entry.d.ts.map +0 -1
  164. package/dist/components/supported-model-provider-catalog-entry.js +0 -9
  165. package/dist/components/supported-model-provider-catalog-entry.js.map +0 -1
  166. package/src/components/provider-search.test.ts +0 -45
  167. package/src/components/provider-search.ts +0 -18
  168. package/src/components/supported-model-provider-catalog-entry.ts +0 -19
@@ -1,4 +1,4 @@
1
- import {isReservedModelProviderId, MODEL_PROVIDER_SLUG_PATTERN} from '@shipfox/api-agent-dto';
1
+ import {MODEL_PROVIDER_SLUG_PATTERN} from '@shipfox/api-agent-dto';
2
2
 
3
3
  const MAX_SLUG_LENGTH = 40;
4
4
  const SLUG_FORMAT_ERROR =
@@ -15,9 +15,12 @@ export function deriveCustomModelProviderSlug(displayName: string): string {
15
15
  .replace(/-+$/g, '');
16
16
  }
17
17
 
18
- export function customModelProviderSlugError(slug: string): string | undefined {
18
+ export function customModelProviderSlugError(
19
+ slug: string,
20
+ reservedProviderIds: readonly string[] = [],
21
+ ): string | undefined {
19
22
  const trimmed = slug.trim();
20
23
  if (!MODEL_PROVIDER_SLUG_PATTERN.test(trimmed)) return SLUG_FORMAT_ERROR;
21
- if (isReservedModelProviderId(trimmed)) return 'This id is reserved for a built-in provider.';
24
+ if (reservedProviderIds.includes(trimmed)) return 'This id is reserved for a built-in provider.';
22
25
  return undefined;
23
26
  }
@@ -1,7 +1,7 @@
1
- import type {ModelProviderCatalogEntryDto} from '@shipfox/api-agent-dto';
2
1
  import {FormField, useFormField} from '@shipfox/react-ui/form-field';
3
2
  import {cn} from '@shipfox/react-ui/utils';
4
3
  import type {ComponentProps, ReactNode} from 'react';
4
+ import type {SupportedProvider} from '#core/models.js';
5
5
 
6
6
  export const LATEST_MODEL_VALUE = '__latest__';
7
7
 
@@ -13,7 +13,7 @@ export function DefaultModelField({
13
13
  onChange,
14
14
  onBlur,
15
15
  }: {
16
- entry: ModelProviderCatalogEntryDto;
16
+ entry: SupportedProvider;
17
17
  value: string;
18
18
  error: string | undefined;
19
19
  children?: ReactNode;
@@ -52,13 +52,13 @@ export function selectedModelForModelPayload(selectedModel: string): string | nu
52
52
  return selectedModel === LATEST_MODEL_VALUE ? null : selectedModel.trim();
53
53
  }
54
54
 
55
- function defaultModelLabel(entry: ModelProviderCatalogEntryDto): string {
56
- const defaultModel = entry.default_model ?? entry.models[0]?.id ?? '';
55
+ function defaultModelLabel(entry: SupportedProvider): string {
56
+ const defaultModel = entry.defaultModel ?? entry.models[0]?.id ?? '';
57
57
  return entry.models.find((model) => model.id === defaultModel)?.label ?? defaultModel;
58
58
  }
59
59
 
60
60
  function defaultModelDescription(
61
- entry: ModelProviderCatalogEntryDto,
61
+ entry: SupportedProvider,
62
62
  selectedModel: string | undefined,
63
63
  ): string | undefined {
64
64
  if (selectedModel === LATEST_MODEL_VALUE) {
@@ -1,28 +1,31 @@
1
- import {getHarnessDescriptor} from '@shipfox/api-agent-dto';
1
+ import {getHarness} from '#core/harness-policy.js';
2
+ import {toProviderConfig} from '#hooks/api/model-provider-mapper.js';
2
3
  import {customModelProviderConfig, modelProviderConfig} from '#test/fixtures/model-providers.js';
3
4
  import {compatibleHarnessIds, isHarnessAvailable} from './harness-availability.js';
4
5
 
5
6
  describe('harness availability', () => {
6
7
  test('marks pi available with any builtin provider config', () => {
7
- const result = isHarnessAvailable(getHarnessDescriptor('pi'), [
8
- modelProviderConfig({provider_id: 'openai'}),
8
+ const result = isHarnessAvailable(getHarness('pi'), [
9
+ toProviderConfig(modelProviderConfig({provider_id: 'openai'})),
9
10
  ]);
10
11
 
11
12
  expect(result).toBe(true);
12
13
  });
13
14
 
14
15
  test('marks pi available with only a custom provider config', () => {
15
- const result = isHarnessAvailable(getHarnessDescriptor('pi'), [customModelProviderConfig()]);
16
+ const result = isHarnessAvailable(getHarness('pi'), [
17
+ toProviderConfig(customModelProviderConfig()),
18
+ ]);
16
19
 
17
20
  expect(result).toBe(true);
18
21
  });
19
22
 
20
23
  test('marks claude available only when Anthropic is configured', () => {
21
- const openai = isHarnessAvailable(getHarnessDescriptor('claude'), [
22
- modelProviderConfig({provider_id: 'openai'}),
24
+ const openai = isHarnessAvailable(getHarness('claude'), [
25
+ toProviderConfig(modelProviderConfig({provider_id: 'openai'})),
23
26
  ]);
24
- const anthropic = isHarnessAvailable(getHarnessDescriptor('claude'), [
25
- modelProviderConfig({provider_id: 'anthropic'}),
27
+ const anthropic = isHarnessAvailable(getHarness('claude'), [
28
+ toProviderConfig(modelProviderConfig({provider_id: 'anthropic'})),
26
29
  ]);
27
30
 
28
31
  expect(openai).toBe(false);
@@ -30,8 +33,8 @@ describe('harness availability', () => {
30
33
  });
31
34
 
32
35
  test('marks both harnesses unavailable when no providers are configured', () => {
33
- expect(isHarnessAvailable(getHarnessDescriptor('pi'), [])).toBe(false);
34
- expect(isHarnessAvailable(getHarnessDescriptor('claude'), [])).toBe(false);
36
+ expect(isHarnessAvailable(getHarness('pi'), [])).toBe(false);
37
+ expect(isHarnessAvailable(getHarness('claude'), [])).toBe(false);
35
38
  });
36
39
 
37
40
  test('returns compatible harnesses for builtin and custom providers', () => {
@@ -1,35 +1,11 @@
1
- import {
2
- DEFAULT_HARNESS,
3
- type Harness,
4
- type HarnessDescriptor,
5
- listHarnessDescriptors,
6
- type ModelProviderConfigResponseDto,
7
- } from '@shipfox/api-agent-dto';
8
-
9
- export function configSupportsHarness(
10
- config: ModelProviderConfigResponseDto,
11
- descriptor: HarnessDescriptor,
12
- ): boolean {
13
- if (config.kind === 'custom') return descriptor.id === DEFAULT_HARNESS;
14
- return descriptor.supportedProviderIds.includes(config.provider_id);
15
- }
1
+ import {compatibleHarnessIds, configSupportsHarness} from '#core/harness-policy.js';
2
+ import type {HarnessDescriptor, ProviderConfig} from '#core/models.js';
16
3
 
17
4
  export function isHarnessAvailable(
18
5
  descriptor: HarnessDescriptor,
19
- configs: ModelProviderConfigResponseDto[],
6
+ configs: readonly ProviderConfig[],
20
7
  ): boolean {
21
8
  return configs.some((config) => configSupportsHarness(config, descriptor));
22
9
  }
23
10
 
24
- export function compatibleHarnessIds({
25
- isCustom,
26
- providerId,
27
- }: {
28
- isCustom: boolean;
29
- providerId: string;
30
- }): Harness[] {
31
- if (isCustom) return [DEFAULT_HARNESS];
32
- return listHarnessDescriptors()
33
- .filter((descriptor) => descriptor.supportedProviderIds.includes(providerId))
34
- .map((descriptor) => descriptor.id);
35
- }
11
+ export {compatibleHarnessIds};
@@ -1,9 +1,3 @@
1
- import {
2
- DEFAULT_HARNESS,
3
- type Harness,
4
- type HarnessDescriptor,
5
- listHarnessDescriptors,
6
- } from '@shipfox/api-agent-dto';
7
1
  import {QueryLoadError} from '@shipfox/client-ui';
8
2
  import {IconButton} from '@shipfox/react-ui/button';
9
3
  import {Callout} from '@shipfox/react-ui/callout';
@@ -20,6 +14,8 @@ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip
20
14
  import {Header, Text} from '@shipfox/react-ui/typography';
21
15
  import {cn} from '@shipfox/react-ui/utils';
22
16
  import {useRef, useState} from 'react';
17
+ import {DEFAULT_HARNESS, listHarnesses} from '#core/harness-policy.js';
18
+ import type {HarnessDescriptor, HarnessId} from '#core/models.js';
23
19
  import {
24
20
  useModelProviderConfigsQuery,
25
21
  useSetDefaultHarnessMutation,
@@ -38,13 +34,13 @@ export function WorkspaceHarnessesSection({workspaceId}: {workspaceId: string})
38
34
  const activeDefaultRequestRef = useRef<{workspaceId: string; id: number} | null>(null);
39
35
  const [pendingDefaultHarness, setPendingDefaultHarness] = useState<{
40
36
  workspaceId: string;
41
- harnessId: Harness;
37
+ harnessId: HarnessId;
42
38
  } | null>(null);
43
39
  const [defaultError, setDefaultError] = useState<
44
- {workspaceId: string; harnessId: Harness; message: string} | undefined
40
+ {workspaceId: string; harnessId: HarnessId; message: string} | undefined
45
41
  >();
46
42
  const configs = configsQuery.data?.configs ?? [];
47
- const defaultHarnessId = configsQuery.data?.default_harness_id ?? DEFAULT_HARNESS;
43
+ const defaultHarnessId = configsQuery.data?.defaultHarnessId ?? DEFAULT_HARNESS;
48
44
  activeWorkspaceIdRef.current = workspaceId;
49
45
 
50
46
  function isActiveDefaultRequest(request: {workspaceId: string; id: number}) {
@@ -67,7 +63,7 @@ export function WorkspaceHarnessesSection({workspaceId}: {workspaceId: string})
67
63
  try {
68
64
  await setDefaultHarness.mutateAsync({
69
65
  workspaceId,
70
- body: {harness_id: harness.id},
66
+ harnessId: harness.id,
71
67
  });
72
68
  if (!isActiveDefaultRequest(request)) return;
73
69
  toast.success(`${harness.label} is now the default harness`);
@@ -106,7 +102,7 @@ export function WorkspaceHarnessesSection({workspaceId}: {workspaceId: string})
106
102
 
107
103
  {configsQuery.data !== undefined ? (
108
104
  <ul className={cn('divide-y divide-border-neutral-base', SURFACE_CLASS)}>
109
- {listHarnessDescriptors().map((harness) => (
105
+ {listHarnesses().map((harness) => (
110
106
  <HarnessRow
111
107
  key={harness.id}
112
108
  harness={harness}
@@ -1,9 +1,6 @@
1
- import type {
2
- CustomModelProviderConfigDto,
3
- ModelProviderCatalogEntryDto,
4
- } from '@shipfox/api-agent-dto';
5
1
  import type {Meta, StoryObj} from '@storybook/react';
6
2
  import {useState} from 'react';
3
+ import type {CustomProviderConfig, SupportedProvider} from '#core/models.js';
7
4
  import {ModelProviderUsageModal} from './model-provider-usage-modal.js';
8
5
  import {
9
6
  usageTargetFromCatalogEntry,
@@ -26,7 +23,7 @@ function ModelProviderUsageModalStory({variant}: ModelProviderUsageModalStoryPro
26
23
  <div className="min-h-screen bg-background-neutral-background p-24">
27
24
  <ModelProviderUsageModal
28
25
  target={target}
29
- initialModel={target.default_model}
26
+ initialModel={target.defaultModel}
30
27
  workspaceDefaultHarnessId="claude"
31
28
  open={open}
32
29
  onOpenChange={setOpen}
@@ -55,14 +52,13 @@ export const CustomProvider: Story = {
55
52
  args: {variant: 'custom'},
56
53
  };
57
54
 
58
- function anthropicEntry(): ModelProviderCatalogEntryDto {
55
+ function anthropicEntry(): SupportedProvider {
59
56
  return {
57
+ kind: 'supported',
60
58
  id: 'anthropic',
61
59
  label: 'Anthropic',
62
- support_status: 'supported',
63
- default_model: 'claude-opus-4-8',
64
- credential_fields: [{key: 'api_key', label: 'API key', secret: true}],
65
- unsupported_reason: null,
60
+ defaultModel: 'claude-opus-4-8',
61
+ credentialFields: [{key: 'api_key', label: 'API key', secret: true}],
66
62
  models: [
67
63
  {id: 'claude-opus-4-8', label: 'Claude Opus 4.8'},
68
64
  {id: 'claude-sonnet-4-8', label: 'Claude Sonnet 4.8'},
@@ -71,14 +67,13 @@ function anthropicEntry(): ModelProviderCatalogEntryDto {
71
67
  };
72
68
  }
73
69
 
74
- function longListEntry(): ModelProviderCatalogEntryDto {
70
+ function longListEntry(): SupportedProvider {
75
71
  return {
72
+ kind: 'supported',
76
73
  id: 'cloudflare-workers-ai',
77
74
  label: 'Cloudflare Workers AI',
78
- support_status: 'supported',
79
- default_model: '@cf/moonshotai/kimi-k2.7-code',
80
- credential_fields: [{key: 'api_token', label: 'API token', secret: true}],
81
- unsupported_reason: null,
75
+ defaultModel: '@cf/moonshotai/kimi-k2.7-code',
76
+ credentialFields: [{key: 'api_token', label: 'API token', secret: true}],
82
77
  models: Array.from({length: 56}, (_, index) => ({
83
78
  id:
84
79
  index === 0
@@ -89,18 +84,18 @@ function longListEntry(): ModelProviderCatalogEntryDto {
89
84
  };
90
85
  }
91
86
 
92
- function customProviderConfig(): CustomModelProviderConfigDto {
87
+ function customProviderConfig(): CustomProviderConfig {
93
88
  return {
94
89
  kind: 'custom',
95
- provider_id: 'local-vllm',
96
- display_name: 'Local vLLM',
90
+ providerId: 'local-vllm',
91
+ displayName: 'Local vLLM',
97
92
  api: 'openai-completions',
98
- base_url: 'http://localhost:8000/v1',
93
+ baseUrl: 'http://localhost:8000/v1',
99
94
  headers: [],
100
- secret_header_names: [],
95
+ secretHeaderNames: [],
101
96
  models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
102
- default_model: 'llama-3.1',
103
- created_at: '2026-05-08T00:00:00.000Z',
104
- updated_at: '2026-05-08T00:00:00.000Z',
97
+ defaultModel: 'llama-3.1',
98
+ createdAt: '2026-05-08T00:00:00.000Z',
99
+ updatedAt: '2026-05-08T00:00:00.000Z',
105
100
  };
106
101
  }
@@ -1,6 +1,6 @@
1
1
  import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import {customModelProviderConfig, modelProviderEntry} from '#test/fixtures/model-providers.js';
3
+ import type {CustomProviderConfig, SupportedProvider} from '#core/models.js';
4
4
  import {ModelProviderUsageModal} from './model-provider-usage-modal.js';
5
5
  import {
6
6
  usageTargetFromCatalogEntry,
@@ -12,8 +12,8 @@ const KIMI_MODEL_ROW_NAME = 'Copy Kimi K2.7 Code model id @cf/moonshotai/kimi-k2
12
12
 
13
13
  function renderUsageModal() {
14
14
  const onOpenChange = vi.fn();
15
- const entry = modelProviderEntry({
16
- default_model: 'claude-opus-4-8',
15
+ const entry = supportedProvider({
16
+ defaultModel: 'claude-opus-4-8',
17
17
  models: [
18
18
  {id: 'claude-opus-4-8', label: 'Claude Opus 4.8'},
19
19
  {id: '@cf/moonshotai/kimi-k2.7-code', label: 'Kimi K2.7 Code'},
@@ -79,7 +79,7 @@ describe('ModelProviderUsageModal', () => {
79
79
 
80
80
  test('uses the workspace default harness when it is compatible', async () => {
81
81
  const onOpenChange = vi.fn();
82
- const entry = modelProviderEntry();
82
+ const entry = supportedProvider();
83
83
 
84
84
  render(
85
85
  <ModelProviderUsageModal
@@ -100,8 +100,8 @@ describe('ModelProviderUsageModal', () => {
100
100
 
101
101
  test('clamps the selected harness when the target changes', async () => {
102
102
  const onOpenChange = vi.fn();
103
- const anthropic = modelProviderEntry();
104
- const openai = modelProviderEntry({
103
+ const anthropic = supportedProvider();
104
+ const openai = supportedProvider({
105
105
  id: 'openai',
106
106
  label: 'OpenAI',
107
107
  models: [{id: 'gpt-5.5-pro', label: 'GPT-5.5 Pro'}],
@@ -144,7 +144,7 @@ describe('ModelProviderUsageModal', () => {
144
144
 
145
145
  render(
146
146
  <ModelProviderUsageModal
147
- target={usageTargetFromCustomConfig(customModelProviderConfig())}
147
+ target={usageTargetFromCustomConfig(customProviderConfig())}
148
148
  initialModel="custom-model"
149
149
  workspaceDefaultHarnessId="claude"
150
150
  open
@@ -208,3 +208,31 @@ describe('ModelProviderUsageModal', () => {
208
208
  );
209
209
  });
210
210
  });
211
+
212
+ function supportedProvider(overrides: Partial<SupportedProvider> = {}): SupportedProvider {
213
+ return {
214
+ kind: 'supported',
215
+ id: 'anthropic',
216
+ label: 'Anthropic',
217
+ defaultModel: 'claude-opus-4-8',
218
+ credentialFields: [{key: 'api_key', label: 'API key', secret: true}],
219
+ models: [{id: 'claude-opus-4-8', label: 'Claude Opus 4.8'}],
220
+ ...overrides,
221
+ };
222
+ }
223
+
224
+ function customProviderConfig(): CustomProviderConfig {
225
+ return {
226
+ kind: 'custom',
227
+ providerId: 'openai-compatible',
228
+ displayName: 'OpenAI Compatible',
229
+ api: 'openai-completions',
230
+ baseUrl: 'https://llm.example.test/v1',
231
+ headers: [],
232
+ secretHeaderNames: [],
233
+ models: [{id: 'custom-model', label: 'Custom Model'}],
234
+ defaultModel: 'custom-model',
235
+ createdAt: '2026-05-08T00:00:00.000Z',
236
+ updatedAt: '2026-05-08T00:00:00.000Z',
237
+ };
238
+ }
@@ -1,9 +1,3 @@
1
- import {
2
- DEFAULT_HARNESS,
3
- getHarnessDescriptor,
4
- type Harness,
5
- listHarnessDescriptors,
6
- } from '@shipfox/api-agent-dto';
7
1
  import {Button} from '@shipfox/react-ui/button';
8
2
  import {
9
3
  CodeBlock,
@@ -28,6 +22,8 @@ import {
28
22
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
29
23
  import {Code, Text} from '@shipfox/react-ui/typography';
30
24
  import {useEffect, useMemo, useRef, useState} from 'react';
25
+ import {DEFAULT_HARNESS, getHarness, listHarnesses} from '#core/harness-policy.js';
26
+ import type {HarnessId} from '#core/models.js';
31
27
  import {buildAgentWorkflowExample} from './agent-workflow-example.js';
32
28
  import {compatibleHarnessIds} from './harness-availability.js';
33
29
  import type {ModelProviderUsageTarget} from './model-provider-usage-target.js';
@@ -49,14 +45,14 @@ export function ModelProviderUsageModal({
49
45
  open: boolean;
50
46
  onOpenChange: (open: boolean) => void;
51
47
  closeFocusTarget?: HTMLElement | null | undefined;
52
- workspaceDefaultHarnessId?: Harness | null | undefined;
48
+ workspaceDefaultHarnessId?: HarnessId | null | undefined;
53
49
  }) {
54
50
  const [selectedModel, setSelectedModel] = useState('');
55
- const [selectedHarness, setSelectedHarness] = useState<Harness>(DEFAULT_HARNESS);
51
+ const [selectedHarness, setSelectedHarness] = useState<HarnessId>(DEFAULT_HARNESS);
56
52
 
57
53
  useEffect(() => {
58
54
  if (!target) return;
59
- setSelectedModel(initialModel ?? target.default_model ?? target.models[0]?.id ?? '');
55
+ setSelectedModel(initialModel ?? target.defaultModel ?? target.models[0]?.id ?? '');
60
56
  }, [target, initialModel]);
61
57
 
62
58
  useEffect(() => {
@@ -72,14 +68,14 @@ export function ModelProviderUsageModal({
72
68
  () =>
73
69
  target
74
70
  ? compatibleHarnessIds({isCustom: target.isCustom, providerId: target.id})
75
- : ([] as Harness[]),
71
+ : ([] as HarnessId[]),
76
72
  [target],
77
73
  );
78
74
  const harnessOptions = useMemo(
79
75
  () =>
80
76
  compatibleIds.map((harnessId) => ({
81
77
  value: harnessId,
82
- label: getHarnessDescriptor(harnessId).label,
78
+ label: getHarness(harnessId).label,
83
79
  })),
84
80
  [compatibleIds],
85
81
  );
@@ -239,9 +235,9 @@ export function ModelProviderUsageModal({
239
235
  }
240
236
 
241
237
  function selectInitialHarness(
242
- compatibleIds: Harness[],
243
- workspaceDefaultHarnessId: Harness | null | undefined,
244
- ): Harness {
238
+ compatibleIds: HarnessId[],
239
+ workspaceDefaultHarnessId: HarnessId | null | undefined,
240
+ ): HarnessId {
245
241
  if (workspaceDefaultHarnessId && compatibleIds.includes(workspaceDefaultHarnessId)) {
246
242
  return workspaceDefaultHarnessId;
247
243
  }
@@ -249,8 +245,8 @@ function selectInitialHarness(
249
245
  return compatibleIds[0] ?? DEFAULT_HARNESS;
250
246
  }
251
247
 
252
- function isHarness(value: string): value is Harness {
253
- return listHarnessDescriptors().some((descriptor) => descriptor.id === value);
248
+ function isHarness(value: string): value is HarnessId {
249
+ return listHarnesses().some((descriptor) => descriptor.id === value);
254
250
  }
255
251
 
256
252
  function ModelProviderModelRow({label, id}: {label: string; id: string}) {
@@ -1,36 +1,33 @@
1
- import type {
2
- CustomModelProviderConfigDto,
3
- ModelProviderCatalogEntryDto,
4
- } from '@shipfox/api-agent-dto';
1
+ import type {CustomProviderConfig, SupportedProvider} from '#core/models.js';
5
2
 
6
3
  export interface ModelProviderUsageTarget {
7
4
  id: string;
8
5
  label: string;
9
6
  isCustom: boolean;
10
- models: Array<{id: string; label: string}>;
11
- default_model: string | null;
7
+ models: ReadonlyArray<{id: string; label: string}>;
8
+ defaultModel: string | null;
12
9
  }
13
10
 
14
- export function usageTargetFromCatalogEntry(
15
- entry: ModelProviderCatalogEntryDto,
16
- ): ModelProviderUsageTarget {
11
+ export function usageTargetFromCatalogEntry(entry: SupportedProvider): ModelProviderUsageTarget {
12
+ const provider = entry;
17
13
  return {
18
- id: entry.id,
19
- label: entry.label,
14
+ id: provider.id,
15
+ label: provider.label,
20
16
  isCustom: false,
21
- models: entry.models,
22
- default_model: entry.default_model,
17
+ models: provider.models,
18
+ defaultModel: provider.defaultModel,
23
19
  };
24
20
  }
25
21
 
26
22
  export function usageTargetFromCustomConfig(
27
- config: CustomModelProviderConfigDto,
23
+ config: CustomProviderConfig,
28
24
  ): ModelProviderUsageTarget {
25
+ const provider = config;
29
26
  return {
30
- id: config.provider_id,
31
- label: config.display_name,
27
+ id: provider.providerId,
28
+ label: provider.displayName,
32
29
  isCustom: true,
33
- models: config.models,
34
- default_model: config.default_model,
30
+ models: provider.models,
31
+ defaultModel: provider.defaultModel,
35
32
  };
36
33
  }
@@ -255,7 +255,7 @@ function catalogForScenario(scenario: Scenario): ModelProviderCatalogEntryDto[]
255
255
 
256
256
  function configsForScenario(scenario: Scenario) {
257
257
  if (scenario === 'empty-configured' || scenario === 'configs-error') {
258
- return {configs: [], default_provider_id: null};
258
+ return {configs: [], default_provider_id: null, default_harness_id: null};
259
259
  }
260
260
  if (scenario === 'all-configured') {
261
261
  return {
@@ -265,17 +265,20 @@ function configsForScenario(scenario: Scenario) {
265
265
  }),
266
266
  ),
267
267
  default_provider_id: 'anthropic',
268
+ default_harness_id: null,
268
269
  };
269
270
  }
270
271
  if (scenario === 'custom-configured') {
271
272
  return {
272
273
  configs: [providerConfig({provider_id: 'anthropic'}), customProviderConfig()],
273
274
  default_provider_id: 'local-vllm',
275
+ default_harness_id: null,
274
276
  };
275
277
  }
276
278
  return {
277
279
  configs: [providerConfig({provider_id: 'anthropic'})],
278
280
  default_provider_id: 'anthropic',
281
+ default_harness_id: null,
279
282
  };
280
283
  }
281
284