@modelprofile.com/flexharness 10.11.0 → 10.13.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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +21 -2
- package/dist_ts/classes.flexharness.js +430 -63
- package/dist_ts/classes.toolcatalog.d.ts +80 -0
- package/dist_ts/classes.toolcatalog.js +1336 -0
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +179 -2
- package/dist_ts/interfaces.js +29 -1
- package/dist_ts/plugins.d.ts +7 -3
- package/dist_ts/plugins.js +7 -3
- package/dist_ts/utils.harnessoptions.d.ts +2 -1
- package/dist_ts/utils.harnessoptions.js +19 -1
- package/dist_ts/utils.json.js +37 -2
- package/dist_ts/utils.schemadeclarations.d.ts +53 -0
- package/dist_ts/utils.schemadeclarations.js +315 -0
- package/dist_ts/utils.sessionmetrics.d.ts +17 -0
- package/dist_ts/utils.sessionmetrics.js +72 -0
- package/package.json +6 -5
- package/readme.md +58 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +484 -58
- package/ts/classes.toolcatalog.ts +1431 -0
- package/ts/errors.ts +8 -0
- package/ts/index.ts +1 -0
- package/ts/interfaces.ts +198 -2
- package/ts/plugins.ts +10 -2
- package/ts/readme.md +58 -8
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +33 -1
- package/ts/utils.schemadeclarations.ts +322 -0
- package/ts/utils.sessionmetrics.ts +104 -0
- package/ts_chat_harness/readme.md +1 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders JSON Schema as TypeScript declarations for the tool catalog. The output depends only on
|
|
3
|
+
* the schema, so the same schema always renders to the same bytes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type TJsonSchemaNode = boolean | { [key: string]: unknown };
|
|
7
|
+
|
|
8
|
+
export interface ISchemaDeclarationContext {
|
|
9
|
+
/** The named type a `$ref` denotes, or undefined to inline the referenced schema. */
|
|
10
|
+
namedType(ref: string): string | undefined;
|
|
11
|
+
/** The schema a local `$ref` points to. */
|
|
12
|
+
resolve(ref: string): TJsonSchemaNode | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IRenderedDeclaration {
|
|
16
|
+
text: string;
|
|
17
|
+
/** Named types the text refers to, sorted. */
|
|
18
|
+
types: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
22
|
+
const maxRenderDepth = 32;
|
|
23
|
+
const maxInlineObjectCharacters = 100;
|
|
24
|
+
const maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
|
25
|
+
|
|
26
|
+
export const isSchemaRecord = (value: unknown): value is { [key: string]: unknown } =>
|
|
27
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
export const isIdentifier = (value: string): boolean => identifierPattern.test(value);
|
|
30
|
+
|
|
31
|
+
const literal = (value: unknown): string => {
|
|
32
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
33
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
34
|
+
if (typeof value === 'boolean' || value === null) return String(value);
|
|
35
|
+
return 'unknown';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reports whether `character` occurs outside brackets, braces, parentheses, string literals and
|
|
40
|
+
* comments.
|
|
41
|
+
*/
|
|
42
|
+
const hasTopLevel = (text: string, character: string): boolean => {
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let quote: string | undefined;
|
|
45
|
+
for (let index = 0; index < text.length; index++) {
|
|
46
|
+
const current = text[index];
|
|
47
|
+
if (quote) {
|
|
48
|
+
if (current === '\\') index++;
|
|
49
|
+
else if (current === quote) quote = undefined;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (current === '/' && text[index + 1] === '*') {
|
|
53
|
+
const end = text.indexOf('*/', index + 2);
|
|
54
|
+
if (end === -1) return false;
|
|
55
|
+
index = end + 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (current === '"' || current === "'") quote = current;
|
|
59
|
+
else if (current === '{' || current === '[' || current === '(' || current === '<') depth++;
|
|
60
|
+
else if (current === '}' || current === ']' || current === ')' || current === '>') depth--;
|
|
61
|
+
else if (depth === 0 && current === character) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const union = (parts: string[]): string => {
|
|
67
|
+
const unique = [...new Set(parts)];
|
|
68
|
+
if (unique.includes('unknown')) return 'unknown';
|
|
69
|
+
const withoutNever = unique.length > 1 ? unique.filter((part) => part !== 'never') : unique;
|
|
70
|
+
return withoutNever.join(' | ');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const intersection = (parts: string[]): string => {
|
|
74
|
+
const unique = [...new Set(parts)].filter((part) => part !== 'unknown');
|
|
75
|
+
if (unique.length === 0) return 'unknown';
|
|
76
|
+
return unique.map((part) => hasTopLevel(part, '|') ? `(${part})` : part).join(' & ');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const arrayOf = (element: string): string =>
|
|
80
|
+
hasTopLevel(element, '|') || hasTopLevel(element, '&') ? `(${element})[]` : `${element}[]`;
|
|
81
|
+
|
|
82
|
+
const propertyKey = (name: string): string => isIdentifier(name) ? name : JSON.stringify(name);
|
|
83
|
+
|
|
84
|
+
/** A JSDoc block at `indent`; a short single line stays on one line. */
|
|
85
|
+
export const docBlock = (text: string, indent: string): string => {
|
|
86
|
+
const lines = text.replace(/\*\//g, '*\\/').split('\n').map((line) => line.trimEnd());
|
|
87
|
+
if (lines.length === 1 && indent.length + lines[0].length <= 100) return `${indent}/** ${lines[0]} */\n`;
|
|
88
|
+
return `${indent}/**\n${lines.map((line) => line ? `${indent} * ${line}` : `${indent} *`).join('\n')}\n${indent} */\n`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const countOf = (count: number, unit: string): string => `${count} ${unit}${count === 1 ? '' : 's'}`;
|
|
92
|
+
|
|
93
|
+
/** Constraints a TypeScript type cannot state, in a short readable form. */
|
|
94
|
+
const constraintsOf = (schema: { [key: string]: unknown }): string[] => {
|
|
95
|
+
const constraints: string[] = [];
|
|
96
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
97
|
+
if (types.includes('integer')) constraints.push('integer');
|
|
98
|
+
if (typeof schema.format === 'string') constraints.push(`format ${schema.format}`);
|
|
99
|
+
const bound = (value: unknown): value is number =>
|
|
100
|
+
typeof value === 'number' && Number.isFinite(value) && Math.abs(value) !== maxSafeInteger;
|
|
101
|
+
if (bound(schema.exclusiveMinimum)) constraints.push(`> ${schema.exclusiveMinimum}`);
|
|
102
|
+
else if (bound(schema.minimum)) constraints.push(`${schema.exclusiveMinimum === true ? '>' : '>='} ${schema.minimum}`);
|
|
103
|
+
if (bound(schema.exclusiveMaximum)) constraints.push(`< ${schema.exclusiveMaximum}`);
|
|
104
|
+
else if (bound(schema.maximum)) constraints.push(`${schema.exclusiveMaximum === true ? '<' : '<='} ${schema.maximum}`);
|
|
105
|
+
if (bound(schema.multipleOf)) constraints.push(`multiple of ${schema.multipleOf}`);
|
|
106
|
+
const range = (min: unknown, max: unknown, unit: string): void => {
|
|
107
|
+
const hasMin = bound(min) && min > 0;
|
|
108
|
+
const hasMax = bound(max);
|
|
109
|
+
if (hasMin && hasMax) constraints.push(min === max ? countOf(min, unit) : `${min}-${countOf(max, unit)}`);
|
|
110
|
+
else if (hasMin) constraints.push(`at least ${countOf(min, unit)}`);
|
|
111
|
+
else if (hasMax) constraints.push(`at most ${countOf(max, unit)}`);
|
|
112
|
+
};
|
|
113
|
+
range(schema.minLength, schema.maxLength, 'character');
|
|
114
|
+
range(schema.minItems, schema.maxItems, 'item');
|
|
115
|
+
if (schema.uniqueItems === true) constraints.push('unique items');
|
|
116
|
+
if (typeof schema.pattern === 'string') constraints.push(`pattern /${schema.pattern}/`);
|
|
117
|
+
if (schema.default !== undefined) {
|
|
118
|
+
const value = JSON.stringify(schema.default);
|
|
119
|
+
if (value !== undefined && value.length <= 60) constraints.push(`default ${value}`);
|
|
120
|
+
}
|
|
121
|
+
if (schema.deprecated === true) constraints.push('deprecated');
|
|
122
|
+
return constraints;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The documentation of a property: its description and the constraints its type cannot state. */
|
|
126
|
+
export const schemaDoc = (schema: TJsonSchemaNode, description?: string): string | undefined => {
|
|
127
|
+
if (!isSchemaRecord(schema)) return description?.trim() || undefined;
|
|
128
|
+
const text = (description ?? (typeof schema.description === 'string' ? schema.description : '')).trim();
|
|
129
|
+
const constraints = constraintsOf(schema);
|
|
130
|
+
if (constraints.length === 0) return text || undefined;
|
|
131
|
+
return text ? `${text} (${constraints.join(', ')})` : constraints.join(', ');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export class SchemaDeclarationWriter {
|
|
135
|
+
private readonly used = new Set<string>();
|
|
136
|
+
private readonly inlining = new Set<string>();
|
|
137
|
+
|
|
138
|
+
constructor(private readonly context: ISchemaDeclarationContext) {}
|
|
139
|
+
|
|
140
|
+
public get usedTypes(): string[] {
|
|
141
|
+
return [...this.used].sort();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The TypeScript type of `schema`, with continuation lines indented relative to `indent`. */
|
|
145
|
+
public type(schema: TJsonSchemaNode, indent = '', depth = 0, omitMemberDocs = false): string {
|
|
146
|
+
if (depth > maxRenderDepth) return 'unknown';
|
|
147
|
+
if (schema === true) return 'unknown';
|
|
148
|
+
if (schema === false) return 'never';
|
|
149
|
+
if (!isSchemaRecord(schema)) return 'unknown';
|
|
150
|
+
const base = this.baseType(schema, indent, depth, omitMemberDocs);
|
|
151
|
+
return schema.nullable === true ? union([base, 'null']) : base;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The members of an object schema; undefined when the schema is not a plain object type. */
|
|
155
|
+
public objectMembers(
|
|
156
|
+
schema: TJsonSchemaNode,
|
|
157
|
+
): Array<{ name: string; doc?: string }> | undefined {
|
|
158
|
+
if (!isSchemaRecord(schema) || !this.isPlainObject(schema)) return undefined;
|
|
159
|
+
const properties = isSchemaRecord(schema.properties) ? schema.properties : {};
|
|
160
|
+
return Object.entries(properties).map(([name, property]) => {
|
|
161
|
+
const doc = schemaDoc(property as TJsonSchemaNode);
|
|
162
|
+
return { name, ...(doc === undefined ? {} : { doc }) };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether an object schema requires none of its properties. */
|
|
167
|
+
public hasNoRequiredProperties(schema: TJsonSchemaNode): boolean {
|
|
168
|
+
return isSchemaRecord(schema)
|
|
169
|
+
&& this.isPlainObject(schema)
|
|
170
|
+
&& !(Array.isArray(schema.required) && schema.required.length > 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `interface Name { … }` for a plain object schema, otherwise `type Name = …;`. */
|
|
174
|
+
public namedDeclaration(name: string, schema: TJsonSchemaNode, description?: string): string {
|
|
175
|
+
const doc = isSchemaRecord(schema)
|
|
176
|
+
? (description ?? (typeof schema.description === 'string' ? schema.description : undefined))?.trim()
|
|
177
|
+
: description?.trim();
|
|
178
|
+
const prefix = doc ? docBlock(doc, '') : '';
|
|
179
|
+
if (isSchemaRecord(schema) && this.isPlainObject(schema) && schema.nullable !== true) {
|
|
180
|
+
return `${prefix}interface ${name} ${this.objectType(schema, '', 0, false)}`;
|
|
181
|
+
}
|
|
182
|
+
return `${prefix}type ${name} = ${this.type(schema, '', 0)};`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private isPlainObject(schema: { [key: string]: unknown }): boolean {
|
|
186
|
+
if (schema.$ref !== undefined || 'const' in schema || schema.enum !== undefined) return false;
|
|
187
|
+
if (schema.anyOf !== undefined || schema.oneOf !== undefined || schema.allOf !== undefined) return false;
|
|
188
|
+
if (schema.type === 'object') return true;
|
|
189
|
+
return schema.type === undefined && isSchemaRecord(schema.properties);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private baseType(
|
|
193
|
+
schema: { [key: string]: unknown },
|
|
194
|
+
indent: string,
|
|
195
|
+
depth: number,
|
|
196
|
+
omitMemberDocs: boolean,
|
|
197
|
+
): string {
|
|
198
|
+
if (typeof schema.$ref === 'string') return this.reference(schema.$ref, indent, depth);
|
|
199
|
+
if ('const' in schema) return literal(schema.const);
|
|
200
|
+
if (Array.isArray(schema.enum)) return schema.enum.length ? union(schema.enum.map(literal)) : 'never';
|
|
201
|
+
const alternatives = Array.isArray(schema.anyOf) ? schema.anyOf : Array.isArray(schema.oneOf) ? schema.oneOf : undefined;
|
|
202
|
+
if (alternatives?.length) {
|
|
203
|
+
return union(alternatives.map((alternative) => this.type(alternative as TJsonSchemaNode, indent, depth + 1)));
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length) {
|
|
206
|
+
const own = { ...schema };
|
|
207
|
+
delete own.allOf;
|
|
208
|
+
const hasOwnShape = own.type !== undefined || own.properties !== undefined;
|
|
209
|
+
return intersection([
|
|
210
|
+
...schema.allOf.map((part) => this.type(part as TJsonSchemaNode, indent, depth + 1)),
|
|
211
|
+
...(hasOwnShape ? [this.type(own, indent, depth + 1)] : []),
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
const types = Array.isArray(schema.type)
|
|
215
|
+
? schema.type.filter((type): type is string => typeof type === 'string')
|
|
216
|
+
: typeof schema.type === 'string'
|
|
217
|
+
? [schema.type]
|
|
218
|
+
: this.inferredTypes(schema);
|
|
219
|
+
if (types.length === 0) return 'unknown';
|
|
220
|
+
return union(types.map((type) => {
|
|
221
|
+
switch (type) {
|
|
222
|
+
case 'string': return 'string';
|
|
223
|
+
case 'number':
|
|
224
|
+
case 'integer': return 'number';
|
|
225
|
+
case 'boolean': return 'boolean';
|
|
226
|
+
case 'null': return 'null';
|
|
227
|
+
case 'array': return this.arrayType(schema, indent, depth);
|
|
228
|
+
case 'object': return this.objectType(schema, indent, depth, omitMemberDocs);
|
|
229
|
+
default: return 'unknown';
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private inferredTypes(schema: { [key: string]: unknown }): string[] {
|
|
235
|
+
if (
|
|
236
|
+
schema.properties !== undefined
|
|
237
|
+
|| schema.additionalProperties !== undefined
|
|
238
|
+
|| schema.patternProperties !== undefined
|
|
239
|
+
|| schema.required !== undefined
|
|
240
|
+
) return ['object'];
|
|
241
|
+
if (schema.items !== undefined || schema.prefixItems !== undefined) return ['array'];
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private reference(ref: string, indent: string, depth: number): string {
|
|
246
|
+
const name = this.context.namedType(ref);
|
|
247
|
+
if (name) {
|
|
248
|
+
this.used.add(name);
|
|
249
|
+
return name;
|
|
250
|
+
}
|
|
251
|
+
const target = this.context.resolve(ref);
|
|
252
|
+
if (target === undefined || this.inlining.has(ref)) return 'unknown';
|
|
253
|
+
this.inlining.add(ref);
|
|
254
|
+
try {
|
|
255
|
+
return this.type(target, indent, depth + 1);
|
|
256
|
+
} finally {
|
|
257
|
+
this.inlining.delete(ref);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private arrayType(schema: { [key: string]: unknown }, indent: string, depth: number): string {
|
|
262
|
+
const tuple = Array.isArray(schema.prefixItems)
|
|
263
|
+
? schema.prefixItems
|
|
264
|
+
: Array.isArray(schema.items) ? schema.items : undefined;
|
|
265
|
+
if (tuple) {
|
|
266
|
+
const rest = Array.isArray(schema.prefixItems) ? schema.items : schema.additionalItems;
|
|
267
|
+
const members = tuple.map((item) => this.type(item as TJsonSchemaNode, indent, depth + 1));
|
|
268
|
+
if (rest !== undefined && rest !== false) {
|
|
269
|
+
members.push(`...${arrayOf(this.type(rest as TJsonSchemaNode, indent, depth + 1))}`);
|
|
270
|
+
}
|
|
271
|
+
return `[${members.join(', ')}]`;
|
|
272
|
+
}
|
|
273
|
+
if (schema.items === undefined) return 'unknown[]';
|
|
274
|
+
return arrayOf(this.type(schema.items as TJsonSchemaNode, indent, depth + 1));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private objectType(
|
|
278
|
+
schema: { [key: string]: unknown },
|
|
279
|
+
indent: string,
|
|
280
|
+
depth: number,
|
|
281
|
+
omitMemberDocs: boolean,
|
|
282
|
+
): string {
|
|
283
|
+
const memberIndent = `${indent} `;
|
|
284
|
+
const properties = isSchemaRecord(schema.properties) ? schema.properties : {};
|
|
285
|
+
const required = new Set(Array.isArray(schema.required)
|
|
286
|
+
? schema.required.filter((name): name is string => typeof name === 'string')
|
|
287
|
+
: []);
|
|
288
|
+
const members: Array<{ doc?: string; text: string }> = [];
|
|
289
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
290
|
+
const type = this.type(property as TJsonSchemaNode, memberIndent, depth + 1);
|
|
291
|
+
const doc = omitMemberDocs ? undefined : schemaDoc(property as TJsonSchemaNode);
|
|
292
|
+
members.push({
|
|
293
|
+
...(doc === undefined ? {} : { doc }),
|
|
294
|
+
text: `${propertyKey(name)}${required.has(name) ? '' : '?'}: ${type}`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const indexTypes: string[] = [];
|
|
298
|
+
if (isSchemaRecord(schema.patternProperties)) {
|
|
299
|
+
for (const property of Object.values(schema.patternProperties)) {
|
|
300
|
+
indexTypes.push(this.type(property as TJsonSchemaNode, memberIndent, depth + 1));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (isSchemaRecord(schema.additionalProperties)) {
|
|
304
|
+
indexTypes.push(this.type(schema.additionalProperties, memberIndent, depth + 1));
|
|
305
|
+
} else if (schema.additionalProperties === true && members.length > 0) {
|
|
306
|
+
indexTypes.push('unknown');
|
|
307
|
+
}
|
|
308
|
+
if (indexTypes.length) members.push({ text: `[key: string]: ${union(indexTypes)}` });
|
|
309
|
+
if (members.length === 0) return schema.additionalProperties === false ? '{}' : 'Record<string, unknown>';
|
|
310
|
+
if (members.every((member) => member.doc === undefined && !member.text.includes('\n'))) {
|
|
311
|
+
const inline = `{ ${members.map((member) => member.text).join('; ')} }`;
|
|
312
|
+
if (inline.length <= maxInlineObjectCharacters) return inline;
|
|
313
|
+
}
|
|
314
|
+
const lines = members.map((member) =>
|
|
315
|
+
`${member.doc === undefined ? '' : docBlock(member.doc, memberIndent)}${memberIndent}${member.text};`);
|
|
316
|
+
return `{\n${lines.join('\n')}\n${indent}}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Indents every non-empty line of `text`. */
|
|
321
|
+
export const indentLines = (text: string, indent: string): string =>
|
|
322
|
+
text.split('\n').map((line) => line ? `${indent}${line}` : line).join('\n');
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IFlexSessionMetrics,
|
|
3
|
+
IFlexSessionMetricsState,
|
|
4
|
+
TFlexModelCallUsage,
|
|
5
|
+
} from './interfaces.js';
|
|
6
|
+
|
|
7
|
+
/** Fixed-size accumulator. One owner per generation / actual compactor invocation. */
|
|
8
|
+
export interface ISessionMetricsOperation {
|
|
9
|
+
id: string;
|
|
10
|
+
source: 'generation' | 'compaction';
|
|
11
|
+
reportedTokens: number;
|
|
12
|
+
reportedTokensExact: boolean;
|
|
13
|
+
complete: boolean;
|
|
14
|
+
sawCall: boolean;
|
|
15
|
+
lastObservedInputTokens?: number;
|
|
16
|
+
lastObservedInputTokensAt?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const createSessionMetricsState = (complete: boolean): IFlexSessionMetricsState => ({
|
|
20
|
+
reportedLifetimeUsedTokens: 0,
|
|
21
|
+
reportedLifetimeUsedTokensExact: true,
|
|
22
|
+
lifetimeUsageComplete: complete,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const createSessionMetricsOperation = (
|
|
26
|
+
id: string,
|
|
27
|
+
source: ISessionMetricsOperation['source'],
|
|
28
|
+
): ISessionMetricsOperation => ({
|
|
29
|
+
id, source, reportedTokens: 0, reportedTokensExact: true, complete: true, sawCall: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const isTokenCount = (value: unknown): value is number =>
|
|
33
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
34
|
+
|
|
35
|
+
/** Saturation is a safe lower bound, never a rounded purported exact total. */
|
|
36
|
+
const addTokens = (left: number, right: number): { tokens: number; exact: boolean } =>
|
|
37
|
+
right > Number.MAX_SAFE_INTEGER - left
|
|
38
|
+
? { tokens: Number.MAX_SAFE_INTEGER, exact: false }
|
|
39
|
+
: { tokens: left + right, exact: true };
|
|
40
|
+
|
|
41
|
+
export function observeSessionMetricsCall(
|
|
42
|
+
operation: ISessionMetricsOperation,
|
|
43
|
+
call: TFlexModelCallUsage,
|
|
44
|
+
): void {
|
|
45
|
+
operation.sawCall = true;
|
|
46
|
+
if (call.status === 'unreported') {
|
|
47
|
+
operation.complete = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { usage } = call;
|
|
51
|
+
if (call.usageComplete === false) operation.complete = false;
|
|
52
|
+
if (!Object.values(usage).every(isTokenCount)
|
|
53
|
+
|| !isTokenCount(usage.totalTokens)
|
|
54
|
+
|| usage.inputTokens > Number.MAX_SAFE_INTEGER - usage.outputTokens
|
|
55
|
+
|| usage.totalTokens !== usage.inputTokens + usage.outputTokens) {
|
|
56
|
+
operation.complete = false;
|
|
57
|
+
operation.reportedTokensExact = false;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const sum = addTokens(operation.reportedTokens, usage.totalTokens);
|
|
61
|
+
operation.reportedTokens = sum.tokens;
|
|
62
|
+
operation.reportedTokensExact &&= sum.exact;
|
|
63
|
+
if (operation.source === 'generation' && call.usageComplete !== false) {
|
|
64
|
+
operation.lastObservedInputTokens = usage.inputTokens;
|
|
65
|
+
operation.lastObservedInputTokensAt = new Date().toISOString();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function settleSessionMetricsOperation(
|
|
70
|
+
metrics: IFlexSessionMetricsState,
|
|
71
|
+
operation: ISessionMetricsOperation,
|
|
72
|
+
): void {
|
|
73
|
+
const key = operation.source === 'generation' ? 'pendingGenerationId' : 'pendingCompactionId';
|
|
74
|
+
if (metrics[key] !== operation.id) throw new Error('Session metrics operation ownership changed.');
|
|
75
|
+
const sum = addTokens(metrics.reportedLifetimeUsedTokens, operation.reportedTokens);
|
|
76
|
+
metrics.reportedLifetimeUsedTokens = sum.tokens;
|
|
77
|
+
metrics.reportedLifetimeUsedTokensExact &&= sum.exact && operation.reportedTokensExact;
|
|
78
|
+
metrics.lifetimeUsageComplete &&= operation.complete
|
|
79
|
+
&& metrics.reportedLifetimeUsedTokensExact
|
|
80
|
+
// An arbitrary compactor may make uninstrumented calls. No report is not proof of zero spend.
|
|
81
|
+
&& (operation.source === 'generation' || operation.sawCall);
|
|
82
|
+
if (operation.lastObservedInputTokens !== undefined) {
|
|
83
|
+
metrics.lastObservedInputTokens = operation.lastObservedInputTokens;
|
|
84
|
+
metrics.lastObservedInputTokensAt = operation.lastObservedInputTokensAt;
|
|
85
|
+
}
|
|
86
|
+
delete metrics[key];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function sessionMetricsView(metrics: IFlexSessionMetricsState): IFlexSessionMetrics {
|
|
90
|
+
const inFlight = metrics.pendingGenerationId !== undefined || metrics.pendingCompactionId !== undefined;
|
|
91
|
+
const lifetimeUsageComplete = metrics.lifetimeUsageComplete && !inFlight;
|
|
92
|
+
return {
|
|
93
|
+
reportedLifetimeUsedTokens: metrics.reportedLifetimeUsedTokens,
|
|
94
|
+
reportedLifetimeUsedTokensExact: metrics.reportedLifetimeUsedTokensExact,
|
|
95
|
+
lifetimeUsageComplete,
|
|
96
|
+
inFlight,
|
|
97
|
+
...(lifetimeUsageComplete ? { lifetimeUsedTokens: metrics.reportedLifetimeUsedTokens } : {}),
|
|
98
|
+
...(metrics.lastObservedInputTokens === undefined ? {} : {
|
|
99
|
+
lastObservedInputTokens: metrics.lastObservedInputTokens,
|
|
100
|
+
lastObservedInputTokensAt: metrics.lastObservedInputTokensAt,
|
|
101
|
+
lastObservedInputTokensFreshness: 'historical' as const,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -4,7 +4,7 @@ A FlexHarness session as the inputs of the `@design.estate/dees-catalog` harness
|
|
|
4
4
|
pnpm add @modelprofile.com/flexharness @design.estate/dees-catalog
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
The optional peer range is `^17.0.0 || ^19.5.0 || ^20.0.0`; the package builds and is tested against
|
|
7
|
+
The optional peer range is `^17.0.0 || ^19.5.0 || ^20.0.0 || ^21.0.0`; the package builds and is tested against 21.
|
|
8
8
|
|
|
9
9
|
The subpath imports only types from `@design.estate/dees-catalog` and no runtime code at all, so it runs in the browser beside the components as well as on the server beside the harness.
|
|
10
10
|
|