nexus-ai-pro 2.18.0 → 2.20.2
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/API_STABILITY.md +64 -1
- package/CHANGELOG.md +113 -1
- package/README.md +11 -3
- package/dist/agent/capabilities.d.ts +3 -0
- package/dist/agent/openapi.d.ts +139 -0
- package/dist/agent/openapi.js +455 -0
- package/dist/capabilities/negotiate.js +5 -0
- package/dist/decisions/completion.d.ts +48 -0
- package/dist/decisions/completion.js +195 -0
- package/dist/decisions/core.d.ts +525 -0
- package/dist/decisions/core.js +493 -0
- package/dist/decisions/index.d.ts +9 -0
- package/dist/decisions/index.js +3 -0
- package/dist/decisions/integrations.d.ts +142 -0
- package/dist/decisions/integrations.js +170 -0
- package/dist/decisions/mock.d.ts +32 -0
- package/dist/decisions/mock.js +47 -0
- package/dist/decisions/typesafe.d.ts +45 -0
- package/dist/decisions/typesafe.js +144 -0
- package/dist/evaluate/emulate.d.ts +73 -6
- package/dist/evaluate/emulate.js +114 -12
- package/dist/index.d.ts +1 -1
- package/dist/optimizer/densifier.js +20 -11
- package/dist/protocols/a2a-grpc.js +1 -1
- package/dist/protocols/a2a-proto.d.ts +2 -2
- package/dist/protocols/a2a-proto.js +76 -1
- package/dist/protocols/a2a.d.ts +121 -0
- package/dist/protocols/a2a.js +88 -29
- package/dist/providers/google.js +28 -0
- package/dist/providers/ollama.js +23 -0
- package/dist/providers/openai.d.ts +6 -0
- package/dist/providers/openai.js +26 -0
- package/dist/types/lifecycle.d.ts +1 -1
- package/dist/types/messages.d.ts +9 -0
- package/dist/types/providers.d.ts +2 -0
- package/dist/types/response.d.ts +17 -0
- package/dist/types/tracing.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow/chains.d.ts +13 -1
- package/dist/workflow/chains.js +40 -0
- package/dist-cjs/agent/capabilities.d.ts +3 -0
- package/dist-cjs/agent/openapi.d.ts +139 -0
- package/dist-cjs/agent/openapi.js +461 -0
- package/dist-cjs/capabilities/negotiate.js +5 -0
- package/dist-cjs/decisions/completion.d.ts +48 -0
- package/dist-cjs/decisions/completion.js +198 -0
- package/dist-cjs/decisions/core.d.ts +525 -0
- package/dist-cjs/decisions/core.js +509 -0
- package/dist-cjs/decisions/index.d.ts +9 -0
- package/dist-cjs/decisions/index.js +27 -0
- package/dist-cjs/decisions/integrations.d.ts +142 -0
- package/dist-cjs/decisions/integrations.js +178 -0
- package/dist-cjs/decisions/mock.d.ts +32 -0
- package/dist-cjs/decisions/mock.js +51 -0
- package/dist-cjs/decisions/typesafe.d.ts +45 -0
- package/dist-cjs/decisions/typesafe.js +148 -0
- package/dist-cjs/evaluate/emulate.d.ts +73 -6
- package/dist-cjs/evaluate/emulate.js +116 -13
- package/dist-cjs/index.d.ts +1 -1
- package/dist-cjs/optimizer/densifier.js +20 -11
- package/dist-cjs/protocols/a2a-grpc.js +1 -1
- package/dist-cjs/protocols/a2a-proto.d.ts +2 -2
- package/dist-cjs/protocols/a2a-proto.js +76 -1
- package/dist-cjs/protocols/a2a.d.ts +121 -0
- package/dist-cjs/protocols/a2a.js +88 -29
- package/dist-cjs/providers/google.js +28 -0
- package/dist-cjs/providers/ollama.js +23 -0
- package/dist-cjs/providers/openai.d.ts +6 -0
- package/dist-cjs/providers/openai.js +26 -0
- package/dist-cjs/types/lifecycle.d.ts +1 -1
- package/dist-cjs/types/messages.d.ts +9 -0
- package/dist-cjs/types/providers.d.ts +2 -0
- package/dist-cjs/types/response.d.ts +17 -0
- package/dist-cjs/types/tracing.d.ts +1 -1
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/dist-cjs/workflow/chains.d.ts +13 -1
- package/dist-cjs/workflow/chains.js +40 -0
- package/llms.txt +2 -1
- package/package.json +54 -2
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { linkSignals } from '../utils/signals.js';
|
|
2
|
+
export class OpenApiError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(message, code, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = 'OpenApiError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
13
|
+
const IGNORED_HEADERS = new Set(['accept', 'content-type', 'authorization']);
|
|
14
|
+
const OPENAPI_ONLY = new Set(['nullable', 'example', 'xml', 'externalDocs', 'discriminator']);
|
|
15
|
+
export function openApiTools(document, options = {}) {
|
|
16
|
+
const doc = parseDocument(document);
|
|
17
|
+
const apiName = options.name ?? (slug(doc.info?.title ?? '') || 'api');
|
|
18
|
+
const baseUrl = baseOf(doc, options);
|
|
19
|
+
const tools = [];
|
|
20
|
+
const operations = [];
|
|
21
|
+
const results = {};
|
|
22
|
+
const skipped = [];
|
|
23
|
+
const used = new Set();
|
|
24
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
25
|
+
const pathItem = deref(doc, item);
|
|
26
|
+
for (const method of METHODS) {
|
|
27
|
+
const raw = pathItem[method];
|
|
28
|
+
if (!isRecord(raw))
|
|
29
|
+
continue;
|
|
30
|
+
const operationId = typeof raw.operationId === 'string' ? raw.operationId : undefined;
|
|
31
|
+
const name = uniqueName([options.prefix, sanitize(operationId ?? `${method}_${path}`) || method].filter(Boolean).join('_'), used);
|
|
32
|
+
const operation = {
|
|
33
|
+
name,
|
|
34
|
+
method: method.toUpperCase(),
|
|
35
|
+
path,
|
|
36
|
+
...(operationId ? { operationId } : {}),
|
|
37
|
+
...(typeof raw.summary === 'string' ? { summary: raw.summary } : {}),
|
|
38
|
+
tags: Array.isArray(raw.tags) ? raw.tags.map(String) : [],
|
|
39
|
+
access: method === 'delete' ? 'destructive' : ['get', 'head', 'options', 'trace'].includes(method) ? 'read' : 'write',
|
|
40
|
+
};
|
|
41
|
+
if (options.include && !options.include(operation)) {
|
|
42
|
+
used.delete(name);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const built = buildOperation(doc, pathItem, raw);
|
|
46
|
+
if ('skip' in built) {
|
|
47
|
+
used.delete(name);
|
|
48
|
+
skipped.push({ method: operation.method, path, reason: built.skip });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
operations.push(operation);
|
|
52
|
+
if (built.result)
|
|
53
|
+
results[name] = built.result;
|
|
54
|
+
tools.push({
|
|
55
|
+
name,
|
|
56
|
+
description: describe(raw, operation),
|
|
57
|
+
parameters: built.parameters,
|
|
58
|
+
capabilities: [
|
|
59
|
+
`api.${operation.access}:${apiName}/${name}`,
|
|
60
|
+
...(baseUrl ? [`network:${new URL(baseUrl).host}`] : []),
|
|
61
|
+
],
|
|
62
|
+
...(baseUrl ? { execute: caller(operation, built.placements, built.bodyType, baseUrl, options) } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { name: apiName, ...(baseUrl ? { baseUrl } : {}), tools, operations, results, skipped };
|
|
67
|
+
}
|
|
68
|
+
export async function loadOpenApiTools(url, options = {}) {
|
|
69
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
70
|
+
headers: { accept: 'application/json' },
|
|
71
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
throw new OpenApiError(`The OpenAPI document at ${url} answered ${response.status}`, 'OPENAPI_REQUEST', response.status);
|
|
75
|
+
return openApiTools(await response.text(), { documentUrl: url, ...options });
|
|
76
|
+
}
|
|
77
|
+
function parseDocument(document) {
|
|
78
|
+
let doc = document;
|
|
79
|
+
if (typeof document === 'string') {
|
|
80
|
+
try {
|
|
81
|
+
doc = JSON.parse(document);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new OpenApiError('The OpenAPI document is not JSON; parse a YAML document first, and pass the object', 'OPENAPI_INVALID');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!isRecord(doc))
|
|
88
|
+
throw new OpenApiError('An OpenAPI document is an object', 'OPENAPI_INVALID');
|
|
89
|
+
if (typeof doc.swagger === 'string') {
|
|
90
|
+
throw new OpenApiError(`Swagger ${doc.swagger} is not read; convert the document to OpenAPI 3`, 'OPENAPI_UNSUPPORTED');
|
|
91
|
+
}
|
|
92
|
+
if (typeof doc.openapi !== 'string' || !doc.openapi.startsWith('3.')) {
|
|
93
|
+
throw new OpenApiError(`An OpenAPI 3 document names its version in "openapi", not ${JSON.stringify(doc.openapi)}`, 'OPENAPI_INVALID');
|
|
94
|
+
}
|
|
95
|
+
return doc;
|
|
96
|
+
}
|
|
97
|
+
function baseOf(doc, options) {
|
|
98
|
+
if (options.baseUrl) {
|
|
99
|
+
try {
|
|
100
|
+
return new URL(options.baseUrl).href.replace(/\/+$/, '');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new OpenApiError(`baseUrl must be an absolute URL, not ${JSON.stringify(options.baseUrl)}`, 'OPENAPI_INVALID');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const server = doc.servers?.[0];
|
|
107
|
+
if (!server || typeof server.url !== 'string')
|
|
108
|
+
return undefined;
|
|
109
|
+
const url = server.url.replace(/\{([^}]+)\}/g, (_match, name) => server.variables?.[name]?.default ?? '');
|
|
110
|
+
try {
|
|
111
|
+
return new URL(url, options.documentUrl).href.replace(/\/+$/, '');
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function deref(doc, value, seen = []) {
|
|
118
|
+
if (!isRecord(value) || typeof value.$ref !== 'string')
|
|
119
|
+
return value;
|
|
120
|
+
const ref = value.$ref;
|
|
121
|
+
if (seen.includes(ref))
|
|
122
|
+
throw new OpenApiError(`${ref} refers to itself`, 'OPENAPI_INVALID');
|
|
123
|
+
return deref(doc, resolve(doc, ref), [...seen, ref]);
|
|
124
|
+
}
|
|
125
|
+
function resolve(doc, ref) {
|
|
126
|
+
if (!ref.startsWith('#')) {
|
|
127
|
+
throw new OpenApiError(`${ref} is outside the document; only references within it are followed`, 'OPENAPI_UNSUPPORTED');
|
|
128
|
+
}
|
|
129
|
+
let node = doc;
|
|
130
|
+
for (const part of ref.slice(1).split('/').slice(1)) {
|
|
131
|
+
const key = decodeURIComponent(part).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
132
|
+
node = isRecord(node) || Array.isArray(node) ? node[key] : undefined;
|
|
133
|
+
if (node === undefined)
|
|
134
|
+
throw new OpenApiError(`${ref} names nothing in the document`, 'OPENAPI_INVALID');
|
|
135
|
+
}
|
|
136
|
+
return node;
|
|
137
|
+
}
|
|
138
|
+
function schemaOf(doc, schema, omit, seen = []) {
|
|
139
|
+
if (!isRecord(schema))
|
|
140
|
+
return {};
|
|
141
|
+
if (typeof schema.$ref === 'string') {
|
|
142
|
+
const ref = schema.$ref;
|
|
143
|
+
if (seen.includes(ref))
|
|
144
|
+
return { description: `The same shape as ${ref.split('/').at(-1)}, nested` };
|
|
145
|
+
const { $ref: _ref, ...siblings } = schema;
|
|
146
|
+
return { ...schemaOf(doc, resolve(doc, ref), omit, [...seen, ref]), ...schemaOf(doc, siblings, omit, seen) };
|
|
147
|
+
}
|
|
148
|
+
const out = {};
|
|
149
|
+
const one = (value) => (isRecord(value) ? schemaOf(doc, value, omit, seen) : value);
|
|
150
|
+
const many = (value) => (Array.isArray(value) ? value.map(one) : value);
|
|
151
|
+
const each = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, one(item)])) : value;
|
|
152
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
153
|
+
if (OPENAPI_ONLY.has(key))
|
|
154
|
+
continue;
|
|
155
|
+
if (key === 'properties' && isRecord(value)) {
|
|
156
|
+
const kept = {};
|
|
157
|
+
for (const [name, property] of Object.entries(value)) {
|
|
158
|
+
const read = schemaOf(doc, property, omit, seen);
|
|
159
|
+
if (read[omit] === true)
|
|
160
|
+
continue;
|
|
161
|
+
kept[name] = read;
|
|
162
|
+
}
|
|
163
|
+
out.properties = kept;
|
|
164
|
+
}
|
|
165
|
+
else if (['allOf', 'anyOf', 'oneOf', 'prefixItems'].includes(key))
|
|
166
|
+
out[key] = many(value);
|
|
167
|
+
else if (['$defs', 'definitions', 'patternProperties', 'dependentSchemas'].includes(key))
|
|
168
|
+
out[key] = each(value);
|
|
169
|
+
else if (['items', 'additionalProperties', 'not', 'contains', 'propertyNames', 'if', 'then', 'else'].includes(key)) {
|
|
170
|
+
out[key] = one(value);
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
out[key] = value;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(out.required) && isRecord(out.properties)) {
|
|
176
|
+
const properties = out.properties;
|
|
177
|
+
out.required = out.required.filter((name) => typeof name === 'string' && name in properties);
|
|
178
|
+
if (out.required.length === 0)
|
|
179
|
+
delete out.required;
|
|
180
|
+
}
|
|
181
|
+
if (schema.nullable === true) {
|
|
182
|
+
if (typeof out.type === 'string')
|
|
183
|
+
out.type = [out.type, 'null'];
|
|
184
|
+
else if (Array.isArray(out.type) && !out.type.includes('null'))
|
|
185
|
+
out.type = [...out.type, 'null'];
|
|
186
|
+
}
|
|
187
|
+
if ('example' in schema && !('examples' in schema))
|
|
188
|
+
out.examples = [schema.example];
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function buildOperation(doc, pathItem, raw) {
|
|
192
|
+
const declared = new Map();
|
|
193
|
+
for (const list of [pathItem.parameters, raw.parameters]) {
|
|
194
|
+
if (!Array.isArray(list))
|
|
195
|
+
continue;
|
|
196
|
+
for (const item of list) {
|
|
197
|
+
const parameter = deref(doc, item);
|
|
198
|
+
if (!isRecord(parameter) || typeof parameter.name !== 'string' || typeof parameter.in !== 'string')
|
|
199
|
+
continue;
|
|
200
|
+
declared.set(`${parameter.in}\u0000${parameter.name}`, parameter);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const properties = {};
|
|
204
|
+
const required = [];
|
|
205
|
+
const placements = [];
|
|
206
|
+
for (const parameter of declared.values()) {
|
|
207
|
+
const place = parameter.in;
|
|
208
|
+
const name = parameter.name;
|
|
209
|
+
if (place === 'cookie' || (place === 'header' && IGNORED_HEADERS.has(name.toLowerCase())))
|
|
210
|
+
continue;
|
|
211
|
+
if (place !== 'path' && place !== 'query' && place !== 'header')
|
|
212
|
+
continue;
|
|
213
|
+
const arg = name in properties ? `${place}_${name}` : name;
|
|
214
|
+
const content = isRecord(parameter.content) ? Object.values(parameter.content)[0] : undefined;
|
|
215
|
+
const schema = schemaOf(doc, parameter.schema ?? (isRecord(content) ? content.schema : undefined), 'readOnly');
|
|
216
|
+
properties[arg] = {
|
|
217
|
+
...schema,
|
|
218
|
+
...(typeof parameter.description === 'string' && !schema.description
|
|
219
|
+
? { description: parameter.description }
|
|
220
|
+
: {}),
|
|
221
|
+
};
|
|
222
|
+
if (place === 'path' || parameter.required === true)
|
|
223
|
+
required.push(arg);
|
|
224
|
+
placements.push({ arg, name, in: place });
|
|
225
|
+
}
|
|
226
|
+
let bodyType;
|
|
227
|
+
const body = deref(doc, raw.requestBody);
|
|
228
|
+
if (isRecord(body) && isRecord(body.content)) {
|
|
229
|
+
const types = Object.keys(body.content);
|
|
230
|
+
const json = types.find((type) => /^application\/(?:[\w.+-]+\+)?json\b/i.test(type));
|
|
231
|
+
const form = types.find((type) => /^application\/x-www-form-urlencoded\b/i.test(type));
|
|
232
|
+
const chosen = json ?? form;
|
|
233
|
+
if (!chosen)
|
|
234
|
+
return { skip: `its request body is ${types.join(' or ')}; only JSON and form bodies are sent` };
|
|
235
|
+
bodyType = json ? 'json' : 'form';
|
|
236
|
+
const media = body.content[chosen];
|
|
237
|
+
const schema = schemaOf(doc, isRecord(media) ? media.schema : undefined, 'readOnly');
|
|
238
|
+
const fields = isRecord(schema.properties) ? schema.properties : undefined;
|
|
239
|
+
const flat = fields &&
|
|
240
|
+
(schema.type === 'object' || schema.type === undefined) &&
|
|
241
|
+
!schema.allOf &&
|
|
242
|
+
!schema.anyOf &&
|
|
243
|
+
!schema.oneOf &&
|
|
244
|
+
Object.keys(fields).every((field) => !(field in properties));
|
|
245
|
+
if (flat) {
|
|
246
|
+
const fieldsRequired = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
|
|
247
|
+
for (const [field, fieldSchema] of Object.entries(fields)) {
|
|
248
|
+
properties[field] = fieldSchema;
|
|
249
|
+
if (body.required === true && fieldsRequired.has(field))
|
|
250
|
+
required.push(field);
|
|
251
|
+
placements.push({ arg: field, name: field, in: 'body-field' });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const arg = 'body' in properties ? 'requestBody' : 'body';
|
|
256
|
+
properties[arg] = {
|
|
257
|
+
...schema,
|
|
258
|
+
...(typeof body.description === 'string' && !schema.description ? { description: body.description } : {}),
|
|
259
|
+
};
|
|
260
|
+
if (body.required === true)
|
|
261
|
+
required.push(arg);
|
|
262
|
+
placements.push({ arg, name: arg, in: 'body' });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
let result;
|
|
266
|
+
const responses = isRecord(raw.responses) ? raw.responses : {};
|
|
267
|
+
const success = Object.keys(responses)
|
|
268
|
+
.filter((code) => /^2(?:\d\d|XX)$/i.test(code))
|
|
269
|
+
.sort()[0];
|
|
270
|
+
if (success) {
|
|
271
|
+
const response = deref(doc, responses[success]);
|
|
272
|
+
const content = isRecord(response) && isRecord(response.content) ? response.content : undefined;
|
|
273
|
+
const json = content && Object.keys(content).find((type) => /json/i.test(type));
|
|
274
|
+
const media = json && content ? content[json] : undefined;
|
|
275
|
+
if (isRecord(media) && media.schema !== undefined)
|
|
276
|
+
result = schemaOf(doc, media.schema, 'writeOnly');
|
|
277
|
+
else if (!content)
|
|
278
|
+
result = { type: 'null' };
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
parameters: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties,
|
|
284
|
+
...(required.length ? { required } : {}),
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
},
|
|
287
|
+
placements,
|
|
288
|
+
...(bodyType ? { bodyType } : {}),
|
|
289
|
+
...(result ? { result } : {}),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function describe(raw, operation) {
|
|
293
|
+
const text = [raw.summary, raw.description].filter((part) => typeof part === 'string' && part.trim() !== '');
|
|
294
|
+
const said = text.length ? text.join('\n\n') : `${operation.method} ${operation.path}`;
|
|
295
|
+
return said.length > 1_024 ? `${said.slice(0, 1_021)}...` : said;
|
|
296
|
+
}
|
|
297
|
+
function caller(operation, placements, bodyType, baseUrl, options) {
|
|
298
|
+
const base = new URL(`${baseUrl}/`);
|
|
299
|
+
return async (args, context) => {
|
|
300
|
+
context?.signal?.throwIfAborted();
|
|
301
|
+
let path = operation.path;
|
|
302
|
+
for (const placement of placements.filter((item) => item.in === 'path')) {
|
|
303
|
+
const value = args[placement.arg];
|
|
304
|
+
if (value === undefined || value === null || String(value) === '') {
|
|
305
|
+
throw new OpenApiError(`${operation.name} needs "${placement.arg}"`, 'OPENAPI_INVALID');
|
|
306
|
+
}
|
|
307
|
+
const text = String(value);
|
|
308
|
+
if (text === '.' || text === '..') {
|
|
309
|
+
throw new OpenApiError(`"${placement.arg}" cannot be "${text}"`, 'OPENAPI_INVALID');
|
|
310
|
+
}
|
|
311
|
+
path = path.split(`{${placement.name}}`).join(encodeURIComponent(text));
|
|
312
|
+
}
|
|
313
|
+
const url = new URL(path.replace(/^\/+/, ''), base);
|
|
314
|
+
const headers = { accept: 'application/json' };
|
|
315
|
+
let body;
|
|
316
|
+
for (const placement of placements) {
|
|
317
|
+
const value = args[placement.arg];
|
|
318
|
+
if (value === undefined || value === null)
|
|
319
|
+
continue;
|
|
320
|
+
if (placement.in === 'query') {
|
|
321
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
322
|
+
url.searchParams.append(placement.name, text(item));
|
|
323
|
+
}
|
|
324
|
+
else if (placement.in === 'header')
|
|
325
|
+
headers[placement.name.toLowerCase()] = text(value);
|
|
326
|
+
else if (placement.in === 'body-field') {
|
|
327
|
+
body ??= {};
|
|
328
|
+
body[placement.name] = value;
|
|
329
|
+
}
|
|
330
|
+
else if (placement.in === 'body')
|
|
331
|
+
body = value;
|
|
332
|
+
}
|
|
333
|
+
const own = typeof options.headers === 'function' ? await options.headers({ operation, context }) : options.headers;
|
|
334
|
+
for (const [name, value] of Object.entries(own ?? {}))
|
|
335
|
+
headers[name.toLowerCase()] = value;
|
|
336
|
+
let payload;
|
|
337
|
+
if (body !== undefined && bodyType === 'form') {
|
|
338
|
+
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
339
|
+
payload = new URLSearchParams(Object.entries(body).map(([key, value]) => [key, text(value)])).toString();
|
|
340
|
+
}
|
|
341
|
+
else if (body !== undefined) {
|
|
342
|
+
headers['content-type'] = 'application/json';
|
|
343
|
+
payload = JSON.stringify(body);
|
|
344
|
+
}
|
|
345
|
+
const linked = linkSignals([context?.signal, AbortSignal.timeout(options.timeoutMs ?? 30_000)]);
|
|
346
|
+
try {
|
|
347
|
+
const response = await (options.fetch ?? globalThis.fetch)(url.href, {
|
|
348
|
+
method: operation.method,
|
|
349
|
+
headers,
|
|
350
|
+
...(payload !== undefined ? { body: payload } : {}),
|
|
351
|
+
redirect: 'manual',
|
|
352
|
+
signal: linked.signal,
|
|
353
|
+
});
|
|
354
|
+
const answer = await readCapped(response, options.maxResponseBytes ?? 1_000_000, operation);
|
|
355
|
+
if (response.status >= 300 && response.status < 400) {
|
|
356
|
+
throw new OpenApiError(`${operation.name} was redirected to ${response.headers.get('location') ?? 'another place'}, which is not followed`, 'OPENAPI_REQUEST', response.status);
|
|
357
|
+
}
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
throw new OpenApiError(`${operation.name} answered ${response.status}: ${detailOf(answer)}`, 'OPENAPI_REQUEST', response.status);
|
|
360
|
+
}
|
|
361
|
+
if (answer.trim() === '')
|
|
362
|
+
return null;
|
|
363
|
+
if (/json/i.test(response.headers.get('content-type') ?? '')) {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse(answer);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
throw new OpenApiError(`${operation.name} answered with JSON that does not parse`, 'OPENAPI_REQUEST', response.status);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return answer;
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof OpenApiError || context?.signal?.aborted)
|
|
375
|
+
throw error;
|
|
376
|
+
throw new OpenApiError(`${operation.name} could not be reached: ${linked.signal.aborted ? `no answer within ${options.timeoutMs ?? 30_000} ms` : messageOf(error)}`, 'OPENAPI_REQUEST');
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
linked.dispose();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async function readCapped(response, limit, operation) {
|
|
384
|
+
if (!response.body)
|
|
385
|
+
return '';
|
|
386
|
+
const reader = response.body.getReader();
|
|
387
|
+
const chunks = [];
|
|
388
|
+
let size = 0;
|
|
389
|
+
for (;;) {
|
|
390
|
+
const { done, value } = await reader.read();
|
|
391
|
+
if (done)
|
|
392
|
+
break;
|
|
393
|
+
size += value.byteLength;
|
|
394
|
+
if (size > limit) {
|
|
395
|
+
await reader.cancel().catch(() => undefined);
|
|
396
|
+
throw new OpenApiError(`${operation.name} answered more than ${limit} bytes`, 'OPENAPI_REQUEST', response.status);
|
|
397
|
+
}
|
|
398
|
+
chunks.push(value);
|
|
399
|
+
}
|
|
400
|
+
const bytes = new Uint8Array(size);
|
|
401
|
+
let offset = 0;
|
|
402
|
+
for (const chunk of chunks) {
|
|
403
|
+
bytes.set(chunk, offset);
|
|
404
|
+
offset += chunk.byteLength;
|
|
405
|
+
}
|
|
406
|
+
return new TextDecoder().decode(bytes);
|
|
407
|
+
}
|
|
408
|
+
function detailOf(answer) {
|
|
409
|
+
try {
|
|
410
|
+
const parsed = JSON.parse(answer);
|
|
411
|
+
if (isRecord(parsed)) {
|
|
412
|
+
for (const key of ['error', 'message', 'detail', 'title']) {
|
|
413
|
+
const value = parsed[key];
|
|
414
|
+
if (typeof value === 'string' && value.trim())
|
|
415
|
+
return value;
|
|
416
|
+
if (isRecord(value) && typeof value.message === 'string')
|
|
417
|
+
return value.message;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
}
|
|
423
|
+
return answer.trim().slice(0, 300) || 'no detail';
|
|
424
|
+
}
|
|
425
|
+
function sanitize(name) {
|
|
426
|
+
return name
|
|
427
|
+
.replace(/[^A-Za-z0-9_-]+/g, '_')
|
|
428
|
+
.replace(/_+/g, '_')
|
|
429
|
+
.replace(/^_+|_+$/g, '');
|
|
430
|
+
}
|
|
431
|
+
function uniqueName(name, used) {
|
|
432
|
+
const base = name.slice(0, 64);
|
|
433
|
+
let candidate = base;
|
|
434
|
+
for (let index = 2; used.has(candidate); index += 1) {
|
|
435
|
+
const suffix = `_${index}`;
|
|
436
|
+
candidate = `${base.slice(0, 64 - suffix.length)}${suffix}`;
|
|
437
|
+
}
|
|
438
|
+
used.add(candidate);
|
|
439
|
+
return candidate;
|
|
440
|
+
}
|
|
441
|
+
function slug(text) {
|
|
442
|
+
return text
|
|
443
|
+
.toLowerCase()
|
|
444
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
445
|
+
.replace(/^-+|-+$/g, '');
|
|
446
|
+
}
|
|
447
|
+
function text(value) {
|
|
448
|
+
return value !== null && typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
449
|
+
}
|
|
450
|
+
function isRecord(value) {
|
|
451
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
452
|
+
}
|
|
453
|
+
function messageOf(error) {
|
|
454
|
+
return error instanceof Error ? error.message : String(error);
|
|
455
|
+
}
|
|
@@ -95,6 +95,11 @@ export function negotiateCompletionRequest(request, capabilities, options = {})
|
|
|
95
95
|
target.seed = undefined;
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
if (request.logprobs !== undefined && capabilities.logprobs === false) {
|
|
99
|
+
refuse('logprobs', request.logprobs, 'the model does not report token log probabilities', (target) => {
|
|
100
|
+
target.logprobs = undefined;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
98
103
|
if (request.topK !== undefined && capabilities.topK === false) {
|
|
99
104
|
refuse('topK', request.topK, 'the model does not support top-k sampling', (target) => {
|
|
100
105
|
target.topK = undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decisions from any completion model: each question asked on its own, answered with a one-token code,
|
|
3
|
+
* and its probabilities read from the code token's log probabilities, or counted over samples where
|
|
4
|
+
* the model reports none.
|
|
5
|
+
*/
|
|
6
|
+
import type { CompletionRequest } from '../types/messages.js';
|
|
7
|
+
import type { NexusResponse } from '../types/response.js';
|
|
8
|
+
import { type DecisionProvider } from './core.js';
|
|
9
|
+
/** Anything that completes a request, such as a `NexusAI` client. */
|
|
10
|
+
export interface DecisionCompletionClient {
|
|
11
|
+
/** Runs one completion. */
|
|
12
|
+
complete(request: CompletionRequest): Promise<NexusResponse>;
|
|
13
|
+
}
|
|
14
|
+
/** How a completion model's probabilities are read. */
|
|
15
|
+
export type CompletionDecisionMethod = 'auto' | 'logprobs' | 'sampling';
|
|
16
|
+
/** Options for `completionDecisions()`. */
|
|
17
|
+
export interface CompletionDecisionsOptions {
|
|
18
|
+
/** The model to ask, as the client names it. */
|
|
19
|
+
model: string;
|
|
20
|
+
/** The provider's name in results. Defaults to `model`. */
|
|
21
|
+
name?: string;
|
|
22
|
+
/**
|
|
23
|
+
* How probabilities are read: `logprobs`, from one call's token log probabilities; `sampling`, by
|
|
24
|
+
* counting the answers of several calls; or `auto`, the default: log probabilities, and sampling for
|
|
25
|
+
* a model that reports none or refuses to, which it then asks by sampling from the start.
|
|
26
|
+
*/
|
|
27
|
+
method?: CompletionDecisionMethod;
|
|
28
|
+
/** Calls per question, when sampling. Defaults to 8. */
|
|
29
|
+
samples?: number;
|
|
30
|
+
/** The temperature samples are drawn at. Defaults to 1. */
|
|
31
|
+
temperature?: number;
|
|
32
|
+
/** Questions asked at once. Defaults to 8. */
|
|
33
|
+
concurrency?: number;
|
|
34
|
+
/** Instructions added to every question, such as what the application's terms mean. */
|
|
35
|
+
instructions?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Any completion model as a decision provider, through a client's own `complete()`, so every call is
|
|
39
|
+
* routed, authorized, budgeted, traced, and priced as every completion is. Each question is asked on
|
|
40
|
+
* its own, at temperature 0, to answer with one code: a letter per option, Y or N, or a level's digit.
|
|
41
|
+
* Its probabilities are read from the log probabilities of that token's alternatives where the model
|
|
42
|
+
* reports them, the way TypeSafe AI's Jev reads its own; elsewhere the question is asked `samples`
|
|
43
|
+
* times, and each answer counted. Sampled probabilities come in steps of one sample, and cost as many
|
|
44
|
+
* calls.
|
|
45
|
+
*/
|
|
46
|
+
export declare function completionDecisions(client: DecisionCompletionClient, options: CompletionDecisionsOptions): DecisionProvider & {
|
|
47
|
+
readonly sampledModels: ReadonlySet<string>;
|
|
48
|
+
};
|