@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-agent",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "14.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"drizzle-orm": "^0.45.2",
|
|
40
40
|
"zod": "^4.4.3",
|
|
41
41
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
42
|
-
"@shipfox/api-agent-dto": "
|
|
42
|
+
"@shipfox/api-agent-dto": "14.0.0",
|
|
43
43
|
"@shipfox/api-auth-context": "12.2.0",
|
|
44
44
|
"@shipfox/api-secrets-dto": "12.0.0",
|
|
45
45
|
"@shipfox/inter-module": "0.2.3",
|
package/src/config.test.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type {ManagedModelEntry, ManagedModelProvider} from '@shipfox/api-agent-dto';
|
|
2
|
+
|
|
1
3
|
describe('agent config', () => {
|
|
2
4
|
afterEach(() => {
|
|
3
5
|
vi.unstubAllEnvs();
|
|
@@ -14,14 +16,56 @@ describe('agent config', () => {
|
|
|
14
16
|
expect(module.config.AGENT_DEFAULT_PROVIDER_API_KEY).toBe('sk-instance-secret');
|
|
15
17
|
});
|
|
16
18
|
|
|
19
|
+
it('defaults workspace provider configuration to enabled', async () => {
|
|
20
|
+
vi.resetModules();
|
|
21
|
+
|
|
22
|
+
const module = await import('./config.js');
|
|
23
|
+
|
|
24
|
+
expect(module.config.AGENT_WORKSPACE_PROVIDERS).toBe('enabled');
|
|
25
|
+
expect(module.workspaceProvidersPolicy).toBe('enabled');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('accepts disabled workspace provider configuration with a managed provider', async () => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
vi.stubEnv('AGENT_WORKSPACE_PROVIDERS', 'disabled');
|
|
31
|
+
|
|
32
|
+
const module = await import('./config.js');
|
|
33
|
+
|
|
34
|
+
expect(module.workspaceProvidersPolicy).toBe('disabled');
|
|
35
|
+
expect(() => module.assertAgentConfig(managedProvider())).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('requires a managed provider when workspace provider configuration is disabled', async () => {
|
|
39
|
+
vi.resetModules();
|
|
40
|
+
vi.stubEnv('AGENT_WORKSPACE_PROVIDERS', 'disabled');
|
|
41
|
+
|
|
42
|
+
const module = await import('./config.js');
|
|
43
|
+
|
|
44
|
+
expect(() => module.assertAgentConfig()).toThrow(
|
|
45
|
+
'AGENT_WORKSPACE_PROVIDERS=disabled requires an injected managed model provider',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects a foreign instance default when workspace provider configuration is disabled', async () => {
|
|
50
|
+
vi.resetModules();
|
|
51
|
+
vi.stubEnv('AGENT_WORKSPACE_PROVIDERS', 'disabled');
|
|
52
|
+
vi.stubEnv('AGENT_DEFAULT_PROVIDER', 'openai');
|
|
53
|
+
|
|
54
|
+
const module = await import('./config.js');
|
|
55
|
+
|
|
56
|
+
expect(() => module.assertAgentConfig(managedProvider())).toThrow(
|
|
57
|
+
'This instance only supports provider `shipfox`',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
17
61
|
it('throws when an instance key is set for a multi-field provider', async () => {
|
|
18
62
|
vi.resetModules();
|
|
19
63
|
vi.stubEnv('AGENT_DEFAULT_PROVIDER', 'azure-openai-responses');
|
|
20
64
|
vi.stubEnv('AGENT_DEFAULT_PROVIDER_API_KEY', 'sk-instance-secret');
|
|
21
65
|
|
|
22
|
-
const
|
|
66
|
+
const module = await import('./config.js');
|
|
23
67
|
|
|
24
|
-
|
|
68
|
+
expect(() => module.assertAgentConfig()).toThrow(
|
|
25
69
|
'AGENT_DEFAULT_PROVIDER_API_KEY requires AGENT_DEFAULT_PROVIDER',
|
|
26
70
|
);
|
|
27
71
|
});
|
|
@@ -30,13 +74,131 @@ describe('agent config', () => {
|
|
|
30
74
|
vi.resetModules();
|
|
31
75
|
vi.stubEnv('AGENT_DEFAULT_PROVIDER_API_KEY', 'sk-instance-secret');
|
|
32
76
|
|
|
33
|
-
const
|
|
77
|
+
const module = await import('./config.js');
|
|
34
78
|
|
|
35
|
-
|
|
79
|
+
expect(() => module.assertAgentConfig()).toThrow(
|
|
36
80
|
'AGENT_DEFAULT_PROVIDER_API_KEY requires AGENT_DEFAULT_PROVIDER',
|
|
37
81
|
);
|
|
38
82
|
});
|
|
39
83
|
|
|
84
|
+
it('accepts an injected managed provider as the instance default', async () => {
|
|
85
|
+
vi.resetModules();
|
|
86
|
+
vi.stubEnv('AGENT_DEFAULT_PROVIDER', 'shipfox');
|
|
87
|
+
|
|
88
|
+
const module = await import('./config.js');
|
|
89
|
+
|
|
90
|
+
expect(() => module.assertAgentConfig(managedProvider())).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('rejects an instance API key for an injected managed provider', async () => {
|
|
94
|
+
vi.resetModules();
|
|
95
|
+
vi.stubEnv('AGENT_DEFAULT_PROVIDER', 'shipfox');
|
|
96
|
+
vi.stubEnv('AGENT_DEFAULT_PROVIDER_API_KEY', 'sk-instance-secret');
|
|
97
|
+
|
|
98
|
+
const module = await import('./config.js');
|
|
99
|
+
|
|
100
|
+
expect(() => module.assertAgentConfig(managedProvider())).toThrow(
|
|
101
|
+
'AGENT_DEFAULT_PROVIDER_API_KEY cannot be used with a managed model provider',
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects an instance default that is not a registered provider', async () => {
|
|
106
|
+
vi.resetModules();
|
|
107
|
+
vi.stubEnv('AGENT_DEFAULT_PROVIDER', 'not-registered');
|
|
108
|
+
|
|
109
|
+
const module = await import('./config.js');
|
|
110
|
+
|
|
111
|
+
expect(() => module.assertAgentConfig()).toThrow(
|
|
112
|
+
'AGENT_DEFAULT_PROVIDER must name a supported registered provider',
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it.each([
|
|
117
|
+
['invalid provider ID', managedProvider({id: 'bad_id'}), 'valid provider slug'],
|
|
118
|
+
['reserved provider ID', managedProvider({id: 'openai'}), 'provider ID is reserved'],
|
|
119
|
+
['empty provider label', managedProvider({label: ''}), 'label must not be empty'],
|
|
120
|
+
['empty model list', managedProvider({models: []}), 'at least one model'],
|
|
121
|
+
[
|
|
122
|
+
'empty model ID',
|
|
123
|
+
managedProvider({
|
|
124
|
+
models: [{id: '', label: 'Managed model', api: 'anthropic-messages'}],
|
|
125
|
+
}),
|
|
126
|
+
'models must have IDs and labels',
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
'empty model label',
|
|
130
|
+
managedProvider({
|
|
131
|
+
models: [{id: 'managed-model', label: '', api: 'anthropic-messages'}],
|
|
132
|
+
}),
|
|
133
|
+
'models must have IDs and labels',
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
'duplicate model IDs',
|
|
137
|
+
managedProvider({
|
|
138
|
+
models: [
|
|
139
|
+
{id: 'managed-model', label: 'Managed model', api: 'anthropic-messages'},
|
|
140
|
+
{id: 'managed-model', label: 'Managed model 2', api: 'openai-responses'},
|
|
141
|
+
],
|
|
142
|
+
}),
|
|
143
|
+
'models must have unique IDs',
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
'invalid model API',
|
|
147
|
+
managedProvider({
|
|
148
|
+
models: [
|
|
149
|
+
{
|
|
150
|
+
id: 'managed-model',
|
|
151
|
+
label: 'Managed model',
|
|
152
|
+
api: 'invalid' as unknown as ManagedModelEntry['api'],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
'model API is invalid',
|
|
157
|
+
],
|
|
158
|
+
...(
|
|
159
|
+
[
|
|
160
|
+
['null context window', {context_window: null}],
|
|
161
|
+
['zero max output tokens', {max_output_tokens: 0}],
|
|
162
|
+
['fractional context window', {context_window: 1.5}],
|
|
163
|
+
['string context window', {context_window: '128000'}],
|
|
164
|
+
['non-boolean reasoning', {reasoning: 'true'}],
|
|
165
|
+
['non-boolean input image', {input_image: 1}],
|
|
166
|
+
] as const
|
|
167
|
+
).map(
|
|
168
|
+
([name, metadata]) =>
|
|
169
|
+
[
|
|
170
|
+
`invalid ${name}`,
|
|
171
|
+
managedProviderWithModelMetadata(metadata),
|
|
172
|
+
'model metadata is invalid',
|
|
173
|
+
] as const,
|
|
174
|
+
),
|
|
175
|
+
[
|
|
176
|
+
'unregistered default model',
|
|
177
|
+
managedProvider({defaultModel: 'missing-model'}),
|
|
178
|
+
'default model is not registered',
|
|
179
|
+
],
|
|
180
|
+
[
|
|
181
|
+
'invalid default thinking',
|
|
182
|
+
managedProvider({
|
|
183
|
+
defaultThinking: 'invalid' as unknown as ManagedModelProvider['defaultThinking'],
|
|
184
|
+
}),
|
|
185
|
+
'default thinking is invalid',
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
'missing credential resolver',
|
|
189
|
+
managedProvider({
|
|
190
|
+
resolveCredentials: undefined as unknown as ManagedModelProvider['resolveCredentials'],
|
|
191
|
+
}),
|
|
192
|
+
'must resolve credentials',
|
|
193
|
+
],
|
|
194
|
+
] as const)('rejects a managed provider with %s', async (_name, provider, message) => {
|
|
195
|
+
vi.resetModules();
|
|
196
|
+
|
|
197
|
+
const module = await import('./config.js');
|
|
198
|
+
|
|
199
|
+
expect(() => module.assertAgentConfig(provider)).toThrow(message);
|
|
200
|
+
});
|
|
201
|
+
|
|
40
202
|
it('imports without an instance key', async () => {
|
|
41
203
|
vi.resetModules();
|
|
42
204
|
vi.stubEnv('AGENT_DEFAULT_PROVIDER', 'azure-openai-responses');
|
|
@@ -116,3 +278,33 @@ describe('agent config', () => {
|
|
|
116
278
|
await expect(importConfig).rejects.toThrow('AGENT_PI_ENABLED_TOOL_PACKAGES');
|
|
117
279
|
});
|
|
118
280
|
});
|
|
281
|
+
|
|
282
|
+
function managedProvider(overrides: Partial<ManagedModelProvider> = {}): ManagedModelProvider {
|
|
283
|
+
return {
|
|
284
|
+
id: 'shipfox',
|
|
285
|
+
label: 'Shipfox',
|
|
286
|
+
models: [{id: 'claude-opus-4-8', label: 'Claude Opus 4.8', api: 'anthropic-messages' as const}],
|
|
287
|
+
defaultModel: 'claude-opus-4-8',
|
|
288
|
+
defaultThinking: 'high' as const,
|
|
289
|
+
resolveCredentials: async () => ({
|
|
290
|
+
api: 'anthropic-messages' as const,
|
|
291
|
+
baseUrl: 'https://gateway.example.com',
|
|
292
|
+
credentials: {api_key: 'token'},
|
|
293
|
+
}),
|
|
294
|
+
...overrides,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function managedProviderWithModelMetadata(metadata: object): ManagedModelProvider {
|
|
299
|
+
return managedProvider({
|
|
300
|
+
models: [
|
|
301
|
+
{
|
|
302
|
+
id: 'managed-model',
|
|
303
|
+
label: 'Managed model',
|
|
304
|
+
api: 'anthropic-messages',
|
|
305
|
+
...metadata,
|
|
306
|
+
} as ManagedModelEntry,
|
|
307
|
+
],
|
|
308
|
+
defaultModel: 'managed-model',
|
|
309
|
+
});
|
|
310
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
agentThinkingSchema,
|
|
3
3
|
buildHarnessToolDeploymentConfig,
|
|
4
|
+
isReservedModelProviderId,
|
|
5
|
+
type ManagedModelProvider,
|
|
6
|
+
managedModelApiSchema,
|
|
7
|
+
managedModelMetadataSchema,
|
|
8
|
+
modelProviderRefSchema,
|
|
4
9
|
SUPPORTED_MODEL_PROVIDER_IDS,
|
|
5
|
-
type
|
|
10
|
+
type WorkspaceProvidersPolicy,
|
|
6
11
|
} from '@shipfox/api-agent-dto';
|
|
7
12
|
import {bool, createConfig, num, str} from '@shipfox/config';
|
|
13
|
+
import {WorkspaceProvidersDisabledError} from '#core/errors.js';
|
|
8
14
|
import {getModelProviderEntry} from '#core/model-provider-policy.js';
|
|
9
15
|
|
|
10
16
|
const AGENT_THINKING_CHOICES = agentThinkingSchema.options;
|
|
17
|
+
const SUPPORTED_PROVIDER_IDS_DESCRIPTION = SUPPORTED_MODEL_PROVIDER_IDS.join(', ');
|
|
11
18
|
|
|
12
19
|
export const config = createConfig({
|
|
20
|
+
AGENT_WORKSPACE_PROVIDERS: str({
|
|
21
|
+
desc: 'Controls whether workspaces can configure model providers. Use enabled to preserve the default workspace provider behavior, or disabled when the injected managed provider is the only provider for this instance.',
|
|
22
|
+
choices: ['enabled', 'disabled'],
|
|
23
|
+
default: 'enabled',
|
|
24
|
+
}),
|
|
13
25
|
AGENT_DEFAULT_PROVIDER: str({
|
|
14
|
-
desc:
|
|
15
|
-
choices: SUPPORTED_MODEL_PROVIDER_IDS,
|
|
26
|
+
desc: `Instance-wide default model provider ID used when a workflow and workspace do not choose one. Optional. Use one of the supported model catalog IDs (${SUPPORTED_PROVIDER_IDS_DESCRIPTION}) or the injected managed provider.`,
|
|
16
27
|
default: undefined,
|
|
17
28
|
}),
|
|
18
29
|
AGENT_DEFAULT_PROVIDER_MODEL: str({
|
|
@@ -50,22 +61,47 @@ export const config = createConfig({
|
|
|
50
61
|
}),
|
|
51
62
|
});
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
export const workspaceProvidersPolicy =
|
|
65
|
+
config.AGENT_WORKSPACE_PROVIDERS as WorkspaceProvidersPolicy;
|
|
54
66
|
|
|
55
67
|
export const harnessToolDeploymentConfig = buildHarnessToolDeploymentConfig({
|
|
56
68
|
piEnabledToolPackages: config.AGENT_PI_ENABLED_TOOL_PACKAGES,
|
|
57
69
|
piWebSearchEnabled: config.AGENT_PI_WEB_SEARCH_ENABLED,
|
|
58
70
|
});
|
|
59
71
|
|
|
60
|
-
function
|
|
72
|
+
export function assertAgentConfig(managedProvider?: ManagedModelProvider): void {
|
|
73
|
+
if (managedProvider !== undefined) assertManagedProvider(managedProvider);
|
|
74
|
+
|
|
75
|
+
if (workspaceProvidersPolicy === 'disabled') {
|
|
76
|
+
if (managedProvider === undefined) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'AGENT_WORKSPACE_PROVIDERS=disabled requires an injected managed model provider.',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
config.AGENT_DEFAULT_PROVIDER !== undefined &&
|
|
83
|
+
config.AGENT_DEFAULT_PROVIDER !== managedProvider.id
|
|
84
|
+
) {
|
|
85
|
+
throw new WorkspaceProvidersDisabledError(managedProvider.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const defaultProvider = config.AGENT_DEFAULT_PROVIDER;
|
|
90
|
+
if (defaultProvider !== undefined && !isRegisteredProvider(defaultProvider, managedProvider)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`AGENT_DEFAULT_PROVIDER must name a supported registered provider: ${defaultProvider}.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
if (!config.AGENT_DEFAULT_PROVIDER_API_KEY) return;
|
|
62
|
-
if (!
|
|
97
|
+
if (!defaultProvider) {
|
|
63
98
|
throw new Error('AGENT_DEFAULT_PROVIDER_API_KEY requires AGENT_DEFAULT_PROVIDER to be set.');
|
|
64
99
|
}
|
|
100
|
+
if (managedProvider?.id === defaultProvider) {
|
|
101
|
+
throw new Error('AGENT_DEFAULT_PROVIDER_API_KEY cannot be used with a managed model provider.');
|
|
102
|
+
}
|
|
65
103
|
|
|
66
|
-
const credentialFields =
|
|
67
|
-
getModelProviderEntry(config.AGENT_DEFAULT_PROVIDER as SupportedModelProviderId)
|
|
68
|
-
?.credential_fields ?? [];
|
|
104
|
+
const credentialFields = getModelProviderEntry(defaultProvider)?.credential_fields ?? [];
|
|
69
105
|
const field = credentialFields[0];
|
|
70
106
|
if (credentialFields.length === 1 && field?.key === 'api_key' && field.secret) return;
|
|
71
107
|
|
|
@@ -73,3 +109,58 @@ function assertInstanceDefaultModelProviderApiKeyConfig(): void {
|
|
|
73
109
|
'AGENT_DEFAULT_PROVIDER_API_KEY requires AGENT_DEFAULT_PROVIDER to use exactly one secret api_key credential field.',
|
|
74
110
|
);
|
|
75
111
|
}
|
|
112
|
+
|
|
113
|
+
function isRegisteredProvider(
|
|
114
|
+
providerId: string,
|
|
115
|
+
managedProvider: ManagedModelProvider | undefined,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (managedProvider?.id === providerId) return true;
|
|
118
|
+
return getModelProviderEntry(providerId)?.support_status === 'supported';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertManagedProvider(provider: ManagedModelProvider): void {
|
|
122
|
+
if (!modelProviderRefSchema.safeParse(provider.id).success) {
|
|
123
|
+
throw new Error(`Managed model provider ID must be a valid provider slug: ${provider.id}.`);
|
|
124
|
+
}
|
|
125
|
+
if (isReservedModelProviderId(provider.id)) {
|
|
126
|
+
throw new Error(`Managed model provider ID is reserved: ${provider.id}.`);
|
|
127
|
+
}
|
|
128
|
+
if (provider.label.length === 0) {
|
|
129
|
+
throw new Error(`Managed model provider label must not be empty: ${provider.id}.`);
|
|
130
|
+
}
|
|
131
|
+
if (provider.models.length === 0) {
|
|
132
|
+
throw new Error(`Managed model provider must define at least one model: ${provider.id}.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const modelIds = new Set<string>();
|
|
136
|
+
for (const model of provider.models) {
|
|
137
|
+
if (model.id.length === 0 || model.label.length === 0) {
|
|
138
|
+
throw new Error(`Managed model provider models must have IDs and labels: ${provider.id}.`);
|
|
139
|
+
}
|
|
140
|
+
if (modelIds.has(model.id)) {
|
|
141
|
+
throw new Error(`Managed model provider models must have unique IDs: ${provider.id}.`);
|
|
142
|
+
}
|
|
143
|
+
modelIds.add(model.id);
|
|
144
|
+
if (!managedModelApiSchema.safeParse(model.api).success) {
|
|
145
|
+
throw new Error(`Managed model provider model API is invalid: ${provider.id}/${model.id}.`);
|
|
146
|
+
}
|
|
147
|
+
if (!managedModelMetadataSchema.safeParse(model).success) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Managed model provider model metadata is invalid: ${provider.id}/${model.id}.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!modelIds.has(provider.defaultModel)) {
|
|
155
|
+
throw new Error(`Managed model provider default model is not registered: ${provider.id}.`);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
provider.defaultThinking !== undefined &&
|
|
159
|
+
!agentThinkingSchema.safeParse(provider.defaultThinking).success
|
|
160
|
+
) {
|
|
161
|
+
throw new Error(`Managed model provider default thinking is invalid: ${provider.id}.`);
|
|
162
|
+
}
|
|
163
|
+
if (typeof provider.resolveCredentials !== 'function') {
|
|
164
|
+
throw new Error(`Managed model provider must resolve credentials: ${provider.id}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/core/errors.ts
CHANGED
|
@@ -32,6 +32,13 @@ export class UnsupportedModelProviderError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export class WorkspaceProvidersDisabledError extends Error {
|
|
36
|
+
constructor(public readonly managedProviderId: string) {
|
|
37
|
+
super(`This instance only supports provider \`${managedProviderId}\`.`);
|
|
38
|
+
this.name = 'WorkspaceProvidersDisabledError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
export class UnsupportedHarnessProviderError extends Error {
|
|
36
43
|
constructor(
|
|
37
44
|
public readonly harness: Harness,
|
package/src/core/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
UnsupportedHarnessProviderError,
|
|
20
20
|
UnsupportedHarnessThinkingError,
|
|
21
21
|
UnsupportedModelProviderError,
|
|
22
|
+
WorkspaceProvidersDisabledError,
|
|
22
23
|
} from './errors.js';
|
|
23
24
|
export {
|
|
24
25
|
getHarnessDescriptor,
|
|
@@ -61,3 +62,7 @@ export {
|
|
|
61
62
|
resolveRuntimeCredentials,
|
|
62
63
|
} from './resolve-runtime-credentials.js';
|
|
63
64
|
export {createWorkspaceAgentDefaultsResolver} from './workspace-agent-defaults-resolver.js';
|
|
65
|
+
export {
|
|
66
|
+
assertWorkspaceProviderConfigurationEnabled,
|
|
67
|
+
type WorkspaceProviderPolicyOptions,
|
|
68
|
+
} from './workspace-provider-policy.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {ManagedModelProvider} from '@shipfox/api-agent-dto';
|
|
2
|
+
import {buildModelProviderCatalog} from './model-provider-catalog.js';
|
|
3
|
+
|
|
4
|
+
describe('buildModelProviderCatalog', () => {
|
|
5
|
+
it('deep-freezes managed provider entries in both policy modes', () => {
|
|
6
|
+
const managedProvider = {
|
|
7
|
+
id: 'shipfox-managed',
|
|
8
|
+
label: 'Shipfox Managed',
|
|
9
|
+
models: [{id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages' as const}],
|
|
10
|
+
defaultModel: 'managed-claude',
|
|
11
|
+
resolveCredentials: async () => ({
|
|
12
|
+
api: 'anthropic-messages' as const,
|
|
13
|
+
baseUrl: 'https://gateway.example.com',
|
|
14
|
+
credentials: {api_key: 'token'},
|
|
15
|
+
}),
|
|
16
|
+
} satisfies ManagedModelProvider;
|
|
17
|
+
|
|
18
|
+
for (const workspaceProviders of ['enabled', 'disabled'] as const) {
|
|
19
|
+
const catalog = buildModelProviderCatalog({managedProvider, workspaceProviders});
|
|
20
|
+
const managedEntry = catalog.find((entry) => entry.id === managedProvider.id);
|
|
21
|
+
|
|
22
|
+
if (managedEntry === undefined) throw new Error('Missing managed provider catalog entry');
|
|
23
|
+
|
|
24
|
+
expect(Object.isFrozen(managedEntry)).toBe(true);
|
|
25
|
+
expect(Object.isFrozen(managedEntry.models)).toBe(true);
|
|
26
|
+
expect(Object.isFrozen(managedEntry.models[0])).toBe(true);
|
|
27
|
+
expect(managedEntry.models[0]?.api).toBe('anthropic-messages');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ManagedModelProvider,
|
|
2
3
|
MODEL_PROVIDER_CATALOG_SEED,
|
|
3
4
|
type ModelProviderCatalogEntryDto,
|
|
4
5
|
modelProviderCatalogEntrySchema,
|
|
6
|
+
type WorkspaceProvidersPolicy,
|
|
5
7
|
} from '@shipfox/api-agent-dto';
|
|
6
8
|
import {listPiProviderModels} from './harness/pi.js';
|
|
7
9
|
|
|
8
10
|
let cachedCatalog: readonly ModelProviderCatalogEntryDto[] | undefined;
|
|
9
11
|
|
|
10
|
-
export function buildModelProviderCatalog(
|
|
12
|
+
export function buildModelProviderCatalog(
|
|
13
|
+
options: {
|
|
14
|
+
managedProvider?: ManagedModelProvider | undefined;
|
|
15
|
+
workspaceProviders?: WorkspaceProvidersPolicy | undefined;
|
|
16
|
+
} = {},
|
|
17
|
+
): readonly ModelProviderCatalogEntryDto[] {
|
|
18
|
+
const workspaceProviders = options.workspaceProviders ?? 'enabled';
|
|
19
|
+
if (workspaceProviders === 'disabled') {
|
|
20
|
+
if (options.managedProvider === undefined) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'workspace provider configuration is disabled but no managed provider is registered',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze([toManagedProviderCatalogEntry(options.managedProvider)]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const catalog = getCatalog();
|
|
29
|
+
if (options.managedProvider === undefined) return catalog;
|
|
30
|
+
|
|
31
|
+
return Object.freeze([...catalog, toManagedProviderCatalogEntry(options.managedProvider)]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getCatalog(): readonly ModelProviderCatalogEntryDto[] {
|
|
11
35
|
if (cachedCatalog) return cachedCatalog;
|
|
12
36
|
|
|
13
37
|
const catalog = modelProviderCatalogEntrySchema.array().parse(
|
|
@@ -22,6 +46,22 @@ export function buildModelProviderCatalog(): readonly ModelProviderCatalogEntryD
|
|
|
22
46
|
return cachedCatalog;
|
|
23
47
|
}
|
|
24
48
|
|
|
49
|
+
function toManagedProviderCatalogEntry(
|
|
50
|
+
managedProvider: ManagedModelProvider,
|
|
51
|
+
): ModelProviderCatalogEntryDto {
|
|
52
|
+
return deepFreeze(
|
|
53
|
+
modelProviderCatalogEntrySchema.parse({
|
|
54
|
+
id: managedProvider.id,
|
|
55
|
+
label: managedProvider.label,
|
|
56
|
+
support_status: 'supported',
|
|
57
|
+
default_model: managedProvider.defaultModel,
|
|
58
|
+
credential_fields: [],
|
|
59
|
+
unsupported_reason: null,
|
|
60
|
+
models: managedProvider.models.map(({id, label, api}) => ({id, label, api})),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
25
65
|
function deepFreeze<T>(value: T): T {
|
|
26
66
|
if (Array.isArray(value)) {
|
|
27
67
|
for (const item of value) deepFreeze(item);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
UnsupportedHarnessProviderError,
|
|
13
13
|
UnsupportedHarnessThinkingError,
|
|
14
14
|
UnsupportedModelProviderError,
|
|
15
|
+
WorkspaceProvidersDisabledError,
|
|
15
16
|
} from './errors.js';
|
|
16
17
|
import * as harnessCatalog from './harness/index.js';
|
|
17
18
|
import {catalogDefaultAgentResolver, resolveAgentConfig} from './resolve-agent-config.js';
|
|
@@ -108,6 +109,78 @@ describe('resolveAgentConfig', () => {
|
|
|
108
109
|
expect(invalidModel).toThrow(InvalidAgentModelError);
|
|
109
110
|
});
|
|
110
111
|
|
|
112
|
+
test('resolves managed provider models by harness API family', () => {
|
|
113
|
+
const provider = managedProvider();
|
|
114
|
+
const context = {managedProvider: provider};
|
|
115
|
+
|
|
116
|
+
const pi = resolveAgentConfig({provider: 'shipfox', model: 'completion-model'}, context);
|
|
117
|
+
const claude = resolveAgentConfig({harness: 'claude', provider: 'shipfox'}, context);
|
|
118
|
+
const incompatible = () =>
|
|
119
|
+
resolveAgentConfig(
|
|
120
|
+
{harness: 'claude', provider: 'shipfox', model: 'responses-model'},
|
|
121
|
+
context,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(pi).toEqual({
|
|
125
|
+
harness: 'pi',
|
|
126
|
+
provider: 'shipfox',
|
|
127
|
+
model: 'completion-model',
|
|
128
|
+
thinking: 'low',
|
|
129
|
+
});
|
|
130
|
+
expect(claude).toEqual({
|
|
131
|
+
harness: 'claude',
|
|
132
|
+
provider: 'shipfox',
|
|
133
|
+
model: 'claude-model',
|
|
134
|
+
thinking: 'low',
|
|
135
|
+
});
|
|
136
|
+
expect(incompatible).toThrow(InvalidAgentModelError);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('instance model and thinking overrides win over managed provider defaults', () => {
|
|
140
|
+
const resolved = resolveAgentConfig(
|
|
141
|
+
{provider: 'shipfox'},
|
|
142
|
+
{
|
|
143
|
+
managedProvider: managedProvider(),
|
|
144
|
+
instanceDefaultProvider: 'shipfox',
|
|
145
|
+
instanceDefaultModel: 'claude-model',
|
|
146
|
+
instanceDefaultThinking: 'high',
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(resolved).toEqual({
|
|
151
|
+
harness: 'pi',
|
|
152
|
+
provider: 'shipfox',
|
|
153
|
+
model: 'claude-model',
|
|
154
|
+
thinking: 'high',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('defaults to the managed provider when workspace providers are disabled', () => {
|
|
159
|
+
const resolved = resolveAgentConfig(
|
|
160
|
+
{},
|
|
161
|
+
{
|
|
162
|
+
workspaceProviders: 'disabled',
|
|
163
|
+
workspaceDefaultProviderId: 'openai',
|
|
164
|
+
instanceDefaultProvider: 'openai',
|
|
165
|
+
managedProvider: managedProvider(),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(resolved.provider).toBe('shipfox');
|
|
170
|
+
expect(resolved.model).toBe('responses-model');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('rejects foreign provider references when workspace providers are disabled', () => {
|
|
174
|
+
const resolve = () =>
|
|
175
|
+
resolveAgentConfig(
|
|
176
|
+
{provider: 'openai'},
|
|
177
|
+
{workspaceProviders: 'disabled', managedProvider: managedProvider()},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(resolve).toThrow(WorkspaceProvidersDisabledError);
|
|
181
|
+
expect(resolve).toThrow('This instance only supports provider `shipfox`');
|
|
182
|
+
});
|
|
183
|
+
|
|
111
184
|
test('uses instance model and thinking only for the resolved instance model provider', () => {
|
|
112
185
|
const resolved = resolveAgentConfig(
|
|
113
186
|
{provider: 'openai'},
|
|
@@ -439,3 +512,22 @@ function createModelProviderConfigParams(params: {
|
|
|
439
512
|
defaultThinking: params.defaultThinking,
|
|
440
513
|
};
|
|
441
514
|
}
|
|
515
|
+
|
|
516
|
+
function managedProvider() {
|
|
517
|
+
return {
|
|
518
|
+
id: 'shipfox',
|
|
519
|
+
label: 'Shipfox',
|
|
520
|
+
models: [
|
|
521
|
+
{id: 'claude-model', label: 'Claude model', api: 'anthropic-messages' as const},
|
|
522
|
+
{id: 'responses-model', label: 'Responses model', api: 'openai-responses' as const},
|
|
523
|
+
{id: 'completion-model', label: 'Completions model', api: 'openai-completions' as const},
|
|
524
|
+
],
|
|
525
|
+
defaultModel: 'responses-model',
|
|
526
|
+
defaultThinking: 'low' as const,
|
|
527
|
+
resolveCredentials: async () => ({
|
|
528
|
+
api: 'anthropic-messages' as const,
|
|
529
|
+
baseUrl: 'https://gateway.example.com',
|
|
530
|
+
credentials: {api_key: 'token'},
|
|
531
|
+
}),
|
|
532
|
+
};
|
|
533
|
+
}
|