@modelprofile.com/flexharness-models 8.1.0 → 8.3.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_models/index.d.ts +1 -0
- package/dist_ts_models/index.js +2 -1
- package/dist_ts_models/smartai.limits.d.ts +68 -0
- package/dist_ts_models/smartai.limits.js +185 -0
- package/package.json +1 -1
- package/readme.md +9 -0
- package/ts_models/index.ts +1 -0
- package/ts_models/readme.md +9 -0
- package/ts_models/smartai.limits.ts +253 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './interfaces.js';
|
|
2
2
|
export * from './classes.modelregistry.js';
|
|
3
3
|
export * from './smartai.cache.js';
|
|
4
|
+
export * from './smartai.limits.js';
|
|
4
5
|
export { generateText, streamText, tool, jsonSchema } from 'ai';
|
|
5
6
|
export type { ModelMessage, ToolSet, StreamTextResult } from 'ai';
|
package/dist_ts_models/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './interfaces.js';
|
|
2
2
|
export * from './classes.modelregistry.js';
|
|
3
3
|
export * from './smartai.cache.js';
|
|
4
|
+
export * from './smartai.limits.js';
|
|
4
5
|
export { generateText, streamText, tool, jsonSchema } from 'ai';
|
|
5
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19tb2RlbHMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxPQUFPLEVBQUUsWUFBWSxFQUFFLFVBQVUsRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFLE1BQU0sSUFBSSxDQUFDIn0=
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { LanguageModelV4Middleware } from '@ai-sdk/provider';
|
|
2
|
+
/**
|
|
3
|
+
* `usage_limit`: the account's quota is spent until it resets, so no retry can succeed earlier.
|
|
4
|
+
* `rate_limit`: a short throttle that a retry after the advertised delay may pass.
|
|
5
|
+
*/
|
|
6
|
+
export type TModelLimitKind = 'usage_limit' | 'rate_limit';
|
|
7
|
+
export interface IModelLimitInfo {
|
|
8
|
+
kind: TModelLimitKind;
|
|
9
|
+
/** The provider that reported the limit, for example `openai` or `anthropic`. */
|
|
10
|
+
provider: string;
|
|
11
|
+
/** When the limit resets, in epoch milliseconds. */
|
|
12
|
+
resetsAt?: number;
|
|
13
|
+
/** The delay the provider asked for before the next request, in milliseconds. */
|
|
14
|
+
retryAfterMs?: number;
|
|
15
|
+
/** The account plan the provider reported. */
|
|
16
|
+
planType?: string;
|
|
17
|
+
/** The exhausted limit window, for example `5h` or `7d`. */
|
|
18
|
+
window?: string;
|
|
19
|
+
}
|
|
20
|
+
/** What a provider dialect reads from a failure; the limit middleware adds the provider. */
|
|
21
|
+
export type TModelLimitClassification = Omit<IModelLimitInfo, 'provider'>;
|
|
22
|
+
/** A failed model call reduced to the parts a limit dialect reads. */
|
|
23
|
+
export interface IModelCallFailure {
|
|
24
|
+
/** When the failure was observed, in epoch milliseconds; relative reset times count from here. */
|
|
25
|
+
observedAt: number;
|
|
26
|
+
/** HTTP status of the failed response, when the provider reported one. */
|
|
27
|
+
statusCode?: number;
|
|
28
|
+
/** Response headers with lower-case names. */
|
|
29
|
+
headers: Readonly<Record<string, string>>;
|
|
30
|
+
/** The parsed JSON error body, or the error frame of a failed stream. */
|
|
31
|
+
body?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** Reads one provider's limit dialect; returns undefined for failures that are not limits. */
|
|
34
|
+
export type TModelLimitClassifier = (failure: IModelCallFailure) => TModelLimitClassification | undefined;
|
|
35
|
+
export interface IModelLimitMiddlewareOptions {
|
|
36
|
+
provider: string;
|
|
37
|
+
classify: TModelLimitClassifier;
|
|
38
|
+
}
|
|
39
|
+
/** Upper bound for the provider, plan and window names a limit carries. */
|
|
40
|
+
export declare const MODEL_LIMIT_TEXT_MAX_LENGTH = 128;
|
|
41
|
+
/** The value as a JSON object, or undefined for arrays, primitives and null. */
|
|
42
|
+
export declare function readJsonRecord(value: unknown): Readonly<Record<string, unknown>> | undefined;
|
|
43
|
+
/** Whether a value is a complete, bounded limit description. */
|
|
44
|
+
export declare function isModelLimitInfo(value: unknown): value is IModelLimitInfo;
|
|
45
|
+
/** A short sentence naming the limit and, when known, its reset time. */
|
|
46
|
+
export declare function describeModelLimit(limit: IModelLimitInfo): string;
|
|
47
|
+
/** A provider refused a model call because a usage or rate limit was reached. */
|
|
48
|
+
export declare class ModelLimitError extends Error {
|
|
49
|
+
readonly limit: Readonly<IModelLimitInfo>;
|
|
50
|
+
constructor(limit: IModelLimitInfo, options?: ErrorOptions);
|
|
51
|
+
}
|
|
52
|
+
/** Recognizes a ModelLimitError, also one created by another copy of this package. */
|
|
53
|
+
export declare function isModelLimitError(error: unknown): error is ModelLimitError;
|
|
54
|
+
/** Reads `retry-after-ms`, then `retry-after` in seconds or as an HTTP date, from lower-case headers. */
|
|
55
|
+
export declare function readRetryAfterMs(headers: Readonly<Record<string, string>> | undefined, now?: number): number | undefined;
|
|
56
|
+
/** Converts a Unix timestamp in seconds, as a number or numeric string, to epoch milliseconds. */
|
|
57
|
+
export declare function readEpochSeconds(value: unknown): number | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* A limit from provider-reported values, which are untrusted: a delay or reset time outside the
|
|
60
|
+
* representable range and an overlong plan or window name are dropped, and a missing reset time
|
|
61
|
+
* follows from the delay. Kind and provider are kept as given.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createModelLimitInfo(reported: IModelLimitInfo, observedAt: number): IModelLimitInfo;
|
|
64
|
+
/**
|
|
65
|
+
* Turns the failures a provider dialect recognizes as limits into ModelLimitError, keeping the
|
|
66
|
+
* original failure as its cause. Covers thrown call errors and error parts of a running stream.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createModelLimitMiddleware(options: IModelLimitMiddlewareOptions): LanguageModelV4Middleware;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { APICallError } from '@ai-sdk/provider';
|
|
2
|
+
/** Upper bound for the provider, plan and window names a limit carries. */
|
|
3
|
+
export const MODEL_LIMIT_TEXT_MAX_LENGTH = 128;
|
|
4
|
+
/** The latest instant a Date can represent, in epoch milliseconds. */
|
|
5
|
+
const MAX_DATE_EPOCH_MS = 8_640_000_000_000_000;
|
|
6
|
+
const modelLimitKinds = ['usage_limit', 'rate_limit'];
|
|
7
|
+
const modelLimitKeys = new Set([
|
|
8
|
+
'kind',
|
|
9
|
+
'provider',
|
|
10
|
+
'resetsAt',
|
|
11
|
+
'retryAfterMs',
|
|
12
|
+
'planType',
|
|
13
|
+
'window',
|
|
14
|
+
]);
|
|
15
|
+
const modelLimitLabels = {
|
|
16
|
+
usage_limit: 'Usage limit',
|
|
17
|
+
rate_limit: 'Rate limit',
|
|
18
|
+
};
|
|
19
|
+
const modelLimitErrorMarker = Symbol.for('@modelprofile.com/flexharness-models/ModelLimitError');
|
|
20
|
+
/** The value as a JSON object, or undefined for arrays, primitives and null. */
|
|
21
|
+
export function readJsonRecord(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
23
|
+
? value
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
function isLimitText(value) {
|
|
27
|
+
return typeof value === 'string' && value.length > 0 && value.length <= MODEL_LIMIT_TEXT_MAX_LENGTH;
|
|
28
|
+
}
|
|
29
|
+
function isLimitDelay(value) {
|
|
30
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
31
|
+
}
|
|
32
|
+
function isLimitInstant(value) {
|
|
33
|
+
return isLimitDelay(value) && value <= MAX_DATE_EPOCH_MS;
|
|
34
|
+
}
|
|
35
|
+
/** Whether a value is a complete, bounded limit description. */
|
|
36
|
+
export function isModelLimitInfo(value) {
|
|
37
|
+
const limit = readJsonRecord(value);
|
|
38
|
+
if (!limit || Object.keys(limit).some((key) => !modelLimitKeys.has(key)))
|
|
39
|
+
return false;
|
|
40
|
+
return modelLimitKinds.includes(limit.kind)
|
|
41
|
+
&& isLimitText(limit.provider)
|
|
42
|
+
&& (limit.resetsAt === undefined || isLimitInstant(limit.resetsAt))
|
|
43
|
+
&& (limit.retryAfterMs === undefined || isLimitDelay(limit.retryAfterMs))
|
|
44
|
+
&& (limit.planType === undefined || isLimitText(limit.planType))
|
|
45
|
+
&& (limit.window === undefined || isLimitText(limit.window));
|
|
46
|
+
}
|
|
47
|
+
/** A short sentence naming the limit and, when known, its reset time. */
|
|
48
|
+
export function describeModelLimit(limit) {
|
|
49
|
+
const label = modelLimitLabels[limit.kind];
|
|
50
|
+
return limit.resetsAt === undefined
|
|
51
|
+
? `${label} reached.`
|
|
52
|
+
: `${label} reached; resets at ${new Date(limit.resetsAt).toISOString()}.`;
|
|
53
|
+
}
|
|
54
|
+
/** A provider refused a model call because a usage or rate limit was reached. */
|
|
55
|
+
export class ModelLimitError extends Error {
|
|
56
|
+
limit;
|
|
57
|
+
constructor(limit, options) {
|
|
58
|
+
if (!isModelLimitInfo(limit))
|
|
59
|
+
throw new TypeError('ModelLimitError requires a valid limit description.');
|
|
60
|
+
super(describeModelLimit(limit), options);
|
|
61
|
+
this.name = 'ModelLimitError';
|
|
62
|
+
this.limit = Object.freeze({ ...limit });
|
|
63
|
+
Object.defineProperty(this, modelLimitErrorMarker, { value: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Recognizes a ModelLimitError, also one created by another copy of this package. */
|
|
67
|
+
export function isModelLimitError(error) {
|
|
68
|
+
return error instanceof Error
|
|
69
|
+
&& error[modelLimitErrorMarker] === true;
|
|
70
|
+
}
|
|
71
|
+
/** Reads `retry-after-ms`, then `retry-after` in seconds or as an HTTP date, from lower-case headers. */
|
|
72
|
+
export function readRetryAfterMs(headers, now = Date.now()) {
|
|
73
|
+
const milliseconds = Number.parseFloat(headers?.['retry-after-ms'] ?? '');
|
|
74
|
+
if (Number.isFinite(milliseconds) && milliseconds >= 0)
|
|
75
|
+
return Math.ceil(milliseconds);
|
|
76
|
+
const retryAfter = headers?.['retry-after'];
|
|
77
|
+
if (retryAfter === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
const seconds = Number.parseFloat(retryAfter);
|
|
80
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
81
|
+
return Math.ceil(seconds * 1000);
|
|
82
|
+
const delay = Date.parse(retryAfter) - now;
|
|
83
|
+
return Number.isFinite(delay) && delay > 0 ? Math.ceil(delay) : undefined;
|
|
84
|
+
}
|
|
85
|
+
/** Converts a Unix timestamp in seconds, as a number or numeric string, to epoch milliseconds. */
|
|
86
|
+
export function readEpochSeconds(value) {
|
|
87
|
+
const seconds = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
88
|
+
return typeof seconds === 'number' && Number.isFinite(seconds) && seconds >= 0
|
|
89
|
+
? Math.round(seconds * 1000)
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
function lowerCaseHeaders(headers) {
|
|
93
|
+
const normalized = {};
|
|
94
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
95
|
+
if (typeof value === 'string')
|
|
96
|
+
normalized[name.toLowerCase()] = value;
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
function parseJsonBody(body) {
|
|
101
|
+
if (body === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(body);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return body;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A limit from provider-reported values, which are untrusted: a delay or reset time outside the
|
|
112
|
+
* representable range and an overlong plan or window name are dropped, and a missing reset time
|
|
113
|
+
* follows from the delay. Kind and provider are kept as given.
|
|
114
|
+
*/
|
|
115
|
+
export function createModelLimitInfo(reported, observedAt) {
|
|
116
|
+
const retryAfterMs = isLimitDelay(reported.retryAfterMs) ? reported.retryAfterMs : undefined;
|
|
117
|
+
const resetsAt = [reported.resetsAt, retryAfterMs === undefined ? undefined : observedAt + retryAfterMs]
|
|
118
|
+
.find(isLimitInstant);
|
|
119
|
+
return {
|
|
120
|
+
kind: reported.kind,
|
|
121
|
+
provider: reported.provider,
|
|
122
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
123
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
124
|
+
...(isLimitText(reported.planType) ? { planType: reported.planType } : {}),
|
|
125
|
+
...(isLimitText(reported.window) ? { window: reported.window } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function describeFailure(error, streamHeaders, observedAt) {
|
|
129
|
+
if (APICallError.isInstance(error)) {
|
|
130
|
+
return {
|
|
131
|
+
observedAt,
|
|
132
|
+
...(error.statusCode === undefined ? {} : { statusCode: error.statusCode }),
|
|
133
|
+
headers: lowerCaseHeaders(error.responseHeaders),
|
|
134
|
+
body: parseJsonBody(error.responseBody) ?? error.data,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { observedAt, headers: lowerCaseHeaders(streamHeaders), body: error };
|
|
138
|
+
}
|
|
139
|
+
function completeLimit(provider, classification, failure) {
|
|
140
|
+
return createModelLimitInfo({
|
|
141
|
+
...classification,
|
|
142
|
+
provider,
|
|
143
|
+
retryAfterMs: classification.retryAfterMs ?? readRetryAfterMs(failure.headers, failure.observedAt),
|
|
144
|
+
}, failure.observedAt);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Turns the failures a provider dialect recognizes as limits into ModelLimitError, keeping the
|
|
148
|
+
* original failure as its cause. Covers thrown call errors and error parts of a running stream.
|
|
149
|
+
*/
|
|
150
|
+
export function createModelLimitMiddleware(options) {
|
|
151
|
+
const classifyError = (error, streamHeaders) => {
|
|
152
|
+
if (isModelLimitError(error))
|
|
153
|
+
return error;
|
|
154
|
+
const failure = describeFailure(error, streamHeaders, Date.now());
|
|
155
|
+
const classification = options.classify(failure);
|
|
156
|
+
return classification === undefined
|
|
157
|
+
? error
|
|
158
|
+
: new ModelLimitError(completeLimit(options.provider, classification, failure), { cause: error });
|
|
159
|
+
};
|
|
160
|
+
const call = async (operation) => {
|
|
161
|
+
try {
|
|
162
|
+
return await operation();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw classifyError(error);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
specificationVersion: 'v4',
|
|
170
|
+
wrapGenerate: ({ doGenerate }) => call(doGenerate),
|
|
171
|
+
wrapStream: async ({ doStream }) => {
|
|
172
|
+
const result = await call(doStream);
|
|
173
|
+
const headers = result.response?.headers;
|
|
174
|
+
return {
|
|
175
|
+
...result,
|
|
176
|
+
stream: result.stream.pipeThrough(new TransformStream({
|
|
177
|
+
transform: (part, controller) => {
|
|
178
|
+
controller.enqueue(part.type === 'error' ? { ...part, error: classifyError(part.error, headers) } : part);
|
|
179
|
+
},
|
|
180
|
+
})),
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhaS5saW1pdHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19tb2RlbHMvc21hcnRhaS5saW1pdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBa0RoRCwyRUFBMkU7QUFDM0UsTUFBTSxDQUFDLE1BQU0sMkJBQTJCLEdBQUcsR0FBRyxDQUFDO0FBRS9DLHNFQUFzRTtBQUN0RSxNQUFNLGlCQUFpQixHQUFHLHFCQUFxQixDQUFDO0FBRWhELE1BQU0sZUFBZSxHQUErQixDQUFDLGFBQWEsRUFBRSxZQUFZLENBQUMsQ0FBQztBQUNsRixNQUFNLGNBQWMsR0FBd0IsSUFBSSxHQUFHLENBQUM7SUFDbEQsTUFBTTtJQUNOLFVBQVU7SUFDVixVQUFVO0lBQ1YsY0FBYztJQUNkLFVBQVU7SUFDVixRQUFRO0NBQ1QsQ0FBQyxDQUFDO0FBQ0gsTUFBTSxnQkFBZ0IsR0FBb0M7SUFDeEQsV0FBVyxFQUFFLGFBQWE7SUFDMUIsVUFBVSxFQUFFLFlBQVk7Q0FDekIsQ0FBQztBQUNGLE1BQU0scUJBQXFCLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxzREFBc0QsQ0FBQyxDQUFDO0FBRWpHLGdGQUFnRjtBQUNoRixNQUFNLFVBQVUsY0FBYyxDQUFDLEtBQWM7SUFDM0MsT0FBTyxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksS0FBSyxLQUFLLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQ3pFLENBQUMsQ0FBQyxLQUFnQztRQUNsQyxDQUFDLENBQUMsU0FBUyxDQUFDO0FBQ2hCLENBQUM7QUFFRCxTQUFTLFdBQVcsQ0FBQyxLQUFjO0lBQ2pDLE9BQU8sT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxNQUFNLEdBQUcsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLElBQUksMkJBQTJCLENBQUM7QUFDdEcsQ0FBQztB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
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Task Venture Capital GmbH",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "@modelprofile.com/flexharness-models",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.3.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "Provider-independent model contracts, prompt caching and explicit model registration.",
|
|
8
8
|
"main": "./dist_ts_models/index.js",
|
package/readme.md
CHANGED
|
@@ -15,6 +15,15 @@ pnpm add @modelprofile.com/flexharness-models
|
|
|
15
15
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
16
16
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
17
17
|
|
|
18
|
+
`ModelLimitError` is the one typed error for provider usage and rate limits; its `limit`
|
|
19
|
+
(`IModelLimitInfo`) carries `kind`, `provider`, and when known `resetsAt` (epoch milliseconds),
|
|
20
|
+
`retryAfterMs`, `planType` and `window`. `createModelLimitMiddleware({ provider, classify })`
|
|
21
|
+
raises it for the failures a provider dialect recognizes, from thrown calls and from stream error
|
|
22
|
+
parts, keeping the provider failure as `cause`. A classifier receives the failure's `observedAt` time for relative
|
|
23
|
+
reset delays; a delay or reset time outside the range a `Date` can represent is dropped from the
|
|
24
|
+
limit. `createModelLimitInfo`, `isModelLimitError`, `isModelLimitInfo`, `describeModelLimit`,
|
|
25
|
+
`readRetryAfterMs`, `readEpochSeconds` and `readJsonRecord` are exported for dialects and hosts.
|
|
26
|
+
|
|
18
27
|
## License and Legal Information
|
|
19
28
|
|
|
20
29
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
package/ts_models/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './interfaces.js';
|
|
2
2
|
export * from './classes.modelregistry.js';
|
|
3
3
|
export * from './smartai.cache.js';
|
|
4
|
+
export * from './smartai.limits.js';
|
|
4
5
|
export { generateText, streamText, tool, jsonSchema } from 'ai';
|
|
5
6
|
export type { ModelMessage, ToolSet, StreamTextResult } from 'ai';
|
package/ts_models/readme.md
CHANGED
|
@@ -15,6 +15,15 @@ pnpm add @modelprofile.com/flexharness-models
|
|
|
15
15
|
See the [toolbox usage and migration guide](https://code.foss.global/modelprofile.com/flexharness#readme)
|
|
16
16
|
for composition examples and the SmartAI/SmartAgent migration map.
|
|
17
17
|
|
|
18
|
+
`ModelLimitError` is the one typed error for provider usage and rate limits; its `limit`
|
|
19
|
+
(`IModelLimitInfo`) carries `kind`, `provider`, and when known `resetsAt` (epoch milliseconds),
|
|
20
|
+
`retryAfterMs`, `planType` and `window`. `createModelLimitMiddleware({ provider, classify })`
|
|
21
|
+
raises it for the failures a provider dialect recognizes, from thrown calls and from stream error
|
|
22
|
+
parts, keeping the provider failure as `cause`. A classifier receives the failure's `observedAt` time for relative
|
|
23
|
+
reset delays; a delay or reset time outside the range a `Date` can represent is dropped from the
|
|
24
|
+
limit. `createModelLimitInfo`, `isModelLimitError`, `isModelLimitInfo`, `describeModelLimit`,
|
|
25
|
+
`readRetryAfterMs`, `readEpochSeconds` and `readJsonRecord` are exported for dialects and hosts.
|
|
26
|
+
|
|
18
27
|
## License and Legal Information
|
|
19
28
|
|
|
20
29
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { APICallError } from '@ai-sdk/provider';
|
|
2
|
+
import type {
|
|
3
|
+
LanguageModelV4Middleware,
|
|
4
|
+
LanguageModelV4StreamPart,
|
|
5
|
+
SharedV4Headers,
|
|
6
|
+
} from '@ai-sdk/provider';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `usage_limit`: the account's quota is spent until it resets, so no retry can succeed earlier.
|
|
10
|
+
* `rate_limit`: a short throttle that a retry after the advertised delay may pass.
|
|
11
|
+
*/
|
|
12
|
+
export type TModelLimitKind = 'usage_limit' | 'rate_limit';
|
|
13
|
+
|
|
14
|
+
export interface IModelLimitInfo {
|
|
15
|
+
kind: TModelLimitKind;
|
|
16
|
+
/** The provider that reported the limit, for example `openai` or `anthropic`. */
|
|
17
|
+
provider: string;
|
|
18
|
+
/** When the limit resets, in epoch milliseconds. */
|
|
19
|
+
resetsAt?: number;
|
|
20
|
+
/** The delay the provider asked for before the next request, in milliseconds. */
|
|
21
|
+
retryAfterMs?: number;
|
|
22
|
+
/** The account plan the provider reported. */
|
|
23
|
+
planType?: string;
|
|
24
|
+
/** The exhausted limit window, for example `5h` or `7d`. */
|
|
25
|
+
window?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What a provider dialect reads from a failure; the limit middleware adds the provider. */
|
|
29
|
+
export type TModelLimitClassification = Omit<IModelLimitInfo, 'provider'>;
|
|
30
|
+
|
|
31
|
+
/** A failed model call reduced to the parts a limit dialect reads. */
|
|
32
|
+
export interface IModelCallFailure {
|
|
33
|
+
/** When the failure was observed, in epoch milliseconds; relative reset times count from here. */
|
|
34
|
+
observedAt: number;
|
|
35
|
+
/** HTTP status of the failed response, when the provider reported one. */
|
|
36
|
+
statusCode?: number;
|
|
37
|
+
/** Response headers with lower-case names. */
|
|
38
|
+
headers: Readonly<Record<string, string>>;
|
|
39
|
+
/** The parsed JSON error body, or the error frame of a failed stream. */
|
|
40
|
+
body?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reads one provider's limit dialect; returns undefined for failures that are not limits. */
|
|
44
|
+
export type TModelLimitClassifier = (failure: IModelCallFailure) => TModelLimitClassification | undefined;
|
|
45
|
+
|
|
46
|
+
export interface IModelLimitMiddlewareOptions {
|
|
47
|
+
provider: string;
|
|
48
|
+
classify: TModelLimitClassifier;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Upper bound for the provider, plan and window names a limit carries. */
|
|
52
|
+
export const MODEL_LIMIT_TEXT_MAX_LENGTH = 128;
|
|
53
|
+
|
|
54
|
+
/** The latest instant a Date can represent, in epoch milliseconds. */
|
|
55
|
+
const MAX_DATE_EPOCH_MS = 8_640_000_000_000_000;
|
|
56
|
+
|
|
57
|
+
const modelLimitKinds: readonly TModelLimitKind[] = ['usage_limit', 'rate_limit'];
|
|
58
|
+
const modelLimitKeys: ReadonlySet<string> = new Set([
|
|
59
|
+
'kind',
|
|
60
|
+
'provider',
|
|
61
|
+
'resetsAt',
|
|
62
|
+
'retryAfterMs',
|
|
63
|
+
'planType',
|
|
64
|
+
'window',
|
|
65
|
+
]);
|
|
66
|
+
const modelLimitLabels: Record<TModelLimitKind, string> = {
|
|
67
|
+
usage_limit: 'Usage limit',
|
|
68
|
+
rate_limit: 'Rate limit',
|
|
69
|
+
};
|
|
70
|
+
const modelLimitErrorMarker = Symbol.for('@modelprofile.com/flexharness-models/ModelLimitError');
|
|
71
|
+
|
|
72
|
+
/** The value as a JSON object, or undefined for arrays, primitives and null. */
|
|
73
|
+
export function readJsonRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
74
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
75
|
+
? value as Record<string, unknown>
|
|
76
|
+
: undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isLimitText(value: unknown): value is string {
|
|
80
|
+
return typeof value === 'string' && value.length > 0 && value.length <= MODEL_LIMIT_TEXT_MAX_LENGTH;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isLimitDelay(value: unknown): value is number {
|
|
84
|
+
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isLimitInstant(value: unknown): value is number {
|
|
88
|
+
return isLimitDelay(value) && value <= MAX_DATE_EPOCH_MS;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether a value is a complete, bounded limit description. */
|
|
92
|
+
export function isModelLimitInfo(value: unknown): value is IModelLimitInfo {
|
|
93
|
+
const limit = readJsonRecord(value);
|
|
94
|
+
if (!limit || Object.keys(limit).some((key) => !modelLimitKeys.has(key))) return false;
|
|
95
|
+
return modelLimitKinds.includes(limit.kind as TModelLimitKind)
|
|
96
|
+
&& isLimitText(limit.provider)
|
|
97
|
+
&& (limit.resetsAt === undefined || isLimitInstant(limit.resetsAt))
|
|
98
|
+
&& (limit.retryAfterMs === undefined || isLimitDelay(limit.retryAfterMs))
|
|
99
|
+
&& (limit.planType === undefined || isLimitText(limit.planType))
|
|
100
|
+
&& (limit.window === undefined || isLimitText(limit.window));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A short sentence naming the limit and, when known, its reset time. */
|
|
104
|
+
export function describeModelLimit(limit: IModelLimitInfo): string {
|
|
105
|
+
const label = modelLimitLabels[limit.kind];
|
|
106
|
+
return limit.resetsAt === undefined
|
|
107
|
+
? `${label} reached.`
|
|
108
|
+
: `${label} reached; resets at ${new Date(limit.resetsAt).toISOString()}.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A provider refused a model call because a usage or rate limit was reached. */
|
|
112
|
+
export class ModelLimitError extends Error {
|
|
113
|
+
public readonly limit: Readonly<IModelLimitInfo>;
|
|
114
|
+
|
|
115
|
+
constructor(limit: IModelLimitInfo, options?: ErrorOptions) {
|
|
116
|
+
if (!isModelLimitInfo(limit)) throw new TypeError('ModelLimitError requires a valid limit description.');
|
|
117
|
+
super(describeModelLimit(limit), options);
|
|
118
|
+
this.name = 'ModelLimitError';
|
|
119
|
+
this.limit = Object.freeze({ ...limit });
|
|
120
|
+
Object.defineProperty(this, modelLimitErrorMarker, { value: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Recognizes a ModelLimitError, also one created by another copy of this package. */
|
|
125
|
+
export function isModelLimitError(error: unknown): error is ModelLimitError {
|
|
126
|
+
return error instanceof Error
|
|
127
|
+
&& (error as Error & Record<symbol, unknown>)[modelLimitErrorMarker] === true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Reads `retry-after-ms`, then `retry-after` in seconds or as an HTTP date, from lower-case headers. */
|
|
131
|
+
export function readRetryAfterMs(
|
|
132
|
+
headers: Readonly<Record<string, string>> | undefined,
|
|
133
|
+
now = Date.now(),
|
|
134
|
+
): number | undefined {
|
|
135
|
+
const milliseconds = Number.parseFloat(headers?.['retry-after-ms'] ?? '');
|
|
136
|
+
if (Number.isFinite(milliseconds) && milliseconds >= 0) return Math.ceil(milliseconds);
|
|
137
|
+
const retryAfter = headers?.['retry-after'];
|
|
138
|
+
if (retryAfter === undefined) return undefined;
|
|
139
|
+
const seconds = Number.parseFloat(retryAfter);
|
|
140
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1000);
|
|
141
|
+
const delay = Date.parse(retryAfter) - now;
|
|
142
|
+
return Number.isFinite(delay) && delay > 0 ? Math.ceil(delay) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Converts a Unix timestamp in seconds, as a number or numeric string, to epoch milliseconds. */
|
|
146
|
+
export function readEpochSeconds(value: unknown): number | undefined {
|
|
147
|
+
const seconds = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
148
|
+
return typeof seconds === 'number' && Number.isFinite(seconds) && seconds >= 0
|
|
149
|
+
? Math.round(seconds * 1000)
|
|
150
|
+
: undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function lowerCaseHeaders(headers: SharedV4Headers | undefined): Record<string, string> {
|
|
154
|
+
const normalized: Record<string, string> = {};
|
|
155
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
156
|
+
if (typeof value === 'string') normalized[name.toLowerCase()] = value;
|
|
157
|
+
}
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseJsonBody(body: string | undefined): unknown {
|
|
162
|
+
if (body === undefined) return undefined;
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(body);
|
|
165
|
+
} catch {
|
|
166
|
+
return body;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A limit from provider-reported values, which are untrusted: a delay or reset time outside the
|
|
172
|
+
* representable range and an overlong plan or window name are dropped, and a missing reset time
|
|
173
|
+
* follows from the delay. Kind and provider are kept as given.
|
|
174
|
+
*/
|
|
175
|
+
export function createModelLimitInfo(reported: IModelLimitInfo, observedAt: number): IModelLimitInfo {
|
|
176
|
+
const retryAfterMs = isLimitDelay(reported.retryAfterMs) ? reported.retryAfterMs : undefined;
|
|
177
|
+
const resetsAt = [reported.resetsAt, retryAfterMs === undefined ? undefined : observedAt + retryAfterMs]
|
|
178
|
+
.find(isLimitInstant);
|
|
179
|
+
return {
|
|
180
|
+
kind: reported.kind,
|
|
181
|
+
provider: reported.provider,
|
|
182
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
183
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
184
|
+
...(isLimitText(reported.planType) ? { planType: reported.planType } : {}),
|
|
185
|
+
...(isLimitText(reported.window) ? { window: reported.window } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function describeFailure(
|
|
190
|
+
error: unknown,
|
|
191
|
+
streamHeaders: SharedV4Headers | undefined,
|
|
192
|
+
observedAt: number,
|
|
193
|
+
): IModelCallFailure {
|
|
194
|
+
if (APICallError.isInstance(error)) {
|
|
195
|
+
return {
|
|
196
|
+
observedAt,
|
|
197
|
+
...(error.statusCode === undefined ? {} : { statusCode: error.statusCode }),
|
|
198
|
+
headers: lowerCaseHeaders(error.responseHeaders),
|
|
199
|
+
body: parseJsonBody(error.responseBody) ?? error.data,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { observedAt, headers: lowerCaseHeaders(streamHeaders), body: error };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function completeLimit(
|
|
206
|
+
provider: string,
|
|
207
|
+
classification: TModelLimitClassification,
|
|
208
|
+
failure: IModelCallFailure,
|
|
209
|
+
): IModelLimitInfo {
|
|
210
|
+
return createModelLimitInfo({
|
|
211
|
+
...classification,
|
|
212
|
+
provider,
|
|
213
|
+
retryAfterMs: classification.retryAfterMs ?? readRetryAfterMs(failure.headers, failure.observedAt),
|
|
214
|
+
}, failure.observedAt);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Turns the failures a provider dialect recognizes as limits into ModelLimitError, keeping the
|
|
219
|
+
* original failure as its cause. Covers thrown call errors and error parts of a running stream.
|
|
220
|
+
*/
|
|
221
|
+
export function createModelLimitMiddleware(options: IModelLimitMiddlewareOptions): LanguageModelV4Middleware {
|
|
222
|
+
const classifyError = (error: unknown, streamHeaders?: SharedV4Headers): unknown => {
|
|
223
|
+
if (isModelLimitError(error)) return error;
|
|
224
|
+
const failure = describeFailure(error, streamHeaders, Date.now());
|
|
225
|
+
const classification = options.classify(failure);
|
|
226
|
+
return classification === undefined
|
|
227
|
+
? error
|
|
228
|
+
: new ModelLimitError(completeLimit(options.provider, classification, failure), { cause: error });
|
|
229
|
+
};
|
|
230
|
+
const call = async <TResult>(operation: () => PromiseLike<TResult>): Promise<TResult> => {
|
|
231
|
+
try {
|
|
232
|
+
return await operation();
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw classifyError(error);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
specificationVersion: 'v4',
|
|
239
|
+
wrapGenerate: ({ doGenerate }) => call(doGenerate),
|
|
240
|
+
wrapStream: async ({ doStream }) => {
|
|
241
|
+
const result = await call(doStream);
|
|
242
|
+
const headers = result.response?.headers;
|
|
243
|
+
return {
|
|
244
|
+
...result,
|
|
245
|
+
stream: result.stream.pipeThrough(new TransformStream<LanguageModelV4StreamPart, LanguageModelV4StreamPart>({
|
|
246
|
+
transform: (part, controller) => {
|
|
247
|
+
controller.enqueue(part.type === 'error' ? { ...part, error: classifyError(part.error, headers) } : part);
|
|
248
|
+
},
|
|
249
|
+
})),
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|