@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +30 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +75 -7
- package/dist/config.js.map +1 -1
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-provider-catalog.d.ts +5 -2
- package/dist/core/model-provider-catalog.d.ts.map +1 -1
- package/dist/core/model-provider-catalog.js +33 -1
- package/dist/core/model-provider-catalog.js.map +1 -1
- package/dist/core/resolve-agent-config.d.ts +4 -2
- package/dist/core/resolve-agent-config.d.ts.map +1 -1
- package/dist/core/resolve-agent-config.js +86 -31
- package/dist/core/resolve-agent-config.js.map +1 -1
- package/dist/core/resolve-runtime-credentials.d.ts +6 -2
- package/dist/core/resolve-runtime-credentials.d.ts.map +1 -1
- package/dist/core/resolve-runtime-credentials.js +60 -3
- package/dist/core/resolve-runtime-credentials.js.map +1 -1
- package/dist/core/validation-catalog.d.ts +2 -1
- package/dist/core/validation-catalog.d.ts.map +1 -1
- package/dist/core/validation-catalog.js +65 -17
- package/dist/core/validation-catalog.js.map +1 -1
- package/dist/core/workspace-agent-defaults-resolver.d.ts +2 -1
- package/dist/core/workspace-agent-defaults-resolver.d.ts.map +1 -1
- package/dist/core/workspace-agent-defaults-resolver.js +4 -2
- package/dist/core/workspace-agent-defaults-resolver.js.map +1 -1
- package/dist/core/workspace-provider-policy.d.ts +7 -0
- package/dist/core/workspace-provider-policy.d.ts.map +1 -0
- package/dist/core/workspace-provider-policy.js +10 -0
- package/dist/core/workspace-provider-policy.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -1
- package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/create-model-provider.js +7 -1
- package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -1
- package/dist/presentation/e2eRoutes/index.d.ts +2 -1
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/index.js +4 -2
- package/dist/presentation/e2eRoutes/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +3 -0
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +22 -6
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -1
- package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -1
- package/dist/presentation/routes/create-custom-model-provider.js +5 -2
- package/dist/presentation/routes/create-custom-model-provider.js.map +1 -1
- package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -1
- package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -1
- package/dist/presentation/routes/delete-model-provider-config.js +7 -2
- package/dist/presentation/routes/delete-model-provider-config.js.map +1 -1
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -1
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -1
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +5 -2
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -1
- package/dist/presentation/routes/discover-custom-model-provider-models.d.ts +2 -0
- package/dist/presentation/routes/discover-custom-model-provider-models.d.ts.map +1 -1
- package/dist/presentation/routes/discover-custom-model-provider-models.js +29 -23
- package/dist/presentation/routes/discover-custom-model-provider-models.js.map +1 -1
- package/dist/presentation/routes/errors.d.ts.map +1 -1
- package/dist/presentation/routes/errors.js +10 -1
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +5 -1
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +21 -14
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/list-model-provider-catalog.d.ts +5 -0
- package/dist/presentation/routes/list-model-provider-catalog.d.ts.map +1 -1
- package/dist/presentation/routes/list-model-provider-catalog.js +21 -13
- package/dist/presentation/routes/list-model-provider-catalog.js.map +1 -1
- package/dist/presentation/routes/set-default-model-provider.d.ts +2 -0
- package/dist/presentation/routes/set-default-model-provider.d.ts.map +1 -1
- package/dist/presentation/routes/set-default-model-provider.js +44 -38
- package/dist/presentation/routes/set-default-model-provider.js.map +1 -1
- package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -1
- package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -1
- package/dist/presentation/routes/update-custom-model-provider.js +5 -2
- package/dist/presentation/routes/update-custom-model-provider.js.map +1 -1
- package/dist/presentation/routes/update-model-provider-default-model.d.ts +2 -0
- package/dist/presentation/routes/update-model-provider-default-model.d.ts.map +1 -1
- package/dist/presentation/routes/update-model-provider-default-model.js +35 -29
- package/dist/presentation/routes/update-model-provider-default-model.js.map +1 -1
- package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -1
- package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -1
- package/dist/presentation/routes/upsert-model-provider-config.js +5 -2
- package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/config.test.ts +196 -4
- package/src/config.ts +100 -9
- package/src/core/errors.ts +7 -0
- package/src/core/index.ts +5 -0
- package/src/core/model-provider-catalog.test.ts +30 -0
- package/src/core/model-provider-catalog.ts +41 -1
- package/src/core/resolve-agent-config.test.ts +92 -0
- package/src/core/resolve-agent-config.ts +152 -43
- package/src/core/resolve-runtime-credentials.test.ts +289 -1
- package/src/core/resolve-runtime-credentials.ts +69 -3
- package/src/core/validation-catalog.test.ts +54 -0
- package/src/core/validation-catalog.ts +124 -19
- package/src/core/workspace-agent-defaults-resolver.ts +7 -2
- package/src/core/workspace-provider-policy.ts +19 -0
- package/src/index.ts +25 -4
- package/src/presentation/e2eRoutes/create-model-provider.ts +11 -1
- package/src/presentation/e2eRoutes/index.ts +9 -2
- package/src/presentation/inter-module.test.ts +51 -0
- package/src/presentation/inter-module.ts +38 -6
- package/src/presentation/routes/create-custom-model-provider.ts +10 -2
- package/src/presentation/routes/delete-model-provider-config.ts +12 -2
- package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +7 -1
- package/src/presentation/routes/discover-custom-model-provider-models.ts +30 -18
- package/src/presentation/routes/errors.ts +11 -0
- package/src/presentation/routes/index.ts +32 -14
- package/src/presentation/routes/list-model-provider-catalog.ts +32 -11
- package/src/presentation/routes/model-provider-catalog.test.ts +49 -1
- package/src/presentation/routes/model-provider-config.test.ts +62 -1
- package/src/presentation/routes/set-default-model-provider.ts +42 -31
- package/src/presentation/routes/update-custom-model-provider.ts +10 -2
- package/src/presentation/routes/update-model-provider-default-model.ts +37 -26
- package/src/presentation/routes/upsert-model-provider-config.ts +10 -2
- 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?:
|
|
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
|
});
|