@shipfox/api-agent 2.0.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 (274) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +105 -0
  5. package/LICENSE +21 -0
  6. package/dist/config.d.ts +13 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +60 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/credential-fingerprints.d.ts +10 -0
  11. package/dist/core/credential-fingerprints.d.ts.map +1 -0
  12. package/dist/core/credential-fingerprints.js +65 -0
  13. package/dist/core/credential-fingerprints.js.map +1 -0
  14. package/dist/core/custom-model-provider-config-service.d.ts +26 -0
  15. package/dist/core/custom-model-provider-config-service.d.ts.map +1 -0
  16. package/dist/core/custom-model-provider-config-service.js +390 -0
  17. package/dist/core/custom-model-provider-config-service.js.map +1 -0
  18. package/dist/core/custom-provider-url.d.ts +4 -0
  19. package/dist/core/custom-provider-url.d.ts.map +1 -0
  20. package/dist/core/custom-provider-url.js +23 -0
  21. package/dist/core/custom-provider-url.js.map +1 -0
  22. package/dist/core/discover-custom-model-provider-models.d.ts +3 -0
  23. package/dist/core/discover-custom-model-provider-models.d.ts.map +1 -0
  24. package/dist/core/discover-custom-model-provider-models.js +122 -0
  25. package/dist/core/discover-custom-model-provider-models.js.map +1 -0
  26. package/dist/core/entities/agent-workspace-settings.d.ts +9 -0
  27. package/dist/core/entities/agent-workspace-settings.d.ts.map +1 -0
  28. package/dist/core/entities/agent-workspace-settings.js +3 -0
  29. package/dist/core/entities/agent-workspace-settings.js.map +1 -0
  30. package/dist/core/entities/model-provider-config.d.ts +19 -0
  31. package/dist/core/entities/model-provider-config.d.ts.map +1 -0
  32. package/dist/core/entities/model-provider-config.js +3 -0
  33. package/dist/core/entities/model-provider-config.js.map +1 -0
  34. package/dist/core/errors.d.ts +62 -0
  35. package/dist/core/errors.d.ts.map +1 -0
  36. package/dist/core/errors.js +80 -0
  37. package/dist/core/errors.js.map +1 -0
  38. package/dist/core/harness/claude.d.ts +6 -0
  39. package/dist/core/harness/claude.d.ts.map +1 -0
  40. package/dist/core/harness/claude.js +104 -0
  41. package/dist/core/harness/claude.js.map +1 -0
  42. package/dist/core/harness/index.d.ts +4 -0
  43. package/dist/core/harness/index.d.ts.map +1 -0
  44. package/dist/core/harness/index.js +5 -0
  45. package/dist/core/harness/index.js.map +1 -0
  46. package/dist/core/harness/pi.d.ts +6 -0
  47. package/dist/core/harness/pi.d.ts.map +1 -0
  48. package/dist/core/harness/pi.js +16 -0
  49. package/dist/core/harness/pi.js.map +1 -0
  50. package/dist/core/harness/registry.d.ts +22 -0
  51. package/dist/core/harness/registry.d.ts.map +1 -0
  52. package/dist/core/harness/registry.js +46 -0
  53. package/dist/core/harness/registry.js.map +1 -0
  54. package/dist/core/index.d.ts +12 -0
  55. package/dist/core/index.d.ts.map +1 -0
  56. package/dist/core/index.js +11 -0
  57. package/dist/core/index.js.map +1 -0
  58. package/dist/core/model-provider-catalog.d.ts +3 -0
  59. package/dist/core/model-provider-catalog.d.ts.map +1 -0
  60. package/dist/core/model-provider-catalog.js +25 -0
  61. package/dist/core/model-provider-catalog.js.map +1 -0
  62. package/dist/core/model-provider-config-service.d.ts +32 -0
  63. package/dist/core/model-provider-config-service.d.ts.map +1 -0
  64. package/dist/core/model-provider-config-service.js +133 -0
  65. package/dist/core/model-provider-config-service.js.map +1 -0
  66. package/dist/core/model-provider-validation.d.ts +31 -0
  67. package/dist/core/model-provider-validation.d.ts.map +1 -0
  68. package/dist/core/model-provider-validation.js +220 -0
  69. package/dist/core/model-provider-validation.js.map +1 -0
  70. package/dist/core/resolve-agent-config.d.ts +32 -0
  71. package/dist/core/resolve-agent-config.d.ts.map +1 -0
  72. package/dist/core/resolve-agent-config.js +143 -0
  73. package/dist/core/resolve-agent-config.js.map +1 -0
  74. package/dist/core/resolve-runtime-credentials.d.ts +21 -0
  75. package/dist/core/resolve-runtime-credentials.d.ts.map +1 -0
  76. package/dist/core/resolve-runtime-credentials.js +101 -0
  77. package/dist/core/resolve-runtime-credentials.js.map +1 -0
  78. package/dist/core/workspace-agent-defaults-resolver.d.ts +3 -0
  79. package/dist/core/workspace-agent-defaults-resolver.d.ts.map +1 -0
  80. package/dist/core/workspace-agent-defaults-resolver.js +26 -0
  81. package/dist/core/workspace-agent-defaults-resolver.js.map +1 -0
  82. package/dist/db/agent-defaults-snapshot.d.ts +9 -0
  83. package/dist/db/agent-defaults-snapshot.d.ts.map +1 -0
  84. package/dist/db/agent-defaults-snapshot.js +24 -0
  85. package/dist/db/agent-defaults-snapshot.js.map +1 -0
  86. package/dist/db/agent-workspace-settings.d.ts +12 -0
  87. package/dist/db/agent-workspace-settings.d.ts.map +1 -0
  88. package/dist/db/agent-workspace-settings.js +53 -0
  89. package/dist/db/agent-workspace-settings.js.map +1 -0
  90. package/dist/db/db.d.ts +771 -0
  91. package/dist/db/db.d.ts.map +1 -0
  92. package/dist/db/db.js +20 -0
  93. package/dist/db/db.js.map +1 -0
  94. package/dist/db/index.d.ts +9 -0
  95. package/dist/db/index.d.ts.map +1 -0
  96. package/dist/db/index.js +11 -0
  97. package/dist/db/index.js.map +1 -0
  98. package/dist/db/model-provider-configs.d.ts +38 -0
  99. package/dist/db/model-provider-configs.d.ts.map +1 -0
  100. package/dist/db/model-provider-configs.js +159 -0
  101. package/dist/db/model-provider-configs.js.map +1 -0
  102. package/dist/db/schema/agent-workspace-settings.d.ts +97 -0
  103. package/dist/db/schema/agent-workspace-settings.d.ts.map +1 -0
  104. package/dist/db/schema/agent-workspace-settings.js +26 -0
  105. package/dist/db/schema/agent-workspace-settings.js.map +1 -0
  106. package/dist/db/schema/common.d.ts +2 -0
  107. package/dist/db/schema/common.d.ts.map +1 -0
  108. package/dist/db/schema/common.js +4 -0
  109. package/dist/db/schema/common.js.map +1 -0
  110. package/dist/db/schema/model-provider-configs.d.ts +296 -0
  111. package/dist/db/schema/model-provider-configs.d.ts.map +1 -0
  112. package/dist/db/schema/model-provider-configs.js +52 -0
  113. package/dist/db/schema/model-provider-configs.js.map +1 -0
  114. package/dist/index.d.ts +5 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +18 -0
  117. package/dist/index.js.map +1 -0
  118. package/dist/metrics/index.d.ts +2 -0
  119. package/dist/metrics/index.d.ts.map +1 -0
  120. package/dist/metrics/index.js +3 -0
  121. package/dist/metrics/index.js.map +1 -0
  122. package/dist/metrics/instance.d.ts +10 -0
  123. package/dist/metrics/instance.d.ts.map +1 -0
  124. package/dist/metrics/instance.js +10 -0
  125. package/dist/metrics/instance.js.map +1 -0
  126. package/dist/presentation/dto/index.d.ts +2 -0
  127. package/dist/presentation/dto/index.d.ts.map +1 -0
  128. package/dist/presentation/dto/index.js +3 -0
  129. package/dist/presentation/dto/index.js.map +1 -0
  130. package/dist/presentation/dto/model-provider-config.d.ts +6 -0
  131. package/dist/presentation/dto/model-provider-config.d.ts.map +1 -0
  132. package/dist/presentation/dto/model-provider-config.js +32 -0
  133. package/dist/presentation/dto/model-provider-config.js.map +1 -0
  134. package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -0
  135. package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -0
  136. package/dist/presentation/e2eRoutes/create-model-provider.js +66 -0
  137. package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -0
  138. package/dist/presentation/e2eRoutes/index.d.ts +3 -0
  139. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  140. package/dist/presentation/e2eRoutes/index.js +9 -0
  141. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  142. package/dist/presentation/index.d.ts +2 -0
  143. package/dist/presentation/index.d.ts.map +1 -0
  144. package/dist/presentation/index.js +3 -0
  145. package/dist/presentation/index.js.map +1 -0
  146. package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -0
  147. package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -0
  148. package/dist/presentation/routes/create-custom-model-provider.js +45 -0
  149. package/dist/presentation/routes/create-custom-model-provider.js.map +1 -0
  150. package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -0
  151. package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -0
  152. package/dist/presentation/routes/delete-model-provider-config.js +38 -0
  153. package/dist/presentation/routes/delete-model-provider-config.js.map +1 -0
  154. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -0
  155. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -0
  156. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +37 -0
  157. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -0
  158. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts +2 -0
  159. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts.map +1 -0
  160. package/dist/presentation/routes/discover-custom-model-provider-models.js +31 -0
  161. package/dist/presentation/routes/discover-custom-model-provider-models.js.map +1 -0
  162. package/dist/presentation/routes/errors.d.ts +2 -0
  163. package/dist/presentation/routes/errors.d.ts.map +1 -0
  164. package/dist/presentation/routes/errors.js +112 -0
  165. package/dist/presentation/routes/errors.js.map +1 -0
  166. package/dist/presentation/routes/index.d.ts +3 -0
  167. package/dist/presentation/routes/index.d.ts.map +1 -0
  168. package/dist/presentation/routes/index.js +39 -0
  169. package/dist/presentation/routes/index.js.map +1 -0
  170. package/dist/presentation/routes/list-model-provider-catalog.d.ts +2 -0
  171. package/dist/presentation/routes/list-model-provider-catalog.d.ts.map +1 -0
  172. package/dist/presentation/routes/list-model-provider-catalog.js +18 -0
  173. package/dist/presentation/routes/list-model-provider-catalog.js.map +1 -0
  174. package/dist/presentation/routes/list-model-provider-configs.d.ts +2 -0
  175. package/dist/presentation/routes/list-model-provider-configs.d.ts.map +1 -0
  176. package/dist/presentation/routes/list-model-provider-configs.js +37 -0
  177. package/dist/presentation/routes/list-model-provider-configs.js.map +1 -0
  178. package/dist/presentation/routes/set-default-harness.d.ts +2 -0
  179. package/dist/presentation/routes/set-default-harness.d.ts.map +1 -0
  180. package/dist/presentation/routes/set-default-harness.js +38 -0
  181. package/dist/presentation/routes/set-default-harness.js.map +1 -0
  182. package/dist/presentation/routes/set-default-model-provider.d.ts +2 -0
  183. package/dist/presentation/routes/set-default-model-provider.d.ts.map +1 -0
  184. package/dist/presentation/routes/set-default-model-provider.js +48 -0
  185. package/dist/presentation/routes/set-default-model-provider.js.map +1 -0
  186. package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -0
  187. package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -0
  188. package/dist/presentation/routes/update-custom-model-provider.js +47 -0
  189. package/dist/presentation/routes/update-custom-model-provider.js.map +1 -0
  190. package/dist/presentation/routes/update-model-provider-default-model.d.ts +2 -0
  191. package/dist/presentation/routes/update-model-provider-default-model.d.ts.map +1 -0
  192. package/dist/presentation/routes/update-model-provider-default-model.js +38 -0
  193. package/dist/presentation/routes/update-model-provider-default-model.js.map +1 -0
  194. package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -0
  195. package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -0
  196. package/dist/presentation/routes/upsert-model-provider-config.js +52 -0
  197. package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -0
  198. package/dist/tsconfig.test.tsbuildinfo +1 -0
  199. package/drizzle/0000_daffy_leopardon.sql +28 -0
  200. package/drizzle/0001_default_harness.sql +1 -0
  201. package/drizzle/meta/0000_snapshot.json +199 -0
  202. package/drizzle/meta/0001_snapshot.json +205 -0
  203. package/drizzle/meta/_journal.json +20 -0
  204. package/drizzle.config.ts +7 -0
  205. package/package.json +109 -0
  206. package/src/config.test.ts +118 -0
  207. package/src/config.ts +75 -0
  208. package/src/core/catalog-registry.test.ts +48 -0
  209. package/src/core/credential-fingerprints.test.ts +34 -0
  210. package/src/core/credential-fingerprints.ts +84 -0
  211. package/src/core/custom-model-provider-config-service.ts +537 -0
  212. package/src/core/custom-provider-url.ts +24 -0
  213. package/src/core/discover-custom-model-provider-models.ts +147 -0
  214. package/src/core/entities/agent-workspace-settings.ts +9 -0
  215. package/src/core/entities/model-provider-config.ts +25 -0
  216. package/src/core/errors.ts +139 -0
  217. package/src/core/harness/claude.ts +75 -0
  218. package/src/core/harness/index.ts +19 -0
  219. package/src/core/harness/pi.ts +18 -0
  220. package/src/core/harness/registry.test.ts +281 -0
  221. package/src/core/harness/registry.ts +108 -0
  222. package/src/core/index.ts +56 -0
  223. package/src/core/model-provider-catalog.ts +33 -0
  224. package/src/core/model-provider-config-service.test.ts +940 -0
  225. package/src/core/model-provider-config-service.ts +207 -0
  226. package/src/core/model-provider-validation.test.ts +354 -0
  227. package/src/core/model-provider-validation.ts +276 -0
  228. package/src/core/resolve-agent-config.test.ts +441 -0
  229. package/src/core/resolve-agent-config.ts +272 -0
  230. package/src/core/resolve-runtime-credentials.test.ts +316 -0
  231. package/src/core/resolve-runtime-credentials.ts +147 -0
  232. package/src/core/workspace-agent-defaults-resolver.ts +42 -0
  233. package/src/db/agent-defaults-snapshot.ts +49 -0
  234. package/src/db/agent-workspace-settings.test.ts +111 -0
  235. package/src/db/agent-workspace-settings.ts +92 -0
  236. package/src/db/db.ts +20 -0
  237. package/src/db/index.ts +29 -0
  238. package/src/db/model-provider-configs.test.ts +281 -0
  239. package/src/db/model-provider-configs.ts +230 -0
  240. package/src/db/schema/agent-workspace-settings.ts +27 -0
  241. package/src/db/schema/common.ts +3 -0
  242. package/src/db/schema/model-provider-configs.ts +80 -0
  243. package/src/index.ts +43 -0
  244. package/src/metrics/index.ts +1 -0
  245. package/src/metrics/instance.ts +18 -0
  246. package/src/presentation/dto/index.ts +5 -0
  247. package/src/presentation/dto/model-provider-config.ts +41 -0
  248. package/src/presentation/e2eRoutes/create-model-provider.test.ts +47 -0
  249. package/src/presentation/e2eRoutes/create-model-provider.ts +61 -0
  250. package/src/presentation/e2eRoutes/index.ts +7 -0
  251. package/src/presentation/index.ts +1 -0
  252. package/src/presentation/routes/create-custom-model-provider.ts +45 -0
  253. package/src/presentation/routes/delete-model-provider-config.ts +31 -0
  254. package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +42 -0
  255. package/src/presentation/routes/discover-custom-model-provider-models.ts +29 -0
  256. package/src/presentation/routes/errors.ts +122 -0
  257. package/src/presentation/routes/index.ts +37 -0
  258. package/src/presentation/routes/list-model-provider-catalog.ts +15 -0
  259. package/src/presentation/routes/list-model-provider-configs.ts +33 -0
  260. package/src/presentation/routes/model-provider-catalog.test.ts +73 -0
  261. package/src/presentation/routes/model-provider-config.test.ts +807 -0
  262. package/src/presentation/routes/set-default-harness.ts +33 -0
  263. package/src/presentation/routes/set-default-model-provider.ts +47 -0
  264. package/src/presentation/routes/update-custom-model-provider.ts +50 -0
  265. package/src/presentation/routes/update-model-provider-default-model.ts +40 -0
  266. package/src/presentation/routes/upsert-model-provider-config.ts +53 -0
  267. package/test/env.ts +8 -0
  268. package/test/globalSetup.ts +25 -0
  269. package/test/setup.ts +17 -0
  270. package/tsconfig.build.json +9 -0
  271. package/tsconfig.build.tsbuildinfo +1 -0
  272. package/tsconfig.json +3 -0
  273. package/tsconfig.test.json +8 -0
  274. package/vitest.config.ts +11 -0
@@ -0,0 +1,80 @@
1
+ import type {
2
+ AgentThinking,
3
+ CustomAgentModelDto,
4
+ CustomModelProviderHeaderDto,
5
+ ModelProviderApi,
6
+ ModelProviderRef,
7
+ } from '@shipfox/api-agent-dto';
8
+ import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
9
+ import {sql} from 'drizzle-orm';
10
+ import {
11
+ boolean,
12
+ check,
13
+ jsonb,
14
+ pgEnum,
15
+ pgTable,
16
+ text,
17
+ timestamp,
18
+ uniqueIndex,
19
+ uuid,
20
+ } from 'drizzle-orm/pg-core';
21
+ import type {ModelProviderConfig} from '#core/entities/model-provider-config.js';
22
+
23
+ export const modelProviderConfigKindEnum = pgEnum('model_provider_config_kind', [
24
+ 'builtin',
25
+ 'custom',
26
+ ]);
27
+
28
+ export const modelProviderConfigs = pgTable(
29
+ 'model_provider_configs',
30
+ {
31
+ id: uuidv7PrimaryKey(),
32
+ workspaceId: uuid('workspace_id').notNull(),
33
+ providerId: text('provider_id').notNull(),
34
+ kind: modelProviderConfigKindEnum('kind').notNull().default('builtin'),
35
+ displayName: text('display_name'),
36
+ api: text('api').$type<ModelProviderApi>(),
37
+ baseUrl: text('base_url'),
38
+ headers: jsonb('headers').$type<CustomModelProviderHeaderDto[]>(),
39
+ secretHeaderNames: jsonb('secret_header_names').$type<string[]>(),
40
+ models: jsonb('models').$type<CustomAgentModelDto[]>(),
41
+ requiresApiKey: boolean('requires_api_key').notNull().default(false),
42
+ defaultModel: text('default_model'),
43
+ defaultThinking: text('default_thinking').notNull(),
44
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
45
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
46
+ },
47
+ (table) => [
48
+ uniqueIndex('model_provider_configs_workspace_provider_unique').on(
49
+ table.workspaceId,
50
+ table.providerId,
51
+ ),
52
+ check(
53
+ 'model_provider_configs_custom_required_fields',
54
+ sql`${table.kind} <> 'custom' OR (${table.api} IS NOT NULL AND ${table.baseUrl} IS NOT NULL AND ${table.models} IS NOT NULL AND ${table.displayName} IS NOT NULL)`,
55
+ ),
56
+ ],
57
+ );
58
+
59
+ export type ModelProviderConfigDb = typeof modelProviderConfigs.$inferSelect;
60
+ export type ModelProviderConfigCreateDb = typeof modelProviderConfigs.$inferInsert;
61
+
62
+ export function toModelProviderConfig(row: ModelProviderConfigDb): ModelProviderConfig {
63
+ return {
64
+ id: row.id,
65
+ workspaceId: row.workspaceId,
66
+ providerId: row.providerId as ModelProviderRef,
67
+ kind: row.kind,
68
+ displayName: row.displayName,
69
+ api: row.api,
70
+ baseUrl: row.baseUrl,
71
+ headers: row.headers,
72
+ secretHeaderNames: row.secretHeaderNames,
73
+ models: row.models,
74
+ requiresApiKey: row.requiresApiKey,
75
+ defaultModel: row.defaultModel,
76
+ defaultThinking: row.defaultThinking as AgentThinking,
77
+ createdAt: row.createdAt,
78
+ updatedAt: row.updatedAt,
79
+ };
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type {ShipfoxModule} from '@shipfox/node-module';
2
+ import {db, migrationsPath} from '#db/index.js';
3
+ import {agentE2eRoutes} from '#presentation/e2eRoutes/index.js';
4
+ import {routes} from '#presentation/index.js';
5
+
6
+ export {
7
+ type AgentConfigResolutionContext,
8
+ type AgentDefaultsResolver,
9
+ type AgentWorkspaceSettings,
10
+ buildModelProviderCatalog,
11
+ type ContextualAgentConfig,
12
+ catalogDefaultAgentResolver,
13
+ createCustomModelProviderConfig,
14
+ createWorkspaceAgentDefaultsResolver,
15
+ deleteModelProviderConfig,
16
+ InvalidAgentModelError,
17
+ InvalidCredentialFieldsError,
18
+ type ModelProviderConfig,
19
+ ModelProviderConfigNotFoundError,
20
+ ModelProviderValidationError,
21
+ ModelProviderValidationUnavailableError,
22
+ type ResolvedAgentConfig,
23
+ type ResolveRuntimeCredentialsParams,
24
+ resolveAgentConfig,
25
+ resolveRuntimeCredentials,
26
+ testAndSaveModelProviderConfig,
27
+ UnsupportedModelProviderError,
28
+ updateCustomModelProviderConfig,
29
+ } from '#core/index.js';
30
+ export {
31
+ getAgentWorkspaceSettings,
32
+ getModelProviderConfig,
33
+ listModelProviderConfigs,
34
+ setDefaultModelProvider,
35
+ upsertModelProviderConfig,
36
+ } from '#db/index.js';
37
+
38
+ export const agentModule: ShipfoxModule = {
39
+ name: 'agent',
40
+ database: {db, migrationsPath},
41
+ routes,
42
+ e2eRoutes: [agentE2eRoutes],
43
+ };
@@ -0,0 +1 @@
1
+ export {agentRuntimeConfigResolvedCount, modelProviderValidationCount} from './instance.js';
@@ -0,0 +1,18 @@
1
+ import type {SupportedModelProviderId} from '@shipfox/api-agent-dto';
2
+ import {instanceMetrics} from '@shipfox/node-opentelemetry';
3
+
4
+ const meter = instanceMetrics.getMeter('agent');
5
+
6
+ export const modelProviderValidationCount = meter.createCounter<{
7
+ model_provider: SupportedModelProviderId | 'custom';
8
+ outcome: 'succeeded' | 'failed';
9
+ }>('model_provider_validation_attempted', {
10
+ description: 'Model provider credential test attempts by model provider and outcome',
11
+ });
12
+
13
+ export const agentRuntimeConfigResolvedCount = meter.createCounter<{
14
+ source: 'workspace' | 'instance';
15
+ outcome: 'resolved' | 'unavailable' | 'decryption_failed';
16
+ }>('agent_runtime_config_resolved', {
17
+ description: 'Lease-scoped agent runtime credential resolution by source and outcome',
18
+ });
@@ -0,0 +1,5 @@
1
+ export {
2
+ toCustomModelProviderConfigDto,
3
+ toModelProviderConfigDto,
4
+ toModelProviderConfigResponseDto,
5
+ } from './model-provider-config.js';
@@ -0,0 +1,41 @@
1
+ import type {
2
+ CustomModelProviderConfigDto,
3
+ ModelProviderConfigDto,
4
+ ModelProviderConfigResponseDto,
5
+ } from '@shipfox/api-agent-dto';
6
+ import type {ModelProviderConfig} from '#core/index.js';
7
+
8
+ export function toModelProviderConfigDto(config: ModelProviderConfig): ModelProviderConfigDto {
9
+ return {
10
+ kind: 'builtin',
11
+ provider_id: config.providerId,
12
+ default_model: config.defaultModel,
13
+ created_at: config.createdAt.toISOString(),
14
+ updated_at: config.updatedAt.toISOString(),
15
+ };
16
+ }
17
+
18
+ export function toCustomModelProviderConfigDto(
19
+ config: ModelProviderConfig,
20
+ ): CustomModelProviderConfigDto {
21
+ return {
22
+ kind: 'custom',
23
+ provider_id: config.providerId,
24
+ display_name: config.displayName ?? config.providerId,
25
+ api: config.api ?? 'openai-responses',
26
+ base_url: config.baseUrl ?? '',
27
+ headers: config.headers ?? [],
28
+ secret_header_names: [...(config.secretHeaderNames ?? [])].sort(),
29
+ models: config.models ?? [],
30
+ default_model: config.defaultModel,
31
+ created_at: config.createdAt.toISOString(),
32
+ updated_at: config.updatedAt.toISOString(),
33
+ };
34
+ }
35
+
36
+ export function toModelProviderConfigResponseDto(
37
+ config: ModelProviderConfig,
38
+ ): ModelProviderConfigResponseDto {
39
+ if (config.kind === 'custom') return toCustomModelProviderConfigDto(config);
40
+ return toModelProviderConfigDto(config);
41
+ }
@@ -0,0 +1,47 @@
1
+ import {closeApp, createApp} from '@shipfox/node-fastify';
2
+ import {agentE2eRoutes} from './index.js';
3
+
4
+ describe('agent e2e routes', () => {
5
+ afterEach(async () => {
6
+ await closeApp();
7
+ });
8
+
9
+ it('rejects malformed workspace ids through route validation', async () => {
10
+ const app = await createApp({routes: [agentE2eRoutes], swagger: false});
11
+
12
+ const res = await app.inject({
13
+ method: 'POST',
14
+ url: '/agent/model-provider',
15
+ payload: {
16
+ workspace_id: 'not-a-uuid',
17
+ provider_id: 'anthropic',
18
+ api_key: 'sk-e2e-placeholder',
19
+ default_model: 'claude-opus-4-8',
20
+ },
21
+ });
22
+
23
+ expect(res.statusCode).toBe(400);
24
+ expect(res.json()).toMatchObject({code: 'validation-error'});
25
+ });
26
+
27
+ it('rejects default models outside the Anthropic catalog', async () => {
28
+ const app = await createApp({routes: [agentE2eRoutes], swagger: false});
29
+
30
+ const res = await app.inject({
31
+ method: 'POST',
32
+ url: '/agent/model-provider',
33
+ payload: {
34
+ workspace_id: crypto.randomUUID(),
35
+ provider_id: 'anthropic',
36
+ api_key: 'sk-e2e-placeholder',
37
+ default_model: 'missing-model',
38
+ },
39
+ });
40
+
41
+ expect(res.statusCode).toBe(400);
42
+ expect(res.json()).toMatchObject({
43
+ code: 'unsupported-model',
44
+ details: {default_model: 'missing-model'},
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,61 @@
1
+ import {DEFAULT_AGENT_THINKING, getModelProviderEntry} from '@shipfox/api-agent-dto';
2
+ import {setSecrets} from '@shipfox/api-secrets';
3
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
+ import {z} from 'zod';
5
+ import {agentSystemNamespace} from '#core/credential-fingerprints.js';
6
+ import {listHarnessProviderModels} from '#core/harness/registry.js';
7
+ import {upsertModelProviderConfig} from '#db/index.js';
8
+
9
+ const createE2eModelProviderBodySchema = z.object({
10
+ workspace_id: z.string().uuid(),
11
+ provider_id: z.literal('anthropic'),
12
+ api_key: z.string().min(1),
13
+ default_model: z.string().min(1).optional(),
14
+ set_as_default: z.boolean().optional(),
15
+ });
16
+
17
+ const createE2eModelProviderResponseSchema = z.object({
18
+ provider_id: z.literal('anthropic'),
19
+ });
20
+
21
+ export const createE2eModelProviderRoute = defineRoute({
22
+ method: 'POST',
23
+ path: '/model-provider',
24
+ description: 'Create an Anthropic model provider config for E2E tests.',
25
+ schema: {
26
+ body: createE2eModelProviderBodySchema,
27
+ response: {201: createE2eModelProviderResponseSchema},
28
+ },
29
+ handler: async (request, reply) => {
30
+ const entry = getModelProviderEntry(request.body.provider_id);
31
+ if (entry === undefined || entry.default_model === null) {
32
+ throw new ClientError('Unsupported E2E model provider', 'unsupported-model-provider', {
33
+ status: 400,
34
+ });
35
+ }
36
+ const defaultModel = request.body.default_model ?? entry.default_model;
37
+ const supportedModels = listHarnessProviderModels('claude', request.body.provider_id);
38
+ if (!supportedModels.some((model) => model.id === defaultModel)) {
39
+ throw new ClientError('Unsupported Anthropic default model', 'unsupported-model', {
40
+ status: 400,
41
+ details: {default_model: defaultModel},
42
+ });
43
+ }
44
+
45
+ await setSecrets({
46
+ workspaceId: request.body.workspace_id,
47
+ namespace: agentSystemNamespace(request.body.provider_id),
48
+ values: {API_KEY: request.body.api_key},
49
+ });
50
+ await upsertModelProviderConfig({
51
+ workspaceId: request.body.workspace_id,
52
+ providerId: request.body.provider_id,
53
+ defaultModel,
54
+ defaultThinking: DEFAULT_AGENT_THINKING,
55
+ setAsDefault: request.body.set_as_default,
56
+ });
57
+
58
+ reply.code(201);
59
+ return {provider_id: request.body.provider_id};
60
+ },
61
+ });
@@ -0,0 +1,7 @@
1
+ import type {RouteGroup} from '@shipfox/node-fastify';
2
+ import {createE2eModelProviderRoute} from './create-model-provider.js';
3
+
4
+ export const agentE2eRoutes: RouteGroup = {
5
+ prefix: '/agent',
6
+ routes: [createE2eModelProviderRoute],
7
+ };
@@ -0,0 +1 @@
1
+ export {agentRoutes as routes} from './routes/index.js';
@@ -0,0 +1,45 @@
1
+ import {
2
+ createCustomModelProviderBodySchema,
3
+ customModelProviderConfigDtoSchema,
4
+ } from '@shipfox/api-agent-dto';
5
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
6
+ import {defineRoute} from '@shipfox/node-fastify';
7
+ import {z} from 'zod';
8
+ import {createCustomModelProviderConfig} from '#core/index.js';
9
+ import {toCustomModelProviderConfigDto} from '#presentation/dto/index.js';
10
+ import {translateModelProviderRouteError} from './errors.js';
11
+
12
+ export const createCustomModelProviderRoute = defineRoute({
13
+ method: 'POST',
14
+ path: '/custom-model-providers',
15
+ description: 'Create a custom model provider configuration for a workspace',
16
+ schema: {
17
+ params: z.object({workspaceId: z.string().uuid()}),
18
+ body: createCustomModelProviderBodySchema,
19
+ response: {
20
+ 200: customModelProviderConfigDtoSchema,
21
+ },
22
+ },
23
+ errorHandler: translateModelProviderRouteError,
24
+ handler: async (request, reply) => {
25
+ const {workspaceId} = request.params;
26
+ const abortController = new AbortController();
27
+ let responseFinished = false;
28
+ reply.raw.on('finish', () => {
29
+ responseFinished = true;
30
+ });
31
+ reply.raw.on('close', () => {
32
+ if (!responseFinished) abortController.abort();
33
+ });
34
+
35
+ requireWorkspaceAccess({request, workspaceId});
36
+
37
+ const config = await createCustomModelProviderConfig({
38
+ workspaceId,
39
+ body: request.body,
40
+ signal: abortController.signal,
41
+ });
42
+
43
+ return toCustomModelProviderConfigDto(config);
44
+ },
45
+ });
@@ -0,0 +1,31 @@
1
+ import {modelProviderRefSchema} from '@shipfox/api-agent-dto';
2
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
3
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
+ import {z} from 'zod';
5
+ import {deleteModelProviderConfig} from '#core/index.js';
6
+
7
+ export const deleteModelProviderConfigRoute = defineRoute({
8
+ method: 'DELETE',
9
+ path: '/model-providers/:providerId',
10
+ description: 'Delete a model provider configuration for a workspace',
11
+ schema: {
12
+ params: z.object({
13
+ workspaceId: z.string().uuid(),
14
+ providerId: modelProviderRefSchema,
15
+ }),
16
+ response: {
17
+ 204: z.void(),
18
+ },
19
+ },
20
+ handler: async (request, reply) => {
21
+ const {workspaceId, providerId} = request.params;
22
+ requireWorkspaceAccess({request, workspaceId});
23
+
24
+ const deleted = await deleteModelProviderConfig({workspaceId, providerId});
25
+ if (!deleted) {
26
+ throw new ClientError('Model provider configuration not found', 'not-found', {status: 404});
27
+ }
28
+
29
+ reply.code(204);
30
+ },
31
+ });
@@ -0,0 +1,42 @@
1
+ import {
2
+ discoverCustomModelProviderModelsBySlugBodySchema,
3
+ discoverCustomModelProviderModelsResponseSchema,
4
+ modelProviderRefSchema,
5
+ } from '@shipfox/api-agent-dto';
6
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
7
+ import {defineRoute} from '@shipfox/node-fastify';
8
+ import {z} from 'zod';
9
+ import {
10
+ discoverCustomModelProviderModels,
11
+ resolveCustomModelProviderDiscoveryParams,
12
+ } from '#core/index.js';
13
+ import {translateModelProviderRouteError} from './errors.js';
14
+
15
+ export const discoverCustomModelProviderModelsBySlugRoute = defineRoute({
16
+ method: 'POST',
17
+ path: '/custom-model-providers/:slug/discover-models',
18
+ description: 'Discover models for a stored custom model provider configuration',
19
+ schema: {
20
+ params: z.object({
21
+ workspaceId: z.string().uuid(),
22
+ slug: modelProviderRefSchema,
23
+ }),
24
+ body: discoverCustomModelProviderModelsBySlugBodySchema,
25
+ response: {
26
+ 200: discoverCustomModelProviderModelsResponseSchema,
27
+ },
28
+ },
29
+ errorHandler: translateModelProviderRouteError,
30
+ handler: async (request) => {
31
+ const {workspaceId, slug} = request.params;
32
+ requireWorkspaceAccess({request, workspaceId});
33
+
34
+ const discoveryParams = await resolveCustomModelProviderDiscoveryParams({
35
+ workspaceId,
36
+ providerId: slug,
37
+ body: request.body ?? {},
38
+ });
39
+
40
+ return await discoverCustomModelProviderModels(discoveryParams);
41
+ },
42
+ });
@@ -0,0 +1,29 @@
1
+ import {
2
+ discoverCustomModelProviderModelsBodySchema,
3
+ discoverCustomModelProviderModelsResponseSchema,
4
+ } from '@shipfox/api-agent-dto';
5
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
6
+ import {defineRoute} from '@shipfox/node-fastify';
7
+ import {z} from 'zod';
8
+ import {discoverCustomModelProviderModels} from '#core/index.js';
9
+ import {translateModelProviderRouteError} from './errors.js';
10
+
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,
20
+ },
21
+ },
22
+ errorHandler: translateModelProviderRouteError,
23
+ handler: async (request) => {
24
+ const {workspaceId} = request.params;
25
+ requireWorkspaceAccess({request, workspaceId});
26
+
27
+ return await discoverCustomModelProviderModels(request.body);
28
+ },
29
+ });
@@ -0,0 +1,122 @@
1
+ import {getModelProviderCredentialKeys} from '@shipfox/api-agent-dto';
2
+ import {SecretValueTooLargeError, WorkspaceSecretCapExceededError} from '@shipfox/api-secrets';
3
+ import {EgressDeniedError} from '@shipfox/node-egress-guard';
4
+ import {ClientError} from '@shipfox/node-fastify';
5
+ import {
6
+ CustomModelProviderConfigNotFoundError,
7
+ CustomModelProviderSlugCollisionError,
8
+ CustomModelProviderStoredSecretBaseUrlChangeError,
9
+ InvalidAgentModelError,
10
+ InvalidCredentialFieldsError,
11
+ InvalidCustomModelProviderHeaderKeepError,
12
+ ModelProviderConfigNotFoundError,
13
+ ModelProviderValidationError,
14
+ UnsupportedModelProviderError,
15
+ } from '#core/index.js';
16
+
17
+ export function translateModelProviderRouteError(error: unknown): never {
18
+ if (error instanceof ModelProviderValidationError) {
19
+ throw new ClientError(error.sanitizedMessage, 'provider-validation-failed', {
20
+ status: 422,
21
+ details: {
22
+ provider_id: error.providerId,
23
+ message: error.sanitizedMessage,
24
+ },
25
+ });
26
+ }
27
+
28
+ if (error instanceof EgressDeniedError) {
29
+ throw new ClientError('Egress denied', 'egress-denied', {
30
+ status: 400,
31
+ details: {
32
+ reason: error.reason,
33
+ target: error.target,
34
+ },
35
+ });
36
+ }
37
+
38
+ if (error instanceof CustomModelProviderSlugCollisionError) {
39
+ throw new ClientError('Custom model provider slug already exists', 'slug-collision', {
40
+ status: 409,
41
+ details: {provider_id: error.providerId},
42
+ });
43
+ }
44
+
45
+ if (error instanceof CustomModelProviderConfigNotFoundError) {
46
+ throw new ClientError('Custom model provider configuration not found', 'not-found', {
47
+ status: 404,
48
+ details: {provider_id: error.providerId},
49
+ });
50
+ }
51
+
52
+ if (error instanceof InvalidCustomModelProviderHeaderKeepError) {
53
+ throw new ClientError('Invalid kept secret header', 'invalid-header-keep', {
54
+ status: 422,
55
+ details: {provider_id: error.providerId, name: error.headerName},
56
+ });
57
+ }
58
+
59
+ if (error instanceof CustomModelProviderStoredSecretBaseUrlChangeError) {
60
+ throw new ClientError(
61
+ 'Stored custom model provider secrets cannot be reused with a changed base URL',
62
+ 'stored-secret-base-url-changed',
63
+ {
64
+ status: 422,
65
+ details: {provider_id: error.providerId},
66
+ },
67
+ );
68
+ }
69
+
70
+ if (error instanceof InvalidCredentialFieldsError) {
71
+ throw new ClientError('Invalid credential fields', 'invalid-credential-fields', {
72
+ status: 422,
73
+ details: {
74
+ provider_id: error.providerId,
75
+ expected_keys: getModelProviderCredentialKeys(error.providerId) ?? [],
76
+ },
77
+ });
78
+ }
79
+
80
+ if (error instanceof InvalidAgentModelError) {
81
+ throw new ClientError('Invalid agent model', 'invalid-agent-model', {
82
+ status: 422,
83
+ details: {
84
+ harness: error.harness,
85
+ provider_id: error.providerId,
86
+ model: error.model,
87
+ },
88
+ });
89
+ }
90
+
91
+ if (error instanceof UnsupportedModelProviderError) {
92
+ throw new ClientError('Provider is not supported', 'provider-unsupported', {
93
+ status: 422,
94
+ details: {provider_id: error.providerId},
95
+ });
96
+ }
97
+
98
+ if (error instanceof ModelProviderConfigNotFoundError) {
99
+ throw new ClientError('Provider is not configured', 'provider-not-configured', {
100
+ status: 422,
101
+ details: {
102
+ provider_id: error.providerId,
103
+ },
104
+ });
105
+ }
106
+
107
+ if (error instanceof WorkspaceSecretCapExceededError) {
108
+ throw new ClientError('Workspace secret cap exceeded', 'workspace-secret-cap-exceeded', {
109
+ status: 409,
110
+ details: {cap: error.cap},
111
+ });
112
+ }
113
+
114
+ if (error instanceof SecretValueTooLargeError) {
115
+ throw new ClientError(error.message, 'value-too-large', {
116
+ status: 400,
117
+ details: {max_bytes: error.maxBytes},
118
+ });
119
+ }
120
+
121
+ throw error;
122
+ }
@@ -0,0 +1,37 @@
1
+ import {AUTH_USER} from '@shipfox/api-auth-context';
2
+ import type {RouteGroup} from '@shipfox/node-fastify';
3
+ import {createCustomModelProviderRoute} from './create-custom-model-provider.js';
4
+ import {deleteModelProviderConfigRoute} from './delete-model-provider-config.js';
5
+ import {discoverCustomModelProviderModelsRoute} from './discover-custom-model-provider-models.js';
6
+ import {discoverCustomModelProviderModelsBySlugRoute} from './discover-custom-model-provider-models-by-slug.js';
7
+ import {listModelProviderCatalogRoute} from './list-model-provider-catalog.js';
8
+ import {listModelProviderConfigsRoute} from './list-model-provider-configs.js';
9
+ import {setDefaultHarnessRoute} from './set-default-harness.js';
10
+ import {setDefaultModelProviderRoute} from './set-default-model-provider.js';
11
+ import {updateCustomModelProviderRoute} from './update-custom-model-provider.js';
12
+ import {updateModelProviderDefaultModelRoute} from './update-model-provider-default-model.js';
13
+ import {upsertModelProviderConfigRoute} from './upsert-model-provider-config.js';
14
+
15
+ export const agentRoutes: RouteGroup[] = [
16
+ {
17
+ prefix: '/workspaces/:workspaceId/agent',
18
+ auth: AUTH_USER,
19
+ routes: [
20
+ listModelProviderConfigsRoute,
21
+ createCustomModelProviderRoute,
22
+ discoverCustomModelProviderModelsRoute,
23
+ discoverCustomModelProviderModelsBySlugRoute,
24
+ updateCustomModelProviderRoute,
25
+ upsertModelProviderConfigRoute,
26
+ updateModelProviderDefaultModelRoute,
27
+ deleteModelProviderConfigRoute,
28
+ setDefaultHarnessRoute,
29
+ setDefaultModelProviderRoute,
30
+ ],
31
+ },
32
+ {
33
+ prefix: '/agent',
34
+ auth: AUTH_USER,
35
+ routes: [listModelProviderCatalogRoute],
36
+ },
37
+ ];
@@ -0,0 +1,15 @@
1
+ import {modelProviderCatalogResponseSchema} from '@shipfox/api-agent-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {buildModelProviderCatalog} from '#core/index.js';
4
+
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,
12
+ },
13
+ },
14
+ handler: () => ({providers: buildModelProviderCatalog()}),
15
+ });
@@ -0,0 +1,33 @@
1
+ import {listModelProviderConfigsResponseSchema} from '@shipfox/api-agent-dto';
2
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
3
+ import {defineRoute} from '@shipfox/node-fastify';
4
+ import {z} from 'zod';
5
+ import {getAgentWorkspaceSettings, listModelProviderConfigs} from '#db/index.js';
6
+ import {toModelProviderConfigResponseDto} from '#presentation/dto/index.js';
7
+
8
+ export const listModelProviderConfigsRoute = defineRoute({
9
+ method: 'GET',
10
+ path: '/model-providers',
11
+ description: 'List model provider configurations for a workspace',
12
+ schema: {
13
+ params: z.object({workspaceId: z.string().uuid()}),
14
+ response: {
15
+ 200: listModelProviderConfigsResponseSchema,
16
+ },
17
+ },
18
+ handler: async (request) => {
19
+ const {workspaceId} = request.params;
20
+ requireWorkspaceAccess({request, workspaceId});
21
+
22
+ const [configs, settings] = await Promise.all([
23
+ listModelProviderConfigs(workspaceId),
24
+ getAgentWorkspaceSettings(workspaceId),
25
+ ]);
26
+
27
+ return {
28
+ configs: configs.map(toModelProviderConfigResponseDto),
29
+ default_harness_id: settings?.defaultHarnessId ?? null,
30
+ default_provider_id: settings?.defaultProviderId ?? null,
31
+ };
32
+ },
33
+ });