@inflexa-ai/harness 0.30.0 → 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/index.d.ts +2 -2
- package/dist/index.js +9 -7
- package/dist/loop/run-agent.d.ts +5 -4
- package/dist/loop/run-agent.js +7 -3
- package/dist/providers/ai-sdk.js +136 -58
- package/dist/providers/reasoning.d.ts +20 -50
- package/dist/providers/reasoning.js +19 -66
- package/dist/providers/types.d.ts +22 -10
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -67,9 +67,9 @@ export { createConfiguredAiSdkProvider, DEFAULT_MAX_OUTPUT_TOKENS } from "./prov
|
|
|
67
67
|
export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps } from "./providers/ai-sdk.js";
|
|
68
68
|
export { createEmbeddingProvider } from "./providers/embedding.js";
|
|
69
69
|
export type { EmbeddingProviderDeps } from "./providers/embedding.js";
|
|
70
|
-
export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy,
|
|
70
|
+
export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, ReasoningPolicy, } from "./providers/types.js";
|
|
71
71
|
export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
|
|
72
|
-
export { DEFAULT_REASONING
|
|
72
|
+
export { DEFAULT_REASONING } from "./providers/reasoning.js";
|
|
73
73
|
export { toProviderError, isProviderError } from "./providers/errors.js";
|
|
74
74
|
export type { ProviderError } from "./providers/errors.js";
|
|
75
75
|
export { createStreamingChat } from "./providers/streaming-chat.js";
|
package/dist/index.js
CHANGED
|
@@ -104,13 +104,15 @@ export { createEmbeddingProvider } from "./providers/embedding.js";
|
|
|
104
104
|
// `"off"`; the `cortex.harness.agent.cache_*_tokens` metrics show which case a
|
|
105
105
|
// deployment is actually in.
|
|
106
106
|
export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
|
|
107
|
-
// Reasoning depth. `ReasoningPolicy` is the vendor-neutral
|
|
108
|
-
//
|
|
109
|
-
// `RunAgentOptions.reasoning`. It defaults to `DEFAULT_REASONING` —
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
107
|
+
// Reasoning depth. `ReasoningPolicy` is the vendor-neutral name for how deep a
|
|
108
|
+
// model reasons; a composition root sets it per run through
|
|
109
|
+
// `RunAgentOptions.reasoning`. It defaults to `DEFAULT_REASONING` — `xhigh` —
|
|
110
|
+
// because an agent loop drives tools over many iterations, and a shallow turn
|
|
111
|
+
// there wastes more calls than the deeper turn costs in tokens. The provider
|
|
112
|
+
// package resolves the name for the model that it is bound to, thus a model
|
|
113
|
+
// that accepts no `xhigh` gets the nearest name that it accepts. A host on a
|
|
114
|
+
// model with no reasoning support passes `"provider-default"`.
|
|
115
|
+
export { DEFAULT_REASONING } from "./providers/reasoning.js";
|
|
114
116
|
// Provider error channel. `ProviderError` is the value `chat`/`embed` fail
|
|
115
117
|
// with; `toProviderError` is its sole constructor. Exposed so an embedder
|
|
116
118
|
// realizing its own `ChatProvider`/`EmbeddingProvider` fails with the exact
|
package/dist/loop/run-agent.d.ts
CHANGED
|
@@ -76,14 +76,15 @@ export interface RunAgentOptions {
|
|
|
76
76
|
readonly promptCache?: PromptCachePolicy;
|
|
77
77
|
/**
|
|
78
78
|
* Reasoning policy for every LLM call this run makes. Defaults to
|
|
79
|
-
* `DEFAULT_REASONING` (
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* `DEFAULT_REASONING` (`xhigh`) — an agent loop drives tools over many
|
|
80
|
+
* iterations, and a shallow turn there costs more in wasted calls than the
|
|
81
|
+
* deeper turn costs in tokens. The provider package resolves the name for
|
|
82
|
+
* the model that it is bound to.
|
|
82
83
|
*
|
|
83
84
|
* The policy lives here, on the run, rather than on the provider, for the
|
|
84
85
|
* same reason that the cache policy does: a one-shot LLM call elsewhere has
|
|
85
86
|
* its own depth needs and must not inherit the depth of a loop. A host on a
|
|
86
|
-
* model with no reasoning support passes `"
|
|
87
|
+
* model with no reasoning support passes `"provider-default"`.
|
|
87
88
|
*/
|
|
88
89
|
readonly reasoning?: ReasoningPolicy;
|
|
89
90
|
/**
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -8,7 +8,7 @@ import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-m
|
|
|
8
8
|
import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
|
|
9
9
|
import { classifyProviderError } from "../providers/errors.js";
|
|
10
10
|
import { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "../providers/prompt-cache.js";
|
|
11
|
-
import { DEFAULT_REASONING
|
|
11
|
+
import { DEFAULT_REASONING } from "../providers/reasoning.js";
|
|
12
12
|
import { resultStep } from "./run-step.js";
|
|
13
13
|
import { AskRejectedError, UnavailableAsk } from "../tools/approval/contract.js";
|
|
14
14
|
import { isToolError, readToolResultImages } from "../tools/define-tool.js";
|
|
@@ -85,7 +85,10 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
85
85
|
// Resolved once, not per iteration: an identical options object across every
|
|
86
86
|
// call is itself part of the cache contract — the request prefix has to be
|
|
87
87
|
// byte-identical to be read back.
|
|
88
|
-
const providerOptions =
|
|
88
|
+
const providerOptions = promptCacheProviderOptions(opts.promptCache ?? DEFAULT_PROMPT_CACHE);
|
|
89
|
+
// The depth is a neutral name, and the provider package resolves it for the
|
|
90
|
+
// model. A vendor key here would turn that resolution off.
|
|
91
|
+
const reasoning = opts.reasoning ?? DEFAULT_REASONING;
|
|
89
92
|
const usage = {};
|
|
90
93
|
// Exactly one record per completed run — never one per iteration. That bound is
|
|
91
94
|
// what keeps the default level affordable for an agent that runs long; the
|
|
@@ -233,6 +236,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
233
236
|
tools: toolDefs,
|
|
234
237
|
...(opts.toolChoice !== undefined ? { toolChoice: opts.toolChoice } : {}),
|
|
235
238
|
providerOptions,
|
|
239
|
+
reasoning,
|
|
236
240
|
};
|
|
237
241
|
const llmStepName = formatStepName.llm(i);
|
|
238
242
|
const reply = await resultStep(runStep)(llmStepName, () => provider.chat(request, session, signal));
|
|
@@ -343,7 +347,7 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
343
347
|
// because it is the one call whose write is pure waste, and the
|
|
344
348
|
// cache_write_tokens counter is what makes that waste visible.
|
|
345
349
|
const wrapUpStepName = formatStepName.llm(agent.maxIterations);
|
|
346
|
-
const wrapUp = await resultStep(runStep)(wrapUpStepName, () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none", providerOptions }, session, signal));
|
|
350
|
+
const wrapUp = await resultStep(runStep)(wrapUpStepName, () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none", providerOptions, reasoning }, session, signal));
|
|
347
351
|
accountForCall(wrapUp, wrapUpStepName);
|
|
348
352
|
if (wrapUp.finishReason === "aborted") {
|
|
349
353
|
// An abort during the tool-less wrap-up is still the user cutting the turn — the
|
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,19 +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,
|
|
493
|
+
reasoning: req.reasoning,
|
|
424
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
|
+
};
|
|
425
519
|
});
|
|
426
|
-
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
|
+
}));
|
|
427
527
|
}
|
|
428
528
|
catch (e) {
|
|
429
529
|
if (isAbortError(e) || signal?.aborted)
|
|
430
530
|
throw e;
|
|
431
|
-
|
|
531
|
+
const failure = toProviderError(unwrapForClassification(e), workloadOf(session));
|
|
532
|
+
logFailure(session, failure, e);
|
|
533
|
+
return err(failure);
|
|
432
534
|
}
|
|
433
535
|
};
|
|
434
536
|
return new ResultAsync(run());
|
|
@@ -467,6 +569,7 @@ export function createAiSdkProvider(deps) {
|
|
|
467
569
|
// thus it feeds neither bound.
|
|
468
570
|
...(requestTimeoutMs !== undefined ? { timeout: { firstChunkMs: requestTimeoutMs, chunkMs: requestTimeoutMs } } : {}),
|
|
469
571
|
providerOptions: req.providerOptions,
|
|
572
|
+
reasoning: req.reasoning,
|
|
470
573
|
});
|
|
471
574
|
const iterator = result.fullStream[Symbol.asyncIterator]();
|
|
472
575
|
const first = await pullNextDelta(iterator);
|
|
@@ -475,6 +578,8 @@ export function createAiSdkProvider(deps) {
|
|
|
475
578
|
// fresh window under the retry policy of a connection error.
|
|
476
579
|
if (first.kind === "aborted")
|
|
477
580
|
throw abortedStreamFailure(signal, first.reason, requestTimeoutMs);
|
|
581
|
+
if (first.kind === "failed")
|
|
582
|
+
throw streamPartFailure(first.error);
|
|
478
583
|
if (first.kind === "end") {
|
|
479
584
|
// A doStream promise rejection — the shape a real connection
|
|
480
585
|
// failure takes — does NOT surface at this first pull: streamText
|
|
@@ -520,6 +625,8 @@ export function createAiSdkProvider(deps) {
|
|
|
520
625
|
// terminal `ProviderError` from the throwable.
|
|
521
626
|
if (pull.kind === "aborted")
|
|
522
627
|
throw abortedStreamFailure(signal, pull.reason, requestTimeoutMs);
|
|
628
|
+
if (pull.kind === "failed")
|
|
629
|
+
throw streamPartFailure(pull.error);
|
|
523
630
|
text += pull.text;
|
|
524
631
|
yield { type: "text-delta", text: pull.text };
|
|
525
632
|
}
|
|
@@ -539,7 +646,9 @@ export function createAiSdkProvider(deps) {
|
|
|
539
646
|
catch (e) {
|
|
540
647
|
if (isAbortError(e) || signal?.aborted)
|
|
541
648
|
throw e;
|
|
542
|
-
|
|
649
|
+
const failure = toProviderError(unwrapForClassification(e), workloadOf(session));
|
|
650
|
+
logFailure(session, failure, e);
|
|
651
|
+
throw failure;
|
|
543
652
|
}
|
|
544
653
|
}
|
|
545
654
|
// The provider advertises the guard limit on its own instance, thus a
|
|
@@ -622,39 +731,6 @@ function withStoreDirective(model, store) {
|
|
|
622
731
|
},
|
|
623
732
|
});
|
|
624
733
|
}
|
|
625
|
-
/**
|
|
626
|
-
* Mirror the neutral reasoning effort into the namespace of an
|
|
627
|
-
* OpenAI-compatible connection.
|
|
628
|
-
*
|
|
629
|
-
* The compatible provider reads `providerOptions[<the name of the connection>]`
|
|
630
|
-
* rather than `providerOptions.openai`, and it derives that namespace from the
|
|
631
|
-
* first dot-separated segment of the name. The loop cannot write that key,
|
|
632
|
-
* because only this seam knows the name. Thus the middleware copies
|
|
633
|
-
* `reasoningEffort` across at request time. A value that the caller already set
|
|
634
|
-
* on the namespace wins, and a request with no effort passes through untouched.
|
|
635
|
-
*/
|
|
636
|
-
function withCompatibleReasoning(model, name) {
|
|
637
|
-
const namespace = name.split(".")[0]?.trim();
|
|
638
|
-
if (namespace === undefined || namespace === "")
|
|
639
|
-
return model;
|
|
640
|
-
return wrapLanguageModel({
|
|
641
|
-
model,
|
|
642
|
-
middleware: {
|
|
643
|
-
transformParams: async ({ params }) => {
|
|
644
|
-
const effort = params.providerOptions?.["openai"]?.["reasoningEffort"];
|
|
645
|
-
if (effort === undefined)
|
|
646
|
-
return params;
|
|
647
|
-
return {
|
|
648
|
-
...params,
|
|
649
|
-
providerOptions: {
|
|
650
|
-
...params.providerOptions,
|
|
651
|
-
[namespace]: { reasoningEffort: effort, ...params.providerOptions?.[namespace] },
|
|
652
|
-
},
|
|
653
|
-
};
|
|
654
|
-
},
|
|
655
|
-
},
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
734
|
/**
|
|
659
735
|
* Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
|
|
660
736
|
* connection and model. The model is closed into the returned provider here (it
|
|
@@ -735,7 +811,9 @@ export function createConfiguredAiSdkProvider(deps) {
|
|
|
735
811
|
fetch: effectiveFetch,
|
|
736
812
|
});
|
|
737
813
|
return createAiSdkProvider({
|
|
738
|
-
|
|
814
|
+
// The compatible package reads the neutral `reasoning` of the call, thus
|
|
815
|
+
// the seam needs no middleware to carry the depth into its namespace.
|
|
816
|
+
model: provider.chatModel(config.model),
|
|
739
817
|
resolveBilling: deps.resolveBilling,
|
|
740
818
|
capabilities: config.capabilities,
|
|
741
819
|
logger: deps.logger,
|
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The reasoning depth of a run.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
* sibling of `prompt-cache.ts`, and it obeys the same rules.
|
|
4
|
+
* The harness states the depth one time, on `ChatRequest.reasoning`, with the
|
|
5
|
+
* neutral names of the AI SDK. It names no vendor key. Each provider package
|
|
6
|
+
* holds the table of what its own models accept, and it maps the neutral name
|
|
7
|
+
* onto the wire. The Anthropic package also selects adaptive thinking there.
|
|
9
8
|
*
|
|
10
|
-
* ## Why the
|
|
9
|
+
* ## Why the harness names no vendor key
|
|
11
10
|
*
|
|
12
|
-
* `providerOptions`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* ## What each namespace does
|
|
18
|
-
*
|
|
19
|
-
* The Anthropic namespace carries two keys, because the vendor splits the
|
|
20
|
-
* concept in two:
|
|
21
|
-
*
|
|
22
|
-
* - `thinking: { type: "adaptive" }` says that the model reasons, and it lets
|
|
23
|
-
* the model choose the depth for each turn. A fixed `budgetTokens` is removed
|
|
24
|
-
* on the current models, and it returns a 400 there.
|
|
25
|
-
* - `effort` says how deep the model reasons and how much it spends in total.
|
|
26
|
-
* The vendor default is `high`.
|
|
27
|
-
*
|
|
28
|
-
* The OpenAI namespace carries one key, `reasoningEffort`, which takes the same
|
|
29
|
-
* five values. An OpenAI-compatible endpoint reads the same key, but from a
|
|
30
|
-
* namespace that carries the *name of the connection* rather than `openai`. The
|
|
31
|
-
* provider seam mirrors the value into that namespace, because only the seam
|
|
32
|
-
* knows the name (`providers/ai-sdk.ts`).
|
|
11
|
+
* The harness wrote `providerOptions.anthropic.effort` before. A value on that
|
|
12
|
+
* key turns the per-model table of the Anthropic package off, thus the raw name
|
|
13
|
+
* reached the wire. A model that accepts `high` but not `xhigh` answered 400.
|
|
14
|
+
* The neutral field has no such hazard, because the package resolves the name
|
|
15
|
+
* for the model that it is bound to.
|
|
33
16
|
*
|
|
34
17
|
* ## Where the policy belongs
|
|
35
18
|
*
|
|
@@ -37,29 +20,16 @@
|
|
|
37
20
|
* cache policy does. A one-shot LLM call elsewhere in the harness has its own
|
|
38
21
|
* depth needs, and it must not inherit the depth of an agent loop.
|
|
39
22
|
*/
|
|
40
|
-
import type {
|
|
23
|
+
import type { ReasoningPolicy } from "./types.js";
|
|
41
24
|
/**
|
|
42
|
-
* The default policy:
|
|
25
|
+
* The default policy: the deepest name of the neutral ladder.
|
|
43
26
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* lower effort, and a host on a model with no reasoning support passes `"off"`.
|
|
49
|
-
*/
|
|
50
|
-
export declare const DEFAULT_REASONING: ReasoningPolicy;
|
|
51
|
-
/**
|
|
52
|
-
* Translate a neutral reasoning policy into provider wire options.
|
|
27
|
+
* Each agent of the harness drives tools over many iterations. A shallow turn
|
|
28
|
+
* there wastes more calls than a deeper turn costs in tokens. The Anthropic
|
|
29
|
+
* package sends `xhigh` to a model that accepts it, and `max` to a model that
|
|
30
|
+
* does not. The OpenAI-compatible package sends the name as it is.
|
|
53
31
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
32
|
+
* A host that wants a cheaper loop passes a lower name. A host on a model with
|
|
33
|
+
* no reasoning support passes `"provider-default"`.
|
|
56
34
|
*/
|
|
57
|
-
export declare
|
|
58
|
-
/**
|
|
59
|
-
* Merge two option bags one namespace at a time.
|
|
60
|
-
*
|
|
61
|
-
* A plain spread of the two bags drops a whole namespace when both sides carry
|
|
62
|
-
* it, and the cache policy and the reasoning policy both write `anthropic`. The
|
|
63
|
-
* right-hand bag wins for a key that both sides set.
|
|
64
|
-
*/
|
|
65
|
-
export declare function mergeProviderOptions(left: ProviderOptions | undefined, right: ProviderOptions | undefined): ProviderOptions | undefined;
|
|
35
|
+
export declare const DEFAULT_REASONING: ReasoningPolicy;
|
|
@@ -1,35 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The reasoning depth of a run.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
* sibling of `prompt-cache.ts`, and it obeys the same rules.
|
|
4
|
+
* The harness states the depth one time, on `ChatRequest.reasoning`, with the
|
|
5
|
+
* neutral names of the AI SDK. It names no vendor key. Each provider package
|
|
6
|
+
* holds the table of what its own models accept, and it maps the neutral name
|
|
7
|
+
* onto the wire. The Anthropic package also selects adaptive thinking there.
|
|
9
8
|
*
|
|
10
|
-
* ## Why the
|
|
9
|
+
* ## Why the harness names no vendor key
|
|
11
10
|
*
|
|
12
|
-
* `providerOptions`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* ## What each namespace does
|
|
18
|
-
*
|
|
19
|
-
* The Anthropic namespace carries two keys, because the vendor splits the
|
|
20
|
-
* concept in two:
|
|
21
|
-
*
|
|
22
|
-
* - `thinking: { type: "adaptive" }` says that the model reasons, and it lets
|
|
23
|
-
* the model choose the depth for each turn. A fixed `budgetTokens` is removed
|
|
24
|
-
* on the current models, and it returns a 400 there.
|
|
25
|
-
* - `effort` says how deep the model reasons and how much it spends in total.
|
|
26
|
-
* The vendor default is `high`.
|
|
27
|
-
*
|
|
28
|
-
* The OpenAI namespace carries one key, `reasoningEffort`, which takes the same
|
|
29
|
-
* five values. An OpenAI-compatible endpoint reads the same key, but from a
|
|
30
|
-
* namespace that carries the *name of the connection* rather than `openai`. The
|
|
31
|
-
* provider seam mirrors the value into that namespace, because only the seam
|
|
32
|
-
* knows the name (`providers/ai-sdk.ts`).
|
|
11
|
+
* The harness wrote `providerOptions.anthropic.effort` before. A value on that
|
|
12
|
+
* key turns the per-model table of the Anthropic package off, thus the raw name
|
|
13
|
+
* reached the wire. A model that accepts `high` but not `xhigh` answered 400.
|
|
14
|
+
* The neutral field has no such hazard, because the package resolves the name
|
|
15
|
+
* for the model that it is bound to.
|
|
33
16
|
*
|
|
34
17
|
* ## Where the policy belongs
|
|
35
18
|
*
|
|
@@ -38,44 +21,14 @@
|
|
|
38
21
|
* depth needs, and it must not inherit the depth of an agent loop.
|
|
39
22
|
*/
|
|
40
23
|
/**
|
|
41
|
-
* The default policy:
|
|
24
|
+
* The default policy: the deepest name of the neutral ladder.
|
|
42
25
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* lower effort, and a host on a model with no reasoning support passes `"off"`.
|
|
48
|
-
*/
|
|
49
|
-
export const DEFAULT_REASONING = { effort: "xhigh" };
|
|
50
|
-
/**
|
|
51
|
-
* Translate a neutral reasoning policy into provider wire options.
|
|
52
|
-
*
|
|
53
|
-
* Returns `undefined` for `"off"`, thus the caller can leave `providerOptions`
|
|
54
|
-
* unset rather than send an empty bag.
|
|
55
|
-
*/
|
|
56
|
-
export function reasoningProviderOptions(policy) {
|
|
57
|
-
if (policy === "off")
|
|
58
|
-
return undefined;
|
|
59
|
-
return {
|
|
60
|
-
anthropic: { effort: policy.effort, thinking: { type: "adaptive" } },
|
|
61
|
-
openai: { reasoningEffort: policy.effort },
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Merge two option bags one namespace at a time.
|
|
26
|
+
* Each agent of the harness drives tools over many iterations. A shallow turn
|
|
27
|
+
* there wastes more calls than a deeper turn costs in tokens. The Anthropic
|
|
28
|
+
* package sends `xhigh` to a model that accepts it, and `max` to a model that
|
|
29
|
+
* does not. The OpenAI-compatible package sends the name as it is.
|
|
66
30
|
*
|
|
67
|
-
* A
|
|
68
|
-
*
|
|
69
|
-
* right-hand bag wins for a key that both sides set.
|
|
31
|
+
* A host that wants a cheaper loop passes a lower name. A host on a model with
|
|
32
|
+
* no reasoning support passes `"provider-default"`.
|
|
70
33
|
*/
|
|
71
|
-
export
|
|
72
|
-
if (left === undefined)
|
|
73
|
-
return right;
|
|
74
|
-
if (right === undefined)
|
|
75
|
-
return left;
|
|
76
|
-
const merged = { ...left };
|
|
77
|
-
for (const [namespace, options] of Object.entries(right)) {
|
|
78
|
-
merged[namespace] = { ...merged[namespace], ...options };
|
|
79
|
-
}
|
|
80
|
-
return merged;
|
|
81
|
-
}
|
|
34
|
+
export const DEFAULT_REASONING = "xhigh";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* attribution is resolved lazily at the provider wire boundary.
|
|
7
7
|
*/
|
|
8
8
|
import type { FinishReason, LanguageModel, ModelMessage, ToolSet } from "ai";
|
|
9
|
+
import type { LanguageModelV4CallOptions } from "@ai-sdk/provider";
|
|
9
10
|
import type { ProviderOptions } from "@ai-sdk/provider-utils";
|
|
10
11
|
import type { ResultAsync } from "neverthrow";
|
|
11
12
|
import type { AgentSession } from "../auth/types.js";
|
|
@@ -40,6 +41,11 @@ export interface ChatRequest {
|
|
|
40
41
|
readonly toolName: string;
|
|
41
42
|
};
|
|
42
43
|
readonly providerOptions?: ProviderOptions;
|
|
44
|
+
/**
|
|
45
|
+
* How deep the model reasons on this call. Absent sends no directive, thus
|
|
46
|
+
* the model applies its own default. Refer to `ReasoningPolicy`.
|
|
47
|
+
*/
|
|
48
|
+
readonly reasoning?: ReasoningPolicy;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
51
|
* Vendor-neutral prompt-cache policy — a harness concept, not a vendor one.
|
|
@@ -57,17 +63,23 @@ export type PromptCachePolicy = {
|
|
|
57
63
|
/**
|
|
58
64
|
* How deep a model reasons, in harness-neutral names.
|
|
59
65
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
+
* This is the `reasoning` call setting of the AI SDK, and the harness passes the
|
|
67
|
+
* value through without a change. Each provider package maps the name onto its
|
|
68
|
+
* own wire shape, and each one holds the table of what its models accept. The
|
|
69
|
+
* Anthropic package selects adaptive thinking with the matching `effort`, and it
|
|
70
|
+
* lowers a level that the model does not accept. The OpenAI-compatible package
|
|
71
|
+
* sends the name as `reasoning_effort`.
|
|
72
|
+
*
|
|
73
|
+
* `"provider-default"` sends no directive, thus the model applies its own
|
|
74
|
+
* default. `"none"` asks for no reasoning. The other names are the ladder from
|
|
75
|
+
* the shallowest depth to the deepest depth.
|
|
76
|
+
*
|
|
77
|
+
* CAUTION: never write `providerOptions.anthropic.effort` or
|
|
78
|
+
* `providerOptions.openai.reasoningEffort` in place of this field. A value on
|
|
79
|
+
* either key turns the per-model table off, and the raw name then reaches the
|
|
80
|
+
* wire. A model that does not accept that name answers 400.
|
|
66
81
|
*/
|
|
67
|
-
export type
|
|
68
|
-
export type ReasoningPolicy = {
|
|
69
|
-
readonly effort: ReasoningEffort;
|
|
70
|
-
} | "off";
|
|
82
|
+
export type ReasoningPolicy = NonNullable<LanguageModelV4CallOptions["reasoning"]>;
|
|
71
83
|
/**
|
|
72
84
|
* Token accounting for one chat call, in harness-neutral names.
|
|
73
85
|
*
|
package/package.json
CHANGED