ag-ai 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.
Files changed (55) hide show
  1. package/README.md +31 -0
  2. package/dist/client/adapters/cluster.d.ts +12 -0
  3. package/dist/client/adapters/cluster.js +567 -0
  4. package/dist/client/adapters/google.d.ts +12 -0
  5. package/dist/client/adapters/google.js +478 -0
  6. package/dist/client/client.d.ts +11 -0
  7. package/dist/client/client.js +192 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +18 -0
  10. package/dist/client/quota.d.ts +55 -0
  11. package/dist/client/quota.js +148 -0
  12. package/dist/client/types.d.ts +124 -0
  13. package/dist/client/types.js +2 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +18 -0
  16. package/dist/internal/apikey.d.ts +5 -0
  17. package/dist/internal/apikey.js +51 -0
  18. package/dist/internal/array.d.ts +2 -0
  19. package/dist/internal/array.js +6 -0
  20. package/dist/internal/log.d.ts +9 -0
  21. package/dist/internal/log.js +92 -0
  22. package/dist/internal/node-cache.d.ts +71 -0
  23. package/dist/internal/node-cache.js +104 -0
  24. package/dist/internal/retryOnError.d.ts +26 -0
  25. package/dist/internal/retryOnError.js +123 -0
  26. package/dist/internal/string/redact.d.ts +2 -0
  27. package/dist/internal/string/redact.js +94 -0
  28. package/dist/internal/string/truncate.d.ts +8 -0
  29. package/dist/internal/string/truncate.js +16 -0
  30. package/dist/k8s/assets/network-init.sh +52 -0
  31. package/dist/k8s/common.d.ts +35 -0
  32. package/dist/k8s/common.js +56 -0
  33. package/dist/k8s/index.d.ts +9 -0
  34. package/dist/k8s/index.js +33 -0
  35. package/dist/k8s/opencode.d.ts +4 -0
  36. package/dist/k8s/opencode.js +55 -0
  37. package/dist/k8s/runner.d.ts +4 -0
  38. package/dist/k8s/runner.js +247 -0
  39. package/dist/k8s/t3code.d.ts +4 -0
  40. package/dist/k8s/t3code.js +60 -0
  41. package/package.json +56 -0
  42. package/runtime/bridge.mjs +1093 -0
  43. package/runtime/executor/opencode.mjs +430 -0
  44. package/runtime/image/adapter.mjs +213 -0
  45. package/runtime/index.mjs +53 -0
  46. package/runtime/protocol.mjs +384 -0
  47. package/runtime/server.mjs +621 -0
  48. package/runtime/store.mjs +568 -0
  49. package/runtime/telemetry.mjs +137 -0
  50. package/runtime/tests/bridge.test.mjs +554 -0
  51. package/runtime/tests/image-adapter.test.mjs +148 -0
  52. package/runtime/tests/protocol.test.mjs +156 -0
  53. package/runtime/tests/server.test.mjs +231 -0
  54. package/runtime/tests/store.test.mjs +114 -0
  55. package/runtime/tests/telemetry.test.mjs +138 -0
@@ -0,0 +1,384 @@
1
+ // ag-ai generation protocol v1. The JSON request/result shape is unchanged from
2
+ // the previous runtime; only provider-neutral naming and capability handling
3
+ // differ.
4
+
5
+ import { createHash } from 'node:crypto';
6
+
7
+ const GENERATION_VERSION = 1;
8
+ const MAX_TEXT_BYTES = 2 * 1024 * 1024;
9
+ const MAX_INPUT_MESSAGES = 256;
10
+ const MAX_CONTENT_PARTS = 128;
11
+ const MAX_INPUT_ATTACHMENTS = 32;
12
+ const MAX_INPUT_ATTACHMENT_BYTES = 20 * 1024 * 1024;
13
+ const MAX_INPUT_TOTAL_BYTES = 40 * 1024 * 1024;
14
+ const MAX_SCHEMA_BYTES = 256 * 1024;
15
+ const MAX_NAME_LENGTH = 200;
16
+ const MAX_IDEMPOTENCY_KEY_LENGTH = 512;
17
+ const INPUT_ROLES = new Set(['user', 'assistant', 'system', 'developer']);
18
+ const INPUT_TYPES = new Set(['text', 'image', 'audio', 'video', 'file']);
19
+ const OUTPUT_TYPES = new Set(['text', 'image', 'audio', 'video', 'file']);
20
+ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,199}$/;
21
+ const MIME_TYPE =
22
+ /^[A-Za-z0-9][A-Za-z0-9!#$%&'*+.^_`|~-]*\/[A-Za-z0-9][A-Za-z0-9!#$%&'*+.^_`|~-]*(?:;[A-Za-z0-9][A-Za-z0-9!#$%&'*+.^_`|~=-]*)*$/;
23
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
24
+
25
+ class GenerationProtocolError extends Error {
26
+ constructor(status, code, message) {
27
+ super(message);
28
+ this.name = 'GenerationProtocolError';
29
+ this.status = status;
30
+ this.code = code;
31
+ }
32
+ }
33
+
34
+ const asObject = (value) => (value && typeof value === 'object' ? value : null);
35
+
36
+ const boundedString = (value, maximum, name, { allowEmpty = false } = {}) => {
37
+ if (typeof value !== 'string' || value.length > maximum || (!allowEmpty && value.length === 0)) {
38
+ throw new GenerationProtocolError(400, 'invalid_request', `${name} is invalid`);
39
+ }
40
+ return value;
41
+ };
42
+
43
+ const normalizeMimeType = (value, name) => {
44
+ const mimeType = boundedString(value, 160, name).toLowerCase();
45
+ if (!MIME_TYPE.test(mimeType) || mimeType.includes('\r') || mimeType.includes('\n')) {
46
+ throw new GenerationProtocolError(400, 'invalid_request', `${name} is invalid`);
47
+ }
48
+ return mimeType;
49
+ };
50
+
51
+ const decodeBase64 = (value, name) => {
52
+ const data = boundedString(value, Math.ceil((MAX_INPUT_ATTACHMENT_BYTES * 4) / 3) + 8, name);
53
+ if (!BASE64.test(data) || data.length % 4 !== 0) {
54
+ throw new GenerationProtocolError(400, 'invalid_request', `${name} is invalid`);
55
+ }
56
+ const bytes = Buffer.from(data, 'base64');
57
+ if (bytes.length === 0 || bytes.length > MAX_INPUT_ATTACHMENT_BYTES) {
58
+ throw new GenerationProtocolError(413, 'input_too_large', `${name} exceeds its size limit`);
59
+ }
60
+ if (bytes.toString('base64') !== data) {
61
+ throw new GenerationProtocolError(400, 'invalid_request', `${name} is invalid`);
62
+ }
63
+ return { data, bytes };
64
+ };
65
+
66
+ const normalizeCapabilities = (value) => {
67
+ const model = asObject(value);
68
+ const id = typeof model?.id === 'string' ? model.id : model?.model;
69
+ const inputModalities = Array.isArray(model?.inputModalities)
70
+ ? model.inputModalities.filter((item) => INPUT_TYPES.has(item))
71
+ : undefined;
72
+ const outputModalities = Array.isArray(model?.outputModalities)
73
+ ? model.outputModalities.filter((item) => OUTPUT_TYPES.has(item))
74
+ : undefined;
75
+ const supportedReasoningEfforts = Array.isArray(model?.supportedReasoningEfforts)
76
+ ? model.supportedReasoningEfforts
77
+ .map((item) => (typeof item === 'string' ? item : asObject(item)?.reasoningEffort))
78
+ .filter((item) => typeof item === 'string')
79
+ : [];
80
+ const defaultReasoningEffort =
81
+ typeof model?.defaultReasoningEffort === 'string'
82
+ ? model.defaultReasoningEffort
83
+ : supportedReasoningEfforts[0];
84
+ // Image output is delegated to the configured image adapter, so the catalog
85
+ // advertises it only when a provider explicitly marks the model capable.
86
+ const imageGeneration = model?.imageGeneration === true;
87
+ const declaredOutputModalities =
88
+ outputModalities === undefined ? undefined : [...new Set(outputModalities)];
89
+ const normalizedOutputModalities =
90
+ declaredOutputModalities === undefined
91
+ ? ['text']
92
+ : declaredOutputModalities.filter((item) => item !== 'image' || imageGeneration);
93
+ if (imageGeneration && declaredOutputModalities === undefined) {
94
+ normalizedOutputModalities.push('image');
95
+ }
96
+ return {
97
+ id: id ?? '',
98
+ inputModalities:
99
+ inputModalities === undefined ? ['text', 'image'] : [...new Set(inputModalities)],
100
+ outputModalities: normalizedOutputModalities,
101
+ imageGeneration,
102
+ supportedReasoningEfforts: [...new Set(supportedReasoningEfforts)],
103
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
104
+ webSearch: model?.webSearch === true,
105
+ prefer: Array.isArray(model?.prefer)
106
+ ? model.prefer.filter((item) => item === 'fast' || item === 'quality')
107
+ : ['quality'],
108
+ isDefault: model?.isDefault === true,
109
+ };
110
+ };
111
+
112
+ const normalizeModelCatalog = (models) => {
113
+ const source = Array.isArray(models) ? models : [];
114
+ return source.map(normalizeCapabilities).filter((model) => model.id.length > 0);
115
+ };
116
+
117
+ const unsupported = (what) =>
118
+ new GenerationProtocolError(422, 'unsupported', `${what} is not supported by the selected model`);
119
+
120
+ const normalizePart = (part, capabilities, total) => {
121
+ const object = asObject(part);
122
+ const type = object?.type;
123
+ if (!INPUT_TYPES.has(type)) {
124
+ throw new GenerationProtocolError(400, 'invalid_request', 'input content part type is invalid');
125
+ }
126
+ if (type === 'text') {
127
+ return {
128
+ type,
129
+ text: boundedString(object.text, MAX_TEXT_BYTES, 'input text', { allowEmpty: true }),
130
+ };
131
+ }
132
+
133
+ if (type !== 'image') {
134
+ throw unsupported(`input ${type}`);
135
+ }
136
+
137
+ if (!capabilities.inputModalities.includes(type)) {
138
+ throw unsupported(`input ${type}`);
139
+ }
140
+ const mimeType = normalizeMimeType(object.mimeType, 'input mimeType');
141
+ if (type === 'image' && !['image/png', 'image/jpeg', 'image/webp'].includes(mimeType)) {
142
+ throw unsupported(`input ${mimeType}`);
143
+ }
144
+ const decoded = decodeBase64(object.data, 'input data');
145
+ total.bytes += decoded.bytes.length;
146
+ total.attachments += 1;
147
+ if (total.bytes > MAX_INPUT_TOTAL_BYTES) {
148
+ throw new GenerationProtocolError(
149
+ 413,
150
+ 'input_too_large',
151
+ 'input attachments exceed their size limit',
152
+ );
153
+ }
154
+ if (total.attachments > MAX_INPUT_ATTACHMENTS) {
155
+ throw new GenerationProtocolError(413, 'input_too_large', 'too many input attachments');
156
+ }
157
+ const result = { type, mimeType, data: decoded.data };
158
+ if (object.name !== undefined) {
159
+ const name = boundedString(object.name, MAX_NAME_LENGTH, 'input name');
160
+ if (!SAFE_NAME.test(name)) {
161
+ throw new GenerationProtocolError(400, 'invalid_request', 'input name is invalid');
162
+ }
163
+ result.name = name;
164
+ }
165
+ return result;
166
+ };
167
+
168
+ const normalizeMessage = (message, capabilities, total) => {
169
+ const object = asObject(message);
170
+ if (!INPUT_ROLES.has(object?.role)) {
171
+ throw new GenerationProtocolError(400, 'invalid_request', 'input message role is invalid');
172
+ }
173
+ if (!Array.isArray(object.content) || object.content.length > MAX_CONTENT_PARTS) {
174
+ throw new GenerationProtocolError(400, 'invalid_request', 'input message content is invalid');
175
+ }
176
+ return {
177
+ role: object.role,
178
+ content: object.content.map((part) => normalizePart(part, capabilities, total)),
179
+ };
180
+ };
181
+
182
+ const normalizeOutput = (output, capabilities) => {
183
+ if (!Array.isArray(output) || output.length === 0 || output.length > 5) {
184
+ throw new GenerationProtocolError(400, 'invalid_request', 'output is invalid');
185
+ }
186
+ const result = [];
187
+ for (const type of output) {
188
+ if (!OUTPUT_TYPES.has(type) || result.includes(type)) {
189
+ throw new GenerationProtocolError(400, 'invalid_request', 'output is invalid');
190
+ }
191
+ if (type !== 'text' && type !== 'image') {
192
+ throw unsupported(`output ${type}`);
193
+ }
194
+ if (!capabilities.outputModalities.includes(type)) {
195
+ throw unsupported(`output ${type}`);
196
+ }
197
+ result.push(type);
198
+ }
199
+ return result;
200
+ };
201
+
202
+ const normalizeOutputSchema = (value) => {
203
+ if (value === undefined) return undefined;
204
+ const object = asObject(value);
205
+ if (!object || Array.isArray(object.schema) || asObject(object.schema) === null) {
206
+ throw new GenerationProtocolError(400, 'invalid_request', 'outputSchema is invalid');
207
+ }
208
+ const name = boundedString(object.name, 128, 'outputSchema name');
209
+ const serialized = JSON.stringify(object.schema);
210
+ if (Buffer.byteLength(serialized) > MAX_SCHEMA_BYTES) {
211
+ throw new GenerationProtocolError(
212
+ 413,
213
+ 'input_too_large',
214
+ 'outputSchema exceeds its size limit',
215
+ );
216
+ }
217
+ return { name, schema: object.schema };
218
+ };
219
+
220
+ const toRunnerInput = (request, runId, models) => {
221
+ const normalized =
222
+ request?.model && request?.reasoningEffort && request?.version === GENERATION_VERSION
223
+ ? request
224
+ : normalizeGenerationRequest(request, models);
225
+ const input = normalized.input.map((message) => ({
226
+ role: message.role,
227
+ content: message.content.map((part) => {
228
+ if (part.type === 'text') return { type: 'text', text: part.text };
229
+ if (part.type === 'image') return { type: 'image', mimeType: part.mimeType, data: part.data };
230
+ throw unsupported(`input ${part.type}`);
231
+ }),
232
+ }));
233
+ return {
234
+ runId,
235
+ instructions: normalized.instructions,
236
+ messages: input,
237
+ output: normalized.output,
238
+ imageGeneration: normalized.output.includes('image'),
239
+ model: normalized.model,
240
+ reasoningEffort: normalized.reasoningEffort,
241
+ ...(normalized.outputSchema === undefined ? {} : { outputSchema: normalized.outputSchema }),
242
+ };
243
+ };
244
+
245
+ const normalizeGenerationRequest = (value, models) => {
246
+ const request = asObject(value);
247
+ if (request?.version !== GENERATION_VERSION) {
248
+ throw new GenerationProtocolError(400, 'invalid_request', 'version must be 1');
249
+ }
250
+ const allowedKeys = new Set([
251
+ 'version',
252
+ 'instructions',
253
+ 'input',
254
+ 'output',
255
+ 'model',
256
+ 'prefer',
257
+ 'reasoningEffort',
258
+ 'maxOutputTokens',
259
+ 'outputSchema',
260
+ 'webSearch',
261
+ ]);
262
+ for (const key of Object.keys(request)) {
263
+ if (!allowedKeys.has(key)) {
264
+ throw new GenerationProtocolError(400, 'invalid_request', `unknown request field ${key}`);
265
+ }
266
+ }
267
+ const catalog = normalizeModelCatalog(models);
268
+ if (catalog.length === 0) {
269
+ throw new GenerationProtocolError(503, 'model_unavailable', 'no AI model is ready');
270
+ }
271
+ const prefer = request.prefer === undefined ? undefined : request.prefer;
272
+ if (prefer !== undefined && prefer !== 'fast' && prefer !== 'quality') {
273
+ throw new GenerationProtocolError(400, 'invalid_request', 'prefer is invalid');
274
+ }
275
+ let requestedModel;
276
+ if (request.model !== undefined) {
277
+ requestedModel = boundedString(request.model, 160, 'model');
278
+ } else if (prefer === 'fast') {
279
+ requestedModel = catalog.find((candidate) => candidate.prefer.includes('fast'))?.id;
280
+ requestedModel ??= catalog.find((candidate) => /(?:mini|luna)$/i.test(candidate.id))?.id;
281
+ if (!requestedModel) throw unsupported('fast model preference');
282
+ } else {
283
+ requestedModel =
284
+ catalog.find((candidate) => candidate.isDefault && candidate.prefer.includes('quality'))
285
+ ?.id ??
286
+ catalog.find((candidate) => candidate.isDefault)?.id ??
287
+ catalog.find((candidate) => candidate.prefer.includes('quality'))?.id ??
288
+ catalog[0].id;
289
+ }
290
+ const model = catalog.find((candidate) => candidate.id === requestedModel);
291
+ if (!model) throw unsupported(`model ${requestedModel}`);
292
+ const capabilities = normalizeCapabilities(model);
293
+ const instructions =
294
+ request.instructions === undefined
295
+ ? ''
296
+ : boundedString(request.instructions, MAX_TEXT_BYTES, 'instructions', { allowEmpty: true });
297
+ const messages = request.input;
298
+ if (!Array.isArray(messages) || messages.length > MAX_INPUT_MESSAGES) {
299
+ throw new GenerationProtocolError(400, 'invalid_request', 'input is invalid');
300
+ }
301
+ const total = { bytes: 0, attachments: 0 };
302
+ const input = messages.map((message) => normalizeMessage(message, capabilities, total));
303
+ const output = normalizeOutput(request.output, capabilities);
304
+ const reasoningEffort =
305
+ request.reasoningEffort === undefined
306
+ ? (capabilities.defaultReasoningEffort ?? capabilities.supportedReasoningEfforts[0])
307
+ : boundedString(request.reasoningEffort, 32, 'reasoningEffort');
308
+ if (!reasoningEffort) {
309
+ throw new GenerationProtocolError(
310
+ 503,
311
+ 'model_unavailable',
312
+ 'selected model has no reasoning effort',
313
+ );
314
+ }
315
+ if (!capabilities.supportedReasoningEfforts.includes(reasoningEffort)) {
316
+ throw unsupported(`reasoning effort ${reasoningEffort}`);
317
+ }
318
+ if (request.maxOutputTokens !== undefined && !Number.isInteger(request.maxOutputTokens)) {
319
+ throw new GenerationProtocolError(400, 'invalid_request', 'maxOutputTokens is invalid');
320
+ }
321
+ if (request.maxOutputTokens !== undefined) {
322
+ throw unsupported('maxOutputTokens');
323
+ }
324
+ if (request.webSearch === true && !capabilities.webSearch) throw unsupported('web search');
325
+ if (request.webSearch !== undefined && typeof request.webSearch !== 'boolean') {
326
+ throw new GenerationProtocolError(400, 'invalid_request', 'webSearch is invalid');
327
+ }
328
+ const outputSchema = normalizeOutputSchema(request.outputSchema);
329
+ return {
330
+ version: GENERATION_VERSION,
331
+ instructions,
332
+ input,
333
+ output,
334
+ model: requestedModel,
335
+ ...(prefer === undefined ? {} : { prefer }),
336
+ reasoningEffort,
337
+ ...(outputSchema === undefined ? {} : { outputSchema }),
338
+ ...(request.webSearch === undefined ? {} : { webSearch: request.webSearch }),
339
+ };
340
+ };
341
+
342
+ const requestDigest = (request) =>
343
+ createHash('sha256').update(JSON.stringify(request)).digest('hex');
344
+ const idempotencyDigest = (key) => createHash('sha256').update(key).digest('hex');
345
+
346
+ const normalizeIdempotencyKey = (value) => {
347
+ const key = boundedString(value, MAX_IDEMPOTENCY_KEY_LENGTH, 'idempotency key');
348
+ // eslint-disable-next-line no-control-regex -- Protocol keys reject ASCII control bytes.
349
+ if (/^[\x00-\x1f\x7f]|[\x00-\x1f\x7f]$/.test(key)) {
350
+ throw new GenerationProtocolError(400, 'invalid_request', 'idempotency key is invalid');
351
+ }
352
+ return key;
353
+ };
354
+
355
+ const publicModelCatalog = (models, { ready = true } = {}) => ({
356
+ version: GENERATION_VERSION,
357
+ models: normalizeModelCatalog(models).map((model) => ({
358
+ id: model.id,
359
+ inputModalities: model.inputModalities,
360
+ outputModalities: model.outputModalities,
361
+ supportedReasoningEfforts: model.supportedReasoningEfforts,
362
+ ...(model.defaultReasoningEffort
363
+ ? { defaultReasoningEffort: model.defaultReasoningEffort }
364
+ : {}),
365
+ webSearch: model.webSearch,
366
+ prefer: model.prefer,
367
+ isDefault: model.isDefault,
368
+ ready,
369
+ })),
370
+ });
371
+
372
+ export {
373
+ GENERATION_VERSION,
374
+ MAX_INPUT_ATTACHMENT_BYTES,
375
+ MAX_INPUT_TOTAL_BYTES,
376
+ GenerationProtocolError,
377
+ idempotencyDigest,
378
+ normalizeGenerationRequest,
379
+ normalizeIdempotencyKey,
380
+ normalizeModelCatalog,
381
+ publicModelCatalog,
382
+ requestDigest,
383
+ toRunnerInput,
384
+ };