@shipfox/api-agent-dto 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/catalog.d.ts +190 -0
- package/dist/schemas/catalog.d.ts.map +1 -0
- package/dist/schemas/catalog.js +231 -0
- package/dist/schemas/catalog.js.map +1 -0
- package/dist/schemas/custom-model-provider.d.ts +187 -0
- package/dist/schemas/custom-model-provider.d.ts.map +1 -0
- package/dist/schemas/custom-model-provider.js +168 -0
- package/dist/schemas/custom-model-provider.js.map +1 -0
- package/dist/schemas/harness.d.ts +49 -0
- package/dist/schemas/harness.d.ts.map +1 -0
- package/dist/schemas/harness.js +135 -0
- package/dist/schemas/harness.js.map +1 -0
- package/dist/schemas/index.d.ts +9 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +10 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/materialized-agent-step-config.d.ts +186 -0
- package/dist/schemas/materialized-agent-step-config.d.ts.map +1 -0
- package/dist/schemas/materialized-agent-step-config.js +56 -0
- package/dist/schemas/materialized-agent-step-config.js.map +1 -0
- package/dist/schemas/model-provider-config.d.ts +108 -0
- package/dist/schemas/model-provider-config.d.ts.map +1 -0
- package/dist/schemas/model-provider-config.js +54 -0
- package/dist/schemas/model-provider-config.js.map +1 -0
- package/dist/schemas/model-provider-id.d.ts +81 -0
- package/dist/schemas/model-provider-id.d.ts.map +1 -0
- package/dist/schemas/model-provider-id.js +53 -0
- package/dist/schemas/model-provider-id.js.map +1 -0
- package/dist/schemas/runtime-config.d.ts +51 -0
- package/dist/schemas/runtime-config.d.ts.map +1 -0
- package/dist/schemas/runtime-config.js +31 -0
- package/dist/schemas/runtime-config.js.map +1 -0
- package/dist/schemas/workspace-defaults.d.ts +16 -0
- package/dist/schemas/workspace-defaults.d.ts.map +1 -0
- package/dist/schemas/workspace-defaults.js +10 -0
- package/dist/schemas/workspace-defaults.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +56 -0
- package/src/index.ts +122 -0
- package/src/schemas/catalog.test.ts +206 -0
- package/src/schemas/catalog.ts +304 -0
- package/src/schemas/custom-model-provider.test.ts +294 -0
- package/src/schemas/custom-model-provider.ts +258 -0
- package/src/schemas/harness.test.ts +133 -0
- package/src/schemas/harness.ts +210 -0
- package/src/schemas/index.ts +136 -0
- package/src/schemas/materialized-agent-step-config.test.ts +198 -0
- package/src/schemas/materialized-agent-step-config.ts +69 -0
- package/src/schemas/model-provider-config.test.ts +152 -0
- package/src/schemas/model-provider-config.ts +88 -0
- package/src/schemas/model-provider-id.ts +60 -0
- package/src/schemas/runtime-config.test.ts +132 -0
- package/src/schemas/runtime-config.ts +38 -0
- package/src/schemas/workspace-defaults.ts +14 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCustomModelProviderBodySchema,
|
|
3
|
+
customAgentModelSchema,
|
|
4
|
+
customModelProviderConfigDtoSchema,
|
|
5
|
+
customModelProviderHeaderRequestSchema,
|
|
6
|
+
customModelProviderRuntimeConfigSchema,
|
|
7
|
+
discoverCustomModelProviderModelsBodySchema,
|
|
8
|
+
discoverCustomModelProviderModelsBySlugBodySchema,
|
|
9
|
+
discoverCustomModelProviderModelsResponseSchema,
|
|
10
|
+
isReservedModelProviderId,
|
|
11
|
+
updateCustomModelProviderBodySchema,
|
|
12
|
+
updateCustomModelProviderHeaderRequestSchema,
|
|
13
|
+
} from './index.js';
|
|
14
|
+
import {MODEL_PROVIDER_IDS} from './model-provider-id.js';
|
|
15
|
+
|
|
16
|
+
describe('custom model provider schemas', () => {
|
|
17
|
+
describe.each(MODEL_PROVIDER_IDS)('modelProviderRefSchema "%s"', (providerId) => {
|
|
18
|
+
it('accepts built-in provider ids as refs', async () => {
|
|
19
|
+
const {modelProviderRefSchema} = await import('./model-provider-id.js');
|
|
20
|
+
|
|
21
|
+
const parsed = modelProviderRefSchema.parse(providerId);
|
|
22
|
+
|
|
23
|
+
expect(parsed).toBe(providerId);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('accepts valid custom model provider slugs', async () => {
|
|
28
|
+
const {modelProviderRefSchema} = await import('./model-provider-id.js');
|
|
29
|
+
|
|
30
|
+
const parsed = modelProviderRefSchema.parse('local-vllm-1');
|
|
31
|
+
|
|
32
|
+
expect(parsed).toBe('local-vllm-1');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([
|
|
36
|
+
'ab',
|
|
37
|
+
'-local',
|
|
38
|
+
'local-',
|
|
39
|
+
'Local',
|
|
40
|
+
'local_vllm',
|
|
41
|
+
])('rejects invalid slug "%s"', async (slug) => {
|
|
42
|
+
const {modelProviderRefSchema} = await import('./model-provider-id.js');
|
|
43
|
+
|
|
44
|
+
const parse = () => modelProviderRefSchema.parse(slug);
|
|
45
|
+
|
|
46
|
+
expect(parse).toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('detects reserved built-in provider ids', () => {
|
|
50
|
+
expect(isReservedModelProviderId('anthropic')).toBe(true);
|
|
51
|
+
expect(isReservedModelProviderId('local-vllm')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects a create body whose slug shadows a built-in provider id', () => {
|
|
55
|
+
const parse = () =>
|
|
56
|
+
createCustomModelProviderBodySchema.parse({
|
|
57
|
+
...createBody(),
|
|
58
|
+
slug: 'anthropic',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(parse).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('parses model optional overrides', () => {
|
|
65
|
+
const parsed = customAgentModelSchema.parse({
|
|
66
|
+
id: 'llama-3.1',
|
|
67
|
+
label: 'Llama 3.1',
|
|
68
|
+
context_window: 128_000,
|
|
69
|
+
max_output_tokens: 4096,
|
|
70
|
+
input_image: true,
|
|
71
|
+
reasoning: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(parsed.context_window).toBe(128_000);
|
|
75
|
+
expect(parsed.max_output_tokens).toBe(4096);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects invalid model bounds', () => {
|
|
79
|
+
const parse = () =>
|
|
80
|
+
customAgentModelSchema.parse({
|
|
81
|
+
id: '',
|
|
82
|
+
label: 'Llama 3.1',
|
|
83
|
+
context_window: 0,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(parse).toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('requires create default_model to be one of the configured models', () => {
|
|
90
|
+
const parsed = createCustomModelProviderBodySchema.parse({
|
|
91
|
+
...createBody(),
|
|
92
|
+
default_model: 'llama-3.1',
|
|
93
|
+
});
|
|
94
|
+
const parse = () =>
|
|
95
|
+
createCustomModelProviderBodySchema.parse({
|
|
96
|
+
...createBody(),
|
|
97
|
+
default_model: 'missing-model',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(parsed.default_model).toBe('llama-3.1');
|
|
101
|
+
expect(parse).toThrow();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('requires at least one unique model id', () => {
|
|
105
|
+
const emptyModels = () =>
|
|
106
|
+
createCustomModelProviderBodySchema.parse({
|
|
107
|
+
...createBody(),
|
|
108
|
+
models: [],
|
|
109
|
+
});
|
|
110
|
+
const duplicateModels = () =>
|
|
111
|
+
createCustomModelProviderBodySchema.parse({
|
|
112
|
+
...createBody(),
|
|
113
|
+
models: [
|
|
114
|
+
{id: 'llama-3.1', label: 'Llama 3.1'},
|
|
115
|
+
{id: 'llama-3.1', label: 'Llama 3.1 duplicate'},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(emptyModels).toThrow();
|
|
120
|
+
expect(duplicateModels).toThrow();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('checks update default_model against models only when both are present', () => {
|
|
124
|
+
const modelOnly = updateCustomModelProviderBodySchema.parse({
|
|
125
|
+
default_model: 'stored-model',
|
|
126
|
+
});
|
|
127
|
+
const validWithModels = updateCustomModelProviderBodySchema.parse({
|
|
128
|
+
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
129
|
+
default_model: 'llama-3.1',
|
|
130
|
+
});
|
|
131
|
+
const invalidWithModels = () =>
|
|
132
|
+
updateCustomModelProviderBodySchema.parse({
|
|
133
|
+
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
134
|
+
default_model: 'missing-model',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(modelOnly.default_model).toBe('stored-model');
|
|
138
|
+
expect(validWithModels.default_model).toBe('llama-3.1');
|
|
139
|
+
expect(invalidWithModels).toThrow();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('parses request header rows and lowercases names', () => {
|
|
143
|
+
const parsed = customModelProviderHeaderRequestSchema.parse({
|
|
144
|
+
name: 'Authorization',
|
|
145
|
+
value: 'Bearer secret',
|
|
146
|
+
secret: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(parsed).toEqual({
|
|
150
|
+
name: 'authorization',
|
|
151
|
+
value: 'Bearer secret',
|
|
152
|
+
secret: true,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('parses update header keep rows for stored secret headers', () => {
|
|
157
|
+
const parsed = updateCustomModelProviderHeaderRequestSchema.parse({
|
|
158
|
+
name: 'Authorization',
|
|
159
|
+
secret: true,
|
|
160
|
+
keep: true,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(parsed).toEqual({
|
|
164
|
+
name: 'authorization',
|
|
165
|
+
secret: true,
|
|
166
|
+
keep: true,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('rejects update header rows that keep non-secret or valued headers', () => {
|
|
171
|
+
const nonSecretKeep = () =>
|
|
172
|
+
updateCustomModelProviderHeaderRequestSchema.parse({
|
|
173
|
+
name: 'authorization',
|
|
174
|
+
secret: false,
|
|
175
|
+
keep: true,
|
|
176
|
+
});
|
|
177
|
+
const valuedKeep = () =>
|
|
178
|
+
updateCustomModelProviderHeaderRequestSchema.parse({
|
|
179
|
+
name: 'authorization',
|
|
180
|
+
value: 'Bearer secret',
|
|
181
|
+
secret: true,
|
|
182
|
+
keep: true,
|
|
183
|
+
});
|
|
184
|
+
const missingValue = () =>
|
|
185
|
+
updateCustomModelProviderHeaderRequestSchema.parse({
|
|
186
|
+
name: 'x-region',
|
|
187
|
+
secret: false,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(nonSecretKeep).toThrow();
|
|
191
|
+
expect(valuedKeep).toThrow();
|
|
192
|
+
expect(missingValue).toThrow();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('rejects duplicate header names case-insensitively', () => {
|
|
196
|
+
const parse = () =>
|
|
197
|
+
createCustomModelProviderBodySchema.parse({
|
|
198
|
+
...createBody(),
|
|
199
|
+
headers: [
|
|
200
|
+
{name: 'Authorization', value: 'Bearer secret', secret: true},
|
|
201
|
+
{name: 'authorization', value: 'debug', secret: false},
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(parse).toThrow();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rejects invalid URLs and oversized header lists', () => {
|
|
209
|
+
const invalidUrl = () =>
|
|
210
|
+
createCustomModelProviderBodySchema.parse({
|
|
211
|
+
...createBody(),
|
|
212
|
+
base_url: 'not a url',
|
|
213
|
+
});
|
|
214
|
+
const tooManyHeaders = () =>
|
|
215
|
+
createCustomModelProviderBodySchema.parse({
|
|
216
|
+
...createBody(),
|
|
217
|
+
headers: Array.from({length: 33}, (_, index) => ({
|
|
218
|
+
name: `x-test-${index}`,
|
|
219
|
+
value: 'value',
|
|
220
|
+
secret: false,
|
|
221
|
+
})),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(invalidUrl).toThrow();
|
|
225
|
+
expect(tooManyHeaders).toThrow();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('parses a custom config read DTO without secret values', () => {
|
|
229
|
+
const parsed = customModelProviderConfigDtoSchema.parse({
|
|
230
|
+
kind: 'custom',
|
|
231
|
+
provider_id: 'local-vllm',
|
|
232
|
+
display_name: 'Local vLLM',
|
|
233
|
+
api: 'openai-responses',
|
|
234
|
+
base_url: 'https://llm.example.test/v1',
|
|
235
|
+
headers: [{name: 'x-region', value: 'local'}],
|
|
236
|
+
secret_header_names: ['authorization'],
|
|
237
|
+
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
238
|
+
default_model: null,
|
|
239
|
+
created_at: '2026-06-27T10:30:00.000Z',
|
|
240
|
+
updated_at: '2026-06-27T10:45:00.000Z',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(parsed.secret_header_names).toEqual(['authorization']);
|
|
244
|
+
expect('requires_api_key' in parsed).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('requires key intent on custom provider runtime descriptors', () => {
|
|
248
|
+
const parsed = customModelProviderRuntimeConfigSchema.parse({
|
|
249
|
+
api: 'openai-responses',
|
|
250
|
+
base_url: 'https://llm.example.test/v1',
|
|
251
|
+
headers: [{name: 'x-region', value: 'local'}],
|
|
252
|
+
secret_header_names: ['authorization'],
|
|
253
|
+
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
254
|
+
requires_api_key: false,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(parsed.requires_api_key).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('parses discovery request and response DTOs', () => {
|
|
261
|
+
const body = discoverCustomModelProviderModelsBodySchema.parse({
|
|
262
|
+
api: 'openai-responses',
|
|
263
|
+
base_url: 'https://llm.example.test/v1',
|
|
264
|
+
api_key: 'sk-local',
|
|
265
|
+
headers: [{name: 'X-Region', value: 'local'}],
|
|
266
|
+
});
|
|
267
|
+
const response = discoverCustomModelProviderModelsResponseSchema.parse({
|
|
268
|
+
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(body.headers?.[0]?.name).toBe('x-region');
|
|
272
|
+
expect(response.models[0]?.id).toBe('llama-3.1');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('parses slug-scoped discovery request DTOs with keep headers', () => {
|
|
276
|
+
const body = discoverCustomModelProviderModelsBySlugBodySchema.parse({
|
|
277
|
+
headers: [{name: 'Authorization', secret: true, keep: true}],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(body.headers?.[0]).toEqual({name: 'authorization', secret: true, keep: true});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
function createBody() {
|
|
285
|
+
return {
|
|
286
|
+
slug: 'local-vllm',
|
|
287
|
+
display_name: 'Local vLLM',
|
|
288
|
+
api: 'openai-responses',
|
|
289
|
+
base_url: 'https://llm.example.test/v1',
|
|
290
|
+
api_key: 'sk-local',
|
|
291
|
+
headers: [{name: 'x-region', value: 'local', secret: false}],
|
|
292
|
+
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
293
|
+
} as const;
|
|
294
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
import {isReservedModelProviderId, modelProviderRefSchema} from './model-provider-id.js';
|
|
3
|
+
|
|
4
|
+
const MAX_HEADER_COUNT = 32;
|
|
5
|
+
export const MAX_MODEL_COUNT = 128;
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW = 128_000;
|
|
8
|
+
export const DEFAULT_CUSTOM_MODEL_MAX_OUTPUT_TOKENS = 16_384;
|
|
9
|
+
export const DEFAULT_CUSTOM_MODEL_INPUT_IMAGE = false;
|
|
10
|
+
export const DEFAULT_CUSTOM_MODEL_REASONING = false;
|
|
11
|
+
|
|
12
|
+
const headerNameSchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.trim()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(128)
|
|
17
|
+
.regex(/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/)
|
|
18
|
+
.transform((name) => name.toLowerCase());
|
|
19
|
+
|
|
20
|
+
const headerValueSchema = z.string().min(1).max(8192);
|
|
21
|
+
const modelIdSchema = z.string().min(1).max(128);
|
|
22
|
+
|
|
23
|
+
export const modelProviderApiSchema = z.enum([
|
|
24
|
+
'openai-completions',
|
|
25
|
+
'openai-responses',
|
|
26
|
+
'anthropic-messages',
|
|
27
|
+
'google-generative-ai',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export type ModelProviderApi = z.infer<typeof modelProviderApiSchema>;
|
|
31
|
+
|
|
32
|
+
export const customAgentModelSchema = z.object({
|
|
33
|
+
id: modelIdSchema,
|
|
34
|
+
label: z.string().min(1).max(160),
|
|
35
|
+
context_window: z.number().int().positive().optional(),
|
|
36
|
+
max_output_tokens: z.number().int().positive().optional(),
|
|
37
|
+
input_image: z.boolean().optional(),
|
|
38
|
+
reasoning: z.boolean().optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type CustomAgentModelDto = z.infer<typeof customAgentModelSchema>;
|
|
42
|
+
|
|
43
|
+
export const customModelProviderHeaderDtoSchema = z.object({
|
|
44
|
+
name: headerNameSchema,
|
|
45
|
+
value: headerValueSchema,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type CustomModelProviderHeaderDto = z.infer<typeof customModelProviderHeaderDtoSchema>;
|
|
49
|
+
|
|
50
|
+
export const customModelProviderHeaderRequestSchema = customModelProviderHeaderDtoSchema.extend({
|
|
51
|
+
secret: z.boolean(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type CustomModelProviderHeaderRequestDto = z.infer<
|
|
55
|
+
typeof customModelProviderHeaderRequestSchema
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
export const updateCustomModelProviderHeaderRequestSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
name: headerNameSchema,
|
|
61
|
+
value: headerValueSchema.optional(),
|
|
62
|
+
secret: z.boolean(),
|
|
63
|
+
keep: z.boolean().optional(),
|
|
64
|
+
})
|
|
65
|
+
.superRefine((header, ctx) => {
|
|
66
|
+
if (header.keep === true) {
|
|
67
|
+
if (!header.secret) {
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: 'custom',
|
|
70
|
+
path: ['secret'],
|
|
71
|
+
message: 'Kept headers must be secret.',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (header.value !== undefined) {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: 'custom',
|
|
77
|
+
path: ['value'],
|
|
78
|
+
message: 'Kept headers must not include a value.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (header.value === undefined) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: 'custom',
|
|
87
|
+
path: ['value'],
|
|
88
|
+
message: 'Header value is required.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type UpdateCustomModelProviderHeaderRequestDto = z.infer<
|
|
94
|
+
typeof updateCustomModelProviderHeaderRequestSchema
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
const customModelProviderHeadersRequestSchema = z
|
|
98
|
+
.array(customModelProviderHeaderRequestSchema)
|
|
99
|
+
.max(MAX_HEADER_COUNT)
|
|
100
|
+
.superRefine(assertUniqueHeaderNames);
|
|
101
|
+
|
|
102
|
+
const updateCustomModelProviderHeadersRequestSchema = z
|
|
103
|
+
.array(updateCustomModelProviderHeaderRequestSchema)
|
|
104
|
+
.max(MAX_HEADER_COUNT)
|
|
105
|
+
.superRefine(assertUniqueHeaderNames);
|
|
106
|
+
|
|
107
|
+
const customModelProviderHeadersDtoSchema = z
|
|
108
|
+
.array(customModelProviderHeaderDtoSchema)
|
|
109
|
+
.max(MAX_HEADER_COUNT)
|
|
110
|
+
.superRefine(assertUniqueHeaderNames);
|
|
111
|
+
|
|
112
|
+
const customModelProviderModelsSchema = z
|
|
113
|
+
.array(customAgentModelSchema)
|
|
114
|
+
.min(1)
|
|
115
|
+
.max(MAX_MODEL_COUNT)
|
|
116
|
+
.superRefine(assertUniqueModelIds);
|
|
117
|
+
|
|
118
|
+
const customModelProviderBodyFields = {
|
|
119
|
+
display_name: z.string().min(1).max(120),
|
|
120
|
+
api: modelProviderApiSchema,
|
|
121
|
+
base_url: z.string().url().max(2048),
|
|
122
|
+
api_key: z.string().min(1).max(8192).optional(),
|
|
123
|
+
models: customModelProviderModelsSchema,
|
|
124
|
+
default_model: modelIdSchema.nullable().optional(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const createCustomModelProviderBodySchema = z
|
|
128
|
+
.object({
|
|
129
|
+
slug: modelProviderRefSchema.refine((slug) => !isReservedModelProviderId(slug), {
|
|
130
|
+
message: 'Provider ID is reserved.',
|
|
131
|
+
}),
|
|
132
|
+
...customModelProviderBodyFields,
|
|
133
|
+
headers: customModelProviderHeadersRequestSchema.optional(),
|
|
134
|
+
})
|
|
135
|
+
.superRefine(assertDefaultModelInModels);
|
|
136
|
+
|
|
137
|
+
export type CreateCustomModelProviderBodyDto = z.infer<typeof createCustomModelProviderBodySchema>;
|
|
138
|
+
|
|
139
|
+
export const updateCustomModelProviderBodySchema = z
|
|
140
|
+
.object({
|
|
141
|
+
...customModelProviderBodyFields,
|
|
142
|
+
headers: updateCustomModelProviderHeadersRequestSchema.optional(),
|
|
143
|
+
models: customModelProviderModelsSchema.optional(),
|
|
144
|
+
})
|
|
145
|
+
.partial()
|
|
146
|
+
.superRefine((body, ctx) => {
|
|
147
|
+
if (body.models === undefined) return;
|
|
148
|
+
assertDefaultModelInModels(body, ctx);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export type UpdateCustomModelProviderBodyDto = z.infer<typeof updateCustomModelProviderBodySchema>;
|
|
152
|
+
|
|
153
|
+
export const discoverCustomModelProviderModelsBodySchema = z.object({
|
|
154
|
+
api: modelProviderApiSchema,
|
|
155
|
+
base_url: z.string().url().max(2048),
|
|
156
|
+
api_key: z.string().min(1).max(8192).optional(),
|
|
157
|
+
headers: customModelProviderHeadersDtoSchema.optional(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export type DiscoverCustomModelProviderModelsBodyDto = z.infer<
|
|
161
|
+
typeof discoverCustomModelProviderModelsBodySchema
|
|
162
|
+
>;
|
|
163
|
+
|
|
164
|
+
export const discoverCustomModelProviderModelsBySlugBodySchema = z.object({
|
|
165
|
+
api: modelProviderApiSchema.optional(),
|
|
166
|
+
base_url: z.string().url().max(2048).optional(),
|
|
167
|
+
api_key: z.string().min(1).max(8192).optional(),
|
|
168
|
+
headers: updateCustomModelProviderHeadersRequestSchema.optional(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export type DiscoverCustomModelProviderModelsBySlugBodyDto = z.infer<
|
|
172
|
+
typeof discoverCustomModelProviderModelsBySlugBodySchema
|
|
173
|
+
>;
|
|
174
|
+
|
|
175
|
+
export const discoverCustomModelProviderModelsResponseSchema = z.object({
|
|
176
|
+
models: z
|
|
177
|
+
.array(z.object({id: modelIdSchema, label: z.string().min(1).max(160)}))
|
|
178
|
+
.max(MAX_MODEL_COUNT),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export type DiscoverCustomModelProviderModelsResponseDto = z.infer<
|
|
182
|
+
typeof discoverCustomModelProviderModelsResponseSchema
|
|
183
|
+
>;
|
|
184
|
+
|
|
185
|
+
const customModelProviderConfigBaseSchema = z.object({
|
|
186
|
+
api: modelProviderApiSchema,
|
|
187
|
+
base_url: z.string().url().max(2048),
|
|
188
|
+
headers: customModelProviderHeadersDtoSchema,
|
|
189
|
+
secret_header_names: z
|
|
190
|
+
.array(headerNameSchema)
|
|
191
|
+
.max(MAX_HEADER_COUNT)
|
|
192
|
+
.superRefine(assertUniqueStrings),
|
|
193
|
+
models: customModelProviderModelsSchema,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export const customModelProviderRuntimeConfigSchema = customModelProviderConfigBaseSchema.extend({
|
|
197
|
+
requires_api_key: z.boolean(),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export type CustomModelProviderRuntimeConfigDto = z.infer<
|
|
201
|
+
typeof customModelProviderRuntimeConfigSchema
|
|
202
|
+
>;
|
|
203
|
+
|
|
204
|
+
export const customModelProviderConfigDtoSchema = customModelProviderConfigBaseSchema.extend({
|
|
205
|
+
kind: z.literal('custom'),
|
|
206
|
+
provider_id: modelProviderRefSchema,
|
|
207
|
+
display_name: z.string().min(1).max(120),
|
|
208
|
+
default_model: modelIdSchema.nullable(),
|
|
209
|
+
created_at: z.string().datetime(),
|
|
210
|
+
updated_at: z.string().datetime(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export type CustomModelProviderConfigDto = z.infer<typeof customModelProviderConfigDtoSchema>;
|
|
214
|
+
|
|
215
|
+
function assertDefaultModelInModels(
|
|
216
|
+
body: {default_model?: string | null | undefined; models?: CustomAgentModelDto[] | undefined},
|
|
217
|
+
ctx: z.RefinementCtx,
|
|
218
|
+
): void {
|
|
219
|
+
if (typeof body.default_model !== 'string' || body.models === undefined) return;
|
|
220
|
+
|
|
221
|
+
if (!body.models.some((model) => model.id === body.default_model)) {
|
|
222
|
+
ctx.addIssue({
|
|
223
|
+
code: 'custom',
|
|
224
|
+
path: ['default_model'],
|
|
225
|
+
message: 'Default model must be one of the configured models.',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assertUniqueHeaderNames(headers: Array<{name: string}>, ctx: z.RefinementCtx): void {
|
|
231
|
+
assertUniqueStrings(
|
|
232
|
+
headers.map((header) => header.name),
|
|
233
|
+
ctx,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertUniqueModelIds(models: CustomAgentModelDto[], ctx: z.RefinementCtx): void {
|
|
238
|
+
assertUniqueStrings(
|
|
239
|
+
models.map((model) => model.id),
|
|
240
|
+
ctx,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function assertUniqueStrings(values: string[], ctx: z.RefinementCtx): void {
|
|
245
|
+
const seen = new Set<string>();
|
|
246
|
+
for (const [index, value] of values.entries()) {
|
|
247
|
+
if (!seen.has(value)) {
|
|
248
|
+
seen.add(value);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
ctx.addIssue({
|
|
253
|
+
code: 'custom',
|
|
254
|
+
path: [index],
|
|
255
|
+
message: 'Duplicate value.',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildHarnessToolDeploymentConfig,
|
|
3
|
+
DEFAULT_HARNESS_TOOL_DEPLOYMENT_CONFIG,
|
|
4
|
+
getHarnessToolDescriptor,
|
|
5
|
+
harnessSupportsTool,
|
|
6
|
+
listEnabledHarnessTools,
|
|
7
|
+
listHarnessTools,
|
|
8
|
+
parsePiEnabledToolPackages,
|
|
9
|
+
} from './harness.js';
|
|
10
|
+
|
|
11
|
+
const PI_BUILT_IN_TOOLS = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'];
|
|
12
|
+
const PI_WEB_ACCESS_TOOLS = ['web_search', 'fetch_content', 'get_search_content'];
|
|
13
|
+
const CLAUDE_TOOLS = ['Read', 'Bash', 'Edit', 'Write', 'Glob', 'Grep', 'WebFetch', 'WebSearch'];
|
|
14
|
+
const piToolPackagesEnvPattern = /AGENT_PI_ENABLED_TOOL_PACKAGES/;
|
|
15
|
+
|
|
16
|
+
describe('harness tool catalog', () => {
|
|
17
|
+
it('lists exact pi tool names in catalog order', () => {
|
|
18
|
+
const toolNames = listHarnessTools('pi').map((tool) => tool.name);
|
|
19
|
+
|
|
20
|
+
expect(toolNames).toEqual([...PI_BUILT_IN_TOOLS, ...PI_WEB_ACCESS_TOOLS]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('lists exact Claude tool names in catalog order', () => {
|
|
24
|
+
const toolNames = listHarnessTools('claude').map((tool) => tool.name);
|
|
25
|
+
|
|
26
|
+
expect(toolNames).toEqual(CLAUDE_TOOLS);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('marks pi web access tools as package-backed catalog entries', () => {
|
|
30
|
+
const packageTools = PI_WEB_ACCESS_TOOLS.map((toolName) =>
|
|
31
|
+
getHarnessToolDescriptor('pi', toolName),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(packageTools).toEqual(
|
|
35
|
+
PI_WEB_ACCESS_TOOLS.map((toolName) =>
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
name: toolName,
|
|
38
|
+
source: 'package',
|
|
39
|
+
packageName: 'pi-web-access',
|
|
40
|
+
enabledByDefault: true,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('leaves pi package-backed tools unavailable unless their package is enabled', () => {
|
|
47
|
+
const enabledToolNames = listEnabledHarnessTools('pi').map((tool) => tool.name);
|
|
48
|
+
|
|
49
|
+
expect(enabledToolNames).toEqual(PI_BUILT_IN_TOOLS);
|
|
50
|
+
expect(harnessSupportsTool('pi', 'fetch_content')).toBe(false);
|
|
51
|
+
expect(harnessSupportsTool('pi', 'web_search')).toBe(false);
|
|
52
|
+
expect(harnessSupportsTool('pi', 'get_search_content')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('enables pi web access tools when pi-web-access is enabled', () => {
|
|
56
|
+
const deploymentConfig = {pi: {enabledToolPackages: ['pi-web-access']}} as const;
|
|
57
|
+
const enabledToolNames = listEnabledHarnessTools('pi', deploymentConfig).map(
|
|
58
|
+
(tool) => tool.name,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(enabledToolNames).toEqual([...PI_BUILT_IN_TOOLS, ...PI_WEB_ACCESS_TOOLS]);
|
|
62
|
+
expect(harnessSupportsTool('pi', 'fetch_content', deploymentConfig)).toBe(true);
|
|
63
|
+
expect(harnessSupportsTool('pi', 'web_search', deploymentConfig)).toBe(true);
|
|
64
|
+
expect(harnessSupportsTool('pi', 'get_search_content', deploymentConfig)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('builds deployment defaults with pi web access and search enabled', () => {
|
|
68
|
+
expect(DEFAULT_HARNESS_TOOL_DEPLOYMENT_CONFIG).toEqual({
|
|
69
|
+
pi: {enabledToolPackages: ['pi-web-access'], webSearchEnabled: true},
|
|
70
|
+
claude: {enabledToolPackages: []},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const enabledToolNames = listEnabledHarnessTools(
|
|
74
|
+
'pi',
|
|
75
|
+
DEFAULT_HARNESS_TOOL_DEPLOYMENT_CONFIG,
|
|
76
|
+
).map((tool) => tool.name);
|
|
77
|
+
expect(enabledToolNames).toEqual([...PI_BUILT_IN_TOOLS, ...PI_WEB_ACCESS_TOOLS]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('deduplicates comma-delimited Pi tool package names', () => {
|
|
81
|
+
const packageNames = parsePiEnabledToolPackages(' pi-web-access,pi-web-access ');
|
|
82
|
+
|
|
83
|
+
expect(packageNames).toEqual(['pi-web-access']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('allows an empty Pi tool package list', () => {
|
|
87
|
+
const packageNames = parsePiEnabledToolPackages('');
|
|
88
|
+
|
|
89
|
+
expect(packageNames).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects unknown Pi tool package names with the env var name', () => {
|
|
93
|
+
expect(() => parsePiEnabledToolPackages('pi-web-access,unknown')).toThrow(
|
|
94
|
+
piToolPackagesEnvPattern,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('keeps pi fetch content enabled when web search is disabled', () => {
|
|
99
|
+
const deploymentConfig = {
|
|
100
|
+
pi: {enabledToolPackages: ['pi-web-access'], webSearchEnabled: false},
|
|
101
|
+
} as const;
|
|
102
|
+
const enabledToolNames = listEnabledHarnessTools('pi', deploymentConfig).map(
|
|
103
|
+
(tool) => tool.name,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(enabledToolNames).toEqual([...PI_BUILT_IN_TOOLS, 'fetch_content']);
|
|
107
|
+
expect(harnessSupportsTool('pi', 'fetch_content', deploymentConfig)).toBe(true);
|
|
108
|
+
expect(harnessSupportsTool('pi', 'web_search', deploymentConfig)).toBe(false);
|
|
109
|
+
expect(harnessSupportsTool('pi', 'get_search_content', deploymentConfig)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('builds deployment config from explicit Pi package and search settings', () => {
|
|
113
|
+
const deploymentConfig = buildHarnessToolDeploymentConfig({
|
|
114
|
+
piEnabledToolPackages: 'pi-web-access, pi-web-access',
|
|
115
|
+
piWebSearchEnabled: false,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(deploymentConfig).toEqual({
|
|
119
|
+
pi: {enabledToolPackages: ['pi-web-access'], webSearchEnabled: false},
|
|
120
|
+
claude: {enabledToolPackages: []},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('matches harness tool names case-sensitively', () => {
|
|
125
|
+
const piDeploymentConfig = {pi: {enabledToolPackages: ['pi-web-access']}} as const;
|
|
126
|
+
|
|
127
|
+
expect(getHarnessToolDescriptor('pi', 'WebSearch')).toBeUndefined();
|
|
128
|
+
expect(harnessSupportsTool('pi', 'WebSearch', piDeploymentConfig)).toBe(false);
|
|
129
|
+
expect(getHarnessToolDescriptor('claude', 'web_search')).toBeUndefined();
|
|
130
|
+
expect(harnessSupportsTool('claude', 'web_search')).toBe(false);
|
|
131
|
+
expect(harnessSupportsTool('claude', 'WebSearch')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|