@inflexa-ai/harness 0.30.1 → 0.30.2
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/providers/ai-sdk.js +131 -24
- package/package.json +1 -1
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { streamText, wrapLanguageModel, } from "ai";
|
|
2
2
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
3
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
4
4
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
@@ -255,10 +255,10 @@ function requestedModelIdOf(model) {
|
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
257
|
* Capture the served model id at the raw model boundary, because that is the
|
|
258
|
-
* only place it is still distinguishable from the requested one: `
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
258
|
+
* only place it is still distinguishable from the requested one: `streamText`
|
|
259
|
+
* coalesces an unreported response model id to the bound model's own id, so
|
|
260
|
+
* reading `result.response.modelId` would silently republish the requested id
|
|
261
|
+
* as an endpoint's claim. A middleware sees the provider's
|
|
262
262
|
* result before that fallback applies, so "reported nothing" stays absent.
|
|
263
263
|
*
|
|
264
264
|
* Built per call, never per provider: one provider instance serves concurrent
|
|
@@ -303,16 +303,6 @@ function responseFromMessages(input) {
|
|
|
303
303
|
}
|
|
304
304
|
return { message, finishReason, rawFinishReason, usage, requestedModelId, servedModelId };
|
|
305
305
|
}
|
|
306
|
-
function responseFromGenerate(result, identity) {
|
|
307
|
-
return responseFromMessages({
|
|
308
|
-
messages: result.responseMessages,
|
|
309
|
-
fallbackText: result.text,
|
|
310
|
-
finishReason: result.finishReason,
|
|
311
|
-
rawFinishReason: result.rawFinishReason,
|
|
312
|
-
usage: result.usage,
|
|
313
|
-
...identity,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
306
|
/**
|
|
317
307
|
* Drop empty text parts — and messages left with no content at all — before
|
|
318
308
|
* the wire call. The Anthropic Messages API rejects any request containing an
|
|
@@ -356,9 +346,57 @@ async function pullNextDelta(iterator) {
|
|
|
356
346
|
return { kind: "delta", text: part.text };
|
|
357
347
|
if (part.type === "abort")
|
|
358
348
|
return { kind: "aborted", reason: part.reason };
|
|
349
|
+
// The SDK reports a wire failure as a part, not as a rejection, and it
|
|
350
|
+
// then closes the stream. Skipping it would turn a failed call into an
|
|
351
|
+
// empty assistant turn that the loop reads as a real answer.
|
|
352
|
+
if (part.type === "error")
|
|
353
|
+
return { kind: "failed", error: part.error };
|
|
359
354
|
}
|
|
360
355
|
return { kind: "end" };
|
|
361
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* The HTTP status that each Anthropic stream error type stands for.
|
|
359
|
+
*
|
|
360
|
+
* A provider names a mid-stream failure in the payload, not in the status line:
|
|
361
|
+
* a stream that carries `overloaded_error` reports the condition that a 529
|
|
362
|
+
* names on the non-streaming wire. The classifier reads the status alone, thus
|
|
363
|
+
* the part has to reach it as an error that names one, or the same condition
|
|
364
|
+
* would classify one way on `chat` and another way on `chatStream`.
|
|
365
|
+
*/
|
|
366
|
+
const STREAM_ERROR_STATUS = {
|
|
367
|
+
invalid_request_error: 400,
|
|
368
|
+
authentication_error: 401,
|
|
369
|
+
permission_error: 403,
|
|
370
|
+
not_found_error: 404,
|
|
371
|
+
request_too_large: 413,
|
|
372
|
+
rate_limit_error: 429,
|
|
373
|
+
timeout_error: 408,
|
|
374
|
+
api_error: 500,
|
|
375
|
+
overloaded_error: 529,
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* The failure that an `error` part of the SDK stream stands for.
|
|
379
|
+
*
|
|
380
|
+
* The part carries whatever the provider put on the wire, and that is often a
|
|
381
|
+
* bare object rather than an `Error`. Left as it is, it reaches the message as
|
|
382
|
+
* `[object Object]` and reaches the classifier with no status at all.
|
|
383
|
+
*/
|
|
384
|
+
function streamPartFailure(error) {
|
|
385
|
+
if (error instanceof Error)
|
|
386
|
+
return error;
|
|
387
|
+
if (typeof error !== "object" || error === null)
|
|
388
|
+
return new Error(String(error));
|
|
389
|
+
const reported = error;
|
|
390
|
+
const type = typeof reported.type === "string" ? reported.type : undefined;
|
|
391
|
+
const message = typeof reported.message === "string" ? reported.message : undefined;
|
|
392
|
+
const failure = new Error(message ?? type ?? "The provider reported a stream error.", { cause: error });
|
|
393
|
+
const status = type === undefined ? undefined : STREAM_ERROR_STATUS[type];
|
|
394
|
+
// `extractStatus` walks the chain for this field, thus the named condition
|
|
395
|
+
// classifies the same way it would from a status line.
|
|
396
|
+
if (status !== undefined)
|
|
397
|
+
Object.assign(failure, { status });
|
|
398
|
+
return failure;
|
|
399
|
+
}
|
|
362
400
|
/**
|
|
363
401
|
* The failure that an `abort` part of the SDK stream stands for.
|
|
364
402
|
*
|
|
@@ -394,16 +432,46 @@ export function createAiSdkProvider(deps) {
|
|
|
394
432
|
const requestTimeoutMs = deps.requestTimeoutMs;
|
|
395
433
|
const requestedModelId = requestedModelIdOf(deps.model);
|
|
396
434
|
routeSdkWarningsTo(logger);
|
|
435
|
+
/**
|
|
436
|
+
* Record a call that the envelope gave up on.
|
|
437
|
+
*
|
|
438
|
+
* The envelope reports one attempt at `debug`, thus a deployment that runs
|
|
439
|
+
* at `info` keeps no trace of a call that exhausted its retries: the reply
|
|
440
|
+
* carries the failure to the user and the operator sees nothing. This is the
|
|
441
|
+
* one record that names the outcome.
|
|
442
|
+
*/
|
|
443
|
+
function logFailure(session, failure, thrown) {
|
|
444
|
+
logger.error("provider call failed", {
|
|
445
|
+
workload: workloadOf(session),
|
|
446
|
+
kind: failure.type,
|
|
447
|
+
retryable: failure.retryable,
|
|
448
|
+
...logger.errorFields(thrown),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
397
451
|
function chat(req, session, signal) {
|
|
398
452
|
const run = async () => {
|
|
399
453
|
const retry = createRetry(signal, logger, maxRetries);
|
|
400
454
|
const capture = captureServedModelId(deps.model);
|
|
401
455
|
try {
|
|
402
|
-
const
|
|
456
|
+
const collected = await retry(async () => {
|
|
403
457
|
// Attribution headers are time-limited; resolving them inside the
|
|
404
458
|
// retried closure keeps them fresh across a multi-minute window.
|
|
405
459
|
const headers = await resolveBillingHeaders(deps.resolveBilling, session);
|
|
406
|
-
|
|
460
|
+
// The call streams on the wire and collapses below.
|
|
461
|
+
//
|
|
462
|
+
// A non-streaming turn sends no header until the model completes,
|
|
463
|
+
// thus the `headersTimeout` of the host HTTP client bounds the whole
|
|
464
|
+
// generation. No option of the SDK lifts that bound, because it sits
|
|
465
|
+
// under the fetch. Node defaults the field to 300s, and a turn near
|
|
466
|
+
// 18k output tokens reaches it, while `DEFAULT_MAX_OUTPUT_TOKENS`
|
|
467
|
+
// asks for up to 64k. A cut turn then costs twice: the envelope
|
|
468
|
+
// starts the same generation again from zero.
|
|
469
|
+
//
|
|
470
|
+
// Anthropic states the rule for its own SDKs: a large `max_tokens`
|
|
471
|
+
// makes streaming necessary, to prevent an HTTP timeout. Thus a
|
|
472
|
+
// ceiling this size streams whether or not a consumer wants the
|
|
473
|
+
// deltas, and `chatStream` stays the seam that forwards them.
|
|
474
|
+
const result = streamText({
|
|
407
475
|
model: capture.model,
|
|
408
476
|
system: req.system,
|
|
409
477
|
messages: sanitizeMessages(req.messages),
|
|
@@ -416,20 +484,53 @@ export function createAiSdkProvider(deps) {
|
|
|
416
484
|
maxRetries: 0,
|
|
417
485
|
headers,
|
|
418
486
|
abortSignal: signal,
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
487
|
+
// `firstChunkMs` bounds the wait for the first content chunk,
|
|
488
|
+
// and `chunkMs` bounds each later gap. Thus each silent interval
|
|
489
|
+
// carries the one configured bound, and a turn that writes
|
|
490
|
+
// steadily runs as long as it needs.
|
|
491
|
+
...(requestTimeoutMs !== undefined ? { timeout: { firstChunkMs: requestTimeoutMs, chunkMs: requestTimeoutMs } } : {}),
|
|
423
492
|
providerOptions: req.providerOptions,
|
|
424
493
|
reasoning: req.reasoning,
|
|
425
494
|
});
|
|
495
|
+
// The drain sits inside the retried closure, thus a failure at any
|
|
496
|
+
// point of the stream re-runs the whole attempt and the envelope
|
|
497
|
+
// keeps its whole-call scope. No delta reaches a consumer here —
|
|
498
|
+
// `chatStream` is the seam that forwards them.
|
|
499
|
+
const iterator = result.fullStream[Symbol.asyncIterator]();
|
|
500
|
+
let text = "";
|
|
501
|
+
for (let pull = await pullNextDelta(iterator); pull.kind !== "end"; pull = await pullNextDelta(iterator)) {
|
|
502
|
+
// A caller abort rides out as the reason of its own signal, thus
|
|
503
|
+
// it stays a cancellation and reaches the caller as a throw.
|
|
504
|
+
if (pull.kind === "aborted")
|
|
505
|
+
throw abortedStreamFailure(signal, pull.reason, requestTimeoutMs);
|
|
506
|
+
if (pull.kind === "failed")
|
|
507
|
+
throw streamPartFailure(pull.error);
|
|
508
|
+
text += pull.text;
|
|
509
|
+
}
|
|
510
|
+
// These promises settle only once the stream is fully drained, thus
|
|
511
|
+
// the loop above must reach its end before this point.
|
|
512
|
+
return {
|
|
513
|
+
messages: await result.responseMessages,
|
|
514
|
+
fallbackText: text,
|
|
515
|
+
finishReason: await result.finishReason,
|
|
516
|
+
rawFinishReason: await result.rawFinishReason,
|
|
517
|
+
usage: await result.usage,
|
|
518
|
+
};
|
|
426
519
|
});
|
|
427
|
-
return ok(
|
|
520
|
+
return ok(responseFromMessages({
|
|
521
|
+
...collected,
|
|
522
|
+
requestedModelId,
|
|
523
|
+
// Read after the drain: the metadata chunk that carries the
|
|
524
|
+
// served id reaches the capture only as the stream is consumed.
|
|
525
|
+
servedModelId: capture.servedModelId(),
|
|
526
|
+
}));
|
|
428
527
|
}
|
|
429
528
|
catch (e) {
|
|
430
529
|
if (isAbortError(e) || signal?.aborted)
|
|
431
530
|
throw e;
|
|
432
|
-
|
|
531
|
+
const failure = toProviderError(unwrapForClassification(e), workloadOf(session));
|
|
532
|
+
logFailure(session, failure, e);
|
|
533
|
+
return err(failure);
|
|
433
534
|
}
|
|
434
535
|
};
|
|
435
536
|
return new ResultAsync(run());
|
|
@@ -477,6 +578,8 @@ export function createAiSdkProvider(deps) {
|
|
|
477
578
|
// fresh window under the retry policy of a connection error.
|
|
478
579
|
if (first.kind === "aborted")
|
|
479
580
|
throw abortedStreamFailure(signal, first.reason, requestTimeoutMs);
|
|
581
|
+
if (first.kind === "failed")
|
|
582
|
+
throw streamPartFailure(first.error);
|
|
480
583
|
if (first.kind === "end") {
|
|
481
584
|
// A doStream promise rejection — the shape a real connection
|
|
482
585
|
// failure takes — does NOT surface at this first pull: streamText
|
|
@@ -522,6 +625,8 @@ export function createAiSdkProvider(deps) {
|
|
|
522
625
|
// terminal `ProviderError` from the throwable.
|
|
523
626
|
if (pull.kind === "aborted")
|
|
524
627
|
throw abortedStreamFailure(signal, pull.reason, requestTimeoutMs);
|
|
628
|
+
if (pull.kind === "failed")
|
|
629
|
+
throw streamPartFailure(pull.error);
|
|
525
630
|
text += pull.text;
|
|
526
631
|
yield { type: "text-delta", text: pull.text };
|
|
527
632
|
}
|
|
@@ -541,7 +646,9 @@ export function createAiSdkProvider(deps) {
|
|
|
541
646
|
catch (e) {
|
|
542
647
|
if (isAbortError(e) || signal?.aborted)
|
|
543
648
|
throw e;
|
|
544
|
-
|
|
649
|
+
const failure = toProviderError(unwrapForClassification(e), workloadOf(session));
|
|
650
|
+
logFailure(session, failure, e);
|
|
651
|
+
throw failure;
|
|
545
652
|
}
|
|
546
653
|
}
|
|
547
654
|
// The provider advertises the guard limit on its own instance, thus a
|
package/package.json
CHANGED