@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,92 @@
1
+ import type {Harness, ModelProviderRef} from '@shipfox/api-agent-dto';
2
+ import {and, eq, sql} from 'drizzle-orm';
3
+ import type {AgentWorkspaceSettings} from '#core/entities/agent-workspace-settings.js';
4
+ import {ModelProviderConfigNotFoundError} from '#core/errors.js';
5
+ import {db} from './db.js';
6
+ import {
7
+ agentWorkspaceSettings,
8
+ toAgentWorkspaceSettings,
9
+ } from './schema/agent-workspace-settings.js';
10
+ import {modelProviderConfigs} from './schema/model-provider-configs.js';
11
+
12
+ export async function getAgentWorkspaceSettings(
13
+ workspaceId: string,
14
+ ): Promise<AgentWorkspaceSettings | undefined> {
15
+ const rows = await db()
16
+ .select()
17
+ .from(agentWorkspaceSettings)
18
+ .where(eq(agentWorkspaceSettings.workspaceId, workspaceId))
19
+ .limit(1);
20
+
21
+ const row = rows[0];
22
+ if (!row) return undefined;
23
+ return toAgentWorkspaceSettings(row);
24
+ }
25
+
26
+ export async function setDefaultModelProvider(params: {
27
+ workspaceId: string;
28
+ providerId: ModelProviderRef | null;
29
+ }): Promise<AgentWorkspaceSettings> {
30
+ return await db().transaction(async (tx) => {
31
+ if (params.providerId !== null) {
32
+ const existingRows = await tx
33
+ .select({id: modelProviderConfigs.id})
34
+ .from(modelProviderConfigs)
35
+ .where(
36
+ and(
37
+ eq(modelProviderConfigs.workspaceId, params.workspaceId),
38
+ eq(modelProviderConfigs.providerId, params.providerId),
39
+ ),
40
+ )
41
+ .limit(1)
42
+ .for('update');
43
+
44
+ if (!existingRows[0]) {
45
+ throw new ModelProviderConfigNotFoundError(params.workspaceId, params.providerId);
46
+ }
47
+ }
48
+
49
+ const rows = await tx
50
+ .insert(agentWorkspaceSettings)
51
+ .values({
52
+ workspaceId: params.workspaceId,
53
+ defaultProviderId: params.providerId,
54
+ })
55
+ .onConflictDoUpdate({
56
+ target: agentWorkspaceSettings.workspaceId,
57
+ set: {
58
+ defaultProviderId: params.providerId,
59
+ updatedAt: sql`NOW()`,
60
+ },
61
+ })
62
+ .returning();
63
+
64
+ const row = rows[0];
65
+ if (!row) throw new Error('Upsert returned no rows');
66
+ return toAgentWorkspaceSettings(row);
67
+ });
68
+ }
69
+
70
+ export async function setDefaultHarness(params: {
71
+ workspaceId: string;
72
+ harnessId: Harness;
73
+ }): Promise<AgentWorkspaceSettings> {
74
+ const rows = await db()
75
+ .insert(agentWorkspaceSettings)
76
+ .values({
77
+ workspaceId: params.workspaceId,
78
+ defaultHarnessId: params.harnessId,
79
+ })
80
+ .onConflictDoUpdate({
81
+ target: agentWorkspaceSettings.workspaceId,
82
+ set: {
83
+ defaultHarnessId: params.harnessId,
84
+ updatedAt: sql`NOW()`,
85
+ },
86
+ })
87
+ .returning();
88
+
89
+ const row = rows[0];
90
+ if (!row) throw new Error('Upsert returned no rows');
91
+ return toAgentWorkspaceSettings(row);
92
+ }
package/src/db/db.ts ADDED
@@ -0,0 +1,20 @@
1
+ import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
2
+ import {pgClient} from '@shipfox/node-postgres';
3
+ import {agentWorkspaceSettings} from './schema/agent-workspace-settings.js';
4
+ import {modelProviderConfigs} from './schema/model-provider-configs.js';
5
+
6
+ export const schema = {
7
+ modelProviderConfigs,
8
+ agentWorkspaceSettings,
9
+ };
10
+
11
+ let _db: NodePgDatabase<typeof schema> | undefined;
12
+
13
+ export function db() {
14
+ if (!_db) _db = drizzle(pgClient(), {schema});
15
+ return _db;
16
+ }
17
+
18
+ export function closeDb(): void {
19
+ _db = undefined;
20
+ }
@@ -0,0 +1,29 @@
1
+ import {dirname, resolve} from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+
4
+ export {
5
+ type AgentWorkspaceDefaultsSnapshot,
6
+ getAgentWorkspaceDefaultsSnapshot,
7
+ } from './agent-defaults-snapshot.js';
8
+ export {
9
+ getAgentWorkspaceSettings,
10
+ setDefaultHarness,
11
+ setDefaultModelProvider,
12
+ } from './agent-workspace-settings.js';
13
+ export {closeDb, db, schema} from './db.js';
14
+ export type {
15
+ InsertCustomModelProviderConfigParams,
16
+ UpsertModelProviderConfigParams,
17
+ } from './model-provider-configs.js';
18
+ export {
19
+ deleteModelProviderConfig,
20
+ getModelProviderConfig,
21
+ insertCustomModelProviderConfig,
22
+ listModelProviderConfigs,
23
+ updateModelProviderDefaultModel,
24
+ upsertModelProviderConfig,
25
+ } from './model-provider-configs.js';
26
+ export {agentWorkspaceSettings} from './schema/agent-workspace-settings.js';
27
+ export {modelProviderConfigs} from './schema/model-provider-configs.js';
28
+
29
+ export const migrationsPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle');
@@ -0,0 +1,281 @@
1
+ import crypto from 'node:crypto';
2
+ import type {
3
+ AgentThinking,
4
+ ModelProviderRef,
5
+ SupportedModelProviderId,
6
+ } from '@shipfox/api-agent-dto';
7
+ import {
8
+ db,
9
+ deleteModelProviderConfig,
10
+ getAgentWorkspaceSettings,
11
+ getModelProviderConfig,
12
+ listModelProviderConfigs,
13
+ setDefaultModelProvider,
14
+ type UpsertModelProviderConfigParams,
15
+ updateModelProviderDefaultModel,
16
+ upsertModelProviderConfig,
17
+ } from '#db/index.js';
18
+ import {modelProviderConfigs} from './schema/model-provider-configs.js';
19
+
20
+ describe('model provider configs', () => {
21
+ let workspaceId: string;
22
+
23
+ beforeEach(() => {
24
+ workspaceId = crypto.randomUUID();
25
+ });
26
+
27
+ it('persists a provider config for a workspace provider pair', async () => {
28
+ const params = createModelProviderConfigParams({workspaceId, providerId: 'anthropic'});
29
+
30
+ const created = await upsertModelProviderConfig(params);
31
+
32
+ const found = await getModelProviderConfig({workspaceId, providerId: 'anthropic'});
33
+ expect(found).toEqual(created);
34
+ expect(found).toMatchObject({
35
+ workspaceId,
36
+ providerId: 'anthropic',
37
+ defaultModel: 'claude-opus-4-8',
38
+ defaultThinking: 'high',
39
+ kind: 'builtin',
40
+ displayName: null,
41
+ api: null,
42
+ baseUrl: null,
43
+ headers: null,
44
+ models: null,
45
+ requiresApiKey: false,
46
+ });
47
+ expect(found?.createdAt).toBeInstanceOf(Date);
48
+ expect(found?.updatedAt).toBeInstanceOf(Date);
49
+ });
50
+
51
+ it('updates the existing config for the same workspace provider pair', async () => {
52
+ await upsertModelProviderConfig(
53
+ createModelProviderConfigParams({workspaceId, providerId: 'openai'}),
54
+ );
55
+
56
+ const updated = await upsertModelProviderConfig(
57
+ createModelProviderConfigParams({
58
+ workspaceId,
59
+ providerId: 'openai',
60
+ defaultModel: 'gpt-5.5-pro',
61
+ defaultThinking: 'medium',
62
+ }),
63
+ );
64
+
65
+ const configs = await listModelProviderConfigs(workspaceId);
66
+ expect(configs).toHaveLength(1);
67
+ expect(configs[0]).toEqual(updated);
68
+ expect(configs[0]).toMatchObject({
69
+ defaultThinking: 'medium',
70
+ });
71
+ });
72
+
73
+ it('lists only configs for the requested workspace', async () => {
74
+ const otherWorkspaceId = crypto.randomUUID();
75
+ await upsertModelProviderConfig(
76
+ createModelProviderConfigParams({workspaceId, providerId: 'openai'}),
77
+ );
78
+ await upsertModelProviderConfig(
79
+ createModelProviderConfigParams({workspaceId, providerId: 'anthropic'}),
80
+ );
81
+ await upsertModelProviderConfig(
82
+ createModelProviderConfigParams({workspaceId: otherWorkspaceId, providerId: 'google'}),
83
+ );
84
+
85
+ const configs = await listModelProviderConfigs(workspaceId);
86
+
87
+ expect(configs.map((config) => config.providerId)).toEqual(['anthropic', 'openai']);
88
+ });
89
+
90
+ it('updates only the provider default model', async () => {
91
+ const created = await upsertModelProviderConfig(
92
+ createModelProviderConfigParams({
93
+ workspaceId,
94
+ providerId: 'anthropic',
95
+ defaultModel: 'claude-opus-4-8',
96
+ }),
97
+ );
98
+
99
+ const updated = await updateModelProviderDefaultModel({
100
+ workspaceId,
101
+ providerId: 'anthropic',
102
+ defaultModel: 'claude-haiku-4-5',
103
+ });
104
+
105
+ expect(updated).toMatchObject({
106
+ defaultModel: 'claude-haiku-4-5',
107
+ defaultThinking: created.defaultThinking,
108
+ });
109
+ });
110
+
111
+ it('returns undefined when updating a missing provider default model', async () => {
112
+ const updated = await updateModelProviderDefaultModel({
113
+ workspaceId,
114
+ providerId: 'anthropic',
115
+ defaultModel: null,
116
+ });
117
+
118
+ expect(updated).toBeUndefined();
119
+ });
120
+
121
+ it('hard-deletes a provider config', async () => {
122
+ await upsertModelProviderConfig(
123
+ createModelProviderConfigParams({workspaceId, providerId: 'anthropic'}),
124
+ );
125
+
126
+ const deleted = await deleteModelProviderConfig({workspaceId, providerId: 'anthropic'});
127
+
128
+ const found = await getModelProviderConfig({workspaceId, providerId: 'anthropic'});
129
+ expect(deleted).toBe(true);
130
+ expect(found).toBeUndefined();
131
+ });
132
+
133
+ it('returns false when deleting a missing provider config', async () => {
134
+ const deleted = await deleteModelProviderConfig({workspaceId, providerId: 'anthropic'});
135
+
136
+ expect(deleted).toBe(false);
137
+ });
138
+
139
+ it('clears the workspace default when the default provider config is deleted', async () => {
140
+ await upsertModelProviderConfig(
141
+ createModelProviderConfigParams({workspaceId, providerId: 'anthropic'}),
142
+ );
143
+ await setDefaultModelProvider({workspaceId, providerId: 'anthropic'});
144
+
145
+ await deleteModelProviderConfig({workspaceId, providerId: 'anthropic'});
146
+
147
+ const settings = await getAgentWorkspaceSettings(workspaceId);
148
+ expect(settings?.defaultProviderId).toBeNull();
149
+ });
150
+
151
+ it('keeps the workspace default when a non-default provider config is deleted', async () => {
152
+ await upsertModelProviderConfig(
153
+ createModelProviderConfigParams({workspaceId, providerId: 'anthropic'}),
154
+ );
155
+ await upsertModelProviderConfig(
156
+ createModelProviderConfigParams({workspaceId, providerId: 'openai'}),
157
+ );
158
+ await setDefaultModelProvider({workspaceId, providerId: 'anthropic'});
159
+
160
+ await deleteModelProviderConfig({workspaceId, providerId: 'openai'});
161
+
162
+ const settings = await getAgentWorkspaceSettings(workspaceId);
163
+ expect(settings?.defaultProviderId).toBe('anthropic');
164
+ });
165
+
166
+ it('round-trips a custom model provider row without storing secret headers in plaintext headers', async () => {
167
+ const created = await upsertModelProviderConfig({
168
+ workspaceId,
169
+ providerId: 'local-vllm',
170
+ kind: 'custom',
171
+ displayName: 'Local vLLM',
172
+ api: 'openai-responses',
173
+ baseUrl: 'https://llm.example.test/v1',
174
+ headers: [{name: 'x-region', value: 'local'}],
175
+ secretHeaderNames: ['authorization'],
176
+ models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
177
+ requiresApiKey: true,
178
+ defaultModel: 'llama-3.1',
179
+ defaultThinking: 'high',
180
+ });
181
+
182
+ const found = await getModelProviderConfig({workspaceId, providerId: 'local-vllm'});
183
+
184
+ expect(found).toEqual(created);
185
+ expect(found).toMatchObject({
186
+ workspaceId,
187
+ providerId: 'local-vllm',
188
+ kind: 'custom',
189
+ displayName: 'Local vLLM',
190
+ api: 'openai-responses',
191
+ baseUrl: 'https://llm.example.test/v1',
192
+ headers: [{name: 'x-region', value: 'local'}],
193
+ secretHeaderNames: ['authorization'],
194
+ models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
195
+ requiresApiKey: true,
196
+ defaultModel: 'llama-3.1',
197
+ });
198
+ expect(found?.headers).not.toContainEqual({
199
+ name: 'authorization',
200
+ value: 'Bearer header-secret',
201
+ });
202
+ });
203
+
204
+ it('does not clobber custom columns when a later upsert omits them', async () => {
205
+ await upsertModelProviderConfig({
206
+ workspaceId,
207
+ providerId: 'local-vllm',
208
+ kind: 'custom',
209
+ displayName: 'Local vLLM',
210
+ api: 'openai-responses',
211
+ baseUrl: 'https://llm.example.test/v1',
212
+ headers: [{name: 'x-region', value: 'local'}],
213
+ secretHeaderNames: ['authorization'],
214
+ models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
215
+ requiresApiKey: true,
216
+ defaultModel: 'llama-3.1',
217
+ defaultThinking: 'high',
218
+ });
219
+
220
+ const updated = await upsertModelProviderConfig({
221
+ workspaceId,
222
+ providerId: 'local-vllm',
223
+ defaultModel: null,
224
+ defaultThinking: 'medium',
225
+ });
226
+
227
+ expect(updated).toMatchObject({
228
+ kind: 'custom',
229
+ displayName: 'Local vLLM',
230
+ api: 'openai-responses',
231
+ baseUrl: 'https://llm.example.test/v1',
232
+ headers: [{name: 'x-region', value: 'local'}],
233
+ secretHeaderNames: ['authorization'],
234
+ models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
235
+ requiresApiKey: true,
236
+ defaultModel: null,
237
+ defaultThinking: 'medium',
238
+ });
239
+ });
240
+
241
+ it('keeps custom columns null for built-in rows', async () => {
242
+ const created = await upsertModelProviderConfig(
243
+ createModelProviderConfigParams({workspaceId, providerId: 'anthropic'}),
244
+ );
245
+
246
+ expect(created).toMatchObject({
247
+ kind: 'builtin',
248
+ displayName: null,
249
+ api: null,
250
+ baseUrl: null,
251
+ headers: null,
252
+ models: null,
253
+ });
254
+ });
255
+
256
+ it('rejects custom rows missing required custom fields', async () => {
257
+ const insert = db().insert(modelProviderConfigs).values({
258
+ workspaceId,
259
+ providerId: 'broken-custom',
260
+ kind: 'custom',
261
+ defaultModel: null,
262
+ defaultThinking: 'high',
263
+ });
264
+
265
+ await expect(insert).rejects.toThrow();
266
+ });
267
+ });
268
+
269
+ function createModelProviderConfigParams(params: {
270
+ workspaceId: string;
271
+ providerId: SupportedModelProviderId | ModelProviderRef;
272
+ defaultModel?: string | undefined;
273
+ defaultThinking?: AgentThinking | undefined;
274
+ }): UpsertModelProviderConfigParams {
275
+ return {
276
+ workspaceId: params.workspaceId,
277
+ providerId: params.providerId,
278
+ defaultModel: params.defaultModel ?? 'claude-opus-4-8',
279
+ defaultThinking: params.defaultThinking ?? 'high',
280
+ };
281
+ }
@@ -0,0 +1,230 @@
1
+ import type {
2
+ AgentThinking,
3
+ CustomAgentModelDto,
4
+ CustomModelProviderHeaderDto,
5
+ ModelProviderApi,
6
+ ModelProviderRef,
7
+ } from '@shipfox/api-agent-dto';
8
+ import {and, eq, sql} from 'drizzle-orm';
9
+ import type {ModelProviderConfig} from '#core/entities/model-provider-config.js';
10
+ import {db} from './db.js';
11
+ import {agentWorkspaceSettings} from './schema/agent-workspace-settings.js';
12
+ import {modelProviderConfigs, toModelProviderConfig} from './schema/model-provider-configs.js';
13
+
14
+ export interface UpsertModelProviderConfigParams {
15
+ workspaceId: string;
16
+ providerId: ModelProviderRef;
17
+ kind?: 'builtin' | 'custom' | undefined;
18
+ displayName?: string | null | undefined;
19
+ api?: ModelProviderApi | null | undefined;
20
+ baseUrl?: string | null | undefined;
21
+ headers?: CustomModelProviderHeaderDto[] | null | undefined;
22
+ secretHeaderNames?: string[] | null | undefined;
23
+ models?: CustomAgentModelDto[] | null | undefined;
24
+ requiresApiKey?: boolean | undefined;
25
+ defaultModel: string | null;
26
+ defaultThinking: AgentThinking;
27
+ setAsDefault?: boolean | undefined;
28
+ }
29
+
30
+ export type InsertCustomModelProviderConfigParams = Omit<
31
+ UpsertModelProviderConfigParams,
32
+ 'kind' | 'requiresApiKey'
33
+ > & {kind: 'custom'; requiresApiKey: boolean};
34
+
35
+ export async function insertCustomModelProviderConfig(
36
+ params: InsertCustomModelProviderConfigParams,
37
+ ): Promise<ModelProviderConfig | undefined> {
38
+ return await db().transaction(async (tx) => {
39
+ const rows = await tx
40
+ .insert(modelProviderConfigs)
41
+ .values({
42
+ workspaceId: params.workspaceId,
43
+ providerId: params.providerId,
44
+ kind: params.kind,
45
+ displayName: params.displayName,
46
+ api: params.api,
47
+ baseUrl: params.baseUrl,
48
+ headers: params.headers,
49
+ secretHeaderNames: params.secretHeaderNames,
50
+ models: params.models,
51
+ requiresApiKey: params.requiresApiKey,
52
+ defaultModel: params.defaultModel,
53
+ defaultThinking: params.defaultThinking,
54
+ })
55
+ .onConflictDoNothing({
56
+ target: [modelProviderConfigs.workspaceId, modelProviderConfigs.providerId],
57
+ })
58
+ .returning();
59
+
60
+ const row = rows[0];
61
+ if (!row) return undefined;
62
+
63
+ if (params.setAsDefault) {
64
+ await tx
65
+ .insert(agentWorkspaceSettings)
66
+ .values({
67
+ workspaceId: params.workspaceId,
68
+ defaultProviderId: params.providerId,
69
+ })
70
+ .onConflictDoUpdate({
71
+ target: agentWorkspaceSettings.workspaceId,
72
+ set: {
73
+ defaultProviderId: params.providerId,
74
+ updatedAt: sql`NOW()`,
75
+ },
76
+ });
77
+ }
78
+
79
+ return toModelProviderConfig(row);
80
+ });
81
+ }
82
+
83
+ export async function upsertModelProviderConfig(
84
+ params: UpsertModelProviderConfigParams,
85
+ ): Promise<ModelProviderConfig> {
86
+ return await db().transaction(async (tx) => {
87
+ const rows = await tx
88
+ .insert(modelProviderConfigs)
89
+ .values({
90
+ workspaceId: params.workspaceId,
91
+ providerId: params.providerId,
92
+ ...(params.kind !== undefined ? {kind: params.kind} : {}),
93
+ ...(params.displayName !== undefined ? {displayName: params.displayName} : {}),
94
+ ...(params.api !== undefined ? {api: params.api} : {}),
95
+ ...(params.baseUrl !== undefined ? {baseUrl: params.baseUrl} : {}),
96
+ ...(params.headers !== undefined ? {headers: params.headers} : {}),
97
+ ...(params.secretHeaderNames !== undefined
98
+ ? {secretHeaderNames: params.secretHeaderNames}
99
+ : {}),
100
+ ...(params.models !== undefined ? {models: params.models} : {}),
101
+ ...(params.requiresApiKey !== undefined ? {requiresApiKey: params.requiresApiKey} : {}),
102
+ defaultModel: params.defaultModel,
103
+ defaultThinking: params.defaultThinking,
104
+ })
105
+ .onConflictDoUpdate({
106
+ target: [modelProviderConfigs.workspaceId, modelProviderConfigs.providerId],
107
+ set: {
108
+ ...(params.kind !== undefined ? {kind: params.kind} : {}),
109
+ ...(params.displayName !== undefined ? {displayName: params.displayName} : {}),
110
+ ...(params.api !== undefined ? {api: params.api} : {}),
111
+ ...(params.baseUrl !== undefined ? {baseUrl: params.baseUrl} : {}),
112
+ ...(params.headers !== undefined ? {headers: params.headers} : {}),
113
+ ...(params.secretHeaderNames !== undefined
114
+ ? {secretHeaderNames: params.secretHeaderNames}
115
+ : {}),
116
+ ...(params.models !== undefined ? {models: params.models} : {}),
117
+ ...(params.requiresApiKey !== undefined ? {requiresApiKey: params.requiresApiKey} : {}),
118
+ defaultModel: params.defaultModel,
119
+ defaultThinking: params.defaultThinking,
120
+ updatedAt: sql`NOW()`,
121
+ },
122
+ })
123
+ .returning();
124
+
125
+ const row = rows[0];
126
+ if (!row) throw new Error('Upsert returned no rows');
127
+
128
+ if (params.setAsDefault) {
129
+ await tx
130
+ .insert(agentWorkspaceSettings)
131
+ .values({
132
+ workspaceId: params.workspaceId,
133
+ defaultProviderId: params.providerId,
134
+ })
135
+ .onConflictDoUpdate({
136
+ target: agentWorkspaceSettings.workspaceId,
137
+ set: {
138
+ defaultProviderId: params.providerId,
139
+ updatedAt: sql`NOW()`,
140
+ },
141
+ });
142
+ }
143
+
144
+ return toModelProviderConfig(row);
145
+ });
146
+ }
147
+
148
+ export async function getModelProviderConfig(params: {
149
+ workspaceId: string;
150
+ providerId: ModelProviderRef;
151
+ }): Promise<ModelProviderConfig | undefined> {
152
+ const rows = await db()
153
+ .select()
154
+ .from(modelProviderConfigs)
155
+ .where(
156
+ and(
157
+ eq(modelProviderConfigs.workspaceId, params.workspaceId),
158
+ eq(modelProviderConfigs.providerId, params.providerId),
159
+ ),
160
+ )
161
+ .limit(1);
162
+
163
+ const row = rows[0];
164
+ if (!row) return undefined;
165
+ return toModelProviderConfig(row);
166
+ }
167
+
168
+ export async function updateModelProviderDefaultModel(params: {
169
+ workspaceId: string;
170
+ providerId: ModelProviderRef;
171
+ defaultModel: string | null;
172
+ }): Promise<ModelProviderConfig | undefined> {
173
+ const rows = await db()
174
+ .update(modelProviderConfigs)
175
+ .set({defaultModel: params.defaultModel, updatedAt: sql`NOW()`})
176
+ .where(
177
+ and(
178
+ eq(modelProviderConfigs.workspaceId, params.workspaceId),
179
+ eq(modelProviderConfigs.providerId, params.providerId),
180
+ ),
181
+ )
182
+ .returning();
183
+
184
+ const row = rows[0];
185
+ if (!row) return undefined;
186
+ return toModelProviderConfig(row);
187
+ }
188
+
189
+ export async function listModelProviderConfigs(
190
+ workspaceId: string,
191
+ ): Promise<ModelProviderConfig[]> {
192
+ const rows = await db()
193
+ .select()
194
+ .from(modelProviderConfigs)
195
+ .where(eq(modelProviderConfigs.workspaceId, workspaceId))
196
+ .orderBy(modelProviderConfigs.providerId);
197
+
198
+ return rows.map(toModelProviderConfig);
199
+ }
200
+
201
+ export async function deleteModelProviderConfig(params: {
202
+ workspaceId: string;
203
+ providerId: ModelProviderRef;
204
+ }): Promise<boolean> {
205
+ return await db().transaction(async (tx) => {
206
+ const deleted = await tx
207
+ .delete(modelProviderConfigs)
208
+ .where(
209
+ and(
210
+ eq(modelProviderConfigs.workspaceId, params.workspaceId),
211
+ eq(modelProviderConfigs.providerId, params.providerId),
212
+ ),
213
+ )
214
+ .returning({id: modelProviderConfigs.id});
215
+
216
+ if (deleted.length === 0) return false;
217
+
218
+ await tx
219
+ .update(agentWorkspaceSettings)
220
+ .set({defaultProviderId: null, updatedAt: sql`NOW()`})
221
+ .where(
222
+ and(
223
+ eq(agentWorkspaceSettings.workspaceId, params.workspaceId),
224
+ eq(agentWorkspaceSettings.defaultProviderId, params.providerId),
225
+ ),
226
+ );
227
+
228
+ return true;
229
+ });
230
+ }
@@ -0,0 +1,27 @@
1
+ import {harnessSchema, type ModelProviderRef} from '@shipfox/api-agent-dto';
2
+ import {text, timestamp, uuid} from 'drizzle-orm/pg-core';
3
+ import type {AgentWorkspaceSettings} from '#core/entities/agent-workspace-settings.js';
4
+ import {pgTable} from './common.js';
5
+
6
+ export const agentWorkspaceSettings = pgTable('workspace_settings', {
7
+ workspaceId: uuid('workspace_id').primaryKey(),
8
+ defaultProviderId: text('default_provider_id'),
9
+ defaultHarnessId: text('default_harness_id'),
10
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
11
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
12
+ });
13
+
14
+ export type AgentWorkspaceSettingsDb = typeof agentWorkspaceSettings.$inferSelect;
15
+ export type AgentWorkspaceSettingsCreateDb = typeof agentWorkspaceSettings.$inferInsert;
16
+
17
+ export function toAgentWorkspaceSettings(row: AgentWorkspaceSettingsDb): AgentWorkspaceSettings {
18
+ const parsedHarness = harnessSchema.safeParse(row.defaultHarnessId);
19
+
20
+ return {
21
+ workspaceId: row.workspaceId,
22
+ defaultProviderId: row.defaultProviderId as ModelProviderRef | null,
23
+ defaultHarnessId: parsedHarness.success ? parsedHarness.data : null,
24
+ createdAt: row.createdAt,
25
+ updatedAt: row.updatedAt,
26
+ };
27
+ }
@@ -0,0 +1,3 @@
1
+ import {pgTableCreator} from 'drizzle-orm/pg-core';
2
+
3
+ export const pgTable = pgTableCreator((name) => `agent_${name}`);