@shipfox/api-agent 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 +105 -0
- package/LICENSE +21 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/core/credential-fingerprints.d.ts +10 -0
- package/dist/core/credential-fingerprints.d.ts.map +1 -0
- package/dist/core/credential-fingerprints.js +65 -0
- package/dist/core/credential-fingerprints.js.map +1 -0
- package/dist/core/custom-model-provider-config-service.d.ts +26 -0
- package/dist/core/custom-model-provider-config-service.d.ts.map +1 -0
- package/dist/core/custom-model-provider-config-service.js +390 -0
- package/dist/core/custom-model-provider-config-service.js.map +1 -0
- package/dist/core/custom-provider-url.d.ts +4 -0
- package/dist/core/custom-provider-url.d.ts.map +1 -0
- package/dist/core/custom-provider-url.js +23 -0
- package/dist/core/custom-provider-url.js.map +1 -0
- package/dist/core/discover-custom-model-provider-models.d.ts +3 -0
- package/dist/core/discover-custom-model-provider-models.d.ts.map +1 -0
- package/dist/core/discover-custom-model-provider-models.js +122 -0
- package/dist/core/discover-custom-model-provider-models.js.map +1 -0
- package/dist/core/entities/agent-workspace-settings.d.ts +9 -0
- package/dist/core/entities/agent-workspace-settings.d.ts.map +1 -0
- package/dist/core/entities/agent-workspace-settings.js +3 -0
- package/dist/core/entities/agent-workspace-settings.js.map +1 -0
- package/dist/core/entities/model-provider-config.d.ts +19 -0
- package/dist/core/entities/model-provider-config.d.ts.map +1 -0
- package/dist/core/entities/model-provider-config.js +3 -0
- package/dist/core/entities/model-provider-config.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +80 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/harness/claude.d.ts +6 -0
- package/dist/core/harness/claude.d.ts.map +1 -0
- package/dist/core/harness/claude.js +104 -0
- package/dist/core/harness/claude.js.map +1 -0
- package/dist/core/harness/index.d.ts +4 -0
- package/dist/core/harness/index.d.ts.map +1 -0
- package/dist/core/harness/index.js +5 -0
- package/dist/core/harness/index.js.map +1 -0
- package/dist/core/harness/pi.d.ts +6 -0
- package/dist/core/harness/pi.d.ts.map +1 -0
- package/dist/core/harness/pi.js +16 -0
- package/dist/core/harness/pi.js.map +1 -0
- package/dist/core/harness/registry.d.ts +22 -0
- package/dist/core/harness/registry.d.ts.map +1 -0
- package/dist/core/harness/registry.js +46 -0
- package/dist/core/harness/registry.js.map +1 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/model-provider-catalog.d.ts +3 -0
- package/dist/core/model-provider-catalog.d.ts.map +1 -0
- package/dist/core/model-provider-catalog.js +25 -0
- package/dist/core/model-provider-catalog.js.map +1 -0
- package/dist/core/model-provider-config-service.d.ts +32 -0
- package/dist/core/model-provider-config-service.d.ts.map +1 -0
- package/dist/core/model-provider-config-service.js +133 -0
- package/dist/core/model-provider-config-service.js.map +1 -0
- package/dist/core/model-provider-validation.d.ts +31 -0
- package/dist/core/model-provider-validation.d.ts.map +1 -0
- package/dist/core/model-provider-validation.js +220 -0
- package/dist/core/model-provider-validation.js.map +1 -0
- package/dist/core/resolve-agent-config.d.ts +32 -0
- package/dist/core/resolve-agent-config.d.ts.map +1 -0
- package/dist/core/resolve-agent-config.js +143 -0
- package/dist/core/resolve-agent-config.js.map +1 -0
- package/dist/core/resolve-runtime-credentials.d.ts +21 -0
- package/dist/core/resolve-runtime-credentials.d.ts.map +1 -0
- package/dist/core/resolve-runtime-credentials.js +101 -0
- package/dist/core/resolve-runtime-credentials.js.map +1 -0
- package/dist/core/workspace-agent-defaults-resolver.d.ts +3 -0
- package/dist/core/workspace-agent-defaults-resolver.d.ts.map +1 -0
- package/dist/core/workspace-agent-defaults-resolver.js +26 -0
- package/dist/core/workspace-agent-defaults-resolver.js.map +1 -0
- package/dist/db/agent-defaults-snapshot.d.ts +9 -0
- package/dist/db/agent-defaults-snapshot.d.ts.map +1 -0
- package/dist/db/agent-defaults-snapshot.js +24 -0
- package/dist/db/agent-defaults-snapshot.js.map +1 -0
- package/dist/db/agent-workspace-settings.d.ts +12 -0
- package/dist/db/agent-workspace-settings.d.ts.map +1 -0
- package/dist/db/agent-workspace-settings.js +53 -0
- package/dist/db/agent-workspace-settings.js.map +1 -0
- package/dist/db/db.d.ts +771 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +20 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +9 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +11 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/model-provider-configs.d.ts +38 -0
- package/dist/db/model-provider-configs.d.ts.map +1 -0
- package/dist/db/model-provider-configs.js +159 -0
- package/dist/db/model-provider-configs.js.map +1 -0
- package/dist/db/schema/agent-workspace-settings.d.ts +97 -0
- package/dist/db/schema/agent-workspace-settings.d.ts.map +1 -0
- package/dist/db/schema/agent-workspace-settings.js +26 -0
- package/dist/db/schema/agent-workspace-settings.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/model-provider-configs.d.ts +296 -0
- package/dist/db/schema/model-provider-configs.d.ts.map +1 -0
- package/dist/db/schema/model-provider-configs.js +52 -0
- package/dist/db/schema/model-provider-configs.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +10 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +10 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/dto/index.d.ts +2 -0
- package/dist/presentation/dto/index.d.ts.map +1 -0
- package/dist/presentation/dto/index.js +3 -0
- package/dist/presentation/dto/index.js.map +1 -0
- package/dist/presentation/dto/model-provider-config.d.ts +6 -0
- package/dist/presentation/dto/model-provider-config.d.ts.map +1 -0
- package/dist/presentation/dto/model-provider-config.js +32 -0
- package/dist/presentation/dto/model-provider-config.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -0
- package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-model-provider.js +66 -0
- package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -0
- package/dist/presentation/e2eRoutes/index.d.ts +3 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/index.js +9 -0
- package/dist/presentation/e2eRoutes/index.js.map +1 -0
- package/dist/presentation/index.d.ts +2 -0
- package/dist/presentation/index.d.ts.map +1 -0
- package/dist/presentation/index.js +3 -0
- package/dist/presentation/index.js.map +1 -0
- package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -0
- package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -0
- package/dist/presentation/routes/create-custom-model-provider.js +45 -0
- package/dist/presentation/routes/create-custom-model-provider.js.map +1 -0
- package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -0
- package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -0
- package/dist/presentation/routes/delete-model-provider-config.js +38 -0
- package/dist/presentation/routes/delete-model-provider-config.js.map +1 -0
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -0
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -0
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +37 -0
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -0
- 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 -0
- package/dist/presentation/routes/discover-custom-model-provider-models.js +31 -0
- package/dist/presentation/routes/discover-custom-model-provider-models.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +112 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +39 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-model-provider-catalog.d.ts +2 -0
- package/dist/presentation/routes/list-model-provider-catalog.d.ts.map +1 -0
- package/dist/presentation/routes/list-model-provider-catalog.js +18 -0
- package/dist/presentation/routes/list-model-provider-catalog.js.map +1 -0
- package/dist/presentation/routes/list-model-provider-configs.d.ts +2 -0
- package/dist/presentation/routes/list-model-provider-configs.d.ts.map +1 -0
- package/dist/presentation/routes/list-model-provider-configs.js +37 -0
- package/dist/presentation/routes/list-model-provider-configs.js.map +1 -0
- package/dist/presentation/routes/set-default-harness.d.ts +2 -0
- package/dist/presentation/routes/set-default-harness.d.ts.map +1 -0
- package/dist/presentation/routes/set-default-harness.js +38 -0
- package/dist/presentation/routes/set-default-harness.js.map +1 -0
- package/dist/presentation/routes/set-default-model-provider.d.ts +2 -0
- package/dist/presentation/routes/set-default-model-provider.d.ts.map +1 -0
- package/dist/presentation/routes/set-default-model-provider.js +48 -0
- package/dist/presentation/routes/set-default-model-provider.js.map +1 -0
- package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -0
- package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -0
- package/dist/presentation/routes/update-custom-model-provider.js +47 -0
- package/dist/presentation/routes/update-custom-model-provider.js.map +1 -0
- 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 -0
- package/dist/presentation/routes/update-model-provider-default-model.js +38 -0
- package/dist/presentation/routes/update-model-provider-default-model.js.map +1 -0
- package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -0
- package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -0
- package/dist/presentation/routes/upsert-model-provider-config.js +52 -0
- package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_daffy_leopardon.sql +28 -0
- package/drizzle/0001_default_harness.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +199 -0
- package/drizzle/meta/0001_snapshot.json +205 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +7 -0
- package/package.json +109 -0
- package/src/config.test.ts +118 -0
- package/src/config.ts +75 -0
- package/src/core/catalog-registry.test.ts +48 -0
- package/src/core/credential-fingerprints.test.ts +34 -0
- package/src/core/credential-fingerprints.ts +84 -0
- package/src/core/custom-model-provider-config-service.ts +537 -0
- package/src/core/custom-provider-url.ts +24 -0
- package/src/core/discover-custom-model-provider-models.ts +147 -0
- package/src/core/entities/agent-workspace-settings.ts +9 -0
- package/src/core/entities/model-provider-config.ts +25 -0
- package/src/core/errors.ts +139 -0
- package/src/core/harness/claude.ts +75 -0
- package/src/core/harness/index.ts +19 -0
- package/src/core/harness/pi.ts +18 -0
- package/src/core/harness/registry.test.ts +281 -0
- package/src/core/harness/registry.ts +108 -0
- package/src/core/index.ts +56 -0
- package/src/core/model-provider-catalog.ts +33 -0
- package/src/core/model-provider-config-service.test.ts +940 -0
- package/src/core/model-provider-config-service.ts +207 -0
- package/src/core/model-provider-validation.test.ts +354 -0
- package/src/core/model-provider-validation.ts +276 -0
- package/src/core/resolve-agent-config.test.ts +441 -0
- package/src/core/resolve-agent-config.ts +272 -0
- package/src/core/resolve-runtime-credentials.test.ts +316 -0
- package/src/core/resolve-runtime-credentials.ts +147 -0
- package/src/core/workspace-agent-defaults-resolver.ts +42 -0
- package/src/db/agent-defaults-snapshot.ts +49 -0
- package/src/db/agent-workspace-settings.test.ts +111 -0
- package/src/db/agent-workspace-settings.ts +92 -0
- package/src/db/db.ts +20 -0
- package/src/db/index.ts +29 -0
- package/src/db/model-provider-configs.test.ts +281 -0
- package/src/db/model-provider-configs.ts +230 -0
- package/src/db/schema/agent-workspace-settings.ts +27 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/model-provider-configs.ts +80 -0
- package/src/index.ts +43 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +18 -0
- package/src/presentation/dto/index.ts +5 -0
- package/src/presentation/dto/model-provider-config.ts +41 -0
- package/src/presentation/e2eRoutes/create-model-provider.test.ts +47 -0
- package/src/presentation/e2eRoutes/create-model-provider.ts +61 -0
- package/src/presentation/e2eRoutes/index.ts +7 -0
- package/src/presentation/index.ts +1 -0
- package/src/presentation/routes/create-custom-model-provider.ts +45 -0
- package/src/presentation/routes/delete-model-provider-config.ts +31 -0
- package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +42 -0
- package/src/presentation/routes/discover-custom-model-provider-models.ts +29 -0
- package/src/presentation/routes/errors.ts +122 -0
- package/src/presentation/routes/index.ts +37 -0
- package/src/presentation/routes/list-model-provider-catalog.ts +15 -0
- package/src/presentation/routes/list-model-provider-configs.ts +33 -0
- package/src/presentation/routes/model-provider-catalog.test.ts +73 -0
- package/src/presentation/routes/model-provider-config.test.ts +807 -0
- package/src/presentation/routes/set-default-harness.ts +33 -0
- package/src/presentation/routes/set-default-model-provider.ts +47 -0
- package/src/presentation/routes/update-custom-model-provider.ts +50 -0
- package/src/presentation/routes/update-model-provider-default-model.ts +40 -0
- package/src/presentation/routes/upsert-model-provider-config.ts +53 -0
- package/test/env.ts +8 -0
- package/test/globalSetup.ts +25 -0
- package/test/setup.ts +17 -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 +11 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CreateCustomModelProviderBodyDto,
|
|
3
|
+
type CustomAgentModelDto,
|
|
4
|
+
type CustomModelProviderHeaderDto,
|
|
5
|
+
type CustomModelProviderHeaderRequestDto,
|
|
6
|
+
DEFAULT_AGENT_THINKING,
|
|
7
|
+
type DiscoverCustomModelProviderModelsBodyDto,
|
|
8
|
+
type DiscoverCustomModelProviderModelsBySlugBodyDto,
|
|
9
|
+
type ModelProviderRef,
|
|
10
|
+
type UpdateCustomModelProviderBodyDto,
|
|
11
|
+
type UpdateCustomModelProviderHeaderRequestDto,
|
|
12
|
+
} from '@shipfox/api-agent-dto';
|
|
13
|
+
import {deleteSecrets, getSecretsByNamespace, setSecrets} from '@shipfox/api-secrets';
|
|
14
|
+
import {assertEgressAllowed} from '@shipfox/node-egress-guard';
|
|
15
|
+
import {
|
|
16
|
+
deleteModelProviderConfig,
|
|
17
|
+
getModelProviderConfig,
|
|
18
|
+
insertCustomModelProviderConfig,
|
|
19
|
+
upsertModelProviderConfig,
|
|
20
|
+
} from '#db/index.js';
|
|
21
|
+
import {
|
|
22
|
+
agentSystemNamespace,
|
|
23
|
+
customCredentialsToStoreValues,
|
|
24
|
+
customCredentialToStoreKey,
|
|
25
|
+
storeValuesToCustomRuntimeCredentials,
|
|
26
|
+
} from './credential-fingerprints.js';
|
|
27
|
+
import {normalizeCustomProviderBaseUrl} from './custom-provider-url.js';
|
|
28
|
+
import type {ModelProviderConfig} from './entities/model-provider-config.js';
|
|
29
|
+
import {
|
|
30
|
+
CustomModelProviderConfigNotFoundError,
|
|
31
|
+
CustomModelProviderSlugCollisionError,
|
|
32
|
+
CustomModelProviderStoredSecretBaseUrlChangeError,
|
|
33
|
+
InvalidAgentModelError,
|
|
34
|
+
InvalidCustomModelProviderHeaderKeepError,
|
|
35
|
+
} from './errors.js';
|
|
36
|
+
import {
|
|
37
|
+
egressPolicy,
|
|
38
|
+
probeCustomModelProviderCredentials,
|
|
39
|
+
runProviderProbe,
|
|
40
|
+
} from './model-provider-validation.js';
|
|
41
|
+
|
|
42
|
+
export interface CreateCustomModelProviderConfigParams {
|
|
43
|
+
workspaceId: string;
|
|
44
|
+
body: CreateCustomModelProviderBodyDto;
|
|
45
|
+
setAsDefault?: boolean | undefined;
|
|
46
|
+
signal?: AbortSignal | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UpdateCustomModelProviderConfigParams {
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
providerId: ModelProviderRef;
|
|
52
|
+
body: UpdateCustomModelProviderBodyDto;
|
|
53
|
+
signal?: AbortSignal | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CustomModelProviderConfigServiceOptions {
|
|
57
|
+
probe?: typeof probeCustomModelProviderCredentials | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SplitHeaders {
|
|
61
|
+
plaintextHeaders: CustomModelProviderHeaderDto[];
|
|
62
|
+
secretHeaderNames: string[];
|
|
63
|
+
secretCredentials: Record<string, string>;
|
|
64
|
+
runtimeHeaders: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function createCustomModelProviderConfig(
|
|
68
|
+
params: CreateCustomModelProviderConfigParams,
|
|
69
|
+
options: CustomModelProviderConfigServiceOptions = {},
|
|
70
|
+
): Promise<ModelProviderConfig> {
|
|
71
|
+
const probe = options.probe ?? probeCustomModelProviderCredentials;
|
|
72
|
+
|
|
73
|
+
const existing = await getModelProviderConfig({
|
|
74
|
+
workspaceId: params.workspaceId,
|
|
75
|
+
providerId: params.body.slug,
|
|
76
|
+
});
|
|
77
|
+
if (existing !== undefined) {
|
|
78
|
+
throw new CustomModelProviderSlugCollisionError(params.workspaceId, params.body.slug);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const splitHeaders = splitCustomHeaders(params.body.headers ?? []);
|
|
82
|
+
const secretCredentials = {
|
|
83
|
+
...(params.body.api_key ? {api_key: params.body.api_key} : {}),
|
|
84
|
+
...splitHeaders.secretCredentials,
|
|
85
|
+
};
|
|
86
|
+
const probeModel = selectProbeModel(params.body.models, params.body.default_model ?? null);
|
|
87
|
+
|
|
88
|
+
await assertEgressAllowed(params.body.base_url, egressPolicy());
|
|
89
|
+
await runProviderProbe({
|
|
90
|
+
probe,
|
|
91
|
+
args: {
|
|
92
|
+
providerId: params.body.slug,
|
|
93
|
+
api: params.body.api,
|
|
94
|
+
baseUrl: params.body.base_url,
|
|
95
|
+
model: probeModel,
|
|
96
|
+
apiKey: params.body.api_key,
|
|
97
|
+
headers: splitHeaders.runtimeHeaders,
|
|
98
|
+
...(params.signal ? {signal: params.signal} : {}),
|
|
99
|
+
},
|
|
100
|
+
metricLabel: 'custom',
|
|
101
|
+
providerId: params.body.slug,
|
|
102
|
+
secrets: Object.values(secretCredentials),
|
|
103
|
+
signal: params.signal,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const config = await insertCustomModelProviderConfig({
|
|
107
|
+
workspaceId: params.workspaceId,
|
|
108
|
+
providerId: params.body.slug,
|
|
109
|
+
kind: 'custom',
|
|
110
|
+
displayName: params.body.display_name,
|
|
111
|
+
api: params.body.api,
|
|
112
|
+
baseUrl: params.body.base_url,
|
|
113
|
+
headers: splitHeaders.plaintextHeaders,
|
|
114
|
+
secretHeaderNames: splitHeaders.secretHeaderNames,
|
|
115
|
+
models: params.body.models,
|
|
116
|
+
requiresApiKey: params.body.api_key !== undefined,
|
|
117
|
+
defaultModel: params.body.default_model ?? null,
|
|
118
|
+
defaultThinking: DEFAULT_AGENT_THINKING,
|
|
119
|
+
setAsDefault: params.setAsDefault,
|
|
120
|
+
});
|
|
121
|
+
if (!config) {
|
|
122
|
+
throw new CustomModelProviderSlugCollisionError(params.workspaceId, params.body.slug);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await setSecrets({
|
|
127
|
+
workspaceId: params.workspaceId,
|
|
128
|
+
namespace: agentSystemNamespace(params.body.slug),
|
|
129
|
+
values: customCredentialsToStoreValues(secretCredentials),
|
|
130
|
+
});
|
|
131
|
+
return config;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
await deleteModelProviderConfig({
|
|
134
|
+
workspaceId: params.workspaceId,
|
|
135
|
+
providerId: params.body.slug,
|
|
136
|
+
});
|
|
137
|
+
await deleteSecrets({
|
|
138
|
+
workspaceId: params.workspaceId,
|
|
139
|
+
namespace: agentSystemNamespace(params.body.slug),
|
|
140
|
+
});
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function updateCustomModelProviderConfig(
|
|
146
|
+
params: UpdateCustomModelProviderConfigParams,
|
|
147
|
+
options: CustomModelProviderConfigServiceOptions = {},
|
|
148
|
+
): Promise<ModelProviderConfig> {
|
|
149
|
+
const probe = options.probe ?? probeCustomModelProviderCredentials;
|
|
150
|
+
|
|
151
|
+
const existing = await getExistingCustomConfig(params.workspaceId, params.providerId);
|
|
152
|
+
const existingSecrets = await getCustomProviderSecrets({
|
|
153
|
+
workspaceId: params.workspaceId,
|
|
154
|
+
providerId: params.providerId,
|
|
155
|
+
});
|
|
156
|
+
const nextModels = params.body.models ?? existing.models ?? [];
|
|
157
|
+
const nextDefaultModel = resolveNextDefaultModel(
|
|
158
|
+
params.providerId,
|
|
159
|
+
existing.defaultModel,
|
|
160
|
+
nextModels,
|
|
161
|
+
params.body,
|
|
162
|
+
);
|
|
163
|
+
const nextApi = params.body.api ?? existing.api ?? 'openai-responses';
|
|
164
|
+
const nextBaseUrl = params.body.base_url ?? existing.baseUrl ?? '';
|
|
165
|
+
const nextDisplayName = params.body.display_name ?? existing.displayName ?? params.providerId;
|
|
166
|
+
assertStoredSecretsNotReusedAfterBaseUrlChange({
|
|
167
|
+
body: params.body,
|
|
168
|
+
existing,
|
|
169
|
+
existingSecrets,
|
|
170
|
+
providerId: params.providerId,
|
|
171
|
+
});
|
|
172
|
+
const nextHeaders = resolveNextHeaders({
|
|
173
|
+
requestHeaders: params.body.headers,
|
|
174
|
+
existing,
|
|
175
|
+
existingSecrets,
|
|
176
|
+
providerId: params.providerId,
|
|
177
|
+
});
|
|
178
|
+
const nextApiKey = params.body.api_key ?? existingSecrets.api_key;
|
|
179
|
+
const newSecretCredentials = {
|
|
180
|
+
...(params.body.api_key ? {api_key: params.body.api_key} : {}),
|
|
181
|
+
...nextHeaders.newSecretCredentials,
|
|
182
|
+
};
|
|
183
|
+
const probeSecretCredentials = {
|
|
184
|
+
...(nextApiKey ? {api_key: nextApiKey} : {}),
|
|
185
|
+
...nextHeaders.probeSecretCredentials,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
await assertEgressAllowed(nextBaseUrl, egressPolicy());
|
|
189
|
+
await runProviderProbe({
|
|
190
|
+
probe,
|
|
191
|
+
args: {
|
|
192
|
+
providerId: params.providerId,
|
|
193
|
+
api: nextApi,
|
|
194
|
+
baseUrl: nextBaseUrl,
|
|
195
|
+
model: selectProbeModel(nextModels, nextDefaultModel),
|
|
196
|
+
apiKey: nextApiKey,
|
|
197
|
+
headers: nextHeaders.runtimeHeaders,
|
|
198
|
+
...(params.signal ? {signal: params.signal} : {}),
|
|
199
|
+
},
|
|
200
|
+
metricLabel: 'custom',
|
|
201
|
+
providerId: params.providerId,
|
|
202
|
+
secrets: Object.values(probeSecretCredentials),
|
|
203
|
+
signal: params.signal,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const namespace = agentSystemNamespace(params.providerId);
|
|
207
|
+
await setSecrets({
|
|
208
|
+
workspaceId: params.workspaceId,
|
|
209
|
+
namespace,
|
|
210
|
+
values: customCredentialsToStoreValues(newSecretCredentials),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
let updated: ModelProviderConfig;
|
|
214
|
+
try {
|
|
215
|
+
updated = await upsertModelProviderConfig({
|
|
216
|
+
workspaceId: params.workspaceId,
|
|
217
|
+
providerId: params.providerId,
|
|
218
|
+
kind: 'custom',
|
|
219
|
+
displayName: nextDisplayName,
|
|
220
|
+
api: nextApi,
|
|
221
|
+
baseUrl: nextBaseUrl,
|
|
222
|
+
headers: nextHeaders.plaintextHeaders,
|
|
223
|
+
secretHeaderNames: nextHeaders.secretHeaderNames,
|
|
224
|
+
models: nextModels,
|
|
225
|
+
requiresApiKey: existing.requiresApiKey || params.body.api_key !== undefined,
|
|
226
|
+
defaultModel: nextDefaultModel,
|
|
227
|
+
defaultThinking: existing.defaultThinking,
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
await rollbackCustomProviderSecretUpdate({
|
|
231
|
+
workspaceId: params.workspaceId,
|
|
232
|
+
namespace,
|
|
233
|
+
existingSecrets,
|
|
234
|
+
attemptedSecrets: newSecretCredentials,
|
|
235
|
+
}).catch(() => undefined);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await pruneStaleCustomHeaderSecrets({
|
|
240
|
+
workspaceId: params.workspaceId,
|
|
241
|
+
providerId: params.providerId,
|
|
242
|
+
existingSecrets,
|
|
243
|
+
newSecrets: newSecretCredentials,
|
|
244
|
+
replaceHeaders: params.body.headers !== undefined,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return updated;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function resolveCustomModelProviderDiscoveryParams(params: {
|
|
251
|
+
workspaceId: string;
|
|
252
|
+
providerId: ModelProviderRef;
|
|
253
|
+
body: DiscoverCustomModelProviderModelsBySlugBodyDto;
|
|
254
|
+
}): Promise<DiscoverCustomModelProviderModelsBodyDto> {
|
|
255
|
+
const existing = await getExistingCustomConfig(params.workspaceId, params.providerId);
|
|
256
|
+
const existingSecrets = await getCustomProviderSecrets({
|
|
257
|
+
workspaceId: params.workspaceId,
|
|
258
|
+
providerId: params.providerId,
|
|
259
|
+
});
|
|
260
|
+
assertStoredSecretsNotReusedAfterBaseUrlChange({
|
|
261
|
+
body: params.body,
|
|
262
|
+
existing,
|
|
263
|
+
existingSecrets,
|
|
264
|
+
providerId: params.providerId,
|
|
265
|
+
});
|
|
266
|
+
const headers = resolveNextHeaders({
|
|
267
|
+
requestHeaders: params.body.headers,
|
|
268
|
+
existing,
|
|
269
|
+
existingSecrets,
|
|
270
|
+
providerId: params.providerId,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
api: params.body.api ?? existing.api ?? 'openai-responses',
|
|
275
|
+
base_url: params.body.base_url ?? existing.baseUrl ?? '',
|
|
276
|
+
...((params.body.api_key ?? existingSecrets.api_key)
|
|
277
|
+
? {api_key: params.body.api_key ?? existingSecrets.api_key}
|
|
278
|
+
: {}),
|
|
279
|
+
headers: Object.entries(headers.runtimeHeaders).map(([name, value]) => ({name, value})),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function getExistingCustomConfig(
|
|
284
|
+
workspaceId: string,
|
|
285
|
+
providerId: ModelProviderRef,
|
|
286
|
+
): Promise<ModelProviderConfig> {
|
|
287
|
+
const existing = await getModelProviderConfig({workspaceId, providerId});
|
|
288
|
+
if (existing?.kind !== 'custom') {
|
|
289
|
+
throw new CustomModelProviderConfigNotFoundError(workspaceId, providerId);
|
|
290
|
+
}
|
|
291
|
+
return existing;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function splitCustomHeaders(headers: CustomModelProviderHeaderRequestDto[]): SplitHeaders {
|
|
295
|
+
const plaintextHeaders: CustomModelProviderHeaderDto[] = [];
|
|
296
|
+
const secretHeaderNames: string[] = [];
|
|
297
|
+
const secretCredentials: Record<string, string> = {};
|
|
298
|
+
const runtimeHeaders: Record<string, string> = {};
|
|
299
|
+
|
|
300
|
+
for (const header of headers) {
|
|
301
|
+
runtimeHeaders[header.name] = header.value;
|
|
302
|
+
if (header.secret) {
|
|
303
|
+
secretHeaderNames.push(header.name);
|
|
304
|
+
secretCredentials[`header:${header.name}`] = header.value;
|
|
305
|
+
} else {
|
|
306
|
+
plaintextHeaders.push({name: header.name, value: header.value});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {plaintextHeaders, secretHeaderNames, secretCredentials, runtimeHeaders};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveNextHeaders(params: {
|
|
314
|
+
requestHeaders: UpdateCustomModelProviderHeaderRequestDto[] | undefined;
|
|
315
|
+
existing: ModelProviderConfig;
|
|
316
|
+
existingSecrets: Record<string, string>;
|
|
317
|
+
providerId: ModelProviderRef;
|
|
318
|
+
}): {
|
|
319
|
+
plaintextHeaders: CustomModelProviderHeaderDto[];
|
|
320
|
+
secretHeaderNames: string[];
|
|
321
|
+
runtimeHeaders: Record<string, string>;
|
|
322
|
+
probeSecretCredentials: Record<string, string>;
|
|
323
|
+
newSecretCredentials: Record<string, string>;
|
|
324
|
+
} {
|
|
325
|
+
if (params.requestHeaders !== undefined) {
|
|
326
|
+
return splitUpdateHeaders({
|
|
327
|
+
requestHeaders: params.requestHeaders,
|
|
328
|
+
existingSecrets: params.existingSecrets,
|
|
329
|
+
providerId: params.providerId,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const runtimeHeaders: Record<string, string> = {};
|
|
334
|
+
for (const header of params.existing.headers ?? []) {
|
|
335
|
+
runtimeHeaders[header.name] = header.value;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const probeSecretCredentials: Record<string, string> = {};
|
|
339
|
+
for (const [key, value] of Object.entries(params.existingSecrets)) {
|
|
340
|
+
if (!key.startsWith('header:')) continue;
|
|
341
|
+
const name = key.slice('header:'.length);
|
|
342
|
+
runtimeHeaders[name] = value;
|
|
343
|
+
probeSecretCredentials[key] = value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
plaintextHeaders: params.existing.headers ?? [],
|
|
348
|
+
secretHeaderNames:
|
|
349
|
+
params.existing.secretHeaderNames ?? secretHeaderNamesFromCredentials(params.existingSecrets),
|
|
350
|
+
runtimeHeaders,
|
|
351
|
+
probeSecretCredentials,
|
|
352
|
+
newSecretCredentials: {},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function splitUpdateHeaders(params: {
|
|
357
|
+
requestHeaders: UpdateCustomModelProviderHeaderRequestDto[];
|
|
358
|
+
existingSecrets: Record<string, string>;
|
|
359
|
+
providerId: ModelProviderRef;
|
|
360
|
+
}): {
|
|
361
|
+
plaintextHeaders: CustomModelProviderHeaderDto[];
|
|
362
|
+
secretHeaderNames: string[];
|
|
363
|
+
runtimeHeaders: Record<string, string>;
|
|
364
|
+
probeSecretCredentials: Record<string, string>;
|
|
365
|
+
newSecretCredentials: Record<string, string>;
|
|
366
|
+
} {
|
|
367
|
+
const plaintextHeaders: CustomModelProviderHeaderDto[] = [];
|
|
368
|
+
const secretHeaderNames: string[] = [];
|
|
369
|
+
const runtimeHeaders: Record<string, string> = {};
|
|
370
|
+
const probeSecretCredentials: Record<string, string> = {};
|
|
371
|
+
const newSecretCredentials: Record<string, string> = {};
|
|
372
|
+
|
|
373
|
+
for (const header of params.requestHeaders) {
|
|
374
|
+
if (header.keep === true) {
|
|
375
|
+
const key = `header:${header.name}`;
|
|
376
|
+
const value = params.existingSecrets[key];
|
|
377
|
+
if (value === undefined) {
|
|
378
|
+
throw new InvalidCustomModelProviderHeaderKeepError(params.providerId, header.name);
|
|
379
|
+
}
|
|
380
|
+
runtimeHeaders[header.name] = value;
|
|
381
|
+
secretHeaderNames.push(header.name);
|
|
382
|
+
probeSecretCredentials[key] = value;
|
|
383
|
+
newSecretCredentials[key] = value;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const value = header.value;
|
|
388
|
+
if (value === undefined) {
|
|
389
|
+
throw new InvalidCustomModelProviderHeaderKeepError(params.providerId, header.name);
|
|
390
|
+
}
|
|
391
|
+
runtimeHeaders[header.name] = value;
|
|
392
|
+
if (header.secret) {
|
|
393
|
+
const key = `header:${header.name}`;
|
|
394
|
+
secretHeaderNames.push(header.name);
|
|
395
|
+
probeSecretCredentials[key] = value;
|
|
396
|
+
newSecretCredentials[key] = value;
|
|
397
|
+
} else {
|
|
398
|
+
plaintextHeaders.push({name: header.name, value});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
plaintextHeaders,
|
|
404
|
+
secretHeaderNames,
|
|
405
|
+
runtimeHeaders,
|
|
406
|
+
probeSecretCredentials,
|
|
407
|
+
newSecretCredentials,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function resolveNextDefaultModel(
|
|
412
|
+
providerId: ModelProviderRef,
|
|
413
|
+
existingDefaultModel: string | null,
|
|
414
|
+
models: CustomAgentModelDto[],
|
|
415
|
+
body: UpdateCustomModelProviderBodyDto,
|
|
416
|
+
): string | null {
|
|
417
|
+
if (!('default_model' in body)) {
|
|
418
|
+
if (existingDefaultModel === null) return null;
|
|
419
|
+
return models.some((model) => model.id === existingDefaultModel) ? existingDefaultModel : null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const requestedDefaultModel = body.default_model ?? null;
|
|
423
|
+
if (requestedDefaultModel === null) return null;
|
|
424
|
+
if (models.some((model) => model.id === requestedDefaultModel)) return requestedDefaultModel;
|
|
425
|
+
throw new InvalidAgentModelError('pi', providerId, requestedDefaultModel);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function selectProbeModel(
|
|
429
|
+
models: CustomAgentModelDto[],
|
|
430
|
+
defaultModel: string | null,
|
|
431
|
+
): CustomAgentModelDto {
|
|
432
|
+
const model = models.find((candidate) => candidate.id === defaultModel) ?? models[0];
|
|
433
|
+
if (!model) throw new Error('Custom model provider must include at least one model.');
|
|
434
|
+
return model;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function getCustomProviderSecrets(params: {
|
|
438
|
+
workspaceId: string;
|
|
439
|
+
providerId: ModelProviderRef;
|
|
440
|
+
}): Promise<Record<string, string>> {
|
|
441
|
+
const values = await getSecretsByNamespace({
|
|
442
|
+
workspaceId: params.workspaceId,
|
|
443
|
+
namespace: agentSystemNamespace(params.providerId),
|
|
444
|
+
});
|
|
445
|
+
return storeValuesToCustomRuntimeCredentials(values);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function assertStoredSecretsNotReusedAfterBaseUrlChange(params: {
|
|
449
|
+
body: UpdateCustomModelProviderBodyDto | DiscoverCustomModelProviderModelsBySlugBodyDto;
|
|
450
|
+
existing: ModelProviderConfig;
|
|
451
|
+
existingSecrets: Record<string, string>;
|
|
452
|
+
providerId: ModelProviderRef;
|
|
453
|
+
}): void {
|
|
454
|
+
if (
|
|
455
|
+
params.body.base_url === undefined ||
|
|
456
|
+
normalizedUrl(params.body.base_url) === normalizedUrl(params.existing.baseUrl ?? '')
|
|
457
|
+
) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const reusesStoredApiKey =
|
|
462
|
+
params.existingSecrets.api_key !== undefined && params.body.api_key === undefined;
|
|
463
|
+
const reusesStoredSecretHeader =
|
|
464
|
+
params.body.headers === undefined
|
|
465
|
+
? Object.keys(params.existingSecrets).some((key) => key.startsWith('header:'))
|
|
466
|
+
: params.body.headers.some((header) => header.keep === true);
|
|
467
|
+
|
|
468
|
+
if (reusesStoredApiKey || reusesStoredSecretHeader) {
|
|
469
|
+
throw new CustomModelProviderStoredSecretBaseUrlChangeError(params.providerId);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function normalizedUrl(value: string): string {
|
|
474
|
+
return normalizeCustomProviderBaseUrl(value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function rollbackCustomProviderSecretUpdate(params: {
|
|
478
|
+
workspaceId: string;
|
|
479
|
+
namespace: string;
|
|
480
|
+
existingSecrets: Record<string, string>;
|
|
481
|
+
attemptedSecrets: Record<string, string>;
|
|
482
|
+
}): Promise<void> {
|
|
483
|
+
const attemptedKeys = Object.keys(params.attemptedSecrets);
|
|
484
|
+
if (attemptedKeys.length === 0) return;
|
|
485
|
+
|
|
486
|
+
const secretsToRestore = Object.fromEntries(
|
|
487
|
+
attemptedKeys.flatMap((key) => {
|
|
488
|
+
const value = params.existingSecrets[key];
|
|
489
|
+
return value === undefined ? [] : [[key, value]];
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
if (Object.keys(secretsToRestore).length > 0) {
|
|
493
|
+
await setSecrets({
|
|
494
|
+
workspaceId: params.workspaceId,
|
|
495
|
+
namespace: params.namespace,
|
|
496
|
+
values: customCredentialsToStoreValues(secretsToRestore),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const newKeys = attemptedKeys.filter((key) => params.existingSecrets[key] === undefined);
|
|
501
|
+
if (newKeys.length === 0) return;
|
|
502
|
+
|
|
503
|
+
await deleteSecrets({
|
|
504
|
+
workspaceId: params.workspaceId,
|
|
505
|
+
namespace: params.namespace,
|
|
506
|
+
keys: newKeys.map(customCredentialToStoreKey),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function pruneStaleCustomHeaderSecrets(params: {
|
|
511
|
+
workspaceId: string;
|
|
512
|
+
providerId: ModelProviderRef;
|
|
513
|
+
existingSecrets: Record<string, string>;
|
|
514
|
+
newSecrets: Record<string, string>;
|
|
515
|
+
replaceHeaders: boolean;
|
|
516
|
+
}): Promise<void> {
|
|
517
|
+
if (!params.replaceHeaders) return;
|
|
518
|
+
|
|
519
|
+
const nextKeys = new Set(Object.keys(params.newSecrets));
|
|
520
|
+
const staleKeys = Object.keys(params.existingSecrets).filter(
|
|
521
|
+
(key) => key.startsWith('header:') && !nextKeys.has(key),
|
|
522
|
+
);
|
|
523
|
+
if (staleKeys.length === 0) return;
|
|
524
|
+
|
|
525
|
+
await deleteSecrets({
|
|
526
|
+
workspaceId: params.workspaceId,
|
|
527
|
+
namespace: agentSystemNamespace(params.providerId),
|
|
528
|
+
keys: staleKeys.map(customCredentialToStoreKey),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function secretHeaderNamesFromCredentials(credentials: Record<string, string>): string[] {
|
|
533
|
+
return Object.keys(credentials)
|
|
534
|
+
.filter((key) => key.startsWith('header:'))
|
|
535
|
+
.map((key) => key.slice('header:'.length))
|
|
536
|
+
.sort();
|
|
537
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const TRAILING_SLASHES_PATTERN = /\/+$/;
|
|
2
|
+
const GOOGLE_DISCOVERY_VERSION_PATTERN = /\/v1beta$/;
|
|
3
|
+
|
|
4
|
+
export function appendCustomProviderPath(baseUrl: string, segment: string): URL {
|
|
5
|
+
const url = new URL(normalizeCustomProviderBaseUrl(baseUrl));
|
|
6
|
+
const path = url.pathname.replace(TRAILING_SLASHES_PATTERN, '');
|
|
7
|
+
url.pathname = `${path}/${segment}`;
|
|
8
|
+
return url;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeCustomProviderBaseUrl(baseUrl: string): string {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(baseUrl);
|
|
14
|
+
url.pathname = url.pathname.replace(TRAILING_SLASHES_PATTERN, '');
|
|
15
|
+
return url.href;
|
|
16
|
+
} catch {
|
|
17
|
+
return baseUrl.replace(TRAILING_SLASHES_PATTERN, '');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function googleDiscoveryUrl(baseUrl: string): string {
|
|
22
|
+
const normalizedBaseUrl = baseUrl.replace(GOOGLE_DISCOVERY_VERSION_PATTERN, '');
|
|
23
|
+
return appendCustomProviderPath(normalizedBaseUrl, 'v1beta/models').toString();
|
|
24
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CustomModelProviderHeaderDto,
|
|
3
|
+
DiscoverCustomModelProviderModelsBodyDto,
|
|
4
|
+
DiscoverCustomModelProviderModelsResponseDto,
|
|
5
|
+
ModelProviderApi,
|
|
6
|
+
} from '@shipfox/api-agent-dto';
|
|
7
|
+
import {MAX_MODEL_COUNT} from '@shipfox/api-agent-dto';
|
|
8
|
+
import {assertEgressAllowed} from '@shipfox/node-egress-guard';
|
|
9
|
+
import {config} from '#config.js';
|
|
10
|
+
import {appendCustomProviderPath, googleDiscoveryUrl} from './custom-provider-url.js';
|
|
11
|
+
import {egressPolicy} from './model-provider-validation.js';
|
|
12
|
+
|
|
13
|
+
const GOOGLE_MODEL_PREFIX_PATTERN = /^models\//;
|
|
14
|
+
const MAX_DISCOVERY_RESPONSE_BYTES = 512 * 1024;
|
|
15
|
+
|
|
16
|
+
interface DiscoveredModel {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function discoverCustomModelProviderModels(
|
|
22
|
+
params: DiscoverCustomModelProviderModelsBodyDto,
|
|
23
|
+
): Promise<DiscoverCustomModelProviderModelsResponseDto> {
|
|
24
|
+
await assertEgressAllowed(params.base_url, egressPolicy());
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(discoveryUrl(params.base_url, params.api), {
|
|
28
|
+
method: 'GET',
|
|
29
|
+
headers: discoveryHeaders(params.api, params.api_key, params.headers ?? []),
|
|
30
|
+
redirect: 'error',
|
|
31
|
+
signal: AbortSignal.timeout(config.AGENT_PROVIDER_VALIDATION_TIMEOUT_MS),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) return {models: []};
|
|
34
|
+
|
|
35
|
+
const payload = await readBoundedJson(response);
|
|
36
|
+
if (payload === undefined) return {models: []};
|
|
37
|
+
return {models: parseModelList(payload)};
|
|
38
|
+
} catch {
|
|
39
|
+
return {models: []};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function discoveryUrl(baseUrl: string, api: ModelProviderApi): string {
|
|
44
|
+
if (api === 'google-generative-ai') return googleDiscoveryUrl(baseUrl);
|
|
45
|
+
return appendCustomProviderPath(baseUrl, 'models').toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function discoveryHeaders(
|
|
49
|
+
api: ModelProviderApi,
|
|
50
|
+
apiKey: string | undefined,
|
|
51
|
+
headers: CustomModelProviderHeaderDto[],
|
|
52
|
+
): Headers {
|
|
53
|
+
const result = new Headers(headers.map((header) => [header.name, header.value]));
|
|
54
|
+
if (!apiKey) return result;
|
|
55
|
+
|
|
56
|
+
switch (api) {
|
|
57
|
+
case 'anthropic-messages':
|
|
58
|
+
result.set('x-api-key', apiKey);
|
|
59
|
+
result.set('anthropic-version', '2023-06-01');
|
|
60
|
+
break;
|
|
61
|
+
case 'google-generative-ai':
|
|
62
|
+
result.set('x-goog-api-key', apiKey);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
result.set('authorization', `Bearer ${apiKey}`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readBoundedJson(response: Response): Promise<unknown | undefined> {
|
|
73
|
+
if (!response.body) return undefined;
|
|
74
|
+
|
|
75
|
+
const reader = response.body.getReader();
|
|
76
|
+
const chunks: Uint8Array[] = [];
|
|
77
|
+
let totalBytes = 0;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
while (true) {
|
|
81
|
+
const {value, done} = await reader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
if (value === undefined) continue;
|
|
84
|
+
|
|
85
|
+
totalBytes += value.byteLength;
|
|
86
|
+
if (totalBytes > MAX_DISCOVERY_RESPONSE_BYTES) {
|
|
87
|
+
await reader.cancel();
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
chunks.push(value);
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
reader.releaseLock();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const buffer = new Uint8Array(totalBytes);
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
buffer.set(chunk, offset);
|
|
100
|
+
offset += chunk.byteLength;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return JSON.parse(new TextDecoder().decode(buffer));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseModelList(payload: unknown): DiscoveredModel[] {
|
|
107
|
+
const candidates = extractModelCandidates(payload);
|
|
108
|
+
const models = candidates.map(parseModelCandidate).filter(isDiscoveredModel);
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
return models
|
|
111
|
+
.filter((model) => {
|
|
112
|
+
if (seen.has(model.id)) return false;
|
|
113
|
+
seen.add(model.id);
|
|
114
|
+
return true;
|
|
115
|
+
})
|
|
116
|
+
.slice(0, MAX_MODEL_COUNT);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractModelCandidates(payload: unknown): unknown[] {
|
|
120
|
+
if (Array.isArray(payload)) return payload;
|
|
121
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
122
|
+
|
|
123
|
+
const record = payload as Record<string, unknown>;
|
|
124
|
+
if (Array.isArray(record.data)) return record.data;
|
|
125
|
+
if (Array.isArray(record.models)) return record.models;
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseModelCandidate(candidate: unknown): DiscoveredModel | undefined {
|
|
130
|
+
if (!candidate || typeof candidate !== 'object') return undefined;
|
|
131
|
+
const record = candidate as Record<string, unknown>;
|
|
132
|
+
const rawId = firstString(record.id, record.name);
|
|
133
|
+
if (!rawId) return undefined;
|
|
134
|
+
|
|
135
|
+
const id = rawId.replace(GOOGLE_MODEL_PREFIX_PATTERN, '');
|
|
136
|
+
const label =
|
|
137
|
+
firstString(record.label, record.display_name, record.displayName, record.name) ?? id;
|
|
138
|
+
return {id, label: label.replace(GOOGLE_MODEL_PREFIX_PATTERN, '')};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function firstString(...values: unknown[]): string | undefined {
|
|
142
|
+
return values.find((value): value is string => typeof value === 'string' && value.length > 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isDiscoveredModel(model: DiscoveredModel | undefined): model is DiscoveredModel {
|
|
146
|
+
return model !== undefined;
|
|
147
|
+
}
|