@shipfox/client-agent 0.2.0
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/.storybook/main.ts +1 -0
- package/.storybook/preview.css +4 -0
- package/.storybook/preview.tsx +56 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +93 -0
- package/LICENSE +21 -0
- package/dist/components/add-custom-provider-card.d.ts +4 -0
- package/dist/components/add-custom-provider-card.d.ts.map +1 -0
- package/dist/components/add-custom-provider-card.js +41 -0
- package/dist/components/add-custom-provider-card.js.map +1 -0
- package/dist/components/agent-workflow-example.d.ts +11 -0
- package/dist/components/agent-workflow-example.d.ts.map +1 -0
- package/dist/components/agent-workflow-example.js +38 -0
- package/dist/components/agent-workflow-example.js.map +1 -0
- package/dist/components/available-provider-card.d.ts +6 -0
- package/dist/components/available-provider-card.d.ts.map +1 -0
- package/dist/components/available-provider-card.js +41 -0
- package/dist/components/available-provider-card.js.map +1 -0
- package/dist/components/available-providers-grid.d.ts +10 -0
- package/dist/components/available-providers-grid.d.ts.map +1 -0
- package/dist/components/available-providers-grid.js +89 -0
- package/dist/components/available-providers-grid.js.map +1 -0
- package/dist/components/change-default-model-form.d.ts +9 -0
- package/dist/components/change-default-model-form.d.ts.map +1 -0
- package/dist/components/change-default-model-form.js +98 -0
- package/dist/components/change-default-model-form.js.map +1 -0
- package/dist/components/custom-model-provider-api-options.d.ts +7 -0
- package/dist/components/custom-model-provider-api-options.d.ts.map +1 -0
- package/dist/components/custom-model-provider-api-options.js +23 -0
- package/dist/components/custom-model-provider-api-options.js.map +1 -0
- package/dist/components/custom-model-provider-form.d.ts +8 -0
- package/dist/components/custom-model-provider-form.d.ts.map +1 -0
- package/dist/components/custom-model-provider-form.js +667 -0
- package/dist/components/custom-model-provider-form.js.map +1 -0
- package/dist/components/custom-model-provider-form.stories.d.ts +20 -0
- package/dist/components/custom-model-provider-form.stories.d.ts.map +1 -0
- package/dist/components/custom-model-provider-form.stories.js +104 -0
- package/dist/components/custom-model-provider-form.stories.js.map +1 -0
- package/dist/components/custom-model-provider-payload.d.ts +39 -0
- package/dist/components/custom-model-provider-payload.d.ts.map +1 -0
- package/dist/components/custom-model-provider-payload.js +203 -0
- package/dist/components/custom-model-provider-payload.js.map +1 -0
- package/dist/components/custom-model-provider-slug.d.ts +3 -0
- package/dist/components/custom-model-provider-slug.d.ts.map +1 -0
- package/dist/components/custom-model-provider-slug.js +14 -0
- package/dist/components/custom-model-provider-slug.js.map +1 -0
- package/dist/components/default-model-field.d.ts +16 -0
- package/dist/components/default-model-field.d.ts.map +1 -0
- package/dist/components/default-model-field.js +60 -0
- package/dist/components/default-model-field.js.map +1 -0
- package/dist/components/form-errors.d.ts +11 -0
- package/dist/components/form-errors.d.ts.map +1 -0
- package/dist/components/form-errors.js +101 -0
- package/dist/components/form-errors.js.map +1 -0
- package/dist/components/harness-availability.d.ts +8 -0
- package/dist/components/harness-availability.d.ts.map +1 -0
- package/dist/components/harness-availability.js +16 -0
- package/dist/components/harness-availability.js.map +1 -0
- package/dist/components/harnesses-section.d.ts +4 -0
- package/dist/components/harnesses-section.d.ts.map +1 -0
- package/dist/components/harnesses-section.js +249 -0
- package/dist/components/harnesses-section.js.map +1 -0
- package/dist/components/harnesses-section.stories.d.ts +25 -0
- package/dist/components/harnesses-section.stories.d.ts.map +1 -0
- package/dist/components/harnesses-section.stories.js +138 -0
- package/dist/components/harnesses-section.stories.js.map +1 -0
- package/dist/components/model-provider-usage-modal.d.ts +11 -0
- package/dist/components/model-provider-usage-modal.d.ts.map +1 -0
- package/dist/components/model-provider-usage-modal.js +349 -0
- package/dist/components/model-provider-usage-modal.js.map +1 -0
- package/dist/components/model-provider-usage-modal.stories.d.ts +21 -0
- package/dist/components/model-provider-usage-modal.stories.d.ts.map +1 -0
- package/dist/components/model-provider-usage-modal.stories.js +115 -0
- package/dist/components/model-provider-usage-modal.stories.js.map +1 -0
- package/dist/components/model-provider-usage-target.d.ts +14 -0
- package/dist/components/model-provider-usage-target.d.ts.map +1 -0
- package/dist/components/model-provider-usage-target.js +20 -0
- package/dist/components/model-provider-usage-target.js.map +1 -0
- package/dist/components/model-providers-section.d.ts +4 -0
- package/dist/components/model-providers-section.d.ts.map +1 -0
- package/dist/components/model-providers-section.js +668 -0
- package/dist/components/model-providers-section.js.map +1 -0
- package/dist/components/model-providers-section.stories.d.ts +32 -0
- package/dist/components/model-providers-section.stories.d.ts.map +1 -0
- package/dist/components/model-providers-section.stories.js +442 -0
- package/dist/components/model-providers-section.stories.js.map +1 -0
- package/dist/components/provider-search.d.ts +4 -0
- package/dist/components/provider-search.d.ts.map +1 -0
- package/dist/components/provider-search.js +13 -0
- package/dist/components/provider-search.js.map +1 -0
- package/dist/components/supported-model-provider-catalog-entry.d.ts +8 -0
- package/dist/components/supported-model-provider-catalog-entry.d.ts.map +1 -0
- package/dist/components/supported-model-provider-catalog-entry.js +9 -0
- package/dist/components/supported-model-provider-catalog-entry.js.map +1 -0
- package/dist/components/test-and-save-form.d.ts +10 -0
- package/dist/components/test-and-save-form.d.ts.map +1 -0
- package/dist/components/test-and-save-form.js +138 -0
- package/dist/components/test-and-save-form.js.map +1 -0
- package/dist/feature.d.ts +9 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/feature.js +13 -0
- package/dist/feature.js.map +1 -0
- package/dist/hooks/api/model-providers.d.ts +334 -0
- package/dist/hooks/api/model-providers.d.ts.map +1 -0
- package/dist/hooks/api/model-providers.js +190 -0
- package/dist/hooks/api/model-providers.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/model-provider-onboarding-page.d.ts +6 -0
- package/dist/pages/model-provider-onboarding-page.d.ts.map +1 -0
- package/dist/pages/model-provider-onboarding-page.js +295 -0
- package/dist/pages/model-provider-onboarding-page.js.map +1 -0
- package/dist/pages/model-provider-onboarding-page.stories.d.ts +27 -0
- package/dist/pages/model-provider-onboarding-page.stories.d.ts.map +1 -0
- package/dist/pages/model-provider-onboarding-page.stories.js +368 -0
- package/dist/pages/model-provider-onboarding-page.stories.js.map +1 -0
- package/dist/routes/model-provider.d.ts +5 -0
- package/dist/routes/model-provider.d.ts.map +1 -0
- package/dist/routes/model-provider.js +28 -0
- package/dist/routes/model-provider.js.map +1 -0
- package/dist/state/model-provider-onboarding.d.ts +3 -0
- package/dist/state/model-provider-onboarding.d.ts.map +1 -0
- package/dist/state/model-provider-onboarding.js +28 -0
- package/dist/state/model-provider-onboarding.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +114 -0
- package/src/components/add-custom-provider-card.tsx +32 -0
- package/src/components/agent-workflow-example.test.ts +74 -0
- package/src/components/agent-workflow-example.ts +55 -0
- package/src/components/available-provider-card.tsx +39 -0
- package/src/components/available-providers-grid.test.tsx +55 -0
- package/src/components/available-providers-grid.tsx +106 -0
- package/src/components/change-default-model-form.tsx +104 -0
- package/src/components/custom-model-provider-api-options.test.ts +18 -0
- package/src/components/custom-model-provider-api-options.ts +12 -0
- package/src/components/custom-model-provider-form.stories.tsx +91 -0
- package/src/components/custom-model-provider-form.tsx +710 -0
- package/src/components/custom-model-provider-payload.test.ts +170 -0
- package/src/components/custom-model-provider-payload.ts +264 -0
- package/src/components/custom-model-provider-slug.test.ts +40 -0
- package/src/components/custom-model-provider-slug.ts +23 -0
- package/src/components/default-model-field.tsx +85 -0
- package/src/components/form-errors.test.ts +159 -0
- package/src/components/form-errors.ts +102 -0
- package/src/components/harness-availability.test.ts +45 -0
- package/src/components/harness-availability.ts +35 -0
- package/src/components/harnesses-section.stories.tsx +129 -0
- package/src/components/harnesses-section.test.tsx +188 -0
- package/src/components/harnesses-section.tsx +245 -0
- package/src/components/model-provider-usage-modal.stories.tsx +106 -0
- package/src/components/model-provider-usage-modal.test.tsx +210 -0
- package/src/components/model-provider-usage-modal.tsx +326 -0
- package/src/components/model-provider-usage-target.ts +36 -0
- package/src/components/model-providers-section.stories.tsx +345 -0
- package/src/components/model-providers-section.test.tsx +831 -0
- package/src/components/model-providers-section.tsx +645 -0
- package/src/components/provider-search.test.ts +45 -0
- package/src/components/provider-search.ts +18 -0
- package/src/components/supported-model-provider-catalog-entry.ts +19 -0
- package/src/components/test-and-save-form.tsx +166 -0
- package/src/feature.ts +12 -0
- package/src/hooks/api/model-providers.test.ts +167 -0
- package/src/hooks/api/model-providers.ts +281 -0
- package/src/index.ts +8 -0
- package/src/pages/model-provider-onboarding-page.stories.tsx +284 -0
- package/src/pages/model-provider-onboarding-page.test.tsx +368 -0
- package/src/pages/model-provider-onboarding-page.tsx +290 -0
- package/src/routes/model-provider.tsx +20 -0
- package/src/state/model-provider-onboarding.test.ts +55 -0
- package/src/state/model-provider-onboarding.ts +34 -0
- package/test/fixtures/model-providers.ts +111 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vercel.json +8 -0
- package/vitest.config.ts +72 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {ModelProviderCatalogEntryDto} from '@shipfox/api-agent-dto';
|
|
2
|
+
|
|
3
|
+
export function providerMatchesSearch(entry: ModelProviderCatalogEntryDto, query: string): boolean {
|
|
4
|
+
const needle = query.trim().toLowerCase();
|
|
5
|
+
if (needle === '') return true;
|
|
6
|
+
|
|
7
|
+
const haystack = `${entry.label} ${entry.id} ${entry.models
|
|
8
|
+
.map((model) => `${model.id} ${model.label}`)
|
|
9
|
+
.join(' ')}`.toLowerCase();
|
|
10
|
+
return haystack.includes(needle);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function customProviderCardMatchesSearch(query: string): boolean {
|
|
14
|
+
const needle = query.trim().toLowerCase();
|
|
15
|
+
if (needle === '') return true;
|
|
16
|
+
|
|
17
|
+
return 'custom provider openai anthropic google endpoint https public'.includes(needle);
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {ModelProviderCatalogEntryDto, SupportedModelProviderId} from '@shipfox/api-agent-dto';
|
|
2
|
+
|
|
3
|
+
export type SupportedModelProviderCatalogEntry = ModelProviderCatalogEntryDto & {
|
|
4
|
+
id: SupportedModelProviderId;
|
|
5
|
+
support_status: 'supported';
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function isSupportedCatalogEntry(
|
|
9
|
+
entry: ModelProviderCatalogEntryDto | undefined,
|
|
10
|
+
): entry is SupportedModelProviderCatalogEntry {
|
|
11
|
+
return entry?.support_status === 'supported';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toSupportedCatalogEntry(
|
|
15
|
+
entry: ModelProviderCatalogEntryDto | undefined,
|
|
16
|
+
): SupportedModelProviderCatalogEntry | undefined {
|
|
17
|
+
if (!isSupportedCatalogEntry(entry)) return undefined;
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModelProviderCatalogEntryDto,
|
|
3
|
+
ModelProviderConfigDto,
|
|
4
|
+
SupportedModelProviderId,
|
|
5
|
+
} from '@shipfox/api-agent-dto';
|
|
6
|
+
import {Button} from '@shipfox/react-ui/button';
|
|
7
|
+
import {Callout} from '@shipfox/react-ui/callout';
|
|
8
|
+
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
9
|
+
import {ModalBody, ModalFooter} from '@shipfox/react-ui/modal';
|
|
10
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
11
|
+
import {useForm} from '@tanstack/react-form';
|
|
12
|
+
import {useState} from 'react';
|
|
13
|
+
import {useUpsertModelProviderConfigMutation} from '#hooks/api/model-providers.js';
|
|
14
|
+
import {
|
|
15
|
+
DefaultModelField,
|
|
16
|
+
defaultModelFormValue,
|
|
17
|
+
LATEST_MODEL_VALUE,
|
|
18
|
+
selectedModelForCredentialsPayload,
|
|
19
|
+
} from './default-model-field.js';
|
|
20
|
+
import {modelProviderConfigErrorToFormError} from './form-errors.js';
|
|
21
|
+
|
|
22
|
+
export const MODEL_PROVIDER_TEST_AND_SAVE_FORM_ID = 'model-provider-test-and-save-form';
|
|
23
|
+
|
|
24
|
+
export function ModelProviderTestAndSaveForm({
|
|
25
|
+
workspaceId,
|
|
26
|
+
entry,
|
|
27
|
+
existingConfig,
|
|
28
|
+
onSaved,
|
|
29
|
+
setAsDefaultOnSave = false,
|
|
30
|
+
}: {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
entry: ModelProviderCatalogEntryDto;
|
|
33
|
+
existingConfig?: ModelProviderConfigDto | undefined;
|
|
34
|
+
onSaved: (savedDefaultModel: string | null) => void;
|
|
35
|
+
setAsDefaultOnSave?: boolean | undefined;
|
|
36
|
+
}) {
|
|
37
|
+
const upsertConfig = useUpsertModelProviderConfigMutation();
|
|
38
|
+
const [formError, setFormError] = useState<string | undefined>();
|
|
39
|
+
const form = useForm({
|
|
40
|
+
defaultValues: defaultFormValues(entry, existingConfig),
|
|
41
|
+
onSubmit: async ({value}) => {
|
|
42
|
+
setFormError(undefined);
|
|
43
|
+
const credentials = Object.fromEntries(
|
|
44
|
+
entry.credential_fields.map((credentialField) => [
|
|
45
|
+
credentialField.key,
|
|
46
|
+
value[credentialField.key]?.trim() ?? '',
|
|
47
|
+
]),
|
|
48
|
+
);
|
|
49
|
+
const selectedModel = value.default_model ?? LATEST_MODEL_VALUE;
|
|
50
|
+
const defaultModel =
|
|
51
|
+
existingConfig === undefined
|
|
52
|
+
? selectedModelForCredentialsPayload(selectedModel)
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await upsertConfig.mutateAsync({
|
|
57
|
+
workspaceId,
|
|
58
|
+
providerId: entry.id as SupportedModelProviderId,
|
|
59
|
+
body: {
|
|
60
|
+
...(defaultModel !== undefined ? {default_model: defaultModel} : {}),
|
|
61
|
+
credentials,
|
|
62
|
+
...(setAsDefaultOnSave ? {set_as_default: true} : {}),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
onSaved(selectedModelForCredentialsPayload(selectedModel));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const mapped = modelProviderConfigErrorToFormError(error);
|
|
68
|
+
setFormError(mapped.message);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<ModalBody className="gap-16">
|
|
76
|
+
<form
|
|
77
|
+
id={MODEL_PROVIDER_TEST_AND_SAVE_FORM_ID}
|
|
78
|
+
className="flex w-full flex-col gap-14"
|
|
79
|
+
noValidate
|
|
80
|
+
onSubmit={(event) => {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
event.stopPropagation();
|
|
83
|
+
void form.handleSubmit();
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{existingConfig === undefined ? (
|
|
87
|
+
<form.Field
|
|
88
|
+
name="default_model"
|
|
89
|
+
validators={{
|
|
90
|
+
onBlur: ({value}) => (value.trim() ? undefined : 'Default model is required.'),
|
|
91
|
+
onSubmit: ({value}) => (value.trim() ? undefined : 'Default model is required.'),
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{(field) => (
|
|
95
|
+
<DefaultModelField
|
|
96
|
+
entry={entry}
|
|
97
|
+
value={field.state.value}
|
|
98
|
+
error={fieldError(field)}
|
|
99
|
+
onChange={(event) => field.handleChange(event.target.value)}
|
|
100
|
+
onBlur={field.handleBlur}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
</form.Field>
|
|
104
|
+
) : null}
|
|
105
|
+
{entry.credential_fields.map((credentialField) => (
|
|
106
|
+
<form.Field
|
|
107
|
+
key={credentialField.key}
|
|
108
|
+
name={credentialField.key}
|
|
109
|
+
validators={{
|
|
110
|
+
onBlur: ({value}) =>
|
|
111
|
+
value.trim() ? undefined : `${credentialField.label} is required.`,
|
|
112
|
+
onSubmit: ({value}) =>
|
|
113
|
+
value.trim() ? undefined : `${credentialField.label} is required.`,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{(field) => (
|
|
117
|
+
<FormField
|
|
118
|
+
label={credentialField.label}
|
|
119
|
+
id={`model-provider-${entry.id}-${credentialField.key}`}
|
|
120
|
+
error={fieldError(field)}
|
|
121
|
+
>
|
|
122
|
+
<FormFieldInput
|
|
123
|
+
type={credentialField.secret ? 'password' : 'text'}
|
|
124
|
+
autoComplete="off"
|
|
125
|
+
value={field.state.value}
|
|
126
|
+
onChange={(event) => field.handleChange(event.target.value)}
|
|
127
|
+
onBlur={field.handleBlur}
|
|
128
|
+
/>
|
|
129
|
+
</FormField>
|
|
130
|
+
)}
|
|
131
|
+
</form.Field>
|
|
132
|
+
))}
|
|
133
|
+
</form>
|
|
134
|
+
{formError ? (
|
|
135
|
+
<Callout role="alert" type="error">
|
|
136
|
+
<div className="flex flex-col gap-8">
|
|
137
|
+
<Text size="sm" bold>
|
|
138
|
+
Could not save provider
|
|
139
|
+
</Text>
|
|
140
|
+
<Text size="sm">{formError}</Text>
|
|
141
|
+
</div>
|
|
142
|
+
</Callout>
|
|
143
|
+
) : null}
|
|
144
|
+
</ModalBody>
|
|
145
|
+
<ModalFooter>
|
|
146
|
+
<Button
|
|
147
|
+
type="submit"
|
|
148
|
+
form={MODEL_PROVIDER_TEST_AND_SAVE_FORM_ID}
|
|
149
|
+
isLoading={upsertConfig.isPending}
|
|
150
|
+
>
|
|
151
|
+
Test & save
|
|
152
|
+
</Button>
|
|
153
|
+
</ModalFooter>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function defaultFormValues(
|
|
159
|
+
entry: ModelProviderCatalogEntryDto,
|
|
160
|
+
existingConfig: ModelProviderConfigDto | undefined,
|
|
161
|
+
): Record<string, string> {
|
|
162
|
+
return {
|
|
163
|
+
default_model: defaultModelFormValue(existingConfig?.default_model),
|
|
164
|
+
...Object.fromEntries(entry.credential_fields.map((field) => [field.key, ''])),
|
|
165
|
+
};
|
|
166
|
+
}
|
package/src/feature.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {defineClientFeature} from '@shipfox/client-shell';
|
|
2
|
+
|
|
3
|
+
export const agentFeature = defineClientFeature({
|
|
4
|
+
id: 'shipfox.agent',
|
|
5
|
+
routes: [
|
|
6
|
+
{
|
|
7
|
+
path: '/workspaces/$wid/model-provider',
|
|
8
|
+
parent: 'workspaceLayout',
|
|
9
|
+
impl: '@shipfox/client-agent/routes/model-provider',
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
+
import {
|
|
3
|
+
AGENT_TEST_WORKSPACE_ID,
|
|
4
|
+
modelProviderCatalogResponse,
|
|
5
|
+
modelProviderConfig,
|
|
6
|
+
modelProviderConfigsResponse,
|
|
7
|
+
} from '#test/fixtures/model-providers.js';
|
|
8
|
+
import {
|
|
9
|
+
deleteModelProviderConfig,
|
|
10
|
+
getModelProviderCatalog,
|
|
11
|
+
listModelProviderConfigs,
|
|
12
|
+
setDefaultHarness,
|
|
13
|
+
setDefaultModelProvider,
|
|
14
|
+
updateModelProviderDefaultModel,
|
|
15
|
+
upsertModelProviderConfig,
|
|
16
|
+
} from './model-providers.js';
|
|
17
|
+
|
|
18
|
+
function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
|
|
19
|
+
return new Response(JSON.stringify(body), {
|
|
20
|
+
headers: {'content-type': 'application/json'},
|
|
21
|
+
status: 200,
|
|
22
|
+
...init,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('model provider transport', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('fetches the model provider catalog', async () => {
|
|
32
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(modelProviderCatalogResponse()));
|
|
33
|
+
configureApiClient({fetchImpl});
|
|
34
|
+
|
|
35
|
+
const result = await getModelProviderCatalog();
|
|
36
|
+
|
|
37
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
38
|
+
expect(result.providers[0]?.label).toBe('Anthropic');
|
|
39
|
+
expect(request.url).toBe('https://api.example.test/agent/model-provider-catalog');
|
|
40
|
+
expect(request.method).toBe('GET');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('fetches workspace model provider configs', async () => {
|
|
44
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(modelProviderConfigsResponse()));
|
|
45
|
+
configureApiClient({fetchImpl});
|
|
46
|
+
|
|
47
|
+
const result = await listModelProviderConfigs({workspaceId: AGENT_TEST_WORKSPACE_ID});
|
|
48
|
+
|
|
49
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
50
|
+
expect(result.default_provider_id).toBe('anthropic');
|
|
51
|
+
expect(request.url).toBe(
|
|
52
|
+
`https://api.example.test/workspaces/${AGENT_TEST_WORKSPACE_ID}/agent/model-providers`,
|
|
53
|
+
);
|
|
54
|
+
expect(request.method).toBe('GET');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('puts model provider credentials', async () => {
|
|
58
|
+
let requestBody: unknown;
|
|
59
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
60
|
+
requestBody = await (input as Request).clone().json();
|
|
61
|
+
return jsonResponse(modelProviderConfig(), {status: 201});
|
|
62
|
+
});
|
|
63
|
+
configureApiClient({fetchImpl});
|
|
64
|
+
const body = {default_model: 'claude-haiku-4-5', credentials: {api_key: 'sk-ant-secret'}};
|
|
65
|
+
|
|
66
|
+
const result = await upsertModelProviderConfig({
|
|
67
|
+
workspaceId: AGENT_TEST_WORKSPACE_ID,
|
|
68
|
+
providerId: 'anthropic',
|
|
69
|
+
body,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
73
|
+
expect(result.provider_id).toBe('anthropic');
|
|
74
|
+
expect(request.url).toBe(
|
|
75
|
+
`https://api.example.test/workspaces/${AGENT_TEST_WORKSPACE_ID}/agent/model-providers/anthropic`,
|
|
76
|
+
);
|
|
77
|
+
expect(request.method).toBe('PUT');
|
|
78
|
+
expect(requestBody).toEqual(body);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('deletes model provider credentials', async () => {
|
|
82
|
+
const fetchImpl = vi.fn().mockResolvedValue(new Response(null, {status: 204}));
|
|
83
|
+
configureApiClient({fetchImpl});
|
|
84
|
+
|
|
85
|
+
const result = await deleteModelProviderConfig({
|
|
86
|
+
workspaceId: AGENT_TEST_WORKSPACE_ID,
|
|
87
|
+
providerId: 'anthropic',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
91
|
+
expect(result).toBeUndefined();
|
|
92
|
+
expect(request.url).toBe(
|
|
93
|
+
`https://api.example.test/workspaces/${AGENT_TEST_WORKSPACE_ID}/agent/model-providers/anthropic`,
|
|
94
|
+
);
|
|
95
|
+
expect(request.method).toBe('DELETE');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('puts a model provider default model without credentials', async () => {
|
|
99
|
+
let requestBody: unknown;
|
|
100
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
101
|
+
requestBody = await (input as Request).clone().json();
|
|
102
|
+
return jsonResponse(modelProviderConfig({default_model: null}));
|
|
103
|
+
});
|
|
104
|
+
configureApiClient({fetchImpl});
|
|
105
|
+
const body = {default_model: null};
|
|
106
|
+
|
|
107
|
+
const result = await updateModelProviderDefaultModel({
|
|
108
|
+
workspaceId: AGENT_TEST_WORKSPACE_ID,
|
|
109
|
+
providerId: 'anthropic',
|
|
110
|
+
body,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
114
|
+
expect(result.default_model).toBeNull();
|
|
115
|
+
expect(request.url).toBe(
|
|
116
|
+
`https://api.example.test/workspaces/${AGENT_TEST_WORKSPACE_ID}/agent/model-providers/anthropic/default-model`,
|
|
117
|
+
);
|
|
118
|
+
expect(request.method).toBe('PUT');
|
|
119
|
+
expect(requestBody).toEqual(body);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('sets the default model provider', async () => {
|
|
123
|
+
let requestBody: unknown;
|
|
124
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
125
|
+
requestBody = await (input as Request).clone().json();
|
|
126
|
+
return jsonResponse({default_provider_id: 'anthropic'});
|
|
127
|
+
});
|
|
128
|
+
configureApiClient({fetchImpl});
|
|
129
|
+
const body = {provider_id: 'anthropic'} as const;
|
|
130
|
+
|
|
131
|
+
const result = await setDefaultModelProvider({
|
|
132
|
+
workspaceId: AGENT_TEST_WORKSPACE_ID,
|
|
133
|
+
body,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
137
|
+
expect(result.default_provider_id).toBe('anthropic');
|
|
138
|
+
expect(request.url).toBe(
|
|
139
|
+
`https://api.example.test/workspaces/${AGENT_TEST_WORKSPACE_ID}/agent/default-model-provider`,
|
|
140
|
+
);
|
|
141
|
+
expect(request.method).toBe('PUT');
|
|
142
|
+
expect(requestBody).toEqual(body);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('sets the default harness', async () => {
|
|
146
|
+
let requestBody: unknown;
|
|
147
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
148
|
+
requestBody = await (input as Request).clone().json();
|
|
149
|
+
return jsonResponse({default_harness_id: 'claude'});
|
|
150
|
+
});
|
|
151
|
+
configureApiClient({fetchImpl});
|
|
152
|
+
const body = {harness_id: 'claude'} as const;
|
|
153
|
+
|
|
154
|
+
const result = await setDefaultHarness({
|
|
155
|
+
workspaceId: AGENT_TEST_WORKSPACE_ID,
|
|
156
|
+
body,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
160
|
+
expect(result.default_harness_id).toBe('claude');
|
|
161
|
+
expect(request.url).toBe(
|
|
162
|
+
`https://api.example.test/workspaces/${AGENT_TEST_WORKSPACE_ID}/agent/default-harness`,
|
|
163
|
+
);
|
|
164
|
+
expect(request.method).toBe('PUT');
|
|
165
|
+
expect(requestBody).toEqual(body);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateCustomModelProviderBodyDto,
|
|
3
|
+
CustomModelProviderConfigDto,
|
|
4
|
+
DiscoverCustomModelProviderModelsBodyDto,
|
|
5
|
+
DiscoverCustomModelProviderModelsBySlugBodyDto,
|
|
6
|
+
DiscoverCustomModelProviderModelsResponseDto,
|
|
7
|
+
ListModelProviderConfigsResponseDto,
|
|
8
|
+
ModelProviderCatalogResponseDto,
|
|
9
|
+
ModelProviderConfigDto,
|
|
10
|
+
ModelProviderRef,
|
|
11
|
+
SetDefaultHarnessBodyDto,
|
|
12
|
+
SetDefaultHarnessResponseDto,
|
|
13
|
+
SetDefaultModelProviderBodyDto,
|
|
14
|
+
SetDefaultModelProviderResponseDto,
|
|
15
|
+
SupportedModelProviderId,
|
|
16
|
+
UpdateCustomModelProviderBodyDto,
|
|
17
|
+
UpdateModelProviderConfigBodyDto,
|
|
18
|
+
UpdateModelProviderDefaultModelBodyDto,
|
|
19
|
+
} from '@shipfox/api-agent-dto';
|
|
20
|
+
import {apiRequest} from '@shipfox/client-api';
|
|
21
|
+
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
|
22
|
+
|
|
23
|
+
export const modelProviderQueryKeys = {
|
|
24
|
+
all: ['model-providers'] as const,
|
|
25
|
+
catalog: () => [...modelProviderQueryKeys.all, 'catalog'] as const,
|
|
26
|
+
configs: (workspaceId: string) =>
|
|
27
|
+
[...modelProviderQueryKeys.all, 'configs', workspaceId] as const,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function getModelProviderCatalog({signal}: {signal?: AbortSignal} = {}) {
|
|
31
|
+
return await apiRequest<ModelProviderCatalogResponseDto>('/agent/model-provider-catalog', {
|
|
32
|
+
signal,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function listModelProviderConfigs({
|
|
37
|
+
workspaceId,
|
|
38
|
+
signal,
|
|
39
|
+
}: {
|
|
40
|
+
workspaceId: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}) {
|
|
43
|
+
return await apiRequest<ListModelProviderConfigsResponseDto>(
|
|
44
|
+
`/workspaces/${workspaceId}/agent/model-providers`,
|
|
45
|
+
{signal},
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function upsertModelProviderConfig({
|
|
50
|
+
workspaceId,
|
|
51
|
+
providerId,
|
|
52
|
+
body,
|
|
53
|
+
}: {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
providerId: SupportedModelProviderId;
|
|
56
|
+
body: UpdateModelProviderConfigBodyDto;
|
|
57
|
+
}) {
|
|
58
|
+
return await apiRequest<ModelProviderConfigDto>(
|
|
59
|
+
`/workspaces/${workspaceId}/agent/model-providers/${providerId}`,
|
|
60
|
+
{method: 'PUT', body},
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function createCustomModelProviderConfig({
|
|
65
|
+
workspaceId,
|
|
66
|
+
body,
|
|
67
|
+
}: {
|
|
68
|
+
workspaceId: string;
|
|
69
|
+
body: CreateCustomModelProviderBodyDto;
|
|
70
|
+
}) {
|
|
71
|
+
return await apiRequest<CustomModelProviderConfigDto>(
|
|
72
|
+
`/workspaces/${workspaceId}/agent/custom-model-providers`,
|
|
73
|
+
{method: 'POST', body},
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function updateCustomModelProviderConfig({
|
|
78
|
+
workspaceId,
|
|
79
|
+
providerId,
|
|
80
|
+
body,
|
|
81
|
+
}: {
|
|
82
|
+
workspaceId: string;
|
|
83
|
+
providerId: ModelProviderRef;
|
|
84
|
+
body: UpdateCustomModelProviderBodyDto;
|
|
85
|
+
}) {
|
|
86
|
+
return await apiRequest<CustomModelProviderConfigDto>(
|
|
87
|
+
`/workspaces/${workspaceId}/agent/custom-model-providers/${providerId}`,
|
|
88
|
+
{method: 'PUT', body},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function discoverCustomModelProviderModels({
|
|
93
|
+
workspaceId,
|
|
94
|
+
body,
|
|
95
|
+
}: {
|
|
96
|
+
workspaceId: string;
|
|
97
|
+
body: DiscoverCustomModelProviderModelsBodyDto;
|
|
98
|
+
}) {
|
|
99
|
+
return await apiRequest<DiscoverCustomModelProviderModelsResponseDto>(
|
|
100
|
+
`/workspaces/${workspaceId}/agent/custom-model-providers/discover-models`,
|
|
101
|
+
{method: 'POST', body},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function discoverCustomModelProviderModelsBySlug({
|
|
106
|
+
workspaceId,
|
|
107
|
+
providerId,
|
|
108
|
+
body,
|
|
109
|
+
}: {
|
|
110
|
+
workspaceId: string;
|
|
111
|
+
providerId: ModelProviderRef;
|
|
112
|
+
body: DiscoverCustomModelProviderModelsBySlugBodyDto;
|
|
113
|
+
}) {
|
|
114
|
+
return await apiRequest<DiscoverCustomModelProviderModelsResponseDto>(
|
|
115
|
+
`/workspaces/${workspaceId}/agent/custom-model-providers/${providerId}/discover-models`,
|
|
116
|
+
{method: 'POST', body},
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function deleteModelProviderConfig({
|
|
121
|
+
workspaceId,
|
|
122
|
+
providerId,
|
|
123
|
+
}: {
|
|
124
|
+
workspaceId: string;
|
|
125
|
+
providerId: ModelProviderRef;
|
|
126
|
+
}) {
|
|
127
|
+
return await apiRequest<void>(`/workspaces/${workspaceId}/agent/model-providers/${providerId}`, {
|
|
128
|
+
method: 'DELETE',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function updateModelProviderDefaultModel({
|
|
133
|
+
workspaceId,
|
|
134
|
+
providerId,
|
|
135
|
+
body,
|
|
136
|
+
}: {
|
|
137
|
+
workspaceId: string;
|
|
138
|
+
providerId: SupportedModelProviderId;
|
|
139
|
+
body: UpdateModelProviderDefaultModelBodyDto;
|
|
140
|
+
}) {
|
|
141
|
+
return await apiRequest<ModelProviderConfigDto>(
|
|
142
|
+
`/workspaces/${workspaceId}/agent/model-providers/${providerId}/default-model`,
|
|
143
|
+
{method: 'PUT', body},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function setDefaultModelProvider({
|
|
148
|
+
workspaceId,
|
|
149
|
+
body,
|
|
150
|
+
}: {
|
|
151
|
+
workspaceId: string;
|
|
152
|
+
body: SetDefaultModelProviderBodyDto;
|
|
153
|
+
}) {
|
|
154
|
+
return await apiRequest<SetDefaultModelProviderResponseDto>(
|
|
155
|
+
`/workspaces/${workspaceId}/agent/default-model-provider`,
|
|
156
|
+
{method: 'PUT', body},
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function setDefaultHarness({
|
|
161
|
+
workspaceId,
|
|
162
|
+
body,
|
|
163
|
+
}: {
|
|
164
|
+
workspaceId: string;
|
|
165
|
+
body: SetDefaultHarnessBodyDto;
|
|
166
|
+
}) {
|
|
167
|
+
return await apiRequest<SetDefaultHarnessResponseDto>(
|
|
168
|
+
`/workspaces/${workspaceId}/agent/default-harness`,
|
|
169
|
+
{method: 'PUT', body},
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useModelProviderCatalogQuery() {
|
|
174
|
+
return useQuery({
|
|
175
|
+
queryKey: modelProviderQueryKeys.catalog(),
|
|
176
|
+
queryFn: ({signal}) => getModelProviderCatalog({signal}),
|
|
177
|
+
staleTime: 1000 * 60 * 60,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function useModelProviderConfigsQuery(workspaceId: string | undefined) {
|
|
182
|
+
return useQuery({
|
|
183
|
+
queryKey: workspaceId
|
|
184
|
+
? modelProviderQueryKeys.configs(workspaceId)
|
|
185
|
+
: [...modelProviderQueryKeys.all, 'configs'],
|
|
186
|
+
enabled: Boolean(workspaceId),
|
|
187
|
+
queryFn: ({signal}) => listModelProviderConfigs({workspaceId: workspaceId ?? '', signal}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function useUpsertModelProviderConfigMutation() {
|
|
192
|
+
const queryClient = useQueryClient();
|
|
193
|
+
return useMutation({
|
|
194
|
+
mutationFn: upsertModelProviderConfig,
|
|
195
|
+
onSuccess: async (_config, variables) => {
|
|
196
|
+
await queryClient.invalidateQueries({
|
|
197
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function useCreateCustomModelProviderMutation() {
|
|
204
|
+
const queryClient = useQueryClient();
|
|
205
|
+
return useMutation({
|
|
206
|
+
mutationFn: createCustomModelProviderConfig,
|
|
207
|
+
onSuccess: async (_config, variables) => {
|
|
208
|
+
await queryClient.invalidateQueries({
|
|
209
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function useUpdateCustomModelProviderMutation() {
|
|
216
|
+
const queryClient = useQueryClient();
|
|
217
|
+
return useMutation({
|
|
218
|
+
mutationFn: updateCustomModelProviderConfig,
|
|
219
|
+
onSuccess: async (_config, variables) => {
|
|
220
|
+
await queryClient.invalidateQueries({
|
|
221
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function useDiscoverCustomModelProviderModelsMutation() {
|
|
228
|
+
return useMutation({mutationFn: discoverCustomModelProviderModels});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function useDiscoverCustomModelProviderModelsBySlugMutation() {
|
|
232
|
+
return useMutation({mutationFn: discoverCustomModelProviderModelsBySlug});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function useDeleteModelProviderConfigMutation() {
|
|
236
|
+
const queryClient = useQueryClient();
|
|
237
|
+
return useMutation({
|
|
238
|
+
mutationFn: deleteModelProviderConfig,
|
|
239
|
+
onSuccess: async (_config, variables) => {
|
|
240
|
+
await queryClient.invalidateQueries({
|
|
241
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function useUpdateModelProviderDefaultModelMutation() {
|
|
248
|
+
const queryClient = useQueryClient();
|
|
249
|
+
return useMutation({
|
|
250
|
+
mutationFn: updateModelProviderDefaultModel,
|
|
251
|
+
onSuccess: async (_config, variables) => {
|
|
252
|
+
await queryClient.invalidateQueries({
|
|
253
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function useSetDefaultModelProviderMutation() {
|
|
260
|
+
const queryClient = useQueryClient();
|
|
261
|
+
return useMutation({
|
|
262
|
+
mutationFn: setDefaultModelProvider,
|
|
263
|
+
onSuccess: async (_config, variables) => {
|
|
264
|
+
await queryClient.invalidateQueries({
|
|
265
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function useSetDefaultHarnessMutation() {
|
|
272
|
+
const queryClient = useQueryClient();
|
|
273
|
+
return useMutation({
|
|
274
|
+
mutationFn: setDefaultHarness,
|
|
275
|
+
onSuccess: async (_config, variables) => {
|
|
276
|
+
await queryClient.invalidateQueries({
|
|
277
|
+
queryKey: modelProviderQueryKeys.configs(variables.workspaceId),
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './components/available-provider-card.js';
|
|
2
|
+
export * from './components/form-errors.js';
|
|
3
|
+
export * from './components/harnesses-section.js';
|
|
4
|
+
export * from './components/model-providers-section.js';
|
|
5
|
+
export * from './components/test-and-save-form.js';
|
|
6
|
+
export * from './hooks/api/model-providers.js';
|
|
7
|
+
export * from './pages/model-provider-onboarding-page.js';
|
|
8
|
+
export * from './state/model-provider-onboarding.js';
|