@cubicecho/agent-core 2.23.0 → 2.25.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/README.md +14 -9
- package/dist/agent-loop.d.ts +84 -204
- package/dist/agent-loop.js +416 -643
- package/dist/calibration.d.ts +14 -10
- package/dist/calibration.js +30 -22
- package/dist/capabilities.d.ts +140 -35
- package/dist/capabilities.js +345 -152
- package/dist/client.d.ts +104 -35
- package/dist/client.js +210 -78
- package/dist/compaction.d.ts +103 -60
- package/dist/compaction.js +163 -93
- package/dist/config.d.ts +48 -12
- package/dist/config.js +20 -1
- package/dist/continuation.d.ts +25 -17
- package/dist/continuation.js +70 -47
- package/dist/digest.d.ts +7 -0
- package/dist/digest.js +18 -0
- package/dist/errors.d.ts +24 -15
- package/dist/errors.js +23 -15
- package/dist/events.d.ts +108 -58
- package/dist/events.js +265 -128
- package/dist/guards.d.ts +53 -0
- package/dist/guards.js +55 -0
- package/dist/hook-events.d.ts +38 -0
- package/dist/hook-events.js +36 -0
- package/dist/hooks.d.ts +105 -83
- package/dist/hooks.js +224 -139
- package/dist/index.d.ts +30 -25
- package/dist/index.js +29 -23
- package/dist/ledger.d.ts +62 -30
- package/dist/ledger.js +163 -56
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +13 -0
- package/dist/preselect.d.ts +222 -0
- package/dist/preselect.js +290 -0
- package/dist/request-body.d.ts +32 -0
- package/dist/request-body.js +47 -0
- package/dist/reset.d.ts +1 -0
- package/dist/reset.js +7 -6
- package/dist/retry.d.ts +40 -143
- package/dist/retry.js +79 -285
- package/dist/run-calls.d.ts +123 -0
- package/dist/run-calls.js +351 -0
- package/dist/run-turn.d.ts +44 -16
- package/dist/run-turn.js +74 -37
- package/dist/runtime.d.ts +23 -17
- package/dist/runtime.js +34 -24
- package/dist/schema-compat.d.ts +51 -11
- package/dist/schema-compat.js +375 -193
- package/dist/scope.d.ts +28 -5
- package/dist/scope.js +37 -10
- package/dist/side-task.d.ts +127 -39
- package/dist/side-task.js +189 -100
- package/dist/snapshot.d.ts +12 -7
- package/dist/snapshot.js +87 -65
- package/dist/spec.d.ts +66 -36
- package/dist/spec.js +514 -265
- package/dist/stream.d.ts +73 -16
- package/dist/stream.js +228 -98
- package/dist/thinking.d.ts +23 -8
- package/dist/thinking.js +96 -46
- package/dist/tokens.d.ts +171 -6
- package/dist/tokens.js +326 -6
- package/dist/tool-calls.d.ts +89 -15
- package/dist/tool-calls.js +205 -85
- package/dist/tool-loading.d.ts +109 -187
- package/dist/tool-loading.js +286 -372
- package/dist/wire.d.ts +81 -0
- package/dist/wire.js +73 -0
- package/llms.txt +87 -43
- package/package.json +4 -4
package/dist/capabilities.js
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
1
|
-
import { endpointId } from
|
|
2
|
-
import { errorMessage } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { endpointId } from './client.js';
|
|
2
|
+
import { errorMessage } from './errors.js';
|
|
3
|
+
import { getOrCreate } from './guards.js';
|
|
4
|
+
import { isGrammarError } from './schema-compat.js';
|
|
5
|
+
import { scoped } from './scope.js';
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* first one's refusal must not quietly strip pattern/format from the second one's requests, or
|
|
13
|
-
* silently cost it its token counts, for the rest of the process. Bounded by the number of
|
|
14
|
-
* endpoints ever configured, which is a settings row's worth.
|
|
15
|
-
*
|
|
16
|
-
* The API key is part of that identity, the same as it is for the client pool and the model
|
|
17
|
-
* listings. A router — LiteLLM, OpenRouter, a gateway with several boxes behind it — is free to
|
|
18
|
-
* send two keys to two different backends, and then what one of them refused is not a fact about
|
|
19
|
-
* the other. Keyed on the URL alone the first caller through the gateway latched for everyone
|
|
20
|
-
* behind it, including the `models` map underneath, which is the level where two keys through
|
|
21
|
-
* one host are most likely to differ at all.
|
|
7
|
+
* A model's on/off flags as it starts out, every one optimistic.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A record over every boolean on `ModelCapabilities`, so a flag added there does not compile until
|
|
11
|
+
* it is listed here — and this is the list a snapshot is exported, imported and judged empty by,
|
|
12
|
+
* where each of those used to name the flags again and one could be missed.
|
|
22
13
|
*/
|
|
14
|
+
export const OPTIMISTIC_MODEL = {
|
|
15
|
+
reasoningEffort: true,
|
|
16
|
+
legacyTokenLimit: true,
|
|
17
|
+
chosenTemperature: true,
|
|
18
|
+
structuredOutput: true,
|
|
19
|
+
assistantPrefill: true,
|
|
20
|
+
};
|
|
21
|
+
/** The names of those flags, for a caller that reads or latches each in turn. */
|
|
22
|
+
export const MODEL_FLAGS = Object.keys(OPTIMISTIC_MODEL);
|
|
23
23
|
const capabilities = scoped(() => new Map());
|
|
24
24
|
/**
|
|
25
|
-
* What this endpoint is known not to support.
|
|
26
|
-
* latches off stays off.
|
|
25
|
+
* What this endpoint is known not to support.
|
|
27
26
|
*
|
|
28
|
-
* @param baseUrl Where the endpoint is. The two flags on it are per-server; what is per-model
|
|
27
|
+
* @param baseUrl - Where the endpoint is. The two flags on it are per-server; what is per-model
|
|
29
28
|
* hangs off `models`, which `modelCapabilitiesFor` reads.
|
|
30
|
-
* @param apiKey The rest of the endpoint's identity. Optional, because it changes nothing for
|
|
29
|
+
* @param [apiKey] - The rest of the endpoint's identity. Optional, because it changes nothing for
|
|
31
30
|
* the ordinary case of one key per base URL and a caller with one need not thread it through;
|
|
32
31
|
* absent reads as `NO_KEY`, exactly as it does in `getClient`, so an endpoint with no key and
|
|
33
32
|
* one that passes `undefined` share an entry rather than holding two.
|
|
33
|
+
* @returns The endpoint's live entry, made with both flags on and no models the first time it is
|
|
34
|
+
* asked for.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* The same object every time, so what `negotiate` latches off stays off.
|
|
34
38
|
*/
|
|
35
39
|
export function capabilitiesFor(baseUrl, apiKey) {
|
|
36
40
|
return capabilitiesById(endpointId({ baseUrl, apiKey }));
|
|
@@ -39,57 +43,60 @@ export function capabilitiesFor(baseUrl, apiKey) {
|
|
|
39
43
|
* What this model on this endpoint is known not to support. The same object every time, so what
|
|
40
44
|
* `negotiate` latches off stays off.
|
|
41
45
|
*
|
|
46
|
+
* @param supports - The endpoint's own, as `capabilitiesFor` hands it over.
|
|
47
|
+
* @param model - The name the endpoint knows the model as — whatever goes in the request body,
|
|
48
|
+
* since that is the only name the refusal is about.
|
|
49
|
+
* @returns The model's live entry under `supports.models`, made with every flag on and nothing
|
|
50
|
+
* refused the first time it is asked for.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
42
53
|
* Nested under the endpoint rather than keyed by name alone, because `gpt-4o` at OpenAI and
|
|
43
54
|
* `gpt-4o` behind a proxy need not be the same weights — and a proxy is free to answer to a name
|
|
44
55
|
* it does not really serve. One that refused a reasoning effort must not speak for the other.
|
|
45
56
|
* Bounded by the models actually asked for on that endpoint, which is what a dropdown holds.
|
|
46
|
-
*
|
|
47
|
-
* @param supports The endpoint's own, as `capabilitiesFor` hands it over.
|
|
48
|
-
* @param model The name the endpoint knows the model as — whatever goes in the request body,
|
|
49
|
-
* since that is the only name the refusal is about.
|
|
50
57
|
*/
|
|
51
58
|
export function modelCapabilitiesFor(supports, model) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
chosenTemperature: true,
|
|
58
|
-
refusedFields: new Set(),
|
|
59
|
-
structuredOutput: true,
|
|
60
|
-
refusedEfforts: new Set(),
|
|
61
|
-
assistantPrefill: true,
|
|
62
|
-
};
|
|
63
|
-
supports.models.set(model, known);
|
|
64
|
-
}
|
|
65
|
-
return known;
|
|
59
|
+
return getOrCreate(supports.models, model, () => ({
|
|
60
|
+
...OPTIMISTIC_MODEL,
|
|
61
|
+
refusedFields: new Set(),
|
|
62
|
+
refusedEfforts: new Set(),
|
|
63
|
+
}));
|
|
66
64
|
}
|
|
67
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Every endpoint's capabilities by `endpointId`, the live objects, for `exportCapabilities`.
|
|
67
|
+
*
|
|
68
|
+
* @returns The map itself rather than a copy, read-only by type alone: what is latched or forgotten
|
|
69
|
+
* afterwards shows through it.
|
|
70
|
+
*/
|
|
68
71
|
export const knownCapabilities = () => capabilities();
|
|
69
72
|
/**
|
|
70
73
|
* The capabilities of the endpoint with this `endpointId`, created optimistic if unseen — the way
|
|
71
74
|
* `importCapabilities` reaches an endpoint it has only a digest for.
|
|
75
|
+
*
|
|
76
|
+
* @param id - The digest `endpointId` gives. Not checked: any string opens an entry under itself.
|
|
77
|
+
* @returns The live entry, the same object `capabilitiesFor` hands over for that endpoint.
|
|
72
78
|
*/
|
|
73
79
|
export function capabilitiesById(id) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
return getOrCreate(capabilities(), id, () => ({
|
|
81
|
+
strictSchemas: true,
|
|
82
|
+
usageInStream: true,
|
|
83
|
+
models: new Map(),
|
|
84
|
+
since: Date.now(),
|
|
85
|
+
}));
|
|
80
86
|
}
|
|
81
87
|
/**
|
|
82
88
|
* Forgets what one endpoint refused, or every endpoint's when told none.
|
|
83
89
|
*
|
|
90
|
+
* @param [endpoint] - Whose to forget, by the same identity `capabilitiesFor` takes. Absent clears
|
|
91
|
+
* every endpoint, which is what tests and `resetAll` mean by it.
|
|
92
|
+
* @returns Whether there was anything to forget.
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
84
95
|
* A latch never unlatches on its own, so a server upgraded behind the same URL — a newer
|
|
85
96
|
* llama.cpp that compiles the grammar, a proxy that has learned `stream_options` — keeps being
|
|
86
97
|
* sent the downgraded request for the life of the process. This is the seam for a consumer that
|
|
87
98
|
* *knows* it changed: a settings row saved, a health check that reads a new build string, an
|
|
88
99
|
* operator pressing a button. See `expireCapabilities` for the case where nobody knows.
|
|
89
|
-
*
|
|
90
|
-
* @param endpoint Whose to forget, by the same identity `capabilitiesFor` takes. Absent clears
|
|
91
|
-
* every endpoint, which is what tests and `resetAll` mean by it.
|
|
92
|
-
* @returns Whether there was anything to forget.
|
|
93
100
|
*/
|
|
94
101
|
export function resetCapabilities(endpoint) {
|
|
95
102
|
if (!endpoint) {
|
|
@@ -102,6 +109,11 @@ export function resetCapabilities(endpoint) {
|
|
|
102
109
|
/**
|
|
103
110
|
* Forgets every endpoint whose entry is older than this, so the next request finds out again.
|
|
104
111
|
*
|
|
112
|
+
* @param maxAgeMs - How old an entry may be. Zero or less expires everything.
|
|
113
|
+
* @param [now] - The clock, for tests.
|
|
114
|
+
* @returns How many endpoints were forgotten.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
105
117
|
* The other half of the problem `resetCapabilities` solves: a server upgraded behind the same URL
|
|
106
118
|
* with nobody to notice. What an expiry costs is one round trip per endpoint and model — the next
|
|
107
119
|
* request carries the field again, and a server that still refuses it refuses it once and
|
|
@@ -111,16 +123,13 @@ export function resetCapabilities(endpoint) {
|
|
|
111
123
|
*
|
|
112
124
|
* Nothing calls this on a timer. When to sweep is the consumer's, the same way how stale a
|
|
113
125
|
* snapshot is too stale is, and a sweep costs a walk of one settings row's worth of entries.
|
|
114
|
-
*
|
|
115
|
-
* @param maxAgeMs How old an entry may be. Zero or less expires everything.
|
|
116
|
-
* @param now The clock, for tests.
|
|
117
|
-
* @returns How many endpoints were forgotten.
|
|
118
126
|
*/
|
|
119
127
|
export function expireCapabilities(maxAgeMs, now = Date.now()) {
|
|
120
128
|
let dropped = 0;
|
|
121
129
|
for (const [id, known] of capabilities()) {
|
|
122
|
-
if (now - known.since < maxAgeMs)
|
|
130
|
+
if (now - known.since < maxAgeMs) {
|
|
123
131
|
continue;
|
|
132
|
+
}
|
|
124
133
|
capabilities().delete(id);
|
|
125
134
|
dropped++;
|
|
126
135
|
}
|
|
@@ -128,6 +137,15 @@ export function expireCapabilities(maxAgeMs, now = Date.now()) {
|
|
|
128
137
|
}
|
|
129
138
|
/**
|
|
130
139
|
* Every latching flag in play on one attempt, endpoint and model together, in a stable order.
|
|
140
|
+
*
|
|
141
|
+
* @param supports - The endpoint's. Only its booleans are read, which leaves `models` and `since`
|
|
142
|
+
* out of the reading.
|
|
143
|
+
* @param model - The named model's. Absent, for a request that named none, the reading is the
|
|
144
|
+
* endpoint's alone.
|
|
145
|
+
* @returns A fresh array: the endpoint's booleans, then every value on the model, a set standing
|
|
146
|
+
* as its size.
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
131
149
|
* Read positionally and only against another reading of the same two objects: what it answers is
|
|
132
150
|
* whether anything moved while the request was out, and a flag added to either interface later is
|
|
133
151
|
* compared without an edit here. `models` is not one of them — it is the second level, not a
|
|
@@ -135,22 +153,26 @@ export function expireCapabilities(maxAgeMs, now = Date.now()) {
|
|
|
135
153
|
* set is the same object on both readings and only ever grows.
|
|
136
154
|
*/
|
|
137
155
|
const flagsOf = (supports, model) => [
|
|
138
|
-
...Object.values(supports).filter((value) => typeof value ===
|
|
139
|
-
...(model
|
|
140
|
-
? Object.values(model).map((value) => (value instanceof Set ? value.size : value))
|
|
141
|
-
: []),
|
|
156
|
+
...Object.values(supports).filter((value) => typeof value === 'boolean'),
|
|
157
|
+
...(model ? Object.values(model).map((value) => (value instanceof Set ? value.size : value)) : []),
|
|
142
158
|
];
|
|
143
159
|
/** `stream_options` is named in the refusal by every server that has not heard of it. */
|
|
144
160
|
const REJECTS_USAGE = /stream_options/i;
|
|
145
161
|
/**
|
|
146
|
-
* `response_format` or its `json_schema` type refused, rather than the schema in it.
|
|
147
|
-
*
|
|
148
|
-
*
|
|
162
|
+
* `response_format` or its `json_schema` type refused, rather than the schema in it.
|
|
163
|
+
*
|
|
164
|
+
* @param detail - The server's error text.
|
|
165
|
+
* @returns `true` where the text names either; `false` where it also says `invalid schema`.
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* A server that validates the schema and finds it wanting — `Invalid schema for response_format`
|
|
169
|
+
* — is telling the caller about their schema, and falling back to words for good would hide that.
|
|
149
170
|
*/
|
|
150
171
|
const rejectsResponseFormat = (detail) => /response_format|json_schema/i.test(detail) && !/invalid schema/i.test(detail);
|
|
151
172
|
/**
|
|
152
173
|
* A refusal of the *value* rather than of the field, which names the field either way.
|
|
153
174
|
*
|
|
175
|
+
* @remarks
|
|
154
176
|
* `Unsupported value: 'reasoning_effort' does not support 'none' with this model. Supported
|
|
155
177
|
* values are: 'minimal', 'low', 'medium', and 'high'.` A model that answers this reasons
|
|
156
178
|
* perfectly well; it was handed an effort off a list this package does not know. Dropping the
|
|
@@ -161,16 +183,26 @@ const rejectsResponseFormat = (detail) => /response_format|json_schema/i.test(de
|
|
|
161
183
|
*/
|
|
162
184
|
const REFUSED_VALUE = /unsupported value|invalid value|supported values/i;
|
|
163
185
|
/**
|
|
164
|
-
* A model that cannot reason refuses the field by name — and only the field.
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
186
|
+
* A model that cannot reason refuses the field by name — and only the field.
|
|
187
|
+
*
|
|
188
|
+
* @param detail - The server's error text.
|
|
189
|
+
* @returns `true` where the text names `reasoning_effort` and is not a refusal of its value.
|
|
190
|
+
*
|
|
191
|
+
* @remarks
|
|
192
|
+
* See `REFUSED_VALUE` for the refusal that names it too and means the opposite, which is the
|
|
193
|
+
* caller's to see rather than ours to work around. `does not support` is deliberately not the
|
|
194
|
+
* marker: a proxy that words a real field refusal as `this model does not support
|
|
195
|
+
* reasoning_effort` has to keep latching.
|
|
169
196
|
*/
|
|
170
197
|
const rejectsEffort = (detail) => /reasoning_effort/i.test(detail) && !REFUSED_VALUE.test(detail);
|
|
198
|
+
/** The setting that asks for no `reasoning_effort` field at all. Ours, and never sent. */
|
|
199
|
+
export const EFFORT_OFF = 'off';
|
|
200
|
+
/** The lowest effort the wire has a word for, which is what a side task asks for. */
|
|
201
|
+
export const EFFORT_NONE = 'none';
|
|
171
202
|
/**
|
|
172
203
|
* The efforts this package can place, cheapest first. A substitution only ever walks *up* it.
|
|
173
204
|
*
|
|
205
|
+
* @remarks
|
|
174
206
|
* Not a list of what any model takes — `medium` is refused by a model whose list is
|
|
175
207
|
* `minimal, low, high`, and `xhigh` is real and deliberately absent. It is the order the values
|
|
176
208
|
* OpenAI has shipped stand in, which is all a step needs to know. A value not on it cannot be
|
|
@@ -178,21 +210,37 @@ const rejectsEffort = (detail) => /reasoning_effort/i.test(detail) && !REFUSED_V
|
|
|
178
210
|
* refused `xhigh` to `high` would quietly answer a question with less deliberation than whoever
|
|
179
211
|
* typed it asked for, where stepping up from `none` to `minimal` only costs tokens and says so.
|
|
180
212
|
*/
|
|
181
|
-
export const EFFORT_LADDER = [
|
|
182
|
-
|
|
213
|
+
export const EFFORT_LADDER = [EFFORT_NONE, 'minimal', 'low', 'medium', 'high'];
|
|
214
|
+
/** The ladder as plain strings, so a value that is not on it can be looked for. */
|
|
215
|
+
const LADDER = EFFORT_LADDER;
|
|
216
|
+
/**
|
|
217
|
+
* Where an effort stands on the ladder, whatever case it was written in.
|
|
218
|
+
*
|
|
219
|
+
* @param effort - The effort as a config or a refusal spelled it.
|
|
220
|
+
* @returns Its index in `EFFORT_LADDER`, or -1 for one the ladder cannot place.
|
|
221
|
+
*/
|
|
222
|
+
const rankOf = (effort) => LADDER.indexOf(effort.toLowerCase());
|
|
183
223
|
/**
|
|
184
224
|
* The efforts a refusal lists as this model's: `Supported values are: 'minimal', 'low', 'medium',
|
|
185
|
-
* and 'high'.`
|
|
225
|
+
* and 'high'.`
|
|
226
|
+
*
|
|
227
|
+
* @param detail - The server's error text.
|
|
228
|
+
* @returns The listed efforts the ladder can place, lowercased and in the order the refusal gave
|
|
229
|
+
* them; `undefined` where it lists none, or none that can be placed.
|
|
230
|
+
*
|
|
231
|
+
* @remarks
|
|
232
|
+
* Quoted or bare, separated by commas and a trailing `and` or `or`.
|
|
186
233
|
*/
|
|
187
234
|
function listedEfforts(detail) {
|
|
188
235
|
const listed = detail.match(/supported values(?:\s+\w+)?\s*(?:are|is|include)?\s*:?\s*([^\n.]+)/i)?.[1];
|
|
189
|
-
if (!listed)
|
|
236
|
+
if (!listed) {
|
|
190
237
|
return undefined;
|
|
238
|
+
}
|
|
191
239
|
const values = listed
|
|
192
240
|
.split(/,|\band\b|\bor\b/)
|
|
193
241
|
.map((value) => value
|
|
194
242
|
.trim()
|
|
195
|
-
.replace(/^['"`]+|['"`]+$/g,
|
|
243
|
+
.replace(/^['"`]+|['"`]+$/g, '')
|
|
196
244
|
.toLowerCase())
|
|
197
245
|
.filter((value) => rankOf(value) >= 0);
|
|
198
246
|
return values.length ? values : undefined;
|
|
@@ -201,13 +249,16 @@ function listedEfforts(detail) {
|
|
|
201
249
|
* Which effort a refusal says was refused, in the two shapes the wording takes: the field quoted
|
|
202
250
|
* then `does not support 'none'`, and `reasoning_effort: none`.
|
|
203
251
|
*
|
|
252
|
+
* @param detail - The refusal.
|
|
253
|
+
* @param [supported] - What the same refusal listed, which the value cannot be one of.
|
|
254
|
+
* @returns The value, lowercased; `undefined` where the refusal names none in either shape, or
|
|
255
|
+
* names one that `supported` holds.
|
|
256
|
+
*
|
|
257
|
+
* @remarks
|
|
204
258
|
* Read rather than remembered, because `negotiate` builds no request and so does not know what
|
|
205
259
|
* went out — and a proxy that rewrites the value before passing it on is refusing the one it sent
|
|
206
260
|
* rather than the one it was given. A refusal that names no value steps nothing: guessing which
|
|
207
261
|
* rung was refused is how a ladder walks past the value that would have worked.
|
|
208
|
-
*
|
|
209
|
-
* @param detail The refusal.
|
|
210
|
-
* @param supported What the same refusal listed, which the value cannot be one of.
|
|
211
262
|
*/
|
|
212
263
|
function refusedEffortValue(detail, supported = []) {
|
|
213
264
|
const found = detail.match(/reasoning_effort['"`]?\s*(?:does not support|is not supported with|:)\s*['"`]?([\w-]+)/i)?.[1];
|
|
@@ -218,70 +269,122 @@ function refusedEffortValue(detail, supported = []) {
|
|
|
218
269
|
* The cheapest effort above this one that the model has not refused, or `undefined` when the
|
|
219
270
|
* ladder is out of rungs.
|
|
220
271
|
*
|
|
272
|
+
* @param refused - What the model has refused and, where a refusal published one, its list. Not
|
|
273
|
+
* written to.
|
|
274
|
+
* @param asked - The effort to step up from, in any case. One the ladder cannot place steps
|
|
275
|
+
* nowhere.
|
|
276
|
+
* @returns The effort to send instead, lowercased as the list or the ladder holds it; `undefined`
|
|
277
|
+
* too where `asked` is not on the ladder.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
221
280
|
* Above, never below: see `EFFORT_LADDER`. Where a refusal published a list that is the whole of
|
|
222
281
|
* what is tried, so the step lands in one request; where it published none the ladder is walked a
|
|
223
282
|
* rung at a time, each refusal latching the rung it named.
|
|
224
283
|
*/
|
|
225
284
|
function nextEffort(refused, asked) {
|
|
226
285
|
const rank = rankOf(asked);
|
|
227
|
-
if (rank < 0)
|
|
286
|
+
if (rank < 0) {
|
|
228
287
|
return undefined;
|
|
288
|
+
}
|
|
229
289
|
let best;
|
|
230
290
|
for (const value of refused.supportedEfforts ?? EFFORT_LADDER) {
|
|
231
291
|
const at = rankOf(value);
|
|
232
|
-
if (at <= rank || refused.refusedEfforts.has(value))
|
|
292
|
+
if (at <= rank || refused.refusedEfforts.has(value)) {
|
|
233
293
|
continue;
|
|
234
|
-
|
|
294
|
+
}
|
|
295
|
+
if (best === undefined || at < rankOf(best)) {
|
|
235
296
|
best = value;
|
|
297
|
+
}
|
|
236
298
|
}
|
|
237
299
|
return best;
|
|
238
300
|
}
|
|
239
301
|
/**
|
|
240
302
|
* What to actually put in `reasoning_effort` for a model that has refused the value asked for.
|
|
241
303
|
*
|
|
304
|
+
* @param refused - What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
305
|
+
* has refused nothing.
|
|
306
|
+
* @param asked - What the config asks for. `"off"` and absent mean no effort.
|
|
307
|
+
* @returns The value for the field, or the empty string for no field. `asked` itself, as written,
|
|
308
|
+
* where the model has not refused it — and where it has but there is no rung left to step to.
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
242
311
|
* The caller's own value, until this model has said that value is not one of its own; then the
|
|
243
312
|
* cheapest it will take that is at least as much deliberation. A model that takes no effort at
|
|
244
313
|
* all, and an absent or `"off"` setting, both answer the empty string, which is the body
|
|
245
314
|
* builders' signal to send no field.
|
|
246
|
-
*
|
|
247
|
-
* @param refused What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
248
|
-
* has refused nothing.
|
|
249
|
-
* @param asked What the config asks for. `"off"` and absent mean no effort.
|
|
250
315
|
*/
|
|
251
316
|
export function effortFor(refused, asked) {
|
|
252
|
-
if (!asked || asked ===
|
|
253
|
-
return
|
|
254
|
-
|
|
317
|
+
if (!asked || asked === EFFORT_OFF) {
|
|
318
|
+
return '';
|
|
319
|
+
}
|
|
320
|
+
if (!refused) {
|
|
255
321
|
return asked;
|
|
256
|
-
|
|
257
|
-
|
|
322
|
+
}
|
|
323
|
+
if (!refused.reasoningEffort) {
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
258
326
|
const known = refused.supportedEfforts;
|
|
259
327
|
const listed = known ? known.includes(asked.toLowerCase()) : true;
|
|
260
|
-
if (listed && !refused.refusedEfforts.has(asked.toLowerCase()))
|
|
328
|
+
if (listed && !refused.refusedEfforts.has(asked.toLowerCase())) {
|
|
261
329
|
return asked;
|
|
330
|
+
}
|
|
262
331
|
return nextEffort(refused, asked) ?? asked;
|
|
263
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* The reply ceiling and the temperature as this model takes them, to spread into a request body.
|
|
335
|
+
*
|
|
336
|
+
* @param refused - What the model has refused, as `negotiate` hands it over. Absent is a model that
|
|
337
|
+
* has refused nothing.
|
|
338
|
+
* @param maxTokens - The ceiling. Zero or less sends none and leaves it to the server: a zero sent
|
|
339
|
+
* as `max_tokens: 0` asks for an empty reply.
|
|
340
|
+
* @param temperature - What to sample at, where the model takes one that was picked for it.
|
|
341
|
+
* @returns A fresh object holding the ceiling under one of its two names, or under neither, and
|
|
342
|
+
* `temperature` unless the model refused ours.
|
|
343
|
+
*
|
|
344
|
+
* @remarks
|
|
345
|
+
* The reasoning models want the ceiling spelled the other way, and one that will only run at the
|
|
346
|
+
* temperature it was built with is sent none. Both are tested `=== false`, so a caller that named
|
|
347
|
+
* no model sends what one that has refused nothing is sent; see `ModelCapabilities.legacyTokenLimit`.
|
|
348
|
+
*/
|
|
349
|
+
export const ceilingAndTemperature = (refused, maxTokens, temperature) => {
|
|
350
|
+
const ceiling = refused?.legacyTokenLimit === false ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens };
|
|
351
|
+
return {
|
|
352
|
+
...(maxTokens > 0 ? ceiling : {}),
|
|
353
|
+
...(refused?.chosenTemperature === false ? {} : { temperature }),
|
|
354
|
+
};
|
|
355
|
+
};
|
|
264
356
|
/**
|
|
265
357
|
* How to answer a refused effort *value*, or `undefined` when there is nothing new to learn or
|
|
266
358
|
* nowhere left to go — in which case the refusal is the caller's, as it was before this existed.
|
|
267
359
|
*
|
|
360
|
+
* @param detail - The server's error text.
|
|
361
|
+
* @param refused - What the model has refused so far. Read and never written to: the next rung is
|
|
362
|
+
* worked out against a copy.
|
|
363
|
+
* @returns The value to latch, what would be sent once it is, and the list the refusal published
|
|
364
|
+
* where it published one.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
268
367
|
* Pure, so `negotiate` can work out whether this refusal is one of its own before deciding to
|
|
269
368
|
* answer it, and latch only in the branch it takes.
|
|
270
369
|
*/
|
|
271
370
|
function planEffortStep(detail, refused) {
|
|
272
|
-
if (!refused.reasoningEffort)
|
|
371
|
+
if (!refused.reasoningEffort) {
|
|
273
372
|
return undefined;
|
|
274
|
-
|
|
373
|
+
}
|
|
374
|
+
if (!/reasoning_effort/i.test(detail) || !REFUSED_VALUE.test(detail)) {
|
|
275
375
|
return undefined;
|
|
376
|
+
}
|
|
276
377
|
const supported = listedEfforts(detail);
|
|
277
378
|
const value = refusedEffortValue(detail, supported);
|
|
278
|
-
if (value === undefined)
|
|
379
|
+
if (value === undefined) {
|
|
279
380
|
return undefined;
|
|
381
|
+
}
|
|
280
382
|
// Something has to be new, or a server that answers every request by naming an effort nobody
|
|
281
383
|
// sent would have `negotiate` re-send for as long as it kept saying it.
|
|
282
384
|
const fresh = supported?.join() !== refused.supportedEfforts?.join();
|
|
283
|
-
if (refused.refusedEfforts.has(value) && !fresh)
|
|
385
|
+
if (refused.refusedEfforts.has(value) && !fresh) {
|
|
284
386
|
return undefined;
|
|
387
|
+
}
|
|
285
388
|
const next = nextEffort({
|
|
286
389
|
...refused,
|
|
287
390
|
refusedEfforts: new Set(refused.refusedEfforts).add(value),
|
|
@@ -293,6 +396,10 @@ function planEffortStep(detail, refused) {
|
|
|
293
396
|
* Read only alongside the name it is asking for: `'max_tokens' is not supported with this model.
|
|
294
397
|
* Use 'max_completion_tokens' instead.`
|
|
295
398
|
*
|
|
399
|
+
* @param detail - The server's error text.
|
|
400
|
+
* @returns `true` where the text names both spellings; `max_tokens` on its own is not enough.
|
|
401
|
+
*
|
|
402
|
+
* @remarks
|
|
296
403
|
* A bare `max_tokens` complaint is also how a server says the *number* was too large —
|
|
297
404
|
* `max_tokens is too large: 200000. This model supports at most 16384.` — and the answer to that
|
|
298
405
|
* is not to send the same number under a different name. It is to let the error out, where
|
|
@@ -302,6 +409,7 @@ const wantsCompletionLimit = (detail) => /max_tokens/i.test(detail) && /max_comp
|
|
|
302
409
|
/**
|
|
303
410
|
* Temperature named as the field being refused, rather than mentioned in passing.
|
|
304
411
|
*
|
|
412
|
+
* @remarks
|
|
305
413
|
* A bare search of the message is not that question. `Unsupported value: 'top_p' does not
|
|
306
414
|
* support 3 with this model. Adjust temperature instead.` refuses another field and merely says
|
|
307
415
|
* the word, and it disabled ours — the word is ordinary English about a model, so any advice,
|
|
@@ -312,6 +420,11 @@ const NAMES_TEMPERATURE = /(['"`])temperature\1|\btemperature\s+(?:is|does|must|
|
|
|
312
420
|
/**
|
|
313
421
|
* `'temperature' does not support 0.7 with this model. Only the default (1) is supported.`
|
|
314
422
|
*
|
|
423
|
+
* @param detail - The server's error text.
|
|
424
|
+
* @returns `true` where the text names temperature as the field refused and says `only the
|
|
425
|
+
* default`; either without the other is `false`.
|
|
426
|
+
*
|
|
427
|
+
* @remarks
|
|
315
428
|
* The qualifier is load-bearing, and `does not support` is not the qualifier: that is how a
|
|
316
429
|
* server words a refusal of the *value* — `Invalid value: 'temperature' does not support 5.
|
|
317
430
|
* Supported values are between 0 and 2.` — which is the caller's mistake to see rather than
|
|
@@ -333,28 +446,139 @@ const refusesChosenTemperature = (detail) => NAMES_TEMPERATURE.test(detail) && /
|
|
|
333
446
|
/**
|
|
334
447
|
* The fields a refusal names as ones the endpoint has never heard of, in the two wordings OpenAI
|
|
335
448
|
* has used: `Unrecognized request argument supplied: min_p` (or `arguments`, listing several) and
|
|
336
|
-
* `Unknown parameter: 'min_p'.`
|
|
337
|
-
*
|
|
449
|
+
* `Unknown parameter: 'min_p'.`
|
|
450
|
+
*
|
|
451
|
+
* @param detail - The server's error text.
|
|
452
|
+
* @returns The top-level names, in the refusal's order and not deduplicated; empty where it is in
|
|
453
|
+
* neither wording.
|
|
454
|
+
*
|
|
455
|
+
* @remarks
|
|
456
|
+
* A nested name — `'chat_template_kwargs.enable_thinking'` — is answered at its top-level field,
|
|
457
|
+
* which is the only level a body builder leaves things out at.
|
|
338
458
|
*/
|
|
339
459
|
function unknownFields(detail) {
|
|
340
|
-
const listed = detail.match(/unrecognized request arguments? supplied:\s*([^\n]+)/i)
|
|
341
|
-
const quoted = detail.match(/unknown parameter:\s*(['"`])([^'"`]+)\1/i)
|
|
342
|
-
|
|
460
|
+
const [, listed] = detail.match(/unrecognized request arguments? supplied:\s*([^\n]+)/i) ?? [];
|
|
461
|
+
const [, , quoted] = detail.match(/unknown parameter:\s*(['"`])([^'"`]+)\1/i) ?? [];
|
|
462
|
+
let names = [];
|
|
463
|
+
if (listed) {
|
|
464
|
+
names = listed.split(',');
|
|
465
|
+
}
|
|
466
|
+
else if (quoted) {
|
|
467
|
+
names = [quoted];
|
|
468
|
+
}
|
|
343
469
|
return names
|
|
344
470
|
.map((name) => name
|
|
345
471
|
.trim()
|
|
346
|
-
.replace(/^['"`]|['"`.]+$/g,
|
|
472
|
+
.replace(/^['"`]|['"`.]+$/g, '')
|
|
347
473
|
.split(/[.[]/)[0])
|
|
348
474
|
.filter(Boolean);
|
|
349
475
|
}
|
|
350
|
-
/**
|
|
351
|
-
|
|
352
|
-
|
|
476
|
+
/**
|
|
477
|
+
* The answer to a refusal that is the endpoint's: a flag still set, and a wording that clears it.
|
|
478
|
+
*
|
|
479
|
+
* @param flag - Which of the endpoint's flags the answer latches off.
|
|
480
|
+
* @param matches - Whether an error text is this refusal. Not asked once the flag is off.
|
|
481
|
+
* @param notice - What a watcher is told when it latches.
|
|
482
|
+
* @returns An `Answer` that clears the flag on `supports` and returns the notice, or returns
|
|
483
|
+
* `undefined` having touched nothing.
|
|
484
|
+
*/
|
|
485
|
+
const endpointAnswer = (flag, matches, notice) => ({ detail, supports }) => {
|
|
486
|
+
if (!supports[flag] || !matches(detail)) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
supports[flag] = false;
|
|
490
|
+
return notice;
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* The same for a refusal that is the model's, which only a request that named one can meet.
|
|
494
|
+
*
|
|
495
|
+
* @param flag - Which of the model's flags the answer latches off.
|
|
496
|
+
* @param matches - Whether an error text is this refusal. Not asked once the flag is off, or where
|
|
497
|
+
* no model was named.
|
|
498
|
+
* @param notice - Builds what a watcher is told, from the model's name as the request gave it.
|
|
499
|
+
* @returns An `Answer` that clears the flag on the named model and returns the notice, or returns
|
|
500
|
+
* `undefined` having touched nothing.
|
|
501
|
+
*/
|
|
502
|
+
const modelAnswer = (flag, matches, notice) => ({ detail, named }) => {
|
|
503
|
+
if (!named?.refused[flag] || !matches(detail)) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
named.refused[flag] = false;
|
|
507
|
+
return notice(named.name);
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* An effort refused by value: latched, so `effortFor` sends the next rung up.
|
|
511
|
+
*
|
|
512
|
+
* @param refusal - The refusal. Its named model is written to: the value joins `refusedEfforts`,
|
|
513
|
+
* and a list the refusal published replaces `supportedEfforts`.
|
|
514
|
+
* @returns The notice, naming the effort refused and the one tried next; `undefined`, having
|
|
515
|
+
* latched nothing, where no model was named or `planEffortStep` has no step to take.
|
|
516
|
+
*/
|
|
517
|
+
const effortStepAnswer = ({ detail, named }) => {
|
|
518
|
+
const stepped = named && planEffortStep(detail, named.refused);
|
|
519
|
+
if (!named || !stepped) {
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
named.refused.refusedEfforts.add(stepped.value);
|
|
523
|
+
if (stepped.supported) {
|
|
524
|
+
named.refused.supportedEfforts = stepped.supported;
|
|
525
|
+
}
|
|
526
|
+
return `${named.name} does not reason at ${stepped.value}; retrying at ${stepped.next}`;
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* Fields the endpoint has never heard of, where they are droppable and not refused already.
|
|
530
|
+
*
|
|
531
|
+
* @param refusal - The refusal. Each such field it names joins the named model's `refusedFields`.
|
|
532
|
+
* @returns The notice, listing those fields; `undefined`, having latched nothing, where no model
|
|
533
|
+
* was named, nothing is droppable, or the refusal names no field that is both droppable and new.
|
|
534
|
+
*/
|
|
535
|
+
const droppedFieldsAnswer = ({ detail, named, droppable }) => {
|
|
536
|
+
if (!named || droppable.size === 0) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
const fields = unknownFields(detail).filter((field) => droppable.has(field) && !named.refused.refusedFields.has(field));
|
|
540
|
+
if (!fields.length) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
for (const field of fields) {
|
|
544
|
+
named.refused.refusedFields.add(field);
|
|
545
|
+
}
|
|
546
|
+
return `${named.name} does not take ${fields.join(', ')}; retrying without ${fields.length > 1 ? 'them' : 'it'}`;
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* Every refusal `negotiate` answers, in the order it asks. The first to answer is the only one.
|
|
550
|
+
*
|
|
551
|
+
* @remarks
|
|
552
|
+
* The order is behaviour rather than layout: one error text can satisfy two of these — a message
|
|
553
|
+
* about `reasoning_effort` is a refused field to one and a refused value to the next — and the
|
|
554
|
+
* earlier row is the reading that wins.
|
|
555
|
+
*/
|
|
556
|
+
const ANSWERS = [
|
|
557
|
+
endpointAnswer('strictSchemas', isGrammarError, 'server could not build a grammar; retrying without pattern/format'),
|
|
558
|
+
endpointAnswer('usageInStream', (detail) => REJECTS_USAGE.test(detail), 'server rejected stream_options; token counts unavailable'),
|
|
559
|
+
modelAnswer('reasoningEffort', rejectsEffort, (name) => `${name} does not take a reasoning effort; retrying without one`),
|
|
560
|
+
effortStepAnswer,
|
|
561
|
+
modelAnswer('legacyTokenLimit', wantsCompletionLimit, (name) => `${name} wants max_completion_tokens; retrying with the limit spelled that way`),
|
|
562
|
+
modelAnswer('chosenTemperature', refusesChosenTemperature, (name) => `${name} takes only its own temperature; retrying without ours`),
|
|
563
|
+
modelAnswer('structuredOutput', rejectsResponseFormat, (name) => `${name} does not take response_format; asking for JSON in words instead`),
|
|
564
|
+
droppedFieldsAnswer,
|
|
565
|
+
];
|
|
353
566
|
/**
|
|
354
567
|
* Sends a request, re-sending it each time the answer is this endpoint refusing something the
|
|
355
568
|
* request can do without. Returns once the endpoint has answered, or throws if the refusal is
|
|
356
569
|
* not one of ours.
|
|
357
570
|
*
|
|
571
|
+
* @typeParam T - What `send` resolves, which is handed back as it came.
|
|
572
|
+
* @param supports - What this endpoint has already refused. Latched off further as it refuses more.
|
|
573
|
+
* @param send - Builds and sends the request. Called again per downgrade, never once tokens
|
|
574
|
+
* have arrived. Its third argument is what the named model has refused, absent when no model
|
|
575
|
+
* was named.
|
|
576
|
+
* @param [options] - `produced` for a caller with its own retry budget, `onNotice` for a watcher,
|
|
577
|
+
* `model` to negotiate the model's refusals alongside the endpoint's.
|
|
578
|
+
* @returns What the first `send` to succeed resolved. A rejection is `send`'s own error, rethrown
|
|
579
|
+
* as it was caught.
|
|
580
|
+
*
|
|
581
|
+
* @remarks
|
|
358
582
|
* A loop rather than one retry. A server that has heard of neither `stream_options` nor a
|
|
359
583
|
* grammar keyword complains about them one at a time, and answering only the first leaves the
|
|
360
584
|
* second to fail the request — so the first run against such an endpoint is spent discovering
|
|
@@ -376,70 +600,39 @@ const refusesDroppable = (detail, droppable, refused) => droppable.size > 0 &&
|
|
|
376
600
|
* ever one flag in a turn — the same box `streamTurn` sets and the re-send below reads — and a
|
|
377
601
|
* caller that passed it to only one of the two got a turn that had already streamed tokens sent
|
|
378
602
|
* again, silently, with the watcher seeing every one of them twice.
|
|
379
|
-
*
|
|
380
|
-
* @param supports What this endpoint has already refused. Latched off further as it refuses more.
|
|
381
|
-
* @param send Builds and sends the request. Called again per downgrade, never once tokens
|
|
382
|
-
* have arrived. Its third argument is what the named model has refused, absent when no model
|
|
383
|
-
* was named.
|
|
384
|
-
* @param options `produced` for a caller with its own retry budget, `onNotice` for a watcher,
|
|
385
|
-
* `model` to negotiate the model's refusals alongside the endpoint's.
|
|
386
603
|
*/
|
|
387
604
|
export async function negotiate(supports, send, { produced = { any: false }, onNotice, model: name, droppable } = {}) {
|
|
388
605
|
const optional = new Set(droppable);
|
|
389
|
-
// The name and what it has refused, bound together because the notices below need both. They
|
|
390
|
-
// exist or are absent as one — the second is resolved from the first — but that is a fact
|
|
391
|
-
// about two locals, and narrowing one of those tells TypeScript nothing about the other.
|
|
392
606
|
const named = name === undefined ? undefined : { name, refused: modelCapabilitiesFor(supports, name) };
|
|
393
607
|
const model = named?.refused;
|
|
394
608
|
for (;;) {
|
|
395
609
|
// What this attempt was built with. `capabilitiesFor` and `modelCapabilitiesFor` hand one
|
|
396
610
|
// object per endpoint and per model to everyone on them, so a run starting alongside this
|
|
397
|
-
// one may latch a flag off while this call is in flight — and
|
|
398
|
-
// on
|
|
611
|
+
// one may latch a flag off while this call is in flight — and every answer in `ANSWERS` is
|
|
612
|
+
// guarded on its flag still being set.
|
|
399
613
|
const sent = flagsOf(supports, model);
|
|
400
614
|
try {
|
|
401
615
|
return await send(supports, produced, model);
|
|
402
616
|
}
|
|
403
617
|
catch (error) {
|
|
404
|
-
if (produced.any)
|
|
618
|
+
if (produced.any) {
|
|
405
619
|
throw error;
|
|
406
|
-
const detail = errorMessage(error);
|
|
407
|
-
const stepped = named && planEffortStep(detail, named.refused);
|
|
408
|
-
if (supports.strictSchemas && isGrammarError(detail)) {
|
|
409
|
-
supports.strictSchemas = false;
|
|
410
|
-
onNotice?.("server could not build a grammar; retrying without pattern/format");
|
|
411
|
-
}
|
|
412
|
-
else if (supports.usageInStream && REJECTS_USAGE.test(detail)) {
|
|
413
|
-
supports.usageInStream = false;
|
|
414
|
-
onNotice?.("server rejected stream_options; token counts unavailable");
|
|
415
|
-
}
|
|
416
|
-
else if (named?.refused.reasoningEffort && rejectsEffort(detail)) {
|
|
417
|
-
named.refused.reasoningEffort = false;
|
|
418
|
-
onNotice?.(`${named.name} does not take a reasoning effort; retrying without one`);
|
|
419
|
-
}
|
|
420
|
-
else if (named && stepped) {
|
|
421
|
-
named.refused.refusedEfforts.add(stepped.value);
|
|
422
|
-
if (stepped.supported)
|
|
423
|
-
named.refused.supportedEfforts = stepped.supported;
|
|
424
|
-
onNotice?.(`${named.name} does not reason at ${stepped.value}; retrying at ${stepped.next}`);
|
|
425
|
-
}
|
|
426
|
-
else if (named?.refused.legacyTokenLimit && wantsCompletionLimit(detail)) {
|
|
427
|
-
named.refused.legacyTokenLimit = false;
|
|
428
|
-
onNotice?.(`${named.name} wants max_completion_tokens; retrying with the limit spelled that way`);
|
|
429
|
-
}
|
|
430
|
-
else if (named?.refused.chosenTemperature && refusesChosenTemperature(detail)) {
|
|
431
|
-
named.refused.chosenTemperature = false;
|
|
432
|
-
onNotice?.(`${named.name} takes only its own temperature; retrying without ours`);
|
|
433
620
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
621
|
+
const refusal = {
|
|
622
|
+
detail: errorMessage(error),
|
|
623
|
+
supports,
|
|
624
|
+
named,
|
|
625
|
+
droppable: optional,
|
|
626
|
+
};
|
|
627
|
+
let notice;
|
|
628
|
+
for (const answer of ANSWERS) {
|
|
629
|
+
notice = answer(refusal);
|
|
630
|
+
if (notice !== undefined) {
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
437
633
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
for (const field of fields)
|
|
441
|
-
named.refused.refusedFields.add(field);
|
|
442
|
-
onNotice?.(`${named.name} does not take ${fields.join(", ")}; retrying without ${fields.length > 1 ? "them" : "it"}`);
|
|
634
|
+
if (notice !== undefined) {
|
|
635
|
+
onNotice?.(notice);
|
|
443
636
|
}
|
|
444
637
|
else if (flagsOf(supports, model).every((flag, index) => flag === sent[index])) {
|
|
445
638
|
throw error;
|