@kindgi/capabilities 0.0.0-bootstrap.0 → 0.1.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/LICENSE +201 -0
- package/README.md +112 -2
- package/dist/adapter-factory.d.ts +121 -0
- package/dist/adapter-factory.d.ts.map +1 -0
- package/dist/adapter-factory.js +36 -0
- package/dist/adapter-factory.js.map +1 -0
- package/dist/capability.schema.json +250 -0
- package/dist/define.d.ts +13 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +45 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +156 -0
- package/dist/registry.js.map +1 -0
- package/dist/router.d.ts +73 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +484 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +498 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -4
- package/src/adapter-factory.ts +155 -0
- package/src/capability.schema.json +250 -0
- package/src/define.ts +54 -0
- package/src/errors.ts +74 -0
- package/src/index.ts +51 -0
- package/src/registry.ts +179 -0
- package/src/router.ts +560 -0
- package/src/types.ts +533 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { Result, TenantId } from '@kindgi/types';
|
|
5
|
+
|
|
6
|
+
import type { CapabilityError, DuplicateProviderError, InvalidProviderError } from './errors.js';
|
|
7
|
+
import type { ModelProvider, ProviderRegistry } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an in-memory registry of model providers. Tenant-scoped:
|
|
11
|
+
* each tenant owns its own `providerId → ModelProvider` map. The
|
|
12
|
+
* router receives a tenant's slice, so cross-tenant leakage is
|
|
13
|
+
* impossible at the routing layer.
|
|
14
|
+
*
|
|
15
|
+
* Registration is validated for shape: provider `id` + `region`
|
|
16
|
+
* non-empty; `models[]` non-empty with unique `name` per entry;
|
|
17
|
+
* per-model `contextWindow` positive integer; per-model `cost`
|
|
18
|
+
* non-negative; optional per-model `p95LatencyMs` / `maxOutputTokens`
|
|
19
|
+
* well-shaped; `invoke` is a function. Duplicate ids within a tenant
|
|
20
|
+
* are refused — accidental clobber of a running provider would be
|
|
21
|
+
* catastrophic under a live workload. The same `providerId` may be
|
|
22
|
+
* registered under different tenants (that's the whole point).
|
|
23
|
+
*/
|
|
24
|
+
export function createProviderRegistry(
|
|
25
|
+
seed: readonly { readonly tenantId: TenantId; readonly provider: ModelProvider }[] = [],
|
|
26
|
+
): {
|
|
27
|
+
readonly registry: ProviderRegistry;
|
|
28
|
+
register(tenantId: TenantId, provider: ModelProvider): Result<void, CapabilityError>;
|
|
29
|
+
} {
|
|
30
|
+
const byTenant = new Map<TenantId, Map<string, ModelProvider>>();
|
|
31
|
+
|
|
32
|
+
function tenantMap(tenantId: TenantId): Map<string, ModelProvider> {
|
|
33
|
+
let m = byTenant.get(tenantId);
|
|
34
|
+
if (m === undefined) {
|
|
35
|
+
m = new Map<string, ModelProvider>();
|
|
36
|
+
byTenant.set(tenantId, m);
|
|
37
|
+
}
|
|
38
|
+
return m;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validate(p: ModelProvider): InvalidProviderError | undefined {
|
|
42
|
+
if (typeof p.metadata.id !== 'string' || p.metadata.id.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
code: 'invalid-provider',
|
|
45
|
+
message: 'provider.metadata.id required',
|
|
46
|
+
reason: 'empty-id',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (typeof p.metadata.region !== 'string' || p.metadata.region.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
code: 'invalid-provider',
|
|
52
|
+
message: `provider "${p.metadata.id}" missing metadata.region`,
|
|
53
|
+
reason: 'empty-region',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(p.metadata.models) || p.metadata.models.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
code: 'invalid-provider',
|
|
59
|
+
message: `provider "${p.metadata.id}" must expose at least one model in metadata.models[]`,
|
|
60
|
+
reason: 'empty-models',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const seenNames = new Set<string>();
|
|
64
|
+
for (const m of p.metadata.models) {
|
|
65
|
+
if (typeof m.name !== 'string' || m.name.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
code: 'invalid-provider',
|
|
68
|
+
message: `provider "${p.metadata.id}" has a model with empty name`,
|
|
69
|
+
reason: 'empty-model-name',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (seenNames.has(m.name)) {
|
|
73
|
+
return {
|
|
74
|
+
code: 'invalid-provider',
|
|
75
|
+
message: `provider "${p.metadata.id}" declares model "${m.name}" more than once in metadata.models[]`,
|
|
76
|
+
reason: 'duplicate-model-name',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
seenNames.add(m.name);
|
|
80
|
+
if (
|
|
81
|
+
typeof m.contextWindow !== 'number' ||
|
|
82
|
+
m.contextWindow <= 0 ||
|
|
83
|
+
!Number.isInteger(m.contextWindow)
|
|
84
|
+
) {
|
|
85
|
+
return {
|
|
86
|
+
code: 'invalid-provider',
|
|
87
|
+
message: `provider "${p.metadata.id}" model "${m.name}" contextWindow must be a positive integer`,
|
|
88
|
+
reason: 'invalid-context-window',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
typeof m.cost?.promptUsdPer1kTokens !== 'number' ||
|
|
93
|
+
m.cost.promptUsdPer1kTokens < 0 ||
|
|
94
|
+
typeof m.cost.completionUsdPer1kTokens !== 'number' ||
|
|
95
|
+
m.cost.completionUsdPer1kTokens < 0
|
|
96
|
+
) {
|
|
97
|
+
return {
|
|
98
|
+
code: 'invalid-provider',
|
|
99
|
+
message: `provider "${p.metadata.id}" model "${m.name}" cost table must have non-negative promptUsdPer1kTokens + completionUsdPer1kTokens`,
|
|
100
|
+
reason: 'invalid-cost',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
m.p95LatencyMs !== undefined &&
|
|
105
|
+
(typeof m.p95LatencyMs !== 'number' || m.p95LatencyMs < 0)
|
|
106
|
+
) {
|
|
107
|
+
return {
|
|
108
|
+
code: 'invalid-provider',
|
|
109
|
+
message: `provider "${p.metadata.id}" model "${m.name}" p95LatencyMs must be a non-negative number`,
|
|
110
|
+
reason: 'invalid-p95-latency',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
m.maxOutputTokens !== undefined &&
|
|
115
|
+
(typeof m.maxOutputTokens !== 'number' ||
|
|
116
|
+
m.maxOutputTokens <= 0 ||
|
|
117
|
+
!Number.isInteger(m.maxOutputTokens))
|
|
118
|
+
) {
|
|
119
|
+
return {
|
|
120
|
+
code: 'invalid-provider',
|
|
121
|
+
message: `provider "${p.metadata.id}" model "${m.name}" maxOutputTokens must be a positive integer`,
|
|
122
|
+
reason: 'invalid-max-output-tokens',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (typeof p.invoke !== 'function') {
|
|
127
|
+
return {
|
|
128
|
+
code: 'invalid-provider',
|
|
129
|
+
message: `provider "${p.metadata.id}" invoke must be a function`,
|
|
130
|
+
reason: 'invalid-invoke',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function register(tenantId: TenantId, provider: ModelProvider): Result<void, CapabilityError> {
|
|
137
|
+
const invalid = validate(provider);
|
|
138
|
+
if (invalid !== undefined) return { kind: 'err', error: invalid };
|
|
139
|
+
const m = tenantMap(tenantId);
|
|
140
|
+
if (m.has(provider.metadata.id)) {
|
|
141
|
+
const dup: DuplicateProviderError = {
|
|
142
|
+
code: 'duplicate-provider',
|
|
143
|
+
message: `Provider "${provider.metadata.id}" is already registered for tenant "${tenantId}"`,
|
|
144
|
+
providerId: provider.metadata.id,
|
|
145
|
+
};
|
|
146
|
+
return { kind: 'err', error: dup };
|
|
147
|
+
}
|
|
148
|
+
m.set(provider.metadata.id, provider);
|
|
149
|
+
return { kind: 'ok', value: undefined };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const registry: ProviderRegistry = {
|
|
153
|
+
register(tenantId, provider): void {
|
|
154
|
+
const r = register(tenantId, provider);
|
|
155
|
+
if (r.kind === 'err') throw new Error(r.error.message);
|
|
156
|
+
},
|
|
157
|
+
get(tenantId, id): ModelProvider | undefined {
|
|
158
|
+
return byTenant.get(tenantId)?.get(id);
|
|
159
|
+
},
|
|
160
|
+
list(tenantId): readonly ModelProvider[] {
|
|
161
|
+
const m = byTenant.get(tenantId);
|
|
162
|
+
if (m === undefined) return [];
|
|
163
|
+
return [...m.values()];
|
|
164
|
+
},
|
|
165
|
+
has(tenantId, id): boolean {
|
|
166
|
+
return byTenant.get(tenantId)?.has(id) ?? false;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const entry of seed) {
|
|
171
|
+
const r = register(entry.tenantId, entry.provider);
|
|
172
|
+
if (r.kind === 'err')
|
|
173
|
+
throw new Error(
|
|
174
|
+
`seed provider "${entry.provider.metadata.id}" rejected for tenant "${entry.tenantId}": ${r.error.message}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { registry, register };
|
|
179
|
+
}
|