@namzu/sdk 52.0.0 → 52.1.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/CHANGELOG.md +19 -0
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +57 -17
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/contracts/a2a.d.ts +2 -2
- package/dist/provider/capabilities.d.ts +9 -0
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +15 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +13 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +10 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/image-input-refusal.d.ts +9 -0
- package/dist/provider/image-input-refusal.d.ts.map +1 -0
- package/dist/provider/image-input-refusal.js +54 -0
- package/dist/provider/image-input-refusal.js.map +1 -0
- package/dist/provider/index.d.ts +2 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/retry.d.ts +7 -2
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +38 -4
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
- package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
- package/dist/runtime/query/deferred-sandbox.js +75 -0
- package/dist/runtime/query/deferred-sandbox.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +10 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +24 -3
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +93 -25
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +26 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +164 -21
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +10 -7
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +10 -7
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +119 -21
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
- package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
- package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
- package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
- package/dist/runtime/query/iteration/request-repair.js +593 -0
- package/dist/runtime/query/iteration/request-repair.js.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +2 -2
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts +5 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +67 -25
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +12 -5
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +30 -5
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/interface.d.ts +12 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +1 -1
- package/dist/types/session/config.d.ts +14 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +20 -5
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/package.json +1 -1
- package/src/compaction/verifier.ts +57 -20
- package/src/provider/capabilities.ts +17 -0
- package/src/provider/errors.ts +13 -0
- package/src/provider/fallback.ts +10 -0
- package/src/provider/idle-timeout.ts +5 -0
- package/src/provider/image-input-refusal.ts +54 -0
- package/src/provider/index.ts +2 -0
- package/src/provider/retry.ts +42 -4
- package/src/public-runtime.ts +3 -0
- package/src/runtime/query/deferred-sandbox.ts +110 -0
- package/src/runtime/query/executor.ts +25 -3
- package/src/runtime/query/index.ts +124 -49
- package/src/runtime/query/iteration/index.ts +188 -23
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +10 -8
- package/src/runtime/query/iteration/phases/compaction.ts +151 -35
- package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
- package/src/runtime/query/iteration/request-repair.ts +743 -0
- package/src/runtime/query/iteration/stream-turn.ts +11 -1
- package/src/runtime/query/request-rich-content.ts +77 -27
- package/src/tools/builtins/computer-use.ts +13 -4
- package/src/types/provider/errors.ts +33 -5
- package/src/types/provider/interface.ts +13 -0
- package/src/types/provider/stream.ts +1 -1
- package/src/types/session/config.ts +14 -0
- package/src/types/session/events.ts +25 -5
|
@@ -16,17 +16,14 @@ import { serializeState } from '../../../../compaction/serializer.js'
|
|
|
16
16
|
import { buildCompactionMessage, isCompactionMessage } from '../../../../compaction/summary.js'
|
|
17
17
|
import { estimateMessagesTokens } from '../../../../compaction/token-estimate.js'
|
|
18
18
|
import { buildVerifiedSummaryWithBoundedProvider } from '../../../../compaction/verifier.js'
|
|
19
|
+
import type { CompactionConfig } from '../../../../config/runtime.js'
|
|
19
20
|
import { CHARS_PER_TOKEN } from '../../../../constants/limits.js'
|
|
20
21
|
import { NAMZU } from '../../../../constants/telemetry/index.js'
|
|
21
22
|
import { invariants } from '../../../../invariants/index.js'
|
|
22
23
|
import { resolveTaskModel } from '../../../../model-router/task-router.js'
|
|
23
24
|
import type { Message } from '../../../../types/message/index.js'
|
|
24
25
|
import { applyLifecycleHookResults } from '../../plugin-hooks.js'
|
|
25
|
-
import {
|
|
26
|
-
COMPACTION_FAILURE_MESSAGE,
|
|
27
|
-
COMPACTION_REFILL_MESSAGE,
|
|
28
|
-
compactionBreaker,
|
|
29
|
-
} from './compaction-breaker.js'
|
|
26
|
+
import { COMPACTION_REFILL_MESSAGE, compactionBreaker } from './compaction-breaker.js'
|
|
30
27
|
import type { IterationContext } from './context.js'
|
|
31
28
|
import { isWorkingMemoryMessage } from './working-memory.js'
|
|
32
29
|
|
|
@@ -223,6 +220,73 @@ export function measureContext(ctx: IterationContext): {
|
|
|
223
220
|
return { tokens: estimateTokens(ctx), source: 'estimate' }
|
|
224
221
|
}
|
|
225
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Reactive compactions allowed for one overflow, as in the reference
|
|
225
|
+
* terminal: the first relief, then at most one deeper pass.
|
|
226
|
+
*/
|
|
227
|
+
export const OVERFLOW_RELIEF_LIMIT = 2
|
|
228
|
+
|
|
229
|
+
/** Round-groups that must remain for the deeper pass to be worth running. */
|
|
230
|
+
export const DEEPER_PASS_MIN_GROUPS = 3
|
|
231
|
+
|
|
232
|
+
export interface OverflowReliefOptions {
|
|
233
|
+
/**
|
|
234
|
+
* The second rung. Keep only the newest round-group verbatim instead of the
|
|
235
|
+
* configured recent window (when at least {@link DEEPER_PASS_MIN_GROUPS}
|
|
236
|
+
* groups remain), and clear oversized tool results even inside the recent
|
|
237
|
+
* window, with the same placeholder and spill pointer a stale result gets.
|
|
238
|
+
*/
|
|
239
|
+
readonly deeper?: boolean
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A round-group starts at each assistant message and runs to the next one, so
|
|
244
|
+
* its tool results stay with the call that made them. The newest group is the
|
|
245
|
+
* last assistant message and everything after it; the opening user messages
|
|
246
|
+
* ride with the first group.
|
|
247
|
+
*/
|
|
248
|
+
function roundGroups(messages: readonly Message[]): number {
|
|
249
|
+
let groups = 0
|
|
250
|
+
for (const message of messages) if (message.role === 'assistant') groups++
|
|
251
|
+
return groups
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The deeper rung's configuration: no tool result protected, and a recent
|
|
256
|
+
* window no larger than the first rung's. With three or more groups the
|
|
257
|
+
* window shrinks to the newest group, but never grows past what the
|
|
258
|
+
* configuration already kept: a newest group bigger than that would make the
|
|
259
|
+
* "deeper" pass shed less than the first one.
|
|
260
|
+
*/
|
|
261
|
+
export function deepenConfig(
|
|
262
|
+
config: CompactionConfig,
|
|
263
|
+
messages: readonly Message[],
|
|
264
|
+
): CompactionConfig {
|
|
265
|
+
let lastAssistant = -1
|
|
266
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
267
|
+
if (messages[i]?.role === 'assistant') {
|
|
268
|
+
lastAssistant = i
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const deeper = { ...config, keepRecentToolResults: 0 }
|
|
273
|
+
if (lastAssistant < 0 || roundGroups(messages) < DEEPER_PASS_MIN_GROUPS) return deeper
|
|
274
|
+
const newest = messages.slice(lastAssistant)
|
|
275
|
+
if (config.keepRecentTokens !== undefined) {
|
|
276
|
+
return {
|
|
277
|
+
...deeper,
|
|
278
|
+
keepRecentTokens: Math.max(
|
|
279
|
+
1,
|
|
280
|
+
Math.min(config.keepRecentTokens, estimateMessagesTokens(newest)),
|
|
281
|
+
),
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
...deeper,
|
|
286
|
+
keepRecentMessages: Math.max(1, Math.min(config.keepRecentMessages, newest.length)),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
226
290
|
/**
|
|
227
291
|
* Shed history because the PROVIDER said the prompt is too long.
|
|
228
292
|
*
|
|
@@ -241,12 +305,18 @@ export function measureContext(ctx: IterationContext): {
|
|
|
241
305
|
* @returns whether anything was actually shed. `false` means retrying would
|
|
242
306
|
* send the same prompt again, so the caller must not.
|
|
243
307
|
*/
|
|
244
|
-
export async function relieveOverflow(
|
|
308
|
+
export async function relieveOverflow(
|
|
309
|
+
ctx: IterationContext,
|
|
310
|
+
options: OverflowReliefOptions = {},
|
|
311
|
+
): Promise<boolean> {
|
|
312
|
+
// The cause belongs to the rung that is about to run: an earlier rung's
|
|
313
|
+
// summary failure must not be blamed for a later rung that compacted cleanly.
|
|
314
|
+
clearCompactionErrorNote(ctx)
|
|
245
315
|
const before = ctx.recorder.messages.length
|
|
246
316
|
const beforeChars = totalChars(ctx.recorder.messages)
|
|
247
317
|
const beforeTokens = estimateMessagesTokens(ctx.recorder.messages)
|
|
248
318
|
|
|
249
|
-
await runCompactionCheck(ctx, { force: true })
|
|
319
|
+
await runCompactionCheck(ctx, { force: true, ...(options.deeper ? { deeper: true } : {}) })
|
|
250
320
|
|
|
251
321
|
const shed = beforeChars - totalChars(ctx.recorder.messages)
|
|
252
322
|
const tokensShed = beforeTokens - estimateMessagesTokens(ctx.recorder.messages)
|
|
@@ -278,6 +348,31 @@ export async function relieveOverflow(ctx: IterationContext): Promise<boolean> {
|
|
|
278
348
|
return true
|
|
279
349
|
}
|
|
280
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Why the last compaction attempt errored, kept for the turn so that when
|
|
353
|
+
* relief runs out the failure it ends on can say so, as the reference
|
|
354
|
+
* terminal's " · automatic compaction failed: <cause>" does.
|
|
355
|
+
*/
|
|
356
|
+
const compactionErrors = new WeakMap<object, string>()
|
|
357
|
+
const MAX_COMPACTION_CAUSE_CHARS = 300
|
|
358
|
+
/** The sentence the cause is appended with. */
|
|
359
|
+
export const AUTOMATIC_COMPACTION_FAILED = ' · automatic compaction failed: '
|
|
360
|
+
|
|
361
|
+
function noteCompactionError(ctx: IterationContext, error: unknown): void {
|
|
362
|
+
const text = error instanceof Error ? error.message : String(error)
|
|
363
|
+
if (text.length > 0) compactionErrors.set(ctx.recorder, text.slice(0, MAX_COMPACTION_CAUSE_CHARS))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The turn's last compaction error, or `undefined` when none errored. */
|
|
367
|
+
export function compactionErrorNote(ctx: IterationContext): string | undefined {
|
|
368
|
+
return compactionErrors.get(ctx.recorder)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** A turn that got through has nothing left to explain. */
|
|
372
|
+
export function clearCompactionErrorNote(ctx: IterationContext): void {
|
|
373
|
+
compactionErrors.delete(ctx.recorder)
|
|
374
|
+
}
|
|
375
|
+
|
|
281
376
|
function totalChars(messages: readonly { content: unknown }[]): number {
|
|
282
377
|
let total = 0
|
|
283
378
|
for (const msg of messages) total += measureContentChars(msg.content)
|
|
@@ -335,8 +430,11 @@ async function declined(
|
|
|
335
430
|
/**
|
|
336
431
|
* The third refill in a row: stop the turn instead of compacting again.
|
|
337
432
|
* Returns true when this pass must not run. Only with `circuitBreaker`.
|
|
433
|
+
* The loop also asks it before the first reactive relief of an overflow, as
|
|
434
|
+
* the reference terminal does (`rhr` runs first in its withheld-overflow
|
|
435
|
+
* handling), so a re-read loop made only of overflows is counted too.
|
|
338
436
|
*/
|
|
339
|
-
async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
|
|
437
|
+
export async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
|
|
340
438
|
const breaker = compactionBreaker(ctx)
|
|
341
439
|
if (!breaker) return false
|
|
342
440
|
// Already announced: the turn is on its way out; do not say it twice.
|
|
@@ -418,6 +516,7 @@ async function applyReducer(
|
|
|
418
516
|
'namzu.runtime.reason': reduction.reason,
|
|
419
517
|
'exception.message': message,
|
|
420
518
|
})
|
|
519
|
+
noteCompactionError(ctx, error)
|
|
421
520
|
await declined(ctx, 'reducer_threw', before, message)
|
|
422
521
|
return
|
|
423
522
|
}
|
|
@@ -549,7 +648,7 @@ interface CompactionPass {
|
|
|
549
648
|
|
|
550
649
|
export async function runCompactionCheck(
|
|
551
650
|
ctx: IterationContext,
|
|
552
|
-
options?: { force?: boolean },
|
|
651
|
+
options?: { force?: boolean; deeper?: boolean },
|
|
553
652
|
): Promise<void> {
|
|
554
653
|
const pass: CompactionPass = {
|
|
555
654
|
attempted: false,
|
|
@@ -585,11 +684,12 @@ export async function runCompactionCheck(
|
|
|
585
684
|
async function runCompactionCheckInner(
|
|
586
685
|
ctx: IterationContext,
|
|
587
686
|
pass: CompactionPass,
|
|
588
|
-
options?: { force?: boolean },
|
|
687
|
+
options?: { force?: boolean; deeper?: boolean },
|
|
589
688
|
): Promise<void> {
|
|
590
|
-
const
|
|
591
|
-
if (!
|
|
592
|
-
if (
|
|
689
|
+
const configured = ctx.compactionConfig
|
|
690
|
+
if (!configured) return
|
|
691
|
+
if (configured.strategy === 'disabled') return
|
|
692
|
+
const config = options?.deeper ? deepenConfig(configured, ctx.recorder.messages) : configured
|
|
593
693
|
|
|
594
694
|
const measured = measureContext(ctx)
|
|
595
695
|
const estimatedTokens = measured.tokens
|
|
@@ -616,11 +716,14 @@ async function runCompactionCheckInner(
|
|
|
616
716
|
const startAt = salience ? Math.min(config.softTarget ?? DEFAULT_SOFT_TARGET, trigger) : trigger
|
|
617
717
|
if (!options?.force && usage < startAt) return
|
|
618
718
|
|
|
619
|
-
// Three failed passes in a row: stop
|
|
620
|
-
//
|
|
719
|
+
// Three failed passes in a row: stop proactive compaction for the rest of
|
|
720
|
+
// the turn. A pass the provider forced is never gated.
|
|
621
721
|
const breaker = compactionBreaker(ctx)
|
|
622
722
|
if (breaker?.failuresOpen && !options?.force) {
|
|
623
723
|
if (breaker.takeFailureNotice()) {
|
|
724
|
+
// Silent for the person, as in the reference terminal: a log line and the
|
|
725
|
+
// event for a host that wants it, but no sentence to show. Only the
|
|
726
|
+
// proactive pass stops; a real overflow still runs the reactive ladder.
|
|
624
727
|
ctx.log.warn('Compaction failed three times in a row — switched off for this turn', {
|
|
625
728
|
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
626
729
|
})
|
|
@@ -630,7 +733,6 @@ async function runCompactionCheckInner(
|
|
|
630
733
|
iteration: ctx.recorder.currentIteration,
|
|
631
734
|
cause: 'breaker_open',
|
|
632
735
|
messages: ctx.recorder.messages.length,
|
|
633
|
-
error: COMPACTION_FAILURE_MESSAGE,
|
|
634
736
|
})
|
|
635
737
|
}
|
|
636
738
|
return
|
|
@@ -851,25 +953,39 @@ async function runCompactionCheckInner(
|
|
|
851
953
|
// to a cheap model while the bill is written against the expensive one
|
|
852
954
|
// is a mistake with no symptom.
|
|
853
955
|
const compactionModel = resolveTaskModel('compaction', ctx.taskRouter, ctx.turnConfig.model)
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
956
|
+
try {
|
|
957
|
+
compactedContent = await buildVerifiedSummaryWithBoundedProvider(
|
|
958
|
+
manager,
|
|
959
|
+
olderMessages as Message[],
|
|
960
|
+
ctx.provider,
|
|
961
|
+
config,
|
|
962
|
+
(usage) =>
|
|
963
|
+
ctx.recorder.accumulateUsage(usage, {
|
|
964
|
+
providerId: ctx.recorder.servingProviderId,
|
|
965
|
+
model: compactionModel,
|
|
966
|
+
}),
|
|
967
|
+
// The one model call a turn makes that the user never asked for. It
|
|
968
|
+
// reads a transcript and writes a summary, which is the cheapest
|
|
969
|
+
// thing a small model does well, and it fires on exactly the long
|
|
970
|
+
// runs where the primary model is most expensive. `taskRouter` had
|
|
971
|
+
// been accepted, validated and threaded through four types since it
|
|
972
|
+
// was added, and nothing ever consulted it.
|
|
973
|
+
compactionModel,
|
|
974
|
+
ctx.abortController.signal,
|
|
975
|
+
)
|
|
976
|
+
} catch (error) {
|
|
977
|
+
// The model step only ADDS to the extracted state, so a failed one (a
|
|
978
|
+
// timeout, a provider error, its own overflow after the ladder in the
|
|
979
|
+
// verifier ran out) is not a reason to leave the history as it is. Cancellation
|
|
980
|
+
// is the caller's, and still stops the pass.
|
|
981
|
+
if (ctx.abortController.signal.aborted) throw error
|
|
982
|
+
noteCompactionError(ctx, error)
|
|
983
|
+
ctx.log.warn('The compaction summary call failed — using the extracted state alone', {
|
|
984
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
985
|
+
'exception.message': error instanceof Error ? error.message : String(error),
|
|
986
|
+
})
|
|
987
|
+
compactedContent = serializeState(manager.getState())
|
|
988
|
+
}
|
|
873
989
|
} else {
|
|
874
990
|
compactedContent = serializeState(manager.getState())
|
|
875
991
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isProviderRequestError } from '../../../provider/errors.js'
|
|
2
|
+
import { isImageInputRefusal } from '../../../provider/image-input-refusal.js'
|
|
2
3
|
import type { Message } from '../../../types/message/index.js'
|
|
3
4
|
import type {
|
|
4
5
|
ChatCompletionParams,
|
|
@@ -8,8 +9,11 @@ import type {
|
|
|
8
9
|
import {
|
|
9
10
|
type RequestImageIdentity,
|
|
10
11
|
findSingleRequestImage,
|
|
12
|
+
projectRequestRichContent,
|
|
11
13
|
projectRequestWithoutImage,
|
|
14
|
+
requestHasImages,
|
|
12
15
|
} from '../request-rich-content.js'
|
|
16
|
+
import type { RequestRepair } from './request-repair.js'
|
|
13
17
|
|
|
14
18
|
const IMAGE_REJECTION_PATTERNS: readonly RegExp[] = [
|
|
15
19
|
/\binvalid[_ -]?image\b/i,
|
|
@@ -52,6 +56,24 @@ function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
|
52
56
|
if (signal?.aborted) throw signal.reason
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
/** The provider and model that refused a request for holding an image. */
|
|
60
|
+
export interface TextOnlyModelRefusal {
|
|
61
|
+
readonly providerId: string
|
|
62
|
+
readonly model: string | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** See {@link streamWithProviderRejectedImageRecovery}. */
|
|
66
|
+
export interface TextOnlyModelRecovery {
|
|
67
|
+
/**
|
|
68
|
+
* Who is serving the request. A fallback chain reports its head's id and
|
|
69
|
+
* moves between members, so without this the refusal would be filed against
|
|
70
|
+
* a model that never saw the request. Read when the refusal arrives.
|
|
71
|
+
*/
|
|
72
|
+
readonly servedBy?: () => { readonly providerId: string; readonly model?: string }
|
|
73
|
+
/** The retry without images was accepted: `refused` cannot read images. */
|
|
74
|
+
readonly onAccepted: (refused: TextOnlyModelRefusal) => void | Promise<void>
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
/**
|
|
56
78
|
* Retry one provider-invalid request without its only distinct image.
|
|
57
79
|
*
|
|
@@ -65,29 +87,73 @@ export async function* streamWithProviderRejectedImageRecovery(
|
|
|
65
87
|
params: ChatCompletionParams,
|
|
66
88
|
onAccepted: (identity: RequestImageIdentity) => Promise<void>,
|
|
67
89
|
onRequest?: (messages: readonly Message[]) => void,
|
|
90
|
+
textOnly?: TextOnlyModelRecovery,
|
|
91
|
+
repair?: RequestRepair,
|
|
68
92
|
): AsyncIterable<StreamChunk> {
|
|
93
|
+
// Every send, including the retries below, goes through the request repair.
|
|
94
|
+
// They share one repair budget: the original request and its retries are
|
|
95
|
+
// one request to the person, and each resend is a paid call.
|
|
96
|
+
const session = repair?.session()
|
|
97
|
+
const send = (request: ChatCompletionParams): AsyncIterable<StreamChunk> =>
|
|
98
|
+
repair ? repair.stream(provider, request, session) : provider.chatStream(request)
|
|
69
99
|
const candidate = findSingleRequestImage(params.messages)
|
|
70
100
|
onRequest?.(params.messages)
|
|
71
|
-
if (candidate === null) {
|
|
72
|
-
yield*
|
|
101
|
+
if (candidate === null && !requestHasImages(params.messages)) {
|
|
102
|
+
yield* send(params)
|
|
73
103
|
return
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
let produced = false
|
|
77
107
|
let confidence: ImageRejectionConfidence | null = null
|
|
108
|
+
let refused: TextOnlyModelRefusal | null = null
|
|
78
109
|
try {
|
|
79
|
-
for await (const chunk of
|
|
110
|
+
for await (const chunk of send(params)) {
|
|
80
111
|
if (isAcceptedChunk(chunk)) produced = true
|
|
81
112
|
yield chunk
|
|
82
113
|
}
|
|
83
114
|
return
|
|
84
115
|
} catch (error) {
|
|
85
116
|
throwIfAborted(params.signal)
|
|
117
|
+
if (produced) throw error
|
|
118
|
+
// A fault in one image (the server's own `invalid_image`, or wording that
|
|
119
|
+
// says an image could not be decoded) is about that image, not about the
|
|
120
|
+
// model, and it wins: the model reads images, so it keeps the others.
|
|
86
121
|
confidence = classifyProviderRejectedImageError(error)
|
|
87
|
-
if (
|
|
122
|
+
if (confidence === null && isImageInputRefusal(error)) {
|
|
123
|
+
// A model that reads text only refuses the request however many images
|
|
124
|
+
// it holds, so every image goes. Named now, while the member that
|
|
125
|
+
// refused is still the one serving.
|
|
126
|
+
const served = textOnly?.servedBy?.()
|
|
127
|
+
refused = {
|
|
128
|
+
providerId: served?.providerId ?? provider.id,
|
|
129
|
+
model: served?.model ?? params.model,
|
|
130
|
+
}
|
|
131
|
+
} else if (confidence === null || candidate === null) {
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
throwIfAborted(params.signal)
|
|
137
|
+
if (refused !== null) {
|
|
138
|
+
// One retry, without any image. The stored conversation is untouched, so
|
|
139
|
+
// a model that does read images gets them again.
|
|
140
|
+
const messages = projectRequestRichContent(params.messages, 0, { imagesReadable: false })
|
|
141
|
+
onRequest?.(messages)
|
|
142
|
+
let accepted = false
|
|
143
|
+
let reportedError = false
|
|
144
|
+
for await (const chunk of send({ ...params, messages })) {
|
|
145
|
+
if (chunk.error !== undefined) reportedError = true
|
|
146
|
+
if (!accepted && isAcceptedChunk(chunk)) {
|
|
147
|
+
throwIfAborted(params.signal)
|
|
148
|
+
await textOnly?.onAccepted(refused)
|
|
149
|
+
accepted = true
|
|
150
|
+
}
|
|
151
|
+
yield chunk
|
|
152
|
+
}
|
|
153
|
+
if (!accepted && !reportedError) await textOnly?.onAccepted(refused)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if (candidate === null) return
|
|
91
157
|
const retryParams: ChatCompletionParams = {
|
|
92
158
|
...params,
|
|
93
159
|
messages: projectRequestWithoutImage(params.messages, candidate),
|
|
@@ -95,7 +161,7 @@ export async function* streamWithProviderRejectedImageRecovery(
|
|
|
95
161
|
let accepted = false
|
|
96
162
|
let retryReportedError = false
|
|
97
163
|
onRequest?.(retryParams.messages)
|
|
98
|
-
for await (const chunk of
|
|
164
|
+
for await (const chunk of send(retryParams)) {
|
|
99
165
|
if (chunk.error !== undefined) retryReportedError = true
|
|
100
166
|
if (!accepted && isAcceptedChunk(chunk)) {
|
|
101
167
|
throwIfAborted(params.signal)
|