@shipfox/api-agent 12.2.0 → 14.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 (132) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/dist/config.d.ts +5 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +75 -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 +60 -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 +196 -4
  101. package/src/config.ts +100 -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 +289 -1
  109. package/src/core/resolve-runtime-credentials.ts +69 -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
@@ -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,189 @@ 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: [
82
+ {
83
+ id: 'responses-model',
84
+ label: 'Responses model',
85
+ context_window: 1_000_000,
86
+ max_output_tokens: 65_536,
87
+ reasoning: true,
88
+ input_image: true,
89
+ },
90
+ ],
91
+ requires_api_key: true,
92
+ },
93
+ });
94
+ });
95
+
96
+ it('omits optional model metadata when the managed model entry does not carry it', async () => {
97
+ const resolveCredentials = vi.fn<ManagedModelProvider['resolveCredentials']>();
98
+ resolveCredentials.mockResolvedValue({
99
+ api: 'openai-completions',
100
+ baseUrl: 'https://gateway.example.test',
101
+ credentials: {api_key: 'managed-token'},
102
+ });
103
+
104
+ const result = await resolveRuntimeCredentials(
105
+ {
106
+ workspaceId,
107
+ runId: crypto.randomUUID(),
108
+ stepAttemptId: crypto.randomUUID(),
109
+ harness: 'pi',
110
+ provider: 'shipfox',
111
+ model: 'plain-model',
112
+ thinking: 'high',
113
+ },
114
+ {managedProvider: managedProvider(resolveCredentials)},
115
+ );
116
+
117
+ expect(result).toEqual({
118
+ harness: 'pi',
119
+ provider_id: 'shipfox',
120
+ model: 'plain-model',
121
+ thinking: 'high',
122
+ credentials: {api_key: 'managed-token'},
123
+ custom_provider: {
124
+ api: 'openai-completions',
125
+ base_url: 'https://gateway.example.test',
126
+ headers: [],
127
+ secret_header_names: [],
128
+ models: [{id: 'plain-model', label: 'Plain model'}],
129
+ requires_api_key: true,
130
+ },
131
+ });
132
+ });
133
+
134
+ it('forwards partial managed model metadata, including false values', async () => {
135
+ const resolveCredentials = vi.fn<ManagedModelProvider['resolveCredentials']>();
136
+ resolveCredentials.mockResolvedValue({
137
+ api: 'openai-completions',
138
+ baseUrl: 'https://gateway.example.test',
139
+ credentials: {api_key: 'managed-token'},
140
+ });
141
+
142
+ const result = await resolveRuntimeCredentials(
143
+ {
144
+ workspaceId,
145
+ runId: crypto.randomUUID(),
146
+ stepAttemptId: crypto.randomUUID(),
147
+ harness: 'pi',
148
+ provider: 'shipfox',
149
+ model: 'partial-model',
150
+ thinking: 'high',
151
+ },
152
+ {managedProvider: managedProvider(resolveCredentials)},
153
+ );
154
+
155
+ expect(result.custom_provider?.models).toEqual([
156
+ {
157
+ id: 'partial-model',
158
+ label: 'Partial model',
159
+ max_output_tokens: 8_192,
160
+ reasoning: false,
161
+ input_image: false,
162
+ },
163
+ ]);
164
+ });
165
+
166
+ it('falls back to an unregistered managed model descriptor', async () => {
167
+ const resolveCredentials = vi.fn<ManagedModelProvider['resolveCredentials']>();
168
+ resolveCredentials.mockResolvedValue({
169
+ api: 'openai-completions',
170
+ baseUrl: 'https://gateway.example.test',
171
+ credentials: {api_key: 'managed-token'},
172
+ });
173
+
174
+ const result = await resolveRuntimeCredentials(
175
+ {
176
+ workspaceId,
177
+ runId: crypto.randomUUID(),
178
+ stepAttemptId: crypto.randomUUID(),
179
+ harness: 'pi',
180
+ provider: 'shipfox',
181
+ model: 'missing-model',
182
+ thinking: 'high',
183
+ },
184
+ {managedProvider: managedProvider(resolveCredentials)},
185
+ );
186
+
187
+ expect(result.custom_provider?.models).toEqual([{id: 'missing-model', label: 'missing-model'}]);
188
+ });
189
+
190
+ it('resolves managed provider credentials into the Claude per-step contract', async () => {
191
+ const resolveCredentials = vi.fn<ManagedModelProvider['resolveCredentials']>();
192
+ resolveCredentials.mockResolvedValue({
193
+ api: 'anthropic-messages',
194
+ baseUrl: 'https://gateway.example.test',
195
+ credentials: {api_key: 'managed-token'},
196
+ });
197
+
198
+ const result = await resolveRuntimeCredentials(
199
+ {
200
+ workspaceId,
201
+ runId: crypto.randomUUID(),
202
+ stepAttemptId: crypto.randomUUID(),
203
+ harness: 'claude',
204
+ provider: 'shipfox',
205
+ model: 'claude-model',
206
+ thinking: 'high',
207
+ },
208
+ {managedProvider: managedProvider(resolveCredentials)},
209
+ );
210
+
211
+ expect(result).toEqual({
212
+ harness: 'claude',
213
+ provider_id: 'shipfox',
214
+ model: 'claude-model',
215
+ thinking: 'high',
216
+ credentials: {api_key: 'managed-token'},
217
+ claude: {
218
+ base_url: 'https://gateway.example.test',
219
+ auth_token: 'managed-token',
220
+ },
221
+ });
222
+ });
223
+
39
224
  it('prefers workspace credentials over the instance fallback', async () => {
40
225
  await saveProviderConfig({
41
226
  workspaceId,
@@ -45,6 +230,8 @@ describe('resolveRuntimeCredentials', () => {
45
230
  const result = await resolveRuntimeCredentials(
46
231
  {
47
232
  workspaceId,
233
+ runId: crypto.randomUUID(),
234
+ stepAttemptId: crypto.randomUUID(),
48
235
  harness: 'pi',
49
236
  provider: 'anthropic',
50
237
  model: 'claude-opus-4-8',
@@ -60,6 +247,8 @@ describe('resolveRuntimeCredentials', () => {
60
247
  const matching = await resolveRuntimeCredentials(
61
248
  {
62
249
  workspaceId,
250
+ runId: crypto.randomUUID(),
251
+ stepAttemptId: crypto.randomUUID(),
63
252
  harness: 'pi',
64
253
  provider: 'anthropic',
65
254
  model: 'claude-opus-4-8',
@@ -70,6 +259,8 @@ describe('resolveRuntimeCredentials', () => {
70
259
  const mismatched = resolveRuntimeCredentials(
71
260
  {
72
261
  workspaceId,
262
+ runId: crypto.randomUUID(),
263
+ stepAttemptId: crypto.randomUUID(),
73
264
  harness: 'pi',
74
265
  provider: 'openai',
75
266
  model: 'gpt-5.5-pro',
@@ -82,6 +273,55 @@ describe('resolveRuntimeCredentials', () => {
82
273
  await expect(mismatched).rejects.toMatchObject({name: 'ModelProviderConfigNotFoundError'});
83
274
  });
84
275
 
276
+ it('refuses workspace credentials and instance fallback when workspace providers are disabled', async () => {
277
+ await saveProviderConfig({
278
+ workspaceId,
279
+ providerId: 'anthropic',
280
+ credentials: {api_key: 'sk-workspace-secret'},
281
+ });
282
+
283
+ const result = resolveRuntimeCredentials(
284
+ {
285
+ workspaceId,
286
+ runId: crypto.randomUUID(),
287
+ stepAttemptId: crypto.randomUUID(),
288
+ harness: 'pi',
289
+ provider: 'anthropic',
290
+ model: 'claude-opus-4-8',
291
+ thinking: 'high',
292
+ },
293
+ {
294
+ workspaceProviders: 'disabled',
295
+ runtimeConfig: instanceConfig(),
296
+ },
297
+ );
298
+
299
+ await expect(result).rejects.toThrow(ModelProviderConfigNotFoundError);
300
+ });
301
+
302
+ it('reports the managed provider when a foreign runtime provider is requested', async () => {
303
+ const result = resolveRuntimeCredentials(
304
+ {
305
+ workspaceId,
306
+ runId: crypto.randomUUID(),
307
+ stepAttemptId: crypto.randomUUID(),
308
+ harness: 'pi',
309
+ provider: 'anthropic',
310
+ model: 'claude-opus-4-8',
311
+ thinking: 'high',
312
+ },
313
+ {
314
+ workspaceProviders: 'disabled',
315
+ managedProvider: managedProvider(vi.fn()),
316
+ },
317
+ );
318
+
319
+ await expect(result).rejects.toMatchObject({
320
+ name: 'WorkspaceProvidersDisabledError',
321
+ managedProviderId: 'shipfox',
322
+ });
323
+ });
324
+
85
325
  it('returns custom provider runtime descriptors for custom rows', async () => {
86
326
  await saveProviderConfig({
87
327
  workspaceId,
@@ -98,6 +338,8 @@ describe('resolveRuntimeCredentials', () => {
98
338
 
99
339
  const result = await resolveRuntimeCredentials({
100
340
  workspaceId,
341
+ runId: crypto.randomUUID(),
342
+ stepAttemptId: crypto.randomUUID(),
101
343
  harness: 'pi',
102
344
  provider: 'local-vllm',
103
345
  model: 'llama-3.1',
@@ -137,6 +379,8 @@ describe('resolveRuntimeCredentials', () => {
137
379
 
138
380
  const result = await resolveRuntimeCredentials({
139
381
  workspaceId,
382
+ runId: crypto.randomUUID(),
383
+ stepAttemptId: crypto.randomUUID(),
140
384
  harness: 'pi',
141
385
  provider: 'local-ollama',
142
386
  model: 'llama-3.1',
@@ -155,6 +399,8 @@ describe('resolveRuntimeCredentials', () => {
155
399
  it('throws when no workspace or instance credential is available', async () => {
156
400
  const result = resolveRuntimeCredentials({
157
401
  workspaceId,
402
+ runId: crypto.randomUUID(),
403
+ stepAttemptId: crypto.randomUUID(),
158
404
  harness: 'pi',
159
405
  provider: 'anthropic',
160
406
  model: 'claude-opus-4-8',
@@ -174,6 +420,8 @@ describe('resolveRuntimeCredentials', () => {
174
420
 
175
421
  const result = resolveRuntimeCredentials({
176
422
  workspaceId,
423
+ runId: crypto.randomUUID(),
424
+ stepAttemptId: crypto.randomUUID(),
177
425
  harness: 'pi',
178
426
  provider: 'anthropic',
179
427
  model: 'claude-opus-4-8',
@@ -193,6 +441,8 @@ describe('resolveRuntimeCredentials', () => {
193
441
 
194
442
  const result = resolveRuntimeCredentials({
195
443
  workspaceId,
444
+ runId: crypto.randomUUID(),
445
+ stepAttemptId: crypto.randomUUID(),
196
446
  harness: 'pi',
197
447
  provider: 'anthropic',
198
448
  model: 'claude-opus-4-8',
@@ -217,6 +467,8 @@ describe('resolveRuntimeCredentials', () => {
217
467
 
218
468
  const result = resolveRuntimeCredentials({
219
469
  workspaceId,
470
+ runId: crypto.randomUUID(),
471
+ stepAttemptId: crypto.randomUUID(),
220
472
  harness: 'pi',
221
473
  provider: 'cloudflare-ai-gateway',
222
474
  model: '@cf/meta/llama-3.1-8b-instruct',
@@ -235,6 +487,8 @@ describe('resolveRuntimeCredentials', () => {
235
487
 
236
488
  const result = resolveRuntimeCredentials({
237
489
  workspaceId,
490
+ runId: crypto.randomUUID(),
491
+ stepAttemptId: crypto.randomUUID(),
238
492
  harness: 'pi',
239
493
  provider: 'anthropic',
240
494
  model: 'claude-opus-4-8',
@@ -256,6 +510,8 @@ describe('resolveRuntimeCredentials', () => {
256
510
  const result = resolveRuntimeCredentials(
257
511
  {
258
512
  workspaceId,
513
+ runId: crypto.randomUUID(),
514
+ stepAttemptId: crypto.randomUUID(),
259
515
  harness: 'pi',
260
516
  provider: 'anthropic',
261
517
  model: 'claude-opus-4-8',
@@ -314,3 +570,35 @@ function instanceConfig() {
314
570
  AGENT_DEFAULT_PROVIDER_API_KEY: 'sk-instance-secret',
315
571
  };
316
572
  }
573
+
574
+ function managedProvider(
575
+ resolveCredentials: ManagedModelProvider['resolveCredentials'],
576
+ ): ManagedModelProvider {
577
+ return {
578
+ id: 'shipfox',
579
+ label: 'Shipfox',
580
+ models: [
581
+ {id: 'claude-model', label: 'Claude model', api: 'anthropic-messages'},
582
+ {
583
+ id: 'responses-model',
584
+ label: 'Responses model',
585
+ api: 'openai-responses',
586
+ context_window: 1_000_000,
587
+ max_output_tokens: 65_536,
588
+ reasoning: true,
589
+ input_image: true,
590
+ },
591
+ {
592
+ id: 'partial-model',
593
+ label: 'Partial model',
594
+ api: 'openai-completions',
595
+ max_output_tokens: 8_192,
596
+ reasoning: false,
597
+ input_image: false,
598
+ },
599
+ {id: 'plain-model', label: 'Plain model', api: 'openai-completions'},
600
+ ],
601
+ defaultModel: 'responses-model',
602
+ resolveCredentials,
603
+ };
604
+ }
@@ -2,12 +2,16 @@ import type {
2
2
  AgentRuntimeCredentialsResponseDto,
3
3
  AgentThinking,
4
4
  Harness,
5
+ ManagedModelProvider,
6
+ ManagedProviderRuntimeConfig,
5
7
  ModelProviderRef,
6
8
  SupportedModelProviderId,
9
+ WorkspaceProvidersPolicy,
7
10
  } from '@shipfox/api-agent-dto';
11
+ import {toCustomAgentModelDto} from '@shipfox/api-agent-dto';
8
12
  import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
9
13
  import {isInterModuleKnownError} from '@shipfox/inter-module';
10
- import {config} from '#config.js';
14
+ import {config, workspaceProvidersPolicy} from '#config.js';
11
15
  import {getModelProviderConfig} from '#db/index.js';
12
16
  import {agentRuntimeConfigResolvedCount} from '#metrics/index.js';
13
17
  import {
@@ -16,12 +20,14 @@ import {
16
20
  storeValuesToRuntimeCredentials,
17
21
  } from './credential-fingerprints.js';
18
22
  import type {ModelProviderConfig} from './entities/model-provider-config.js';
19
- import {ModelProviderConfigNotFoundError} from './errors.js';
23
+ import {ModelProviderConfigNotFoundError, WorkspaceProvidersDisabledError} from './errors.js';
20
24
  import {getModelProviderEntry, modelProviderCredentialKeysMatch} from './model-provider-policy.js';
21
25
  import {type AgentSecretsClient, requireAgentSecretsClient} from './secrets-client.js';
22
26
 
23
27
  export interface ResolveRuntimeCredentialsParams {
24
28
  workspaceId: string;
29
+ runId: string;
30
+ stepAttemptId: string;
25
31
  harness: Harness;
26
32
  provider: ModelProviderRef;
27
33
  model: string;
@@ -29,13 +35,15 @@ export interface ResolveRuntimeCredentialsParams {
29
35
  }
30
36
 
31
37
  interface RuntimeCredentialsConfig {
32
- AGENT_DEFAULT_PROVIDER?: SupportedModelProviderId | undefined;
38
+ AGENT_DEFAULT_PROVIDER?: ModelProviderRef | undefined;
33
39
  AGENT_DEFAULT_PROVIDER_API_KEY?: string | undefined;
34
40
  }
35
41
 
36
42
  interface ResolveRuntimeCredentialsOptions {
37
43
  runtimeConfig?: RuntimeCredentialsConfig | undefined;
44
+ workspaceProviders?: WorkspaceProvidersPolicy | undefined;
38
45
  secrets?: AgentSecretsClient | undefined;
46
+ managedProvider?: ManagedModelProvider | undefined;
39
47
  getCredentialBag?:
40
48
  | ((params: {workspaceId: string; namespace: string}) => Promise<Record<string, string>>)
41
49
  | undefined;
@@ -46,7 +54,34 @@ export async function resolveRuntimeCredentials(
46
54
  options?: ResolveRuntimeCredentialsOptions,
47
55
  ): Promise<AgentRuntimeCredentialsResponseDto> {
48
56
  const runtimeConfig = options?.runtimeConfig ?? config;
57
+ const configuredWorkspaceProviders = options?.workspaceProviders ?? workspaceProvidersPolicy;
49
58
  const secrets = options?.secrets;
59
+ const managedProvider = options?.managedProvider;
60
+ if (managedProvider?.id === params.provider) {
61
+ const managedRuntimeConfig = await managedProvider.resolveCredentials({
62
+ workspaceId: params.workspaceId,
63
+ runId: params.runId,
64
+ stepAttemptId: params.stepAttemptId,
65
+ model: params.model,
66
+ });
67
+ agentRuntimeConfigResolvedCount.add(1, {source: 'instance', outcome: 'resolved'});
68
+ return toResponse(params, managedRuntimeConfig.credentials, undefined, {
69
+ provider: managedProvider,
70
+ runtimeConfig: managedRuntimeConfig,
71
+ });
72
+ }
73
+
74
+ if (configuredWorkspaceProviders === 'disabled') {
75
+ agentRuntimeConfigResolvedCount.add(1, {
76
+ source: params.provider === runtimeConfig.AGENT_DEFAULT_PROVIDER ? 'instance' : 'workspace',
77
+ outcome: 'unavailable',
78
+ });
79
+ if (managedProvider !== undefined) {
80
+ throw new WorkspaceProvidersDisabledError(managedProvider.id);
81
+ }
82
+ throw new ModelProviderConfigNotFoundError(params.workspaceId, params.provider);
83
+ }
84
+
50
85
  const providerConfig = await getModelProviderConfig({
51
86
  workspaceId: params.workspaceId,
52
87
  providerId: params.provider,
@@ -133,6 +168,12 @@ function toResponse(
133
168
  params: ResolveRuntimeCredentialsParams,
134
169
  credentials: Record<string, string>,
135
170
  providerConfig?: ModelProviderConfig | undefined,
171
+ managed?:
172
+ | {
173
+ provider: ManagedModelProvider;
174
+ runtimeConfig: ManagedProviderRuntimeConfig;
175
+ }
176
+ | undefined,
136
177
  ): AgentRuntimeCredentialsResponseDto {
137
178
  const response: AgentRuntimeCredentialsResponseDto = {
138
179
  harness: params.harness,
@@ -156,5 +197,30 @@ function toResponse(
156
197
  };
157
198
  }
158
199
 
200
+ if (managed !== undefined) {
201
+ const model = managed.provider.models.find((candidate) => candidate.id === params.model);
202
+ const modelDescriptor = toCustomAgentModelDto(model ?? {id: params.model, label: params.model});
203
+
204
+ if (params.harness === 'pi') {
205
+ response.custom_provider = {
206
+ api: managed.runtimeConfig.api,
207
+ base_url: managed.runtimeConfig.baseUrl,
208
+ headers: [],
209
+ secret_header_names: [],
210
+ models: [modelDescriptor],
211
+ requires_api_key: true,
212
+ };
213
+ } else {
214
+ const authToken = managed.runtimeConfig.credentials.api_key;
215
+ if (authToken === undefined) {
216
+ throw new ModelProviderConfigNotFoundError(params.workspaceId, params.provider);
217
+ }
218
+ response.claude = {
219
+ base_url: managed.runtimeConfig.baseUrl,
220
+ auth_token: authToken,
221
+ };
222
+ }
223
+ }
224
+
159
225
  return response;
160
226
  }
@@ -1,3 +1,4 @@
1
+ import type {ManagedModelProvider} from '@shipfox/api-agent-dto';
1
2
  import {getAgentValidationCatalog} from './validation-catalog.js';
2
3
 
3
4
  describe('getAgentValidationCatalog', () => {
@@ -13,4 +14,57 @@ describe('getAgentValidationCatalog', () => {
13
14
  expect(pi?.model_ids_by_provider?.openai).toContain('gpt-5.5-pro');
14
15
  expect(claude?.model_ids_by_provider?.anthropic).toContain('claude-opus-4-8');
15
16
  });
17
+
18
+ it('includes injected managed providers with harness-compatible models', () => {
19
+ const managedProvider = {
20
+ id: 'shipfox-managed',
21
+ label: 'Shipfox Managed',
22
+ models: [
23
+ {id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages' as const},
24
+ {id: 'managed-responses', label: 'Managed Responses', api: 'openai-responses' as const},
25
+ ],
26
+ defaultModel: 'managed-claude',
27
+ resolveCredentials: async () => ({
28
+ api: 'anthropic-messages' as const,
29
+ baseUrl: 'https://gateway.example.com',
30
+ credentials: {api_key: 'token'},
31
+ }),
32
+ } satisfies ManagedModelProvider;
33
+
34
+ const catalog = getAgentValidationCatalog(managedProvider);
35
+ const pi = catalog.harnesses.find((harness) => harness.id === 'pi');
36
+ const claude = catalog.harnesses.find((harness) => harness.id === 'claude');
37
+
38
+ expect(catalog.providers).toContainEqual({id: managedProvider.id, support_status: 'supported'});
39
+ expect(pi?.supported_provider_ids).toContain(managedProvider.id);
40
+ expect(pi?.model_ids_by_provider?.[managedProvider.id]).toEqual([
41
+ 'managed-claude',
42
+ 'managed-responses',
43
+ ]);
44
+ expect(claude?.supported_provider_ids).toContain(managedProvider.id);
45
+ expect(claude?.model_ids_by_provider?.[managedProvider.id]).toEqual(['managed-claude']);
46
+ });
47
+
48
+ it('exposes only the managed provider when workspace providers are disabled', () => {
49
+ const managedProvider = {
50
+ id: 'shipfox-managed',
51
+ label: 'Shipfox Managed',
52
+ models: [{id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages' as const}],
53
+ defaultModel: 'managed-claude',
54
+ resolveCredentials: async () => ({
55
+ api: 'anthropic-messages' as const,
56
+ baseUrl: 'https://gateway.example.com',
57
+ credentials: {api_key: 'token'},
58
+ }),
59
+ } satisfies ManagedModelProvider;
60
+
61
+ const catalog = getAgentValidationCatalog(managedProvider, 'disabled');
62
+
63
+ expect(catalog.providers).toEqual([{id: managedProvider.id, support_status: 'supported'}]);
64
+ expect(
65
+ catalog.harnesses.every((harness) =>
66
+ harness.supported_provider_ids.every((providerId) => providerId === managedProvider.id),
67
+ ),
68
+ ).toBe(true);
69
+ });
16
70
  });