typekro 0.32.1 → 0.33.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/dist/.tsbuildinfo +1 -1
- package/dist/alchemy/resource-registration.d.ts +15 -0
- package/dist/alchemy/resource-registration.d.ts.map +1 -1
- package/dist/alchemy/resource-registration.js +13 -1
- package/dist/alchemy/resource-registration.js.map +1 -1
- package/dist/core/dependencies/resolver.d.ts.map +1 -1
- package/dist/core/dependencies/resolver.js +13 -3
- package/dist/core/dependencies/resolver.js.map +1 -1
- package/dist/core/deployment/kro-artifact-binding-migration.d.ts +15 -0
- package/dist/core/deployment/kro-artifact-binding-migration.d.ts.map +1 -0
- package/dist/core/deployment/kro-artifact-binding-migration.js +181 -0
- package/dist/core/deployment/kro-artifact-binding-migration.js.map +1 -0
- package/dist/core/deployment/kro-factory.d.ts +1 -1
- package/dist/core/deployment/kro-factory.d.ts.map +1 -1
- package/dist/core/deployment/kro-factory.js +27 -34
- package/dist/core/deployment/kro-factory.js.map +1 -1
- package/dist/core/planning/compiler.d.ts.map +1 -1
- package/dist/core/planning/compiler.js +9 -23
- package/dist/core/planning/compiler.js.map +1 -1
- package/dist/core/planning/plan-codec.d.ts.map +1 -1
- package/dist/core/planning/plan-codec.js +3 -0
- package/dist/core/planning/plan-codec.js.map +1 -1
- package/dist/core/planning/types.d.ts +3 -0
- package/dist/core/planning/types.d.ts.map +1 -1
- package/dist/core/planning/values.d.ts.map +1 -1
- package/dist/core/planning/values.js +3 -0
- package/dist/core/planning/values.js.map +1 -1
- package/dist/core/references/cel-evaluator.d.ts +0 -5
- package/dist/core/references/cel-evaluator.d.ts.map +1 -1
- package/dist/core/references/cel-evaluator.js +7 -32
- package/dist/core/references/cel-evaluator.js.map +1 -1
- package/dist/core/references/cel-lexical-scanner.d.ts +14 -0
- package/dist/core/references/cel-lexical-scanner.d.ts.map +1 -0
- package/dist/core/references/cel-lexical-scanner.js +90 -0
- package/dist/core/references/cel-lexical-scanner.js.map +1 -0
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +5 -1
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/serialization/cel-references.d.ts.map +1 -1
- package/dist/core/serialization/cel-references.js +3 -1
- package/dist/core/serialization/cel-references.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/compositions/gateway.d.ts +9 -0
- package/dist/factories/envoy-ai-gateway/compositions/gateway.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/gateway.js +773 -0
- package/dist/factories/envoy-ai-gateway/compositions/gateway.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/index.d.ts +4 -0
- package/dist/factories/envoy-ai-gateway/compositions/index.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/index.js +4 -0
- package/dist/factories/envoy-ai-gateway/compositions/index.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/platform.d.ts +70 -0
- package/dist/factories/envoy-ai-gateway/compositions/platform.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/platform.js +376 -0
- package/dist/factories/envoy-ai-gateway/compositions/platform.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/repository.d.ts +13 -0
- package/dist/factories/envoy-ai-gateway/compositions/repository.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/compositions/repository.js +29 -0
- package/dist/factories/envoy-ai-gateway/compositions/repository.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/constants.d.ts +18 -0
- package/dist/factories/envoy-ai-gateway/constants.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/constants.js +18 -0
- package/dist/factories/envoy-ai-gateway/constants.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/index.d.ts +11 -0
- package/dist/factories/envoy-ai-gateway/index.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/index.js +11 -0
- package/dist/factories/envoy-ai-gateway/index.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts +29 -0
- package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/resources/gateway.js +195 -0
- package/dist/factories/envoy-ai-gateway/resources/gateway.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/resources/helm.d.ts +11 -0
- package/dist/factories/envoy-ai-gateway/resources/helm.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/resources/helm.js +54 -0
- package/dist/factories/envoy-ai-gateway/resources/helm.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/resources/index.d.ts +3 -0
- package/dist/factories/envoy-ai-gateway/resources/index.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/resources/index.js +3 -0
- package/dist/factories/envoy-ai-gateway/resources/index.js.map +1 -0
- package/dist/factories/envoy-ai-gateway/types.d.ts +451 -0
- package/dist/factories/envoy-ai-gateway/types.d.ts.map +1 -0
- package/dist/factories/envoy-ai-gateway/types.js +54 -0
- package/dist/factories/envoy-ai-gateway/types.js.map +1 -0
- package/dist/factories/index.d.ts +1 -0
- package/dist/factories/index.d.ts.map +1 -1
- package/dist/factories/index.js +4 -0
- package/dist/factories/index.js.map +1 -1
- package/dist/factories/kro/resource-graph-definition.d.ts.map +1 -1
- package/dist/factories/kro/resource-graph-definition.js +15 -7
- package/dist/factories/kro/resource-graph-definition.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { kubernetesComposition } from '../../../core/composition/imperative.js';
|
|
3
|
+
import { setIncludeWhen } from '../../../core/metadata/resource-metadata.js';
|
|
4
|
+
import { Cel } from '../../../core/references/cel.js';
|
|
5
|
+
import { singleton } from '../../../core/singleton/singleton.js';
|
|
6
|
+
import { isKubernetesRef } from '../../../utils/type-guards.js';
|
|
7
|
+
import { configMap } from '../../kubernetes/config/config-map.js';
|
|
8
|
+
import { namespace } from '../../kubernetes/core/namespace.js';
|
|
9
|
+
import { DEFAULT_ENVOY_AI_GATEWAY_CLASS_NAME, DEFAULT_ENVOY_AI_GATEWAY_LISTENER_PORT, DEFAULT_ENVOY_AI_GATEWAY_VERSION, DEFAULT_ENVOY_GATEWAY_CONTROLLER_NAME, } from '../constants.js';
|
|
10
|
+
import { envoyAIGatewayRoute, envoyAIServiceBackend, envoyBackend, envoyBackendSecurityPolicy, envoyBackendTLSPolicy, envoyBackendTrafficPolicy, envoyGateway, envoyGatewayConfig, } from '../resources/gateway.js';
|
|
11
|
+
import { EnvoyAIGatewaySpecSchema, EnvoyAIGatewayStatusSchema } from '../types.js';
|
|
12
|
+
import { makeEnvoyAIGatewayPlatformBootstrap } from './platform.js';
|
|
13
|
+
/**
|
|
14
|
+
* Build one OpenAI-compatible internal gateway over a fixed, type-safe provider
|
|
15
|
+
* and logical-model topology. Provider graph shape is build-time; instance
|
|
16
|
+
* identity, namespace lifecycle, and listener port remain deploy-time values.
|
|
17
|
+
*/
|
|
18
|
+
export function makeEnvoyAIGateway(options) {
|
|
19
|
+
const providers = validateAndResolveProviders(options.providers);
|
|
20
|
+
validateModels(options, providers);
|
|
21
|
+
const rateLimit = validateRateLimit(options.rateLimit);
|
|
22
|
+
const requestCosts = validateRequestCosts(options.requestCosts ?? defaultRequestCosts());
|
|
23
|
+
const tokenCostMetadataKeys = resolveTokenCostMetadataKeys(requestCosts);
|
|
24
|
+
validateRateLimitMetadataReferences(rateLimit, tokenCostMetadataKeys);
|
|
25
|
+
const profile = options.profile ?? 'production';
|
|
26
|
+
const platformOptions = platformOptionsForGateway(options.platform, rateLimit, profile);
|
|
27
|
+
const platform = makeEnvoyAIGatewayPlatformBootstrap(platformOptions);
|
|
28
|
+
const gatewayClassName = platformOptions.gatewayClassName ?? DEFAULT_ENVOY_AI_GATEWAY_CLASS_NAME;
|
|
29
|
+
const aiGatewayVersion = platformOptions.aiGatewayVersion ?? DEFAULT_ENVOY_AI_GATEWAY_VERSION;
|
|
30
|
+
return kubernetesComposition({
|
|
31
|
+
name: 'envoy-ai-gateway',
|
|
32
|
+
kind: 'EnvoyAIGateway',
|
|
33
|
+
spec: EnvoyAIGatewaySpecSchema,
|
|
34
|
+
status: EnvoyAIGatewayStatusSchema,
|
|
35
|
+
}, (spec) => {
|
|
36
|
+
const lifecycle = spec.lifecycle ?? 'owned';
|
|
37
|
+
const listenerPort = spec.listenerPort ?? DEFAULT_ENVOY_AI_GATEWAY_LISTENER_PORT;
|
|
38
|
+
const gatewayNamespace = namespace({
|
|
39
|
+
metadata: {
|
|
40
|
+
name: spec.namespace,
|
|
41
|
+
labels: {
|
|
42
|
+
'app.kubernetes.io/name': 'envoy-ai-gateway',
|
|
43
|
+
'app.kubernetes.io/instance': spec.name,
|
|
44
|
+
'app.kubernetes.io/managed-by': 'typekro',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
id: 'gatewayNamespace',
|
|
48
|
+
});
|
|
49
|
+
if (isKubernetesRef(spec.name)) {
|
|
50
|
+
setIncludeWhen(gatewayNamespace, [
|
|
51
|
+
Cel.expr('!has(schema.spec.lifecycle) || schema.spec.lifecycle == "owned"'),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
else if (lifecycle === 'external') {
|
|
55
|
+
setIncludeWhen(gatewayNamespace, [false]);
|
|
56
|
+
}
|
|
57
|
+
configMap({
|
|
58
|
+
metadata: {
|
|
59
|
+
name: `${spec.name}-gateway-contract`,
|
|
60
|
+
namespace: spec.namespace,
|
|
61
|
+
labels: {
|
|
62
|
+
'app.kubernetes.io/name': 'envoy-ai-gateway',
|
|
63
|
+
'app.kubernetes.io/instance': spec.name,
|
|
64
|
+
'app.kubernetes.io/component': 'contract',
|
|
65
|
+
'app.kubernetes.io/managed-by': 'typekro',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
data: {
|
|
69
|
+
gatewayClassName,
|
|
70
|
+
providerCount: String(providers.length),
|
|
71
|
+
aiGatewayVersion,
|
|
72
|
+
},
|
|
73
|
+
id: 'gatewayContract',
|
|
74
|
+
});
|
|
75
|
+
const platformOwner = singleton(platform, {
|
|
76
|
+
id: 'envoy-ai-gateway-platform',
|
|
77
|
+
spec: { name: 'envoy-ai-gateway-platform' },
|
|
78
|
+
});
|
|
79
|
+
const gatewayConfig = envoyGatewayConfig({
|
|
80
|
+
name: spec.name,
|
|
81
|
+
namespace: spec.namespace,
|
|
82
|
+
spec: gatewayConfigSpec(options, profile, aiGatewayVersion, requestCosts),
|
|
83
|
+
id: 'gatewayConfig',
|
|
84
|
+
});
|
|
85
|
+
const gateway = envoyGateway({
|
|
86
|
+
name: spec.name,
|
|
87
|
+
namespace: spec.namespace,
|
|
88
|
+
spec: {
|
|
89
|
+
gatewayClassName,
|
|
90
|
+
listeners: [
|
|
91
|
+
{
|
|
92
|
+
name: 'http',
|
|
93
|
+
protocol: 'HTTP',
|
|
94
|
+
port: listenerPort,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
id: 'gateway',
|
|
99
|
+
});
|
|
100
|
+
gateway.dependsOn(gatewayConfig);
|
|
101
|
+
const backendResources = new Map();
|
|
102
|
+
const providerResources = [];
|
|
103
|
+
const readinessResources = [];
|
|
104
|
+
const policyResources = [];
|
|
105
|
+
for (const provider of providers) {
|
|
106
|
+
const resourceId = providerResourceId(provider.name);
|
|
107
|
+
const backendName = `${spec.name}-${provider.name}`;
|
|
108
|
+
const backend = envoyBackend({
|
|
109
|
+
name: backendName,
|
|
110
|
+
namespace: spec.namespace,
|
|
111
|
+
spec: {
|
|
112
|
+
endpoints: [
|
|
113
|
+
{
|
|
114
|
+
fqdn: {
|
|
115
|
+
hostname: provider.hostname,
|
|
116
|
+
port: provider.port,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
id: `${resourceId}Backend`,
|
|
122
|
+
});
|
|
123
|
+
const aiBackend = envoyAIServiceBackend({
|
|
124
|
+
name: backendName,
|
|
125
|
+
namespace: spec.namespace,
|
|
126
|
+
spec: {
|
|
127
|
+
schema: {
|
|
128
|
+
name: provider.schema,
|
|
129
|
+
...(provider.prefix ? { prefix: provider.prefix } : {}),
|
|
130
|
+
},
|
|
131
|
+
backendRef: {
|
|
132
|
+
name: backendName,
|
|
133
|
+
kind: 'Backend',
|
|
134
|
+
group: 'gateway.envoyproxy.io',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
id: `${resourceId}AIServiceBackend`,
|
|
138
|
+
});
|
|
139
|
+
aiBackend.dependsOn(backend);
|
|
140
|
+
backendResources.set(provider.name, aiBackend);
|
|
141
|
+
const providerResource = {
|
|
142
|
+
name: backendName,
|
|
143
|
+
status: aiBackend.status,
|
|
144
|
+
resourceId: `${resourceId}AIServiceBackend`,
|
|
145
|
+
};
|
|
146
|
+
providerResources.push(providerResource);
|
|
147
|
+
readinessResources.push(providerResource);
|
|
148
|
+
const securityPolicy = providerSecurityPolicy(provider, spec.namespace, backendName, `${resourceId}SecurityPolicy`);
|
|
149
|
+
if (securityPolicy) {
|
|
150
|
+
securityPolicy.dependsOn(aiBackend);
|
|
151
|
+
readinessResources.push({
|
|
152
|
+
name: backendName,
|
|
153
|
+
status: securityPolicy.status,
|
|
154
|
+
resourceId: `${resourceId}SecurityPolicy`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (provider.tls) {
|
|
158
|
+
const tlsPolicy = envoyBackendTLSPolicy({
|
|
159
|
+
name: backendName,
|
|
160
|
+
namespace: spec.namespace,
|
|
161
|
+
spec: {
|
|
162
|
+
targetRefs: [
|
|
163
|
+
{
|
|
164
|
+
group: 'gateway.envoyproxy.io',
|
|
165
|
+
kind: 'Backend',
|
|
166
|
+
name: backendName,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
validation: {
|
|
170
|
+
wellKnownCACertificates: 'System',
|
|
171
|
+
hostname: provider.hostname,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
id: `${resourceId}TLSPolicy`,
|
|
175
|
+
});
|
|
176
|
+
tlsPolicy.dependsOn(backend);
|
|
177
|
+
policyResources.push({ resourceId: `${resourceId}TLSPolicy` });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const route = envoyAIGatewayRoute({
|
|
181
|
+
name: spec.name,
|
|
182
|
+
namespace: spec.namespace,
|
|
183
|
+
spec: {
|
|
184
|
+
parentRefs: [
|
|
185
|
+
{
|
|
186
|
+
name: spec.name,
|
|
187
|
+
kind: 'Gateway',
|
|
188
|
+
group: 'gateway.networking.k8s.io',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
rules: options.models.map((model) => ({
|
|
192
|
+
matches: [
|
|
193
|
+
{
|
|
194
|
+
headers: [
|
|
195
|
+
{
|
|
196
|
+
type: 'Exact',
|
|
197
|
+
name: 'x-ai-eg-model',
|
|
198
|
+
value: model.model,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
backendRefs: model.targets.map((target) => {
|
|
204
|
+
const backend = backendResources.get(target.provider);
|
|
205
|
+
if (!backend) {
|
|
206
|
+
throw new Error(`Envoy AI Gateway model ${model.model} references unknown provider ${target.provider}.`);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
name: `${spec.name}-${target.provider}`,
|
|
210
|
+
...(target.model ? { modelNameOverride: target.model } : {}),
|
|
211
|
+
...(target.weight !== undefined ? { weight: target.weight } : {}),
|
|
212
|
+
...(target.priority !== undefined ? { priority: target.priority } : {}),
|
|
213
|
+
};
|
|
214
|
+
}),
|
|
215
|
+
timeouts: {
|
|
216
|
+
request: model.requestTimeout ?? '60s',
|
|
217
|
+
},
|
|
218
|
+
...(model.modelsOwnedBy ? { modelsOwnedBy: model.modelsOwnedBy } : {}),
|
|
219
|
+
})),
|
|
220
|
+
},
|
|
221
|
+
id: 'route',
|
|
222
|
+
});
|
|
223
|
+
route.dependsOn(gateway);
|
|
224
|
+
for (const backend of backendResources.values())
|
|
225
|
+
route.dependsOn(backend);
|
|
226
|
+
if (options.retry !== false) {
|
|
227
|
+
const retry = options.retry ?? {};
|
|
228
|
+
const policy = envoyBackendTrafficPolicy({
|
|
229
|
+
name: spec.name,
|
|
230
|
+
namespace: spec.namespace,
|
|
231
|
+
spec: {
|
|
232
|
+
targetRefs: [
|
|
233
|
+
{
|
|
234
|
+
group: 'gateway.networking.k8s.io',
|
|
235
|
+
kind: 'HTTPRoute',
|
|
236
|
+
name: spec.name,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
retry: {
|
|
240
|
+
numAttemptsPerPriority: retry.attemptsPerPriority ?? 1,
|
|
241
|
+
numRetries: retry.retries ?? Math.max(1, providers.length),
|
|
242
|
+
perRetry: {
|
|
243
|
+
timeout: retry.perRetryTimeout ?? '30s',
|
|
244
|
+
backOff: {
|
|
245
|
+
baseInterval: '100ms',
|
|
246
|
+
maxInterval: '10s',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
retryOn: {
|
|
250
|
+
httpStatusCodes: [...(retry.retryStatusCodes ?? [429, 500, 502, 503, 504])],
|
|
251
|
+
triggers: [
|
|
252
|
+
...(retry.triggers ?? ['connect-failure', 'retriable-status-codes', 'reset']),
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
id: 'retryPolicy',
|
|
258
|
+
});
|
|
259
|
+
policy.dependsOn(route);
|
|
260
|
+
policyResources.push({ resourceId: 'retryPolicy' });
|
|
261
|
+
}
|
|
262
|
+
if (rateLimit) {
|
|
263
|
+
const policy = envoyBackendTrafficPolicy({
|
|
264
|
+
name: `${spec.name}-rate-limit`,
|
|
265
|
+
namespace: spec.namespace,
|
|
266
|
+
spec: {
|
|
267
|
+
targetRefs: [
|
|
268
|
+
{
|
|
269
|
+
group: 'gateway.networking.k8s.io',
|
|
270
|
+
kind: 'Gateway',
|
|
271
|
+
name: spec.name,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
rateLimit: {
|
|
275
|
+
type: 'Global',
|
|
276
|
+
global: {
|
|
277
|
+
rules: rateLimit.rules.map((rule) => ({
|
|
278
|
+
...(rule.identityHeader
|
|
279
|
+
? {
|
|
280
|
+
clientSelectors: [
|
|
281
|
+
{
|
|
282
|
+
headers: [
|
|
283
|
+
{
|
|
284
|
+
name: rule.identityHeader,
|
|
285
|
+
type: 'Distinct',
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
}
|
|
291
|
+
: {}),
|
|
292
|
+
limit: {
|
|
293
|
+
requests: rule.requests,
|
|
294
|
+
unit: rule.unit,
|
|
295
|
+
},
|
|
296
|
+
...(rule.cost && rule.cost !== 'request'
|
|
297
|
+
? {
|
|
298
|
+
cost: {
|
|
299
|
+
request: {
|
|
300
|
+
from: 'Number',
|
|
301
|
+
number: 0,
|
|
302
|
+
},
|
|
303
|
+
response: {
|
|
304
|
+
from: 'Metadata',
|
|
305
|
+
metadata: {
|
|
306
|
+
namespace: 'io.envoy.ai_gateway',
|
|
307
|
+
key: requiredTokenCostMetadataKey(tokenCostMetadataKeys, rule.cost),
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
: {}),
|
|
313
|
+
})),
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
id: 'rateLimitPolicy',
|
|
318
|
+
});
|
|
319
|
+
policy.dependsOn(gateway);
|
|
320
|
+
policyResources.push({ resourceId: 'rateLimitPolicy' });
|
|
321
|
+
}
|
|
322
|
+
const gatewayConfigAccepted = acceptedExpression('gatewayConfig');
|
|
323
|
+
const routeAccepted = acceptedExpression('route');
|
|
324
|
+
const gatewayProgrammed = Cel.expr('gateway.status.conditions.exists(c, c.type == "Accepted" && c.status == "True" && ' +
|
|
325
|
+
'c.observedGeneration == gateway.metadata.generation) && ' +
|
|
326
|
+
'gateway.status.conditions.exists(c, c.type == "Programmed" && c.status == "True" && ' +
|
|
327
|
+
'c.observedGeneration == gateway.metadata.generation)');
|
|
328
|
+
const readinessExpressions = readinessResources.map((resource) => acceptedExpressionFor(resource));
|
|
329
|
+
const allProvidersAccepted = Cel.expr(readinessExpressions.length === 0
|
|
330
|
+
? 'true'
|
|
331
|
+
: readinessExpressions.map(({ expression }) => `(${expression})`).join(' && '));
|
|
332
|
+
const anyProviderFailed = Cel.expr(readinessResources.length === 0
|
|
333
|
+
? 'false'
|
|
334
|
+
: readinessResources
|
|
335
|
+
.map((resource) => `${resourceIdForStatus(resource)}.status.conditions.exists(c, ` +
|
|
336
|
+
'((c.type == "NotAccepted" && c.status == "True") || ' +
|
|
337
|
+
'(c.type == "Accepted" && c.status == "False")) && ' +
|
|
338
|
+
currentConditionExpression(resourceIdForStatus(resource)) +
|
|
339
|
+
')')
|
|
340
|
+
.map((expression) => `(${expression})`)
|
|
341
|
+
.join(' || '));
|
|
342
|
+
const allPoliciesAccepted = Cel.expr(policyResources.length === 0
|
|
343
|
+
? 'true'
|
|
344
|
+
: policyResources
|
|
345
|
+
.map(({ resourceId }) => `(${policyAcceptedExpression(resourceId)})`)
|
|
346
|
+
.join(' && '));
|
|
347
|
+
const anyPolicyFailed = Cel.expr(policyResources.length === 0
|
|
348
|
+
? 'false'
|
|
349
|
+
: policyResources
|
|
350
|
+
.map(({ resourceId }) => `(${policyFailedExpression(resourceId)})`)
|
|
351
|
+
.join(' || '));
|
|
352
|
+
const gatewayFailed = Cel.expr('gateway.status.conditions.exists(c, ' +
|
|
353
|
+
'(c.type == "Accepted" || c.type == "Programmed") && c.status == "False" && ' +
|
|
354
|
+
'c.observedGeneration == gateway.metadata.generation)');
|
|
355
|
+
const routeFailed = Cel.expr('route.status.conditions.exists(c, ' +
|
|
356
|
+
'((c.type == "NotAccepted" && c.status == "True") || ' +
|
|
357
|
+
'(c.type == "Accepted" && c.status == "False")) && ' +
|
|
358
|
+
currentConditionExpression('route') +
|
|
359
|
+
')');
|
|
360
|
+
const platformReady = Cel.expr(platformOwner.status.ready);
|
|
361
|
+
const platformFailed = Cel.expr(platformOwner.status.failed);
|
|
362
|
+
const failed = Cel.expr(`(${platformFailed.expression}) || (${gatewayFailed.expression}) || ` +
|
|
363
|
+
`(${routeFailed.expression}) || (${anyProviderFailed.expression}) || ` +
|
|
364
|
+
`(${anyPolicyFailed.expression})`);
|
|
365
|
+
const ready = Cel.expr(`(${platformReady.expression}) && (${gatewayConfigAccepted.expression}) && ` +
|
|
366
|
+
`(${gatewayProgrammed.expression}) && (${routeAccepted.expression}) && ` +
|
|
367
|
+
`(${allProvidersAccepted.expression}) && (${allPoliciesAccepted.expression})`);
|
|
368
|
+
const acceptedProviderCount = Cel.expr(providerResources
|
|
369
|
+
.map((resource) => acceptedExpressionFor(resource))
|
|
370
|
+
.map(({ expression }) => `(${expression} ? 1 : 0)`)
|
|
371
|
+
.join(' + '));
|
|
372
|
+
return {
|
|
373
|
+
ready,
|
|
374
|
+
failed,
|
|
375
|
+
phase: Cel.expr(`${failed.expression} ? "Failed" : (${ready.expression} ? "Ready" : "Installing")`),
|
|
376
|
+
endpoint: Cel.expr('has(gateway.status.addresses) && size(gateway.status.addresses) > 0 ? "http://" + ' +
|
|
377
|
+
'string(gateway.status.addresses[0].value) + ":" + ' +
|
|
378
|
+
'string(gateway.spec.listeners[0].port) : ""'),
|
|
379
|
+
gatewayClassName: Cel.expr('gatewayContract.data.gatewayClassName'),
|
|
380
|
+
providerCount: Cel.expr('int(gatewayContract.data.providerCount)'),
|
|
381
|
+
acceptedProviderCount,
|
|
382
|
+
routeAccepted,
|
|
383
|
+
gatewayProgrammed,
|
|
384
|
+
aiGatewayVersion: Cel.expr('gatewayContract.data.aiGatewayVersion'),
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function acceptedExpression(resourceId) {
|
|
389
|
+
return Cel.expr(`${resourceId}.status.conditions.exists(c, c.type == "Accepted" && c.status == "True" && ` +
|
|
390
|
+
`${currentConditionExpression(resourceId)})`);
|
|
391
|
+
}
|
|
392
|
+
function currentConditionExpression(resourceId) {
|
|
393
|
+
return `(has(c.observedGeneration) ? c.observedGeneration == ${resourceId}.metadata.generation : true)`;
|
|
394
|
+
}
|
|
395
|
+
function policyAcceptedExpression(resourceId) {
|
|
396
|
+
return (`has(${resourceId}.status.ancestors) && ${resourceId}.status.ancestors.exists(a, ` +
|
|
397
|
+
`has(a.controllerName) && a.controllerName == "${DEFAULT_ENVOY_GATEWAY_CONTROLLER_NAME}" && ` +
|
|
398
|
+
'has(a.conditions) && a.conditions.exists(c, ' +
|
|
399
|
+
'c.type == "Accepted" && c.status == "True" && ' +
|
|
400
|
+
`${currentConditionExpression(resourceId)}))`);
|
|
401
|
+
}
|
|
402
|
+
function policyFailedExpression(resourceId) {
|
|
403
|
+
return (`has(${resourceId}.status.ancestors) && ${resourceId}.status.ancestors.exists(a, ` +
|
|
404
|
+
`has(a.controllerName) && a.controllerName == "${DEFAULT_ENVOY_GATEWAY_CONTROLLER_NAME}" && ` +
|
|
405
|
+
'has(a.conditions) && a.conditions.exists(c, ' +
|
|
406
|
+
'((c.type == "Accepted" || c.type == "ResolvedRefs") && c.status == "False") && ' +
|
|
407
|
+
`${currentConditionExpression(resourceId)}))`);
|
|
408
|
+
}
|
|
409
|
+
function acceptedExpressionFor(resource) {
|
|
410
|
+
return acceptedExpression(resourceIdForStatus(resource));
|
|
411
|
+
}
|
|
412
|
+
function resourceIdForStatus(resource) {
|
|
413
|
+
return resource.resourceId;
|
|
414
|
+
}
|
|
415
|
+
function providerSecurityPolicy(provider, namespace, backendName, id) {
|
|
416
|
+
const targetRefs = [
|
|
417
|
+
{
|
|
418
|
+
group: 'aigateway.envoyproxy.io',
|
|
419
|
+
kind: 'AIServiceBackend',
|
|
420
|
+
name: backendName,
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
let spec;
|
|
424
|
+
if (provider.security.type === 'none')
|
|
425
|
+
return undefined;
|
|
426
|
+
if (provider.security.type === 'api-key') {
|
|
427
|
+
spec = {
|
|
428
|
+
targetRefs,
|
|
429
|
+
type: 'APIKey',
|
|
430
|
+
apiKey: {
|
|
431
|
+
secretRef: {
|
|
432
|
+
name: provider.security.secretName,
|
|
433
|
+
namespace: provider.security.secretNamespace ?? namespace,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
else if (provider.security.type === 'anthropic-api-key') {
|
|
439
|
+
spec = {
|
|
440
|
+
targetRefs,
|
|
441
|
+
type: 'AnthropicAPIKey',
|
|
442
|
+
anthropicAPIKey: {
|
|
443
|
+
secretRef: {
|
|
444
|
+
name: provider.security.secretName,
|
|
445
|
+
namespace: provider.security.secretNamespace ?? namespace,
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
spec = {
|
|
452
|
+
targetRefs,
|
|
453
|
+
type: 'AWSCredentials',
|
|
454
|
+
awsCredentials: {
|
|
455
|
+
region: provider.security.region,
|
|
456
|
+
...(provider.security.secretName
|
|
457
|
+
? {
|
|
458
|
+
credentialsFile: {
|
|
459
|
+
secretRef: {
|
|
460
|
+
name: provider.security.secretName,
|
|
461
|
+
namespace: provider.security.secretNamespace ?? namespace,
|
|
462
|
+
},
|
|
463
|
+
profile: provider.security.profile,
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
: {}),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return envoyBackendSecurityPolicy({
|
|
471
|
+
name: backendName,
|
|
472
|
+
namespace,
|
|
473
|
+
spec,
|
|
474
|
+
id,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function gatewayConfigSpec(options, profile, platformVersion, requestCosts) {
|
|
478
|
+
const environment = [
|
|
479
|
+
{
|
|
480
|
+
name: 'TYPEKRO_ENVOY_AI_GATEWAY_VERSION',
|
|
481
|
+
value: platformVersion,
|
|
482
|
+
},
|
|
483
|
+
...Object.entries(options.telemetry?.environment ?? {}).map(([name, value]) => ({
|
|
484
|
+
name,
|
|
485
|
+
value,
|
|
486
|
+
})),
|
|
487
|
+
];
|
|
488
|
+
return {
|
|
489
|
+
extProc: {
|
|
490
|
+
kubernetes: {
|
|
491
|
+
...(environment.length > 0 ? { env: environment } : {}),
|
|
492
|
+
resources: options.telemetry?.resources ??
|
|
493
|
+
(profile === 'production'
|
|
494
|
+
? {
|
|
495
|
+
requests: { cpu: '100m', memory: '128Mi' },
|
|
496
|
+
limits: { cpu: '1', memory: '512Mi' },
|
|
497
|
+
}
|
|
498
|
+
: {}),
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
globalLLMRequestCosts: requestCosts,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function platformOptionsForGateway(platform, rateLimit, profile) {
|
|
505
|
+
if (platform?.profile && platform.profile !== profile) {
|
|
506
|
+
throw new Error(`Envoy AI Gateway profile ${profile} conflicts with nested platform profile ${platform.profile}.`);
|
|
507
|
+
}
|
|
508
|
+
if (rateLimit &&
|
|
509
|
+
platform?.rateLimitRedisUrl &&
|
|
510
|
+
platform.rateLimitRedisUrl !== rateLimit.redisUrl) {
|
|
511
|
+
throw new Error('Envoy AI Gateway rateLimit.redisUrl conflicts with platform.rateLimitRedisUrl.');
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
...platform,
|
|
515
|
+
profile,
|
|
516
|
+
...(rateLimit ? { rateLimitRedisUrl: rateLimit.redisUrl } : {}),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function defaultRequestCosts() {
|
|
520
|
+
return [
|
|
521
|
+
{ metadataKey: 'llm_input_token', type: 'InputToken' },
|
|
522
|
+
{ metadataKey: 'llm_output_token', type: 'OutputToken' },
|
|
523
|
+
{ metadataKey: 'llm_total_token', type: 'TotalToken' },
|
|
524
|
+
{ metadataKey: 'llm_cached_input_token', type: 'CachedInputToken' },
|
|
525
|
+
{
|
|
526
|
+
metadataKey: 'llm_cache_creation_input_token',
|
|
527
|
+
type: 'CacheCreationInputToken',
|
|
528
|
+
},
|
|
529
|
+
{ metadataKey: 'llm_reasoning_token', type: 'ReasoningToken' },
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
function validateRequestCosts(costs) {
|
|
533
|
+
if (costs.length > 36) {
|
|
534
|
+
throw new Error('Envoy AI Gateway supports at most 36 request-cost dimensions.');
|
|
535
|
+
}
|
|
536
|
+
const metadataKeys = new Set();
|
|
537
|
+
const builtInTypes = new Set();
|
|
538
|
+
for (const cost of costs) {
|
|
539
|
+
if (cost.metadataKey.length === 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(cost.metadataKey)) {
|
|
540
|
+
throw new Error(`Envoy AI Gateway request-cost metadata key ${JSON.stringify(cost.metadataKey)} must be a non-empty identifier.`);
|
|
541
|
+
}
|
|
542
|
+
if (metadataKeys.has(cost.metadataKey)) {
|
|
543
|
+
throw new Error(`Duplicate Envoy AI Gateway request-cost metadata key ${cost.metadataKey}.`);
|
|
544
|
+
}
|
|
545
|
+
metadataKeys.add(cost.metadataKey);
|
|
546
|
+
if (cost.type !== 'CEL') {
|
|
547
|
+
if (builtInTypes.has(cost.type)) {
|
|
548
|
+
throw new Error(`Duplicate Envoy AI Gateway request-cost type ${cost.type}.`);
|
|
549
|
+
}
|
|
550
|
+
builtInTypes.add(cost.type);
|
|
551
|
+
}
|
|
552
|
+
if (cost.type === 'CEL' && cost.cel.trim().length === 0) {
|
|
553
|
+
throw new Error(`Envoy AI Gateway CEL request cost ${cost.metadataKey} requires a non-empty expression.`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return costs.map((cost) => ({ ...cost }));
|
|
557
|
+
}
|
|
558
|
+
function resolveTokenCostMetadataKeys(requestCosts) {
|
|
559
|
+
const byType = new Map(requestCosts
|
|
560
|
+
.filter((cost) => cost.type !== 'CEL')
|
|
561
|
+
.map((cost) => [cost.type, cost.metadataKey]));
|
|
562
|
+
return new Map([
|
|
563
|
+
['input-tokens', 'InputToken'],
|
|
564
|
+
['output-tokens', 'OutputToken'],
|
|
565
|
+
['total-tokens', 'TotalToken'],
|
|
566
|
+
['cached-input-tokens', 'CachedInputToken'],
|
|
567
|
+
['cache-creation-input-tokens', 'CacheCreationInputToken'],
|
|
568
|
+
['reasoning-tokens', 'ReasoningToken'],
|
|
569
|
+
].flatMap(([tokenCost, requestCostType]) => {
|
|
570
|
+
const metadataKey = byType.get(requestCostType);
|
|
571
|
+
return metadataKey ? [[tokenCost, metadataKey]] : [];
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
function validateRateLimitMetadataReferences(rateLimit, metadataKeys) {
|
|
575
|
+
for (const rule of rateLimit?.rules ?? []) {
|
|
576
|
+
if (!rule.cost || rule.cost === 'request')
|
|
577
|
+
continue;
|
|
578
|
+
if (!metadataKeys.has(rule.cost)) {
|
|
579
|
+
throw new Error(`Envoy AI Gateway rate-limit cost ${rule.cost} requires a matching requestCosts dimension.`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function requiredTokenCostMetadataKey(metadataKeys, cost) {
|
|
584
|
+
const metadataKey = metadataKeys.get(cost);
|
|
585
|
+
if (!metadataKey) {
|
|
586
|
+
throw new Error(`Envoy AI Gateway rate-limit cost ${cost} requires a matching requestCosts dimension.`);
|
|
587
|
+
}
|
|
588
|
+
return metadataKey;
|
|
589
|
+
}
|
|
590
|
+
function validateRateLimit(rateLimit) {
|
|
591
|
+
if (!rateLimit)
|
|
592
|
+
return undefined;
|
|
593
|
+
if (rateLimit.redisUrl.trim().length === 0 || rateLimit.redisUrl.includes('://')) {
|
|
594
|
+
throw new Error('Envoy AI Gateway rateLimit.redisUrl must be a non-empty host:port endpoint without a URL scheme.');
|
|
595
|
+
}
|
|
596
|
+
if (rateLimit.rules.length === 0) {
|
|
597
|
+
throw new Error('Envoy AI Gateway rate limiting requires at least one rule.');
|
|
598
|
+
}
|
|
599
|
+
if (rateLimit.rules.length > 64) {
|
|
600
|
+
throw new Error('Envoy AI Gateway supports at most 64 rate-limit rules.');
|
|
601
|
+
}
|
|
602
|
+
for (const rule of rateLimit.rules) {
|
|
603
|
+
if (!Number.isSafeInteger(rule.requests) || rule.requests <= 0) {
|
|
604
|
+
throw new Error('Envoy AI Gateway rate-limit requests must be positive safe integers.');
|
|
605
|
+
}
|
|
606
|
+
if (rule.identityHeader !== undefined && rule.identityHeader.trim().length === 0) {
|
|
607
|
+
throw new Error('Envoy AI Gateway rate-limit identityHeader must be non-empty when supplied.');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
redisUrl: rateLimit.redisUrl,
|
|
612
|
+
rules: rateLimit.rules.map((rule) => ({
|
|
613
|
+
...rule,
|
|
614
|
+
cost: rule.cost ?? 'total-tokens',
|
|
615
|
+
})),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function validateAndResolveProviders(providers) {
|
|
619
|
+
if (providers.length === 0) {
|
|
620
|
+
throw new Error('makeEnvoyAIGateway requires at least one provider.');
|
|
621
|
+
}
|
|
622
|
+
if (providers.length > 32) {
|
|
623
|
+
throw new Error('makeEnvoyAIGateway supports at most 32 providers per gateway.');
|
|
624
|
+
}
|
|
625
|
+
const names = new Set();
|
|
626
|
+
return providers.map((provider) => {
|
|
627
|
+
assertTopologyName('provider', provider.name, 20);
|
|
628
|
+
if (names.has(provider.name)) {
|
|
629
|
+
throw new Error(`Duplicate Envoy AI Gateway provider name ${provider.name}.`);
|
|
630
|
+
}
|
|
631
|
+
names.add(provider.name);
|
|
632
|
+
if (provider.kind === 'openai') {
|
|
633
|
+
const port = provider.port ?? 443;
|
|
634
|
+
assertKubernetesPort(`provider ${provider.name}`, port);
|
|
635
|
+
return {
|
|
636
|
+
name: provider.name,
|
|
637
|
+
hostname: provider.hostname ?? 'api.openai.com',
|
|
638
|
+
port,
|
|
639
|
+
tls: provider.tls ?? true,
|
|
640
|
+
schema: 'OpenAI',
|
|
641
|
+
...(provider.prefix ? { prefix: provider.prefix } : {}),
|
|
642
|
+
security: {
|
|
643
|
+
type: 'api-key',
|
|
644
|
+
secretName: provider.credential.name,
|
|
645
|
+
...(provider.credential.namespace
|
|
646
|
+
? { secretNamespace: provider.credential.namespace }
|
|
647
|
+
: {}),
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (provider.kind === 'anthropic') {
|
|
652
|
+
const port = provider.port ?? 443;
|
|
653
|
+
assertKubernetesPort(`provider ${provider.name}`, port);
|
|
654
|
+
return {
|
|
655
|
+
name: provider.name,
|
|
656
|
+
hostname: provider.hostname ?? 'api.anthropic.com',
|
|
657
|
+
port,
|
|
658
|
+
tls: provider.tls ?? true,
|
|
659
|
+
schema: 'Anthropic',
|
|
660
|
+
security: {
|
|
661
|
+
type: 'anthropic-api-key',
|
|
662
|
+
secretName: provider.credential.name,
|
|
663
|
+
...(provider.credential.namespace
|
|
664
|
+
? { secretNamespace: provider.credential.namespace }
|
|
665
|
+
: {}),
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (provider.kind === 'aws-bedrock') {
|
|
670
|
+
if (provider.region.length === 0) {
|
|
671
|
+
throw new Error(`AWS Bedrock provider ${provider.name} requires a non-empty region.`);
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
name: provider.name,
|
|
675
|
+
hostname: `bedrock-runtime.${provider.region}.amazonaws.com`,
|
|
676
|
+
port: 443,
|
|
677
|
+
tls: true,
|
|
678
|
+
schema: provider.api === 'anthropic' ? 'AWSAnthropic' : 'AWSBedrock',
|
|
679
|
+
security: provider.credential?.source === 'secret'
|
|
680
|
+
? {
|
|
681
|
+
type: 'aws',
|
|
682
|
+
region: provider.region,
|
|
683
|
+
secretName: provider.credential.secret.name,
|
|
684
|
+
...(provider.credential.secret.namespace
|
|
685
|
+
? { secretNamespace: provider.credential.secret.namespace }
|
|
686
|
+
: {}),
|
|
687
|
+
profile: provider.credential.profile ?? 'default',
|
|
688
|
+
}
|
|
689
|
+
: {
|
|
690
|
+
type: 'aws',
|
|
691
|
+
region: provider.region,
|
|
692
|
+
profile: 'default',
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const port = provider.port ?? (provider.tls === false ? 80 : 443);
|
|
697
|
+
assertKubernetesPort(`provider ${provider.name}`, port);
|
|
698
|
+
return {
|
|
699
|
+
name: provider.name,
|
|
700
|
+
hostname: provider.hostname,
|
|
701
|
+
port,
|
|
702
|
+
tls: provider.tls ?? true,
|
|
703
|
+
schema: 'OpenAI',
|
|
704
|
+
...(provider.prefix ? { prefix: provider.prefix } : {}),
|
|
705
|
+
security: provider.credential
|
|
706
|
+
? {
|
|
707
|
+
type: 'api-key',
|
|
708
|
+
secretName: provider.credential.name,
|
|
709
|
+
...(provider.credential.namespace
|
|
710
|
+
? { secretNamespace: provider.credential.namespace }
|
|
711
|
+
: {}),
|
|
712
|
+
}
|
|
713
|
+
: { type: 'none' },
|
|
714
|
+
};
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function validateModels(options, providers) {
|
|
718
|
+
if (options.models.length === 0) {
|
|
719
|
+
throw new Error('makeEnvoyAIGateway requires at least one logical model.');
|
|
720
|
+
}
|
|
721
|
+
if (options.models.length > 128) {
|
|
722
|
+
throw new Error('makeEnvoyAIGateway supports at most 128 logical models.');
|
|
723
|
+
}
|
|
724
|
+
const providerNames = new Set(providers.map(({ name }) => name));
|
|
725
|
+
const modelNames = new Set();
|
|
726
|
+
for (const model of options.models) {
|
|
727
|
+
if (model.model.length === 0) {
|
|
728
|
+
throw new Error('Envoy AI Gateway logical model names must be non-empty.');
|
|
729
|
+
}
|
|
730
|
+
if (modelNames.has(model.model)) {
|
|
731
|
+
throw new Error(`Duplicate Envoy AI Gateway logical model ${model.model}.`);
|
|
732
|
+
}
|
|
733
|
+
modelNames.add(model.model);
|
|
734
|
+
if (model.targets.length === 0) {
|
|
735
|
+
throw new Error(`Envoy AI Gateway model ${model.model} requires at least one target.`);
|
|
736
|
+
}
|
|
737
|
+
for (const target of model.targets) {
|
|
738
|
+
if (!providerNames.has(target.provider)) {
|
|
739
|
+
throw new Error(`Envoy AI Gateway model ${model.model} references unknown provider ${target.provider}.`);
|
|
740
|
+
}
|
|
741
|
+
if (target.weight !== undefined &&
|
|
742
|
+
(!Number.isSafeInteger(target.weight) || target.weight < 0)) {
|
|
743
|
+
throw new Error(`Envoy AI Gateway model ${model.model} target weights must be non-negative integers.`);
|
|
744
|
+
}
|
|
745
|
+
if (target.priority !== undefined &&
|
|
746
|
+
(!Number.isSafeInteger(target.priority) || target.priority < 0)) {
|
|
747
|
+
throw new Error(`Envoy AI Gateway model ${model.model} target priorities must be non-negative integers.`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function assertTopologyName(label, value, maximumLength) {
|
|
753
|
+
if (value.length === 0 ||
|
|
754
|
+
value.length > maximumLength ||
|
|
755
|
+
!/^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/u.test(value)) {
|
|
756
|
+
throw new Error(`Envoy AI Gateway ${label} name ${JSON.stringify(value)} must be a DNS-1123 label no longer than ${maximumLength} characters.`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function assertKubernetesPort(label, value) {
|
|
760
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 65_535) {
|
|
761
|
+
throw new Error(`Envoy AI Gateway ${label} port ${String(value)} must be an integer from 1 through 65535.`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function providerResourceId(name) {
|
|
765
|
+
const readable = name
|
|
766
|
+
.split('-')
|
|
767
|
+
.filter((part) => part.length > 0)
|
|
768
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
769
|
+
.join('');
|
|
770
|
+
const digest = createHash('sha256').update(name, 'utf8').digest('hex').slice(0, 10);
|
|
771
|
+
return `provider${readable}${digest}`;
|
|
772
|
+
}
|
|
773
|
+
//# sourceMappingURL=gateway.js.map
|