@modelprofile.com/flexharness 8.3.0 → 8.5.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.
@@ -24,6 +24,13 @@ When constructing the summary, try to stick to this template:
24
24
 
25
25
  export interface ICompactMessagesOptions {
26
26
  abortSignal?: AbortSignal;
27
+ /**
28
+ * Receives the usage of every model call the compaction makes, once per call: `reported` with the
29
+ * provider's usage, or `unreported` for a call that failed or was aborted, retried attempts
30
+ * included. A context compactor passes its own options through, so the calls reach the session's
31
+ * `onUsage`. Must not throw.
32
+ */
33
+ reportUsage?: plugins.TAgentModelCallUsageReporter;
27
34
  }
28
35
 
29
36
  /**
@@ -32,7 +39,7 @@ export interface ICompactMessagesOptions {
32
39
  *
33
40
  * @param model The same model used by runAgent, or a cheaper small model
34
41
  * @param messages The full message history that overflowed
35
- * @param options Invocation options, including cancellation
42
+ * @param options Invocation options, including cancellation and usage reporting
36
43
  * @returns A minimal ModelMessage[] containing the summary as context
37
44
  */
38
45
  export async function compactMessages(
@@ -40,20 +47,60 @@ export async function compactMessages(
40
47
  messages: plugins.ModelMessage[],
41
48
  options: ICompactMessagesOptions = {},
42
49
  ): Promise<plugins.ModelMessage[]> {
43
- const result = await plugins.generateText({
50
+ const prompt: plugins.ModelMessage[] = [
51
+ ...messages,
52
+ { role: 'user', content: COMPACTION_PROMPT },
53
+ ];
54
+ const reportUsage = options.reportUsage;
55
+ if (!reportUsage) {
56
+ const result = await plugins.generateText({
57
+ model,
58
+ abortSignal: options.abortSignal,
59
+ allowSystemInMessages: true,
60
+ messages: prompt,
61
+ });
62
+ return summaryMessages(result.text);
63
+ }
64
+ const modelCalls = new plugins.AgentModelCallUsageRecorder(model, reportUsage);
65
+ // Each attempt, retries included, is one model call: it starts in the middleware, which also
66
+ // settles an attempt that throws; a call whose response arrives ends in onLanguageModelCallEnd.
67
+ const observedModel = plugins.wrapLanguageModel({
44
68
  model,
45
- abortSignal: options.abortSignal,
46
- allowSystemInMessages: true,
47
- messages: [
48
- ...messages,
49
- { role: 'user', content: COMPACTION_PROMPT },
50
- ],
69
+ middleware: {
70
+ wrapGenerate: async ({ doGenerate, params }) => {
71
+ modelCalls.start();
72
+ try {
73
+ return await doGenerate();
74
+ } catch (error) {
75
+ modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
76
+ throw error;
77
+ }
78
+ },
79
+ },
51
80
  });
81
+ try {
82
+ const result = await plugins.generateText({
83
+ model: observedModel,
84
+ abortSignal: options.abortSignal,
85
+ allowSystemInMessages: true,
86
+ messages: prompt,
87
+ onLanguageModelCallEnd: ({ modelId, usage }) => {
88
+ modelCalls.end(modelId, usage);
89
+ },
90
+ });
91
+ modelCalls.settle('missing');
92
+ return summaryMessages(result.text);
93
+ } catch (error) {
94
+ modelCalls.settle(options.abortSignal?.aborted ? 'aborted' : 'failed');
95
+ throw error;
96
+ }
97
+ }
52
98
 
99
+ function summaryMessages(summary: string): plugins.ModelMessage[] {
53
100
  return [
54
101
  {
55
102
  role: 'user',
56
- content: `[Previous conversation summary]\n\n${result.text}\n\n[End of summary. Continue from here.]`,
103
+ content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary. Continue from here.]`,
57
104
  },
58
105
  ];
59
106
  }
@@ -36,6 +36,14 @@ export interface IDocumentRequestOptions {
36
36
  * exceeded the limit are not rendered.
37
37
  */
38
38
  maxPages?: number;
39
+ /**
40
+ * Called once for every model call of the request, as soon as its usage is known, whatever the
41
+ * request's outcome: `reported` with the provider's usage, or `unreported` with the reason when
42
+ * the call failed, was aborted or its response carried no usage. A retried attempt is a call of
43
+ * its own. Nothing is reported for a request that ends before its model is called. When a request
44
+ * returns, its reported calls sum to its `usage`. Must not throw.
45
+ */
46
+ onUsage?: plugins.TAgentModelCallUsageReporter;
39
47
  }
40
48
 
41
49
  export interface IDocumentOptions extends IDocumentRequestOptions {
@@ -120,6 +128,7 @@ export async function extractTextFromPdfWithUsage(
120
128
  providerOptions: options.providerOptions,
121
129
  abortSignal: options.abortSignal,
122
130
  maxPages: options.maxPages,
131
+ onUsage: options.onUsage,
123
132
  });
124
133
  }
125
134
 
@@ -156,26 +165,61 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
156
165
  // Filter out empty buffers
157
166
  const validPages = imagePages.filter(page => page && page.length > 0);
158
167
 
159
- const result = await plugins.generateText({
160
- model: options.model,
161
- instructions: options.systemMessage,
162
- messages: [
163
- ...(options.messageHistory ?? []),
164
- {
165
- role: 'user',
166
- content: [
167
- { type: 'text', text: options.userMessage },
168
- ...validPages.map(page => ({
169
- type: 'file' as const,
170
- data: page,
171
- mediaType: 'image/png' as const,
172
- })),
173
- ],
174
- },
175
- ],
176
- providerOptions: options.providerOptions,
177
- abortSignal,
178
- });
168
+ const onUsage = options.onUsage;
169
+ const modelCalls = onUsage ? new plugins.AgentModelCallUsageRecorder(options.model, onUsage) : null;
170
+ // Each attempt, retries included, is one model call: it starts in the middleware, which also
171
+ // settles an attempt that throws; a call whose response arrives ends in onLanguageModelCallEnd.
172
+ const model = modelCalls
173
+ ? plugins.wrapLanguageModel({
174
+ model: options.model,
175
+ middleware: {
176
+ wrapGenerate: async ({ doGenerate, params }) => {
177
+ modelCalls.start();
178
+ try {
179
+ return await doGenerate();
180
+ } catch (error) {
181
+ modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
182
+ throw error;
183
+ }
184
+ },
185
+ },
186
+ })
187
+ : options.model;
188
+ const messages: ModelMessage[] = [
189
+ ...(options.messageHistory ?? []),
190
+ {
191
+ role: 'user',
192
+ content: [
193
+ { type: 'text', text: options.userMessage },
194
+ ...validPages.map(page => ({
195
+ type: 'file' as const,
196
+ data: page,
197
+ mediaType: 'image/png' as const,
198
+ })),
199
+ ],
200
+ },
201
+ ];
202
+ let result: Awaited<ReturnType<typeof plugins.generateText>>;
203
+ try {
204
+ result = await plugins.generateText({
205
+ model,
206
+ instructions: options.systemMessage,
207
+ messages,
208
+ providerOptions: options.providerOptions,
209
+ abortSignal,
210
+ ...(modelCalls
211
+ ? {
212
+ onLanguageModelCallEnd: ({ modelId, usage: callUsage }) => {
213
+ modelCalls.end(modelId, callUsage);
214
+ },
215
+ }
216
+ : {}),
217
+ });
218
+ modelCalls?.settle('missing');
219
+ } catch (error) {
220
+ modelCalls?.settle(abortSignal?.aborted ? 'aborted' : 'failed');
221
+ throw error;
222
+ }
179
223
  const usage = result.totalUsage;
180
224
  const inputTokens = usage.inputTokens ?? 0;
181
225
  const outputTokens = usage.outputTokens ?? 0;
@@ -1,3 +1,5 @@
1
- import { generateText } from 'ai';
1
+ import { generateText, wrapLanguageModel } from 'ai';
2
2
  import * as smartpdf from '@push.rocks/smartpdf';
3
- export { generateText, smartpdf };
3
+ import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
4
+ export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
5
+ export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
@@ -23,11 +23,12 @@ Supply a model from your selected FlexHarness provider. `extractTextFromPdf` rep
23
23
 
24
24
  `analyzeDocuments({ model, userMessage, pdfDocuments, systemMessage?, messageHistory? })` supports general analysis across multiple PDFs. This component owns a shared SmartPDF instance; call `stopSmartpdf()` when all document work has finished. Image-only OCR is available independently in `flexharness/ocr`.
25
25
 
26
- Both functions accept three request options:
26
+ Both functions accept four request options:
27
27
 
28
28
  - `providerOptions` is passed to the model call unchanged, for example `{ openai: { store: false } }` so the provider does not keep the request as application state.
29
29
  - `abortSignal` is checked before each document is rendered and before the model call, and is passed to the model call. SmartPDF cannot stop a document it is already rendering, so that render finishes first.
30
30
  - `maxPages` is the most rendered pages one request may send to the model. A request with more pages rejects with `DocumentPageLimitError` (`renderedPages`, `maxPages`) before the model is called, and documents after the one that exceeded the limit are not rendered.
31
+ - `onUsage` receives the usage of each model call of the request, also of a request that fails or is aborted; see [Usage of a request that fails or is aborted](#usage-of-a-request-that-fails-or-is-aborted).
31
32
 
32
33
  `extractTextFromPdfWithUsage` and `analyzeDocumentsWithUsage` take the same arguments and return `{ text, usage, pageCount }`. `usage` has the shape of an agent run's usage (`inputTokens`, `outputTokens`, `totalTokens`, `cacheReadTokens`, `cacheWriteTokens`), summed over the request's model steps, with usage the provider leaves out counted as zero; `pageCount` is the number of rendered pages sent to the model.
33
34
 
@@ -49,6 +50,28 @@ try {
49
50
  }
50
51
  ```
51
52
 
53
+ ### Usage of a request that fails or is aborted
54
+
55
+ A request that throws returns no `usage`. `onUsage` reports each model call of a request once, as soon as its usage is known, whatever the outcome, with the events of `AgentModelCallUsageRecorder` (the type is `TFlexModelCallUsage` from `@modelprofile.com/flexharness`):
56
+
57
+ ```typescript
58
+ import type { TFlexModelCallUsage } from '@modelprofile.com/flexharness';
59
+
60
+ const calls: TFlexModelCallUsage[] = [];
61
+ try {
62
+ await extractTextFromPdfWithUsage(pdfBytes, { model, abortSignal, onUsage: (call) => calls.push(call) });
63
+ } finally {
64
+ const reported = calls.filter((call) => call.status === 'reported');
65
+ const complete = reported.length === calls.length; // an unreported call used an unknown number of tokens
66
+ }
67
+ ```
68
+
69
+ - `status: 'reported'`: the provider reported the call's `usage`; `requestedModelId` is the model's id and `responseModelId` the id the response named.
70
+ - `status: 'unreported'`: the call `failed`, was `aborted`, or its response carried no usage (`missing`). The provider may still have consumed tokens for it.
71
+ - Every attempt is a call of its own: a call retried after a failure reports the failed attempt and the retry.
72
+ - A request that ends before its model is called (an abort during rendering, `DocumentPageLimitError`) reports nothing.
73
+ - When a request returns, its reported calls sum to its `usage`; count one or the other. `onUsage` must not throw.
74
+
52
75
  ## Issue Reporting and Security
53
76
 
54
77
  Report issues and security concerns through [community.foss.global](https://community.foss.global).