@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
@@ -5,14 +5,17 @@ import {
5
5
  DEFAULT_HARNESS,
6
6
  getHarnessDescriptor,
7
7
  type Harness,
8
+ type ManagedModelEntry,
9
+ type ManagedModelProvider,
8
10
  type ModelProviderRef,
9
- type SupportedModelProviderId,
11
+ type WorkspaceProvidersPolicy,
10
12
  } from '@shipfox/api-agent-dto';
11
13
  import {
12
14
  InvalidAgentModelError,
13
15
  UnsupportedHarnessProviderError,
14
16
  UnsupportedHarnessThinkingError,
15
17
  UnsupportedModelProviderError,
18
+ WorkspaceProvidersDisabledError,
16
19
  } from './errors.js';
17
20
  import {listHarnessProviderModels} from './harness/index.js';
18
21
  import {getModelProviderEntry} from './model-provider-policy.js';
@@ -38,9 +41,11 @@ export interface AgentConfigResolutionContext {
38
41
  readonly workspaceDefaultHarnessId?: Harness | null | undefined;
39
42
  readonly workspaceDefaultProviderId?: ModelProviderRef | null | undefined;
40
43
  readonly workspaceProviderConfigs?: ReadonlyMap<ModelProviderRef, WorkspaceProviderDefaults>;
41
- readonly instanceDefaultProvider?: SupportedModelProviderId | undefined;
44
+ readonly instanceDefaultProvider?: ModelProviderRef | undefined;
42
45
  readonly instanceDefaultModel?: string | undefined;
43
46
  readonly instanceDefaultThinking?: AgentThinking | undefined;
47
+ readonly managedProvider?: ManagedModelProvider | undefined;
48
+ readonly workspaceProviders?: WorkspaceProvidersPolicy | undefined;
44
49
  }
45
50
 
46
51
  interface WorkspaceProviderDefaults {
@@ -50,6 +55,15 @@ interface WorkspaceProviderDefaults {
50
55
  readonly models?: readonly CustomAgentModelDto[] | null | undefined;
51
56
  }
52
57
 
58
+ interface ManagedProviderDefaults {
59
+ readonly kind: 'managed';
60
+ readonly defaultModel: string;
61
+ readonly defaultThinking?: AgentThinking | undefined;
62
+ readonly models: readonly ManagedModelEntry[];
63
+ }
64
+
65
+ type ProviderDefaults = WorkspaceProviderDefaults | ManagedProviderDefaults;
66
+
53
67
  type ModelCandidateResolver = () => string | null | undefined;
54
68
 
55
69
  export function resolveAgentConfig(
@@ -58,15 +72,15 @@ export function resolveAgentConfig(
58
72
  ): ResolvedAgentConfig {
59
73
  const harness = step.harness ?? ctx.workspaceDefaultHarnessId ?? DEFAULT_HARNESS;
60
74
  const provider = resolveProvider(step, ctx, harness);
61
- const workspaceProviderConfig = ctx.workspaceProviderConfigs?.get(provider);
75
+ const providerConfig = getProviderConfig(provider, ctx);
62
76
  const model = resolveModel({
63
77
  step,
64
78
  ctx,
65
79
  harness,
66
80
  provider,
67
- workspaceProviderConfig,
81
+ providerConfig,
68
82
  });
69
- const thinking = resolveThinking({step, ctx, harness, provider, workspaceProviderConfig});
83
+ const thinking = resolveThinking({step, ctx, harness, provider, providerConfig});
70
84
 
71
85
  return {harness, provider, model, thinking};
72
86
  }
@@ -80,13 +94,35 @@ function resolveProvider(
80
94
  harness: Harness,
81
95
  ): ModelProviderRef {
82
96
  const descriptor = getHarnessDescriptor(harness);
97
+ if (ctx.workspaceProviders === 'disabled') {
98
+ const managedProvider = ctx.managedProvider;
99
+ if (managedProvider === undefined) {
100
+ throw new Error(
101
+ 'workspace provider configuration is disabled but no managed provider is registered',
102
+ );
103
+ }
104
+ if (step.provider !== undefined && step.provider !== managedProvider.id) {
105
+ throw new WorkspaceProvidersDisabledError(managedProvider.id);
106
+ }
107
+
108
+ const providerConfig = getProviderConfig(managedProvider.id, ctx);
109
+ if (!isHarnessCompatible(harness, managedProvider.id, providerConfig)) {
110
+ throw new UnsupportedHarnessProviderError(
111
+ harness,
112
+ managedProvider.id,
113
+ supportedProviderIds(harness, descriptor, ctx),
114
+ );
115
+ }
116
+ return managedProvider.id;
117
+ }
118
+
83
119
  if (step.provider !== undefined) {
84
120
  const provider = resolveSupportedProvider(step.provider, ctx);
85
- if (!isHarnessCompatible(harness, provider, ctx.workspaceProviderConfigs?.get(provider))) {
121
+ if (!isHarnessCompatible(harness, provider, getProviderConfig(provider, ctx))) {
86
122
  throw new UnsupportedHarnessProviderError(
87
123
  harness,
88
124
  step.provider,
89
- descriptor.supportedProviderIds,
125
+ supportedProviderIds(harness, descriptor, ctx),
90
126
  );
91
127
  }
92
128
  return provider;
@@ -100,8 +136,8 @@ function resolveProvider(
100
136
  for (const candidate of candidates) {
101
137
  if (candidate === undefined || candidate === null) continue;
102
138
 
103
- const workspaceProviderConfig = ctx.workspaceProviderConfigs?.get(candidate);
104
- if (!isHarnessCompatible(harness, candidate, workspaceProviderConfig)) continue;
139
+ const providerConfig = getProviderConfig(candidate, ctx);
140
+ if (!isHarnessCompatible(harness, candidate, providerConfig)) continue;
105
141
 
106
142
  return resolveSupportedProvider(candidate, ctx);
107
143
  }
@@ -113,6 +149,8 @@ function resolveSupportedProvider(
113
149
  provider: string,
114
150
  ctx: AgentConfigResolutionContext,
115
151
  ): ModelProviderRef {
152
+ if (ctx.managedProvider?.id === provider) return provider;
153
+
116
154
  const workspaceProviderConfig = ctx.workspaceProviderConfigs?.get(provider);
117
155
  if (workspaceProviderConfig?.kind === 'custom') return provider;
118
156
 
@@ -120,7 +158,7 @@ function resolveSupportedProvider(
120
158
  if (entry === undefined || entry.support_status !== 'supported') {
121
159
  throw new UnsupportedModelProviderError(provider);
122
160
  }
123
- return provider as SupportedModelProviderId;
161
+ return provider;
124
162
  }
125
163
 
126
164
  function resolveModel(params: {
@@ -128,23 +166,30 @@ function resolveModel(params: {
128
166
  ctx: AgentConfigResolutionContext;
129
167
  harness: Harness;
130
168
  provider: ModelProviderRef;
131
- workspaceProviderConfig: WorkspaceProviderDefaults | undefined;
169
+ providerConfig: ProviderDefaults | undefined;
132
170
  }): string {
171
+ const providerConfig = params.providerConfig;
172
+
133
173
  if (params.step.model !== undefined) {
134
- validateModel(
135
- params.harness,
136
- params.provider,
137
- params.step.model,
138
- params.workspaceProviderConfig,
139
- );
174
+ validateModel(params.harness, params.provider, params.step.model, params.providerConfig);
140
175
  return params.step.model;
141
176
  }
142
177
 
143
- const candidates: ModelCandidateResolver[] = [() => params.workspaceProviderConfig?.defaultModel];
144
- if (params.workspaceProviderConfig?.kind === 'custom') {
145
- candidates.push(() => customDefaultModel(params.workspaceProviderConfig));
178
+ const candidates: ModelCandidateResolver[] = [];
179
+ if (providerConfig?.kind === 'custom') {
180
+ candidates.push(
181
+ () => providerConfig.defaultModel,
182
+ () => customDefaultModel(providerConfig),
183
+ );
184
+ } else if (providerConfig?.kind === 'managed') {
185
+ candidates.push(
186
+ () => instanceDefaultModel(params.provider, params.ctx),
187
+ () => providerConfig.defaultModel,
188
+ () => managedDefaultModel(params.harness, providerConfig),
189
+ );
146
190
  } else {
147
191
  candidates.push(
192
+ () => providerConfig?.defaultModel,
148
193
  () => instanceDefaultModel(params.provider, params.ctx),
149
194
  () => catalogDefaultModel(params.harness, params.provider),
150
195
  );
@@ -153,9 +198,7 @@ function resolveModel(params: {
153
198
  for (const resolveCandidate of candidates) {
154
199
  const candidate = resolveCandidate();
155
200
  if (candidate === undefined || candidate === null) continue;
156
- if (
157
- modelIsAvailable(params.harness, params.provider, candidate, params.workspaceProviderConfig)
158
- ) {
201
+ if (modelIsAvailable(params.harness, params.provider, candidate, params.providerConfig)) {
159
202
  return candidate;
160
203
  }
161
204
  }
@@ -164,7 +207,7 @@ function resolveModel(params: {
164
207
  }
165
208
 
166
209
  function catalogDefaultModel(harness: Harness, provider: ModelProviderRef): string {
167
- const catalogModels = listProviderModels(harness, provider);
210
+ const catalogModels = listProviderModels(harness, provider, undefined);
168
211
  const entry = getModelProviderEntry(provider);
169
212
  if (entry === undefined || entry.support_status !== 'supported') {
170
213
  throw new UnsupportedModelProviderError(provider);
@@ -195,20 +238,25 @@ function instanceDefaultThinking(
195
238
  return provider === ctx.instanceDefaultProvider ? ctx.instanceDefaultThinking : undefined;
196
239
  }
197
240
 
198
- function customDefaultModel(
199
- workspaceProviderConfig: WorkspaceProviderDefaults | undefined,
241
+ function customDefaultModel(providerConfig: ProviderDefaults | undefined): string | undefined {
242
+ if (providerConfig?.kind !== 'custom') return undefined;
243
+ return providerConfig.models?.[0]?.id;
244
+ }
245
+
246
+ function managedDefaultModel(
247
+ harness: Harness,
248
+ providerConfig: ManagedProviderDefaults,
200
249
  ): string | undefined {
201
- if (workspaceProviderConfig?.kind !== 'custom') return undefined;
202
- return workspaceProviderConfig.models?.[0]?.id;
250
+ return listManagedProviderModels(harness, providerConfig)[0]?.id;
203
251
  }
204
252
 
205
253
  function validateModel(
206
254
  harness: Harness,
207
255
  provider: ModelProviderRef,
208
256
  model: string,
209
- workspaceProviderConfig: WorkspaceProviderDefaults | undefined,
257
+ providerConfig: ProviderDefaults | undefined,
210
258
  ): void {
211
- if (modelIsAvailable(harness, provider, model, workspaceProviderConfig)) return;
259
+ if (modelIsAvailable(harness, provider, model, providerConfig)) return;
212
260
  throw new InvalidAgentModelError(harness, provider, model);
213
261
  }
214
262
 
@@ -216,25 +264,37 @@ function modelIsAvailable(
216
264
  harness: Harness,
217
265
  provider: ModelProviderRef,
218
266
  model: string,
219
- workspaceProviderConfig: WorkspaceProviderDefaults | undefined,
267
+ providerConfig: ProviderDefaults | undefined,
220
268
  ): boolean {
221
- if (workspaceProviderConfig?.kind === 'custom') {
222
- return workspaceProviderConfig.models?.some((candidate) => candidate.id === model) ?? false;
269
+ if (providerConfig?.kind === 'custom') {
270
+ return providerConfig.models?.some((candidate) => candidate.id === model) ?? false;
223
271
  }
224
272
 
225
- return listProviderModels(harness, provider).some((candidate) => candidate.id === model);
273
+ return listProviderModels(harness, provider, providerConfig).some(
274
+ (candidate) => candidate.id === model,
275
+ );
226
276
  }
227
277
 
228
- function listProviderModels(harness: Harness, provider: ModelProviderRef): readonly {id: string}[] {
278
+ function listProviderModels(
279
+ harness: Harness,
280
+ provider: ModelProviderRef,
281
+ providerConfig: ProviderDefaults | undefined,
282
+ ): readonly {id: string}[] {
283
+ if (providerConfig?.kind === 'managed') {
284
+ return listManagedProviderModels(harness, providerConfig);
285
+ }
229
286
  return listHarnessProviderModels(harness, provider);
230
287
  }
231
288
 
232
289
  function isHarnessCompatible(
233
290
  harness: Harness,
234
291
  provider: ModelProviderRef,
235
- workspaceProviderConfig: WorkspaceProviderDefaults | undefined,
292
+ providerConfig: ProviderDefaults | undefined,
236
293
  ): boolean {
237
- if (workspaceProviderConfig?.kind === 'custom') return harness === 'pi';
294
+ if (providerConfig?.kind === 'custom') return harness === 'pi';
295
+ if (providerConfig?.kind === 'managed') {
296
+ return listManagedProviderModels(harness, providerConfig).length > 0;
297
+ }
238
298
 
239
299
  return getHarnessDescriptor(harness).supportedProviderIds.includes(provider);
240
300
  }
@@ -244,7 +304,7 @@ function resolveThinking(params: {
244
304
  ctx: AgentConfigResolutionContext;
245
305
  harness: Harness;
246
306
  provider: ModelProviderRef;
247
- workspaceProviderConfig: WorkspaceProviderDefaults | undefined;
307
+ providerConfig: ProviderDefaults | undefined;
248
308
  }): AgentThinking {
249
309
  const thinkingSchema = agentThinkingByHarness[params.harness];
250
310
  const descriptor = getHarnessDescriptor(params.harness);
@@ -261,14 +321,63 @@ function resolveThinking(params: {
261
321
  return parsed.data;
262
322
  }
263
323
 
264
- const candidates = [
265
- params.workspaceProviderConfig?.defaultThinking,
266
- instanceDefaultThinking(params.provider, params.ctx),
267
- descriptor.defaultThinking,
268
- ];
324
+ const candidates =
325
+ params.providerConfig?.kind === 'managed'
326
+ ? [
327
+ instanceDefaultThinking(params.provider, params.ctx),
328
+ params.providerConfig.defaultThinking,
329
+ descriptor.defaultThinking,
330
+ ]
331
+ : [
332
+ params.providerConfig?.defaultThinking,
333
+ instanceDefaultThinking(params.provider, params.ctx),
334
+ descriptor.defaultThinking,
335
+ ];
269
336
  for (const candidate of candidates) {
270
337
  if (candidate !== undefined && thinkingSchema.safeParse(candidate).success) return candidate;
271
338
  }
272
339
 
273
340
  return descriptor.defaultThinking;
274
341
  }
342
+
343
+ function getProviderConfig(
344
+ provider: ModelProviderRef,
345
+ ctx: AgentConfigResolutionContext,
346
+ ): ProviderDefaults | undefined {
347
+ if (ctx.managedProvider?.id === provider) {
348
+ return {
349
+ kind: 'managed',
350
+ defaultModel: ctx.managedProvider.defaultModel,
351
+ defaultThinking: ctx.managedProvider.defaultThinking,
352
+ models: ctx.managedProvider.models,
353
+ };
354
+ }
355
+
356
+ return ctx.workspaceProviderConfigs?.get(provider);
357
+ }
358
+
359
+ function listManagedProviderModels(
360
+ harness: Harness,
361
+ providerConfig: ManagedProviderDefaults,
362
+ ): readonly ManagedModelEntry[] {
363
+ return providerConfig.models.filter((model) => managedModelSupportsHarness(harness, model));
364
+ }
365
+
366
+ function managedModelSupportsHarness(harness: Harness, model: ManagedModelEntry): boolean {
367
+ return harness === 'pi' || model.api === 'anthropic-messages';
368
+ }
369
+
370
+ function supportedProviderIds(
371
+ harness: Harness,
372
+ descriptor: ReturnType<typeof getHarnessDescriptor>,
373
+ ctx: AgentConfigResolutionContext,
374
+ ): readonly string[] {
375
+ if (
376
+ ctx.managedProvider === undefined ||
377
+ !ctx.managedProvider.models.some((model) => managedModelSupportsHarness(harness, model))
378
+ ) {
379
+ return descriptor.supportedProviderIds;
380
+ }
381
+
382
+ return [...descriptor.supportedProviderIds, ctx.managedProvider.id];
383
+ }
@@ -1,4 +1,4 @@
1
- import type {ModelProviderRef} from '@shipfox/api-agent-dto';
1
+ import type {ManagedModelProvider, ModelProviderRef} from '@shipfox/api-agent-dto';
2
2
  import {deleteModelProviderConfig, upsertModelProviderConfig} from '#db/index.js';
3
3
  import {setSecrets} from '#test/fixtures/secrets-client.js';
4
4
  import {agentSystemNamespace, customCredentialsToStoreValues} from './credential-fingerprints.js';
@@ -21,6 +21,8 @@ describe('resolveRuntimeCredentials', () => {
21
21
 
22
22
  const result = await resolveRuntimeCredentials({
23
23
  workspaceId,
24
+ runId: crypto.randomUUID(),
25
+ stepAttemptId: crypto.randomUUID(),
24
26
  harness: 'pi',
25
27
  provider: 'anthropic',
26
28
  model: 'claude-opus-4-8',
@@ -36,6 +38,86 @@ describe('resolveRuntimeCredentials', () => {
36
38
  });
37
39
  });
38
40
 
41
+ it('resolves managed provider credentials into the pi custom provider contract', async () => {
42
+ const runId = crypto.randomUUID();
43
+ const stepAttemptId = crypto.randomUUID();
44
+ const resolveCredentials = vi.fn<ManagedModelProvider['resolveCredentials']>();
45
+ resolveCredentials.mockResolvedValue({
46
+ api: 'openai-responses',
47
+ baseUrl: 'https://gateway.example.test',
48
+ credentials: {api_key: 'managed-token'},
49
+ });
50
+
51
+ const result = await resolveRuntimeCredentials(
52
+ {
53
+ workspaceId,
54
+ runId,
55
+ stepAttemptId,
56
+ harness: 'pi',
57
+ provider: 'shipfox',
58
+ model: 'responses-model',
59
+ thinking: 'high',
60
+ },
61
+ {managedProvider: managedProvider(resolveCredentials)},
62
+ );
63
+
64
+ expect(resolveCredentials).toHaveBeenCalledWith({
65
+ workspaceId,
66
+ runId,
67
+ stepAttemptId,
68
+ model: 'responses-model',
69
+ });
70
+ expect(result).toEqual({
71
+ harness: 'pi',
72
+ provider_id: 'shipfox',
73
+ model: 'responses-model',
74
+ thinking: 'high',
75
+ credentials: {api_key: 'managed-token'},
76
+ custom_provider: {
77
+ api: 'openai-responses',
78
+ base_url: 'https://gateway.example.test',
79
+ headers: [],
80
+ secret_header_names: [],
81
+ models: [{id: 'responses-model', label: 'Responses model'}],
82
+ requires_api_key: true,
83
+ },
84
+ });
85
+ });
86
+
87
+ it('resolves managed provider credentials into the Claude per-step contract', async () => {
88
+ const resolveCredentials = vi.fn<ManagedModelProvider['resolveCredentials']>();
89
+ resolveCredentials.mockResolvedValue({
90
+ api: 'anthropic-messages',
91
+ baseUrl: 'https://gateway.example.test',
92
+ credentials: {api_key: 'managed-token'},
93
+ });
94
+
95
+ const result = await resolveRuntimeCredentials(
96
+ {
97
+ workspaceId,
98
+ runId: crypto.randomUUID(),
99
+ stepAttemptId: crypto.randomUUID(),
100
+ harness: 'claude',
101
+ provider: 'shipfox',
102
+ model: 'claude-model',
103
+ thinking: 'high',
104
+ },
105
+ {managedProvider: managedProvider(resolveCredentials)},
106
+ );
107
+
108
+ expect(result).toEqual({
109
+ harness: 'claude',
110
+ provider_id: 'shipfox',
111
+ model: 'claude-model',
112
+ thinking: 'high',
113
+ credentials: {api_key: 'managed-token'},
114
+ claude: {
115
+ base_url: 'https://gateway.example.test',
116
+ auth_token: 'managed-token',
117
+ },
118
+ });
119
+ });
120
+
39
121
  it('prefers workspace credentials over the instance fallback', async () => {
40
122
  await saveProviderConfig({
41
123
  workspaceId,
@@ -45,6 +127,8 @@ describe('resolveRuntimeCredentials', () => {
45
127
  const result = await resolveRuntimeCredentials(
46
128
  {
47
129
  workspaceId,
130
+ runId: crypto.randomUUID(),
131
+ stepAttemptId: crypto.randomUUID(),
48
132
  harness: 'pi',
49
133
  provider: 'anthropic',
50
134
  model: 'claude-opus-4-8',
@@ -60,6 +144,8 @@ describe('resolveRuntimeCredentials', () => {
60
144
  const matching = await resolveRuntimeCredentials(
61
145
  {
62
146
  workspaceId,
147
+ runId: crypto.randomUUID(),
148
+ stepAttemptId: crypto.randomUUID(),
63
149
  harness: 'pi',
64
150
  provider: 'anthropic',
65
151
  model: 'claude-opus-4-8',
@@ -70,6 +156,8 @@ describe('resolveRuntimeCredentials', () => {
70
156
  const mismatched = resolveRuntimeCredentials(
71
157
  {
72
158
  workspaceId,
159
+ runId: crypto.randomUUID(),
160
+ stepAttemptId: crypto.randomUUID(),
73
161
  harness: 'pi',
74
162
  provider: 'openai',
75
163
  model: 'gpt-5.5-pro',
@@ -82,6 +170,55 @@ describe('resolveRuntimeCredentials', () => {
82
170
  await expect(mismatched).rejects.toMatchObject({name: 'ModelProviderConfigNotFoundError'});
83
171
  });
84
172
 
173
+ it('refuses workspace credentials and instance fallback when workspace providers are disabled', async () => {
174
+ await saveProviderConfig({
175
+ workspaceId,
176
+ providerId: 'anthropic',
177
+ credentials: {api_key: 'sk-workspace-secret'},
178
+ });
179
+
180
+ const result = resolveRuntimeCredentials(
181
+ {
182
+ workspaceId,
183
+ runId: crypto.randomUUID(),
184
+ stepAttemptId: crypto.randomUUID(),
185
+ harness: 'pi',
186
+ provider: 'anthropic',
187
+ model: 'claude-opus-4-8',
188
+ thinking: 'high',
189
+ },
190
+ {
191
+ workspaceProviders: 'disabled',
192
+ runtimeConfig: instanceConfig(),
193
+ },
194
+ );
195
+
196
+ await expect(result).rejects.toThrow(ModelProviderConfigNotFoundError);
197
+ });
198
+
199
+ it('reports the managed provider when a foreign runtime provider is requested', async () => {
200
+ const result = resolveRuntimeCredentials(
201
+ {
202
+ workspaceId,
203
+ runId: crypto.randomUUID(),
204
+ stepAttemptId: crypto.randomUUID(),
205
+ harness: 'pi',
206
+ provider: 'anthropic',
207
+ model: 'claude-opus-4-8',
208
+ thinking: 'high',
209
+ },
210
+ {
211
+ workspaceProviders: 'disabled',
212
+ managedProvider: managedProvider(vi.fn()),
213
+ },
214
+ );
215
+
216
+ await expect(result).rejects.toMatchObject({
217
+ name: 'WorkspaceProvidersDisabledError',
218
+ managedProviderId: 'shipfox',
219
+ });
220
+ });
221
+
85
222
  it('returns custom provider runtime descriptors for custom rows', async () => {
86
223
  await saveProviderConfig({
87
224
  workspaceId,
@@ -98,6 +235,8 @@ describe('resolveRuntimeCredentials', () => {
98
235
 
99
236
  const result = await resolveRuntimeCredentials({
100
237
  workspaceId,
238
+ runId: crypto.randomUUID(),
239
+ stepAttemptId: crypto.randomUUID(),
101
240
  harness: 'pi',
102
241
  provider: 'local-vllm',
103
242
  model: 'llama-3.1',
@@ -137,6 +276,8 @@ describe('resolveRuntimeCredentials', () => {
137
276
 
138
277
  const result = await resolveRuntimeCredentials({
139
278
  workspaceId,
279
+ runId: crypto.randomUUID(),
280
+ stepAttemptId: crypto.randomUUID(),
140
281
  harness: 'pi',
141
282
  provider: 'local-ollama',
142
283
  model: 'llama-3.1',
@@ -155,6 +296,8 @@ describe('resolveRuntimeCredentials', () => {
155
296
  it('throws when no workspace or instance credential is available', async () => {
156
297
  const result = resolveRuntimeCredentials({
157
298
  workspaceId,
299
+ runId: crypto.randomUUID(),
300
+ stepAttemptId: crypto.randomUUID(),
158
301
  harness: 'pi',
159
302
  provider: 'anthropic',
160
303
  model: 'claude-opus-4-8',
@@ -174,6 +317,8 @@ describe('resolveRuntimeCredentials', () => {
174
317
 
175
318
  const result = resolveRuntimeCredentials({
176
319
  workspaceId,
320
+ runId: crypto.randomUUID(),
321
+ stepAttemptId: crypto.randomUUID(),
177
322
  harness: 'pi',
178
323
  provider: 'anthropic',
179
324
  model: 'claude-opus-4-8',
@@ -193,6 +338,8 @@ describe('resolveRuntimeCredentials', () => {
193
338
 
194
339
  const result = resolveRuntimeCredentials({
195
340
  workspaceId,
341
+ runId: crypto.randomUUID(),
342
+ stepAttemptId: crypto.randomUUID(),
196
343
  harness: 'pi',
197
344
  provider: 'anthropic',
198
345
  model: 'claude-opus-4-8',
@@ -217,6 +364,8 @@ describe('resolveRuntimeCredentials', () => {
217
364
 
218
365
  const result = resolveRuntimeCredentials({
219
366
  workspaceId,
367
+ runId: crypto.randomUUID(),
368
+ stepAttemptId: crypto.randomUUID(),
220
369
  harness: 'pi',
221
370
  provider: 'cloudflare-ai-gateway',
222
371
  model: '@cf/meta/llama-3.1-8b-instruct',
@@ -235,6 +384,8 @@ describe('resolveRuntimeCredentials', () => {
235
384
 
236
385
  const result = resolveRuntimeCredentials({
237
386
  workspaceId,
387
+ runId: crypto.randomUUID(),
388
+ stepAttemptId: crypto.randomUUID(),
238
389
  harness: 'pi',
239
390
  provider: 'anthropic',
240
391
  model: 'claude-opus-4-8',
@@ -256,6 +407,8 @@ describe('resolveRuntimeCredentials', () => {
256
407
  const result = resolveRuntimeCredentials(
257
408
  {
258
409
  workspaceId,
410
+ runId: crypto.randomUUID(),
411
+ stepAttemptId: crypto.randomUUID(),
259
412
  harness: 'pi',
260
413
  provider: 'anthropic',
261
414
  model: 'claude-opus-4-8',
@@ -314,3 +467,18 @@ function instanceConfig() {
314
467
  AGENT_DEFAULT_PROVIDER_API_KEY: 'sk-instance-secret',
315
468
  };
316
469
  }
470
+
471
+ function managedProvider(
472
+ resolveCredentials: ManagedModelProvider['resolveCredentials'],
473
+ ): ManagedModelProvider {
474
+ return {
475
+ id: 'shipfox',
476
+ label: 'Shipfox',
477
+ models: [
478
+ {id: 'claude-model', label: 'Claude model', api: 'anthropic-messages'},
479
+ {id: 'responses-model', label: 'Responses model', api: 'openai-responses'},
480
+ ],
481
+ defaultModel: 'responses-model',
482
+ resolveCredentials,
483
+ };
484
+ }