@shipfox/api-agent 12.2.0 → 13.1.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.
Files changed (132) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/dist/config.d.ts +5 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +72 -7
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/errors.d.ts +4 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +6 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/index.d.ts +2 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +2 -1
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/model-provider-catalog.d.ts +5 -2
  16. package/dist/core/model-provider-catalog.d.ts.map +1 -1
  17. package/dist/core/model-provider-catalog.js +33 -1
  18. package/dist/core/model-provider-catalog.js.map +1 -1
  19. package/dist/core/resolve-agent-config.d.ts +4 -2
  20. package/dist/core/resolve-agent-config.d.ts.map +1 -1
  21. package/dist/core/resolve-agent-config.js +86 -31
  22. package/dist/core/resolve-agent-config.js.map +1 -1
  23. package/dist/core/resolve-runtime-credentials.d.ts +6 -2
  24. package/dist/core/resolve-runtime-credentials.d.ts.map +1 -1
  25. package/dist/core/resolve-runtime-credentials.js +59 -3
  26. package/dist/core/resolve-runtime-credentials.js.map +1 -1
  27. package/dist/core/validation-catalog.d.ts +2 -1
  28. package/dist/core/validation-catalog.d.ts.map +1 -1
  29. package/dist/core/validation-catalog.js +65 -17
  30. package/dist/core/validation-catalog.js.map +1 -1
  31. package/dist/core/workspace-agent-defaults-resolver.d.ts +2 -1
  32. package/dist/core/workspace-agent-defaults-resolver.d.ts.map +1 -1
  33. package/dist/core/workspace-agent-defaults-resolver.js +4 -2
  34. package/dist/core/workspace-agent-defaults-resolver.js.map +1 -1
  35. package/dist/core/workspace-provider-policy.d.ts +7 -0
  36. package/dist/core/workspace-provider-policy.d.ts.map +1 -0
  37. package/dist/core/workspace-provider-policy.js +10 -0
  38. package/dist/core/workspace-provider-policy.js.map +1 -0
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +15 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -1
  44. package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -1
  45. package/dist/presentation/e2eRoutes/create-model-provider.js +7 -1
  46. package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -1
  47. package/dist/presentation/e2eRoutes/index.d.ts +2 -1
  48. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
  49. package/dist/presentation/e2eRoutes/index.js +4 -2
  50. package/dist/presentation/e2eRoutes/index.js.map +1 -1
  51. package/dist/presentation/inter-module.d.ts +3 -0
  52. package/dist/presentation/inter-module.d.ts.map +1 -1
  53. package/dist/presentation/inter-module.js +22 -6
  54. package/dist/presentation/inter-module.js.map +1 -1
  55. package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -1
  56. package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -1
  57. package/dist/presentation/routes/create-custom-model-provider.js +5 -2
  58. package/dist/presentation/routes/create-custom-model-provider.js.map +1 -1
  59. package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -1
  60. package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -1
  61. package/dist/presentation/routes/delete-model-provider-config.js +7 -2
  62. package/dist/presentation/routes/delete-model-provider-config.js.map +1 -1
  63. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -1
  64. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -1
  65. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +5 -2
  66. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -1
  67. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts +2 -0
  68. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts.map +1 -1
  69. package/dist/presentation/routes/discover-custom-model-provider-models.js +29 -23
  70. package/dist/presentation/routes/discover-custom-model-provider-models.js.map +1 -1
  71. package/dist/presentation/routes/errors.d.ts.map +1 -1
  72. package/dist/presentation/routes/errors.js +10 -1
  73. package/dist/presentation/routes/errors.js.map +1 -1
  74. package/dist/presentation/routes/index.d.ts +5 -1
  75. package/dist/presentation/routes/index.d.ts.map +1 -1
  76. package/dist/presentation/routes/index.js +21 -14
  77. package/dist/presentation/routes/index.js.map +1 -1
  78. package/dist/presentation/routes/list-model-provider-catalog.d.ts +5 -0
  79. package/dist/presentation/routes/list-model-provider-catalog.d.ts.map +1 -1
  80. package/dist/presentation/routes/list-model-provider-catalog.js +21 -13
  81. package/dist/presentation/routes/list-model-provider-catalog.js.map +1 -1
  82. package/dist/presentation/routes/set-default-model-provider.d.ts +2 -0
  83. package/dist/presentation/routes/set-default-model-provider.d.ts.map +1 -1
  84. package/dist/presentation/routes/set-default-model-provider.js +44 -38
  85. package/dist/presentation/routes/set-default-model-provider.js.map +1 -1
  86. package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -1
  87. package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -1
  88. package/dist/presentation/routes/update-custom-model-provider.js +5 -2
  89. package/dist/presentation/routes/update-custom-model-provider.js.map +1 -1
  90. package/dist/presentation/routes/update-model-provider-default-model.d.ts +2 -0
  91. package/dist/presentation/routes/update-model-provider-default-model.d.ts.map +1 -1
  92. package/dist/presentation/routes/update-model-provider-default-model.js +35 -29
  93. package/dist/presentation/routes/update-model-provider-default-model.js.map +1 -1
  94. package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -1
  95. package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -1
  96. package/dist/presentation/routes/upsert-model-provider-config.js +5 -2
  97. package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -1
  98. package/dist/tsconfig.test.tsbuildinfo +1 -1
  99. package/package.json +2 -2
  100. package/src/config.test.ts +165 -4
  101. package/src/config.ts +94 -9
  102. package/src/core/errors.ts +7 -0
  103. package/src/core/index.ts +5 -0
  104. package/src/core/model-provider-catalog.test.ts +30 -0
  105. package/src/core/model-provider-catalog.ts +41 -1
  106. package/src/core/resolve-agent-config.test.ts +92 -0
  107. package/src/core/resolve-agent-config.ts +152 -43
  108. package/src/core/resolve-runtime-credentials.test.ts +169 -1
  109. package/src/core/resolve-runtime-credentials.ts +68 -3
  110. package/src/core/validation-catalog.test.ts +54 -0
  111. package/src/core/validation-catalog.ts +124 -19
  112. package/src/core/workspace-agent-defaults-resolver.ts +7 -2
  113. package/src/core/workspace-provider-policy.ts +19 -0
  114. package/src/index.ts +25 -4
  115. package/src/presentation/e2eRoutes/create-model-provider.ts +11 -1
  116. package/src/presentation/e2eRoutes/index.ts +9 -2
  117. package/src/presentation/inter-module.test.ts +51 -0
  118. package/src/presentation/inter-module.ts +38 -6
  119. package/src/presentation/routes/create-custom-model-provider.ts +10 -2
  120. package/src/presentation/routes/delete-model-provider-config.ts +12 -2
  121. package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +7 -1
  122. package/src/presentation/routes/discover-custom-model-provider-models.ts +30 -18
  123. package/src/presentation/routes/errors.ts +11 -0
  124. package/src/presentation/routes/index.ts +32 -14
  125. package/src/presentation/routes/list-model-provider-catalog.ts +32 -11
  126. package/src/presentation/routes/model-provider-catalog.test.ts +49 -1
  127. package/src/presentation/routes/model-provider-config.test.ts +62 -1
  128. package/src/presentation/routes/set-default-model-provider.ts +42 -31
  129. package/src/presentation/routes/update-custom-model-provider.ts +10 -2
  130. package/src/presentation/routes/update-model-provider-default-model.ts +37 -26
  131. package/src/presentation/routes/upsert-model-provider-config.ts +10 -2
  132. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,10 +2,18 @@ import {modelProviderRefSchema} from '@shipfox/api-agent-dto';
2
2
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
3
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
4
  import {z} from 'zod';
5
- import {deleteModelProviderConfig} from '#core/index.js';
5
+ import {
6
+ assertWorkspaceProviderConfigurationEnabled,
7
+ deleteModelProviderConfig,
8
+ } from '#core/index.js';
6
9
  import type {AgentSecretsClient} from '#core/secrets-client.js';
10
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
11
+ import {translateModelProviderRouteError} from './errors.js';
7
12
 
8
- export function createDeleteModelProviderConfigRoute(secrets: AgentSecretsClient) {
13
+ export function createDeleteModelProviderConfigRoute(
14
+ secrets: AgentSecretsClient,
15
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
16
+ ) {
9
17
  return defineRoute({
10
18
  method: 'DELETE',
11
19
  path: '/model-providers/:providerId',
@@ -19,9 +27,11 @@ export function createDeleteModelProviderConfigRoute(secrets: AgentSecretsClient
19
27
  204: z.void(),
20
28
  },
21
29
  },
30
+ errorHandler: translateModelProviderRouteError,
22
31
  handler: async (request, reply) => {
23
32
  const {workspaceId, providerId} = request.params;
24
33
  requireWorkspaceAccess({request, workspaceId});
34
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
25
35
 
26
36
  const deleted = await deleteModelProviderConfig({workspaceId, providerId}, {secrets});
27
37
  if (!deleted) {
@@ -7,13 +7,18 @@ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
7
7
  import {defineRoute} from '@shipfox/node-fastify';
8
8
  import {z} from 'zod';
9
9
  import {
10
+ assertWorkspaceProviderConfigurationEnabled,
10
11
  discoverCustomModelProviderModels,
11
12
  resolveCustomModelProviderDiscoveryParams,
12
13
  } from '#core/index.js';
13
14
  import type {AgentSecretsClient} from '#core/secrets-client.js';
15
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
14
16
  import {translateModelProviderRouteError} from './errors.js';
15
17
 
16
- export function createDiscoverCustomModelProviderModelsBySlugRoute(secrets: AgentSecretsClient) {
18
+ export function createDiscoverCustomModelProviderModelsBySlugRoute(
19
+ secrets: AgentSecretsClient,
20
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
21
+ ) {
17
22
  return defineRoute({
18
23
  method: 'POST',
19
24
  path: '/custom-model-providers/:slug/discover-models',
@@ -32,6 +37,7 @@ export function createDiscoverCustomModelProviderModelsBySlugRoute(secrets: Agen
32
37
  handler: async (request) => {
33
38
  const {workspaceId, slug} = request.params;
34
39
  requireWorkspaceAccess({request, workspaceId});
40
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
35
41
 
36
42
  const discoveryParams = await resolveCustomModelProviderDiscoveryParams(
37
43
  {
@@ -5,25 +5,37 @@ import {
5
5
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
6
6
  import {defineRoute} from '@shipfox/node-fastify';
7
7
  import {z} from 'zod';
8
- import {discoverCustomModelProviderModels} from '#core/index.js';
8
+ import {
9
+ assertWorkspaceProviderConfigurationEnabled,
10
+ discoverCustomModelProviderModels,
11
+ } from '#core/index.js';
12
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
9
13
  import {translateModelProviderRouteError} from './errors.js';
10
14
 
11
- export const discoverCustomModelProviderModelsRoute = defineRoute({
12
- method: 'POST',
13
- path: '/custom-model-providers/discover-models',
14
- description: 'Discover models exposed by a custom model provider endpoint',
15
- schema: {
16
- params: z.object({workspaceId: z.string().uuid()}),
17
- body: discoverCustomModelProviderModelsBodySchema,
18
- response: {
19
- 200: discoverCustomModelProviderModelsResponseSchema,
15
+ export function createDiscoverCustomModelProviderModelsRoute(
16
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
17
+ ) {
18
+ return defineRoute({
19
+ method: 'POST',
20
+ path: '/custom-model-providers/discover-models',
21
+ description: 'Discover models exposed by a custom model provider endpoint',
22
+ schema: {
23
+ params: z.object({workspaceId: z.string().uuid()}),
24
+ body: discoverCustomModelProviderModelsBodySchema,
25
+ response: {
26
+ 200: discoverCustomModelProviderModelsResponseSchema,
27
+ },
28
+ },
29
+ errorHandler: translateModelProviderRouteError,
30
+ handler: async (request) => {
31
+ const {workspaceId} = request.params;
32
+ requireWorkspaceAccess({request, workspaceId});
33
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
34
+
35
+ return await discoverCustomModelProviderModels(request.body);
20
36
  },
21
- },
22
- errorHandler: translateModelProviderRouteError,
23
- handler: async (request) => {
24
- const {workspaceId} = request.params;
25
- requireWorkspaceAccess({request, workspaceId});
37
+ });
38
+ }
26
39
 
27
- return await discoverCustomModelProviderModels(request.body);
28
- },
29
- });
40
+ export const discoverCustomModelProviderModelsRoute =
41
+ createDiscoverCustomModelProviderModelsRoute();
@@ -12,6 +12,7 @@ import {
12
12
  ModelProviderConfigNotFoundError,
13
13
  ModelProviderValidationError,
14
14
  UnsupportedModelProviderError,
15
+ WorkspaceProvidersDisabledError,
15
16
  } from '#core/index.js';
16
17
  import {getModelProviderCredentialKeys} from '#core/model-provider-policy.js';
17
18
 
@@ -96,6 +97,16 @@ export function translateModelProviderRouteError(error: unknown): never {
96
97
  });
97
98
  }
98
99
 
100
+ if (error instanceof WorkspaceProvidersDisabledError) {
101
+ throw new ClientError(error.message, 'workspace-providers-disabled', {
102
+ status: 422,
103
+ details: {
104
+ managed_provider_id: error.managedProviderId,
105
+ message: error.message,
106
+ },
107
+ });
108
+ }
109
+
99
110
  if (error instanceof ModelProviderConfigNotFoundError) {
100
111
  throw new ClientError('Provider is not configured', 'provider-not-configured', {
101
112
  status: 422,
@@ -1,40 +1,58 @@
1
+ import type {ManagedModelProvider, WorkspaceProvidersPolicy} from '@shipfox/api-agent-dto';
1
2
  import {AUTH_USER} from '@shipfox/api-auth-context';
2
3
  import type {RouteGroup} from '@shipfox/node-fastify';
3
4
  import type {AgentSecretsClient} from '#core/secrets-client.js';
5
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
4
6
  import {createCustomModelProviderRoute} from './create-custom-model-provider.js';
5
7
  import {createDeleteModelProviderConfigRoute} from './delete-model-provider-config.js';
6
- import {discoverCustomModelProviderModelsRoute} from './discover-custom-model-provider-models.js';
8
+ import {createDiscoverCustomModelProviderModelsRoute} from './discover-custom-model-provider-models.js';
7
9
  import {createDiscoverCustomModelProviderModelsBySlugRoute} from './discover-custom-model-provider-models-by-slug.js';
8
- import {listModelProviderCatalogRoute} from './list-model-provider-catalog.js';
10
+ import {createListModelProviderCatalogRoute} from './list-model-provider-catalog.js';
9
11
  import {listModelProviderConfigsRoute} from './list-model-provider-configs.js';
10
12
  import {setDefaultHarnessRoute} from './set-default-harness.js';
11
- import {setDefaultModelProviderRoute} from './set-default-model-provider.js';
13
+ import {createSetDefaultModelProviderRoute} from './set-default-model-provider.js';
12
14
  import {createUpdateCustomModelProviderRoute} from './update-custom-model-provider.js';
13
- import {updateModelProviderDefaultModelRoute} from './update-model-provider-default-model.js';
15
+ import {createUpdateModelProviderDefaultModelRoute} from './update-model-provider-default-model.js';
14
16
  import {createUpsertModelProviderConfigRoute} from './upsert-model-provider-config.js';
15
17
 
16
- export function createAgentRoutes(secrets: AgentSecretsClient): RouteGroup[] {
18
+ export function createAgentRoutes(
19
+ secrets: AgentSecretsClient,
20
+ options: {
21
+ managedProvider?: ManagedModelProvider | undefined;
22
+ workspaceProviders?: WorkspaceProvidersPolicy | undefined;
23
+ } = {},
24
+ ): RouteGroup[] {
25
+ const workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {
26
+ workspaceProviders: options.workspaceProviders ?? 'enabled',
27
+ managedProviderId: options.managedProvider?.id,
28
+ };
29
+
17
30
  return [
18
31
  {
19
32
  prefix: '/workspaces/:workspaceId/agent',
20
33
  auth: AUTH_USER,
21
34
  routes: [
22
35
  listModelProviderConfigsRoute,
23
- createCustomModelProviderRoute(secrets),
24
- discoverCustomModelProviderModelsRoute,
25
- createDiscoverCustomModelProviderModelsBySlugRoute(secrets),
26
- createUpdateCustomModelProviderRoute(secrets),
27
- createUpsertModelProviderConfigRoute(secrets),
28
- updateModelProviderDefaultModelRoute,
29
- createDeleteModelProviderConfigRoute(secrets),
36
+ createCustomModelProviderRoute(secrets, workspaceProviderPolicy),
37
+ createDiscoverCustomModelProviderModelsRoute(workspaceProviderPolicy),
38
+ createDiscoverCustomModelProviderModelsBySlugRoute(secrets, workspaceProviderPolicy),
39
+ createUpdateCustomModelProviderRoute(secrets, workspaceProviderPolicy),
40
+ createUpsertModelProviderConfigRoute(secrets, workspaceProviderPolicy),
41
+ createUpdateModelProviderDefaultModelRoute(workspaceProviderPolicy),
42
+ createDeleteModelProviderConfigRoute(secrets, workspaceProviderPolicy),
30
43
  setDefaultHarnessRoute,
31
- setDefaultModelProviderRoute,
44
+ createSetDefaultModelProviderRoute(workspaceProviderPolicy),
32
45
  ],
33
46
  },
34
47
  {
35
48
  prefix: '/agent',
36
49
  auth: AUTH_USER,
37
- routes: [listModelProviderCatalogRoute],
50
+ routes: [
51
+ createListModelProviderCatalogRoute({
52
+ managedProvider: options.managedProvider,
53
+ workspaceProviders: options.workspaceProviders,
54
+ }),
55
+ ],
38
56
  },
39
57
  ];
40
58
  }
@@ -1,15 +1,36 @@
1
- import {modelProviderCatalogResponseSchema} from '@shipfox/api-agent-dto';
1
+ import {
2
+ type ManagedModelProvider,
3
+ modelProviderCatalogResponseSchema,
4
+ type WorkspaceProvidersPolicy,
5
+ } from '@shipfox/api-agent-dto';
2
6
  import {defineRoute} from '@shipfox/node-fastify';
3
7
  import {buildModelProviderCatalog} from '#core/index.js';
4
8
 
5
- export const listModelProviderCatalogRoute = defineRoute({
6
- method: 'GET',
7
- path: '/model-provider-catalog',
8
- description: 'List available model providers and models',
9
- schema: {
10
- response: {
11
- 200: modelProviderCatalogResponseSchema,
9
+ export function createListModelProviderCatalogRoute(
10
+ options: {
11
+ managedProvider?: ManagedModelProvider | undefined;
12
+ workspaceProviders?: WorkspaceProvidersPolicy | undefined;
13
+ } = {},
14
+ ) {
15
+ const workspaceProviders = options.workspaceProviders ?? 'enabled';
16
+
17
+ return defineRoute({
18
+ method: 'GET',
19
+ path: '/model-provider-catalog',
20
+ description: 'List available model providers and models',
21
+ schema: {
22
+ response: {
23
+ 200: modelProviderCatalogResponseSchema,
24
+ },
12
25
  },
13
- },
14
- handler: () => ({providers: buildModelProviderCatalog()}),
15
- });
26
+ handler: () => ({
27
+ providers: buildModelProviderCatalog({
28
+ managedProvider: options.managedProvider,
29
+ workspaceProviders,
30
+ }),
31
+ workspace_providers: workspaceProviders,
32
+ }),
33
+ });
34
+ }
35
+
36
+ export const listModelProviderCatalogRoute = createListModelProviderCatalogRoute();
@@ -1,7 +1,8 @@
1
+ import type {ManagedModelProvider} from '@shipfox/api-agent-dto';
1
2
  import {AUTH_USER, buildUserContext, setUserContext} from '@shipfox/api-auth-context';
2
3
  import type {AuthMethod, FastifyRequest} from '@shipfox/node-fastify';
3
4
  import {ClientError, closeApp, createApp} from '@shipfox/node-fastify';
4
- import {agentRoutes} from './index.js';
5
+ import {agentRoutes, createAgentRoutes} from './index.js';
5
6
 
6
7
  const fakeUserAuth: AuthMethod = {
7
8
  name: AUTH_USER,
@@ -69,5 +70,52 @@ describe('model provider catalog route', () => {
69
70
  }
70
71
  }
71
72
  });
73
+
74
+ it('returns only the managed provider and the disabled policy when configured', async () => {
75
+ await closeApp();
76
+ app = await createApp({
77
+ auth: [fakeUserAuth],
78
+ routes: createAgentRoutes(undefined as never, {
79
+ managedProvider: managedProvider(),
80
+ workspaceProviders: 'disabled',
81
+ }),
82
+ swagger: false,
83
+ });
84
+ await app.ready();
85
+
86
+ const res = await app.inject({
87
+ method: 'GET',
88
+ url: '/agent/model-provider-catalog',
89
+ headers: {authorization: 'Bearer user'},
90
+ });
91
+
92
+ expect(res.statusCode, res.body).toBe(200);
93
+ expect(res.json()).toMatchObject({
94
+ workspace_providers: 'disabled',
95
+ providers: [
96
+ expect.objectContaining({
97
+ id: 'shipfox',
98
+ support_status: 'supported',
99
+ default_model: 'managed-claude',
100
+ credential_fields: [],
101
+ models: [{id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages'}],
102
+ }),
103
+ ],
104
+ });
105
+ });
72
106
  });
73
107
  });
108
+
109
+ function managedProvider(): ManagedModelProvider {
110
+ return {
111
+ id: 'shipfox',
112
+ label: 'Shipfox',
113
+ models: [{id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages'}],
114
+ defaultModel: 'managed-claude',
115
+ resolveCredentials: async () => ({
116
+ api: 'anthropic-messages',
117
+ baseUrl: 'https://gateway.example.com',
118
+ credentials: {api_key: 'token'},
119
+ }),
120
+ };
121
+ }
@@ -20,7 +20,7 @@ import {
20
20
  } from '#db/index.js';
21
21
  import {modelProviderConfigs} from '#db/schema/model-provider-configs.js';
22
22
  import {getSecretsByNamespace, setSecrets} from '#test/fixtures/secrets-client.js';
23
- import {agentRoutes} from './index.js';
23
+ import {agentRoutes, createAgentRoutes} from './index.js';
24
24
 
25
25
  vi.mock('@earendil-works/pi-ai/compat', async (importOriginal) => {
26
26
  const actual = await importOriginal<typeof import('@earendil-works/pi-ai/compat')>();
@@ -391,6 +391,53 @@ describe('model provider config routes', () => {
391
391
 
392
392
  expect(res.statusCode).toBe(400);
393
393
  });
394
+
395
+ it('rejects catalog and custom provider writes when workspace providers are disabled', async () => {
396
+ await closeApp();
397
+ app = await createApp({
398
+ auth: [fakeUserAuth],
399
+ routes: createAgentRoutes(undefined as never, {
400
+ managedProvider: managedProvider(),
401
+ workspaceProviders: 'disabled',
402
+ }),
403
+ swagger: false,
404
+ });
405
+ await app.ready();
406
+
407
+ const catalogWrite = await app.inject({
408
+ method: 'PUT',
409
+ url: `/workspaces/${workspaceId}/agent/model-providers/anthropic`,
410
+ headers: {authorization: 'Bearer user'},
411
+ payload: {credentials: {api_key: 'sk-secret'}},
412
+ });
413
+ const customWrite = await app.inject({
414
+ method: 'POST',
415
+ url: `/workspaces/${workspaceId}/agent/custom-model-providers`,
416
+ headers: {authorization: 'Bearer user'},
417
+ payload: {
418
+ slug: 'local-vllm',
419
+ display_name: 'Local vLLM',
420
+ api: 'openai-responses',
421
+ base_url: 'http://127.0.0.1:11434/v1',
422
+ models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
423
+ },
424
+ });
425
+
426
+ expect(catalogWrite.statusCode).toBe(422);
427
+ expect(customWrite.statusCode).toBe(422);
428
+ expect(catalogWrite.json()).toMatchObject({
429
+ code: 'workspace-providers-disabled',
430
+ details: {
431
+ message: 'This instance only supports provider `shipfox`.',
432
+ },
433
+ });
434
+ expect(customWrite.json()).toMatchObject({
435
+ code: 'workspace-providers-disabled',
436
+ details: {
437
+ message: 'This instance only supports provider `shipfox`.',
438
+ },
439
+ });
440
+ });
394
441
  });
395
442
 
396
443
  describe('DELETE /workspaces/:workspaceId/agent/model-providers/:providerId', () => {
@@ -804,4 +851,18 @@ describe('model provider config routes', () => {
804
851
  });
805
852
  expect(rows).toHaveLength(1);
806
853
  }
854
+
855
+ function managedProvider() {
856
+ return {
857
+ id: 'shipfox',
858
+ label: 'Shipfox',
859
+ models: [{id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages' as const}],
860
+ defaultModel: 'managed-claude',
861
+ resolveCredentials: async () => ({
862
+ api: 'anthropic-messages' as const,
863
+ baseUrl: 'https://gateway.example.com',
864
+ credentials: {api_key: 'token'},
865
+ }),
866
+ };
867
+ }
807
868
  });
@@ -5,43 +5,54 @@ import {
5
5
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
6
6
  import {defineRoute} from '@shipfox/node-fastify';
7
7
  import {z} from 'zod';
8
- import {UnsupportedModelProviderError} from '#core/index.js';
8
+ import {
9
+ assertWorkspaceProviderConfigurationEnabled,
10
+ UnsupportedModelProviderError,
11
+ } from '#core/index.js';
9
12
  import {getModelProviderEntry} from '#core/model-provider-policy.js';
13
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
10
14
  import {getModelProviderConfig, setDefaultModelProvider} from '#db/index.js';
11
15
  import {translateModelProviderRouteError} from './errors.js';
12
16
 
13
- export const setDefaultModelProviderRoute = defineRoute({
14
- method: 'PUT',
15
- path: '/default-model-provider',
16
- description: 'Set the default model provider for a workspace',
17
- schema: {
18
- params: z.object({workspaceId: z.string().uuid()}),
19
- body: setDefaultModelProviderBodySchema,
20
- response: {
21
- 200: setDefaultModelProviderResponseSchema,
17
+ export function createSetDefaultModelProviderRoute(
18
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
19
+ ) {
20
+ return defineRoute({
21
+ method: 'PUT',
22
+ path: '/default-model-provider',
23
+ description: 'Set the default model provider for a workspace',
24
+ schema: {
25
+ params: z.object({workspaceId: z.string().uuid()}),
26
+ body: setDefaultModelProviderBodySchema,
27
+ response: {
28
+ 200: setDefaultModelProviderResponseSchema,
29
+ },
22
30
  },
23
- },
24
- errorHandler: translateModelProviderRouteError,
25
- handler: async (request) => {
26
- const {workspaceId} = request.params;
27
- requireWorkspaceAccess({request, workspaceId});
31
+ errorHandler: translateModelProviderRouteError,
32
+ handler: async (request) => {
33
+ const {workspaceId} = request.params;
34
+ requireWorkspaceAccess({request, workspaceId});
35
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
28
36
 
29
- const existingConfig = await getModelProviderConfig({
30
- workspaceId,
31
- providerId: request.body.provider_id,
32
- });
33
- if (existingConfig?.kind !== 'custom') {
34
- const entry = getModelProviderEntry(request.body.provider_id);
35
- if (entry === undefined || entry.support_status !== 'supported') {
36
- throw new UnsupportedModelProviderError(request.body.provider_id);
37
+ const existingConfig = await getModelProviderConfig({
38
+ workspaceId,
39
+ providerId: request.body.provider_id,
40
+ });
41
+ if (existingConfig?.kind !== 'custom') {
42
+ const entry = getModelProviderEntry(request.body.provider_id);
43
+ if (entry === undefined || entry.support_status !== 'supported') {
44
+ throw new UnsupportedModelProviderError(request.body.provider_id);
45
+ }
37
46
  }
38
- }
39
47
 
40
- const settings = await setDefaultModelProvider({
41
- workspaceId,
42
- providerId: request.body.provider_id,
43
- });
48
+ const settings = await setDefaultModelProvider({
49
+ workspaceId,
50
+ providerId: request.body.provider_id,
51
+ });
52
+
53
+ return {default_provider_id: settings.defaultProviderId};
54
+ },
55
+ });
56
+ }
44
57
 
45
- return {default_provider_id: settings.defaultProviderId};
46
- },
47
- });
58
+ export const setDefaultModelProviderRoute = createSetDefaultModelProviderRoute();
@@ -6,12 +6,19 @@ import {
6
6
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
7
7
  import {defineRoute} from '@shipfox/node-fastify';
8
8
  import {z} from 'zod';
9
- import {updateCustomModelProviderConfig} from '#core/index.js';
9
+ import {
10
+ assertWorkspaceProviderConfigurationEnabled,
11
+ updateCustomModelProviderConfig,
12
+ } from '#core/index.js';
10
13
  import type {AgentSecretsClient} from '#core/secrets-client.js';
14
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
11
15
  import {toCustomModelProviderConfigDto} from '#presentation/dto/index.js';
12
16
  import {translateModelProviderRouteError} from './errors.js';
13
17
 
14
- export function createUpdateCustomModelProviderRoute(secrets: AgentSecretsClient) {
18
+ export function createUpdateCustomModelProviderRoute(
19
+ secrets: AgentSecretsClient,
20
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
21
+ ) {
15
22
  return defineRoute({
16
23
  method: 'PUT',
17
24
  path: '/custom-model-providers/:slug',
@@ -39,6 +46,7 @@ export function createUpdateCustomModelProviderRoute(secrets: AgentSecretsClient
39
46
  });
40
47
 
41
48
  requireWorkspaceAccess({request, workspaceId});
49
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
42
50
 
43
51
  const config = await updateCustomModelProviderConfig(
44
52
  {
@@ -6,35 +6,46 @@ import {
6
6
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
7
7
  import {defineRoute} from '@shipfox/node-fastify';
8
8
  import {z} from 'zod';
9
- import {updateModelProviderConfigDefaultModel} from '#core/index.js';
9
+ import {
10
+ assertWorkspaceProviderConfigurationEnabled,
11
+ updateModelProviderConfigDefaultModel,
12
+ } from '#core/index.js';
13
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
10
14
  import {toModelProviderConfigResponseDto} from '#presentation/dto/index.js';
11
15
  import {translateModelProviderRouteError} from './errors.js';
12
16
 
13
- export const updateModelProviderDefaultModelRoute = defineRoute({
14
- method: 'PUT',
15
- path: '/model-providers/:providerId/default-model',
16
- description: 'Update the default model for an existing model provider configuration',
17
- schema: {
18
- params: z.object({
19
- workspaceId: z.string().uuid(),
20
- providerId: modelProviderRefSchema,
21
- }),
22
- body: updateModelProviderDefaultModelBodySchema,
23
- response: {
24
- 200: modelProviderConfigResponseSchema,
17
+ export function createUpdateModelProviderDefaultModelRoute(
18
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
19
+ ) {
20
+ return defineRoute({
21
+ method: 'PUT',
22
+ path: '/model-providers/:providerId/default-model',
23
+ description: 'Update the default model for an existing model provider configuration',
24
+ schema: {
25
+ params: z.object({
26
+ workspaceId: z.string().uuid(),
27
+ providerId: modelProviderRefSchema,
28
+ }),
29
+ body: updateModelProviderDefaultModelBodySchema,
30
+ response: {
31
+ 200: modelProviderConfigResponseSchema,
32
+ },
25
33
  },
26
- },
27
- errorHandler: translateModelProviderRouteError,
28
- handler: async (request) => {
29
- const {workspaceId, providerId} = request.params;
30
- requireWorkspaceAccess({request, workspaceId});
34
+ errorHandler: translateModelProviderRouteError,
35
+ handler: async (request) => {
36
+ const {workspaceId, providerId} = request.params;
37
+ requireWorkspaceAccess({request, workspaceId});
38
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
39
+
40
+ const config = await updateModelProviderConfigDefaultModel({
41
+ workspaceId,
42
+ providerId,
43
+ defaultModel: request.body.default_model,
44
+ });
31
45
 
32
- const config = await updateModelProviderConfigDefaultModel({
33
- workspaceId,
34
- providerId,
35
- defaultModel: request.body.default_model,
36
- });
46
+ return toModelProviderConfigResponseDto(config);
47
+ },
48
+ });
49
+ }
37
50
 
38
- return toModelProviderConfigResponseDto(config);
39
- },
40
- });
51
+ export const updateModelProviderDefaultModelRoute = createUpdateModelProviderDefaultModelRoute();
@@ -6,12 +6,19 @@ import {
6
6
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
7
7
  import {defineRoute} from '@shipfox/node-fastify';
8
8
  import {z} from 'zod';
9
- import {testAndSaveModelProviderConfig} from '#core/index.js';
9
+ import {
10
+ assertWorkspaceProviderConfigurationEnabled,
11
+ testAndSaveModelProviderConfig,
12
+ } from '#core/index.js';
10
13
  import type {AgentSecretsClient} from '#core/secrets-client.js';
14
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
11
15
  import {toModelProviderConfigDto} from '#presentation/dto/index.js';
12
16
  import {translateModelProviderRouteError} from './errors.js';
13
17
 
14
- export function createUpsertModelProviderConfigRoute(secrets: AgentSecretsClient) {
18
+ export function createUpsertModelProviderConfigRoute(
19
+ secrets: AgentSecretsClient,
20
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
21
+ ) {
15
22
  return defineRoute({
16
23
  method: 'PUT',
17
24
  path: '/model-providers/:providerId',
@@ -39,6 +46,7 @@ export function createUpsertModelProviderConfigRoute(secrets: AgentSecretsClient
39
46
  });
40
47
 
41
48
  const membership = requireWorkspaceAccess({request, workspaceId});
49
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
42
50
 
43
51
  const config = await testAndSaveModelProviderConfig(
44
52
  {