@juspay/neurolink 12.14.10 → 12.14.11
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.
|
@@ -244,6 +244,27 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
|
|
|
244
244
|
*/
|
|
245
245
|
generate(optionsOrPrompt: TextGenerationOptions | string, analysisSchema?: ValidationSchema): Promise<EnhancedGenerateResult | null>;
|
|
246
246
|
private executeNativeGenerate;
|
|
247
|
+
/**
|
|
248
|
+
* Recover structured output on a turn where tools forced `response_format`
|
|
249
|
+
* off the wire.
|
|
250
|
+
*
|
|
251
|
+
* Runs only after the tool turn is complete, and only when its answer does
|
|
252
|
+
* not already satisfy the schema. The re-ask drops the tools, which is what
|
|
253
|
+
* makes native `response_format` legal again on a vendor that refuses the two
|
|
254
|
+
* together, so this is a formatting pass over an answer the model has already
|
|
255
|
+
* produced rather than a second attempt at the question.
|
|
256
|
+
*
|
|
257
|
+
* The schema is deliberately NOT spelled into the tool turn's system prompt
|
|
258
|
+
* instead. That was tried and reverted: a model handed a JSON Schema next to a
|
|
259
|
+
* tool list can read it as another tool — Groq's llama-3.3 answers by calling
|
|
260
|
+
* one named "json", which the server rejects — turning a wrong answer into a
|
|
261
|
+
* hard error.
|
|
262
|
+
*
|
|
263
|
+
* Returns the merged result, or the untouched `toolPhase` whenever the re-ask
|
|
264
|
+
* cannot be run or cannot be trusted, so it can improve an outcome but never
|
|
265
|
+
* degrade one.
|
|
266
|
+
*/
|
|
267
|
+
private reformatWithoutTools;
|
|
247
268
|
protected executeStream(options: StreamOptions, _analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
|
|
248
269
|
private runStreamLoop;
|
|
249
270
|
private streamOneStep;
|
|
@@ -30,7 +30,7 @@ import { logger } from "../utils/logger.js";
|
|
|
30
30
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
31
31
|
import { NoOutputGeneratedError } from "../utils/generationErrors.js";
|
|
32
32
|
import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
|
|
33
|
-
import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, } from "../utils/timeout.js";
|
|
33
|
+
import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
|
|
34
34
|
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
35
35
|
import { resolveRequestKind } from "../core/resolveRequestKind.js";
|
|
36
36
|
import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
|
|
@@ -62,6 +62,13 @@ const WINDOW_FIT_MARGIN_TOKENS = 512;
|
|
|
62
62
|
* object. A schema we cannot validate with accepts everything, so an unknown
|
|
63
63
|
* schema never forces a pointless second request.
|
|
64
64
|
*/
|
|
65
|
+
/**
|
|
66
|
+
* Thrown internally to unwind to the "keep the original answer" path when a
|
|
67
|
+
* structured-output re-ask completed but did not produce the object. Never
|
|
68
|
+
* escapes executeNativeGenerate.
|
|
69
|
+
*/
|
|
70
|
+
class StructuredReformatRejected extends Error {
|
|
71
|
+
}
|
|
65
72
|
const yieldsSchemaValidObject = (text, schema) => {
|
|
66
73
|
const coerced = coerceJsonToSchema(text, schema);
|
|
67
74
|
return coerced !== null && schemaAccepts(schema, coerced.structuredData);
|
|
@@ -869,20 +876,33 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
869
876
|
// Structured output rides response_format, which is what Output.object did
|
|
870
877
|
// on the ai path. Suppressed where the provider says the combination with
|
|
871
878
|
// tools is rejected.
|
|
872
|
-
const
|
|
879
|
+
const schemaJson = options.schema
|
|
880
|
+
? convertZodToJsonSchema(options.schema)
|
|
881
|
+
: undefined;
|
|
882
|
+
// Suppression is the DEFAULT (see suppressResponseFormatWithTools) and
|
|
883
|
+
// tools ride along on virtually every turn, so for a `generate({ schema })`
|
|
884
|
+
// call this is the ORDINARY path, not an edge case.
|
|
885
|
+
const responseFormatSuppressed = schemaJson !== undefined &&
|
|
886
|
+
hasTools &&
|
|
887
|
+
this.suppressResponseFormatWithTools();
|
|
888
|
+
const responseFormat = schemaJson && !responseFormatSuppressed
|
|
873
889
|
? {
|
|
874
890
|
type: "json",
|
|
875
|
-
schema:
|
|
891
|
+
schema: schemaJson,
|
|
876
892
|
}
|
|
877
893
|
: undefined;
|
|
878
894
|
const conversation = (await this.buildMessagesForStream(options));
|
|
879
895
|
const toolExecutionSummaries = [];
|
|
880
|
-
const runLoop = (conv, format
|
|
896
|
+
const runLoop = (conv, format,
|
|
897
|
+
// Omit the tools for a pass that only has to reformat an answer the
|
|
898
|
+
// model has already given. Dropping them is what makes `response_format`
|
|
899
|
+
// legal again on a vendor that refuses the two together.
|
|
900
|
+
withTools = true) => runNativeGenerateLoop({
|
|
881
901
|
doGenerate,
|
|
882
902
|
conversation: conv,
|
|
883
|
-
...(v3Tools ? { tools: v3Tools } : {}),
|
|
903
|
+
...(withTools && v3Tools ? { tools: v3Tools } : {}),
|
|
884
904
|
toolsRecord,
|
|
885
|
-
...(hasTools && options.toolChoice
|
|
905
|
+
...(withTools && hasTools && options.toolChoice
|
|
886
906
|
? { toolChoice: resolveToolChoice(options, toolsRecord, true) }
|
|
887
907
|
: {}),
|
|
888
908
|
// The per-call `timeout` keeps its per-MODEL-CALL meaning once
|
|
@@ -950,11 +970,41 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
950
970
|
options.schema !== undefined &&
|
|
951
971
|
!yieldsSchemaValidObject(loop.text, options.schema)) {
|
|
952
972
|
logger.warn(`[${this.providerName}] response_format did not yield a schema-valid object — retrying with the schema in the system prompt`, { provider: this.providerName, model: modelId });
|
|
953
|
-
|
|
973
|
+
// Unlike the catch above — where the first call threw and reported no
|
|
974
|
+
// usage — this branch is reached from a call that SUCCEEDED and was
|
|
975
|
+
// billed. Replacing `loop` outright would drop the tool phase's tokens
|
|
976
|
+
// from the caller's usage while the vendor still charges for them.
|
|
977
|
+
const rejected = loop;
|
|
978
|
+
const retried = await runLoop(appendJsonSchemaInstruction(conversation, responseFormat.schema), undefined);
|
|
979
|
+
loop = {
|
|
980
|
+
...retried,
|
|
981
|
+
inputTokens: rejected.inputTokens + retried.inputTokens,
|
|
982
|
+
outputTokens: rejected.outputTokens + retried.outputTokens,
|
|
983
|
+
cacheReadTokens: (rejected.cacheReadTokens ?? 0) + (retried.cacheReadTokens ?? 0),
|
|
984
|
+
cacheWriteTokens: (rejected.cacheWriteTokens ?? 0) + (retried.cacheWriteTokens ?? 0),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
// Ordinary case: tools rode along, so response_format was dropped and the
|
|
988
|
+
// model was never asked for JSON at all. Recover it with a tool-free
|
|
989
|
+
// re-ask. See reformatWithoutTools for why it is not done in the prompt.
|
|
990
|
+
if (responseFormatSuppressed && schemaJson && options.schema) {
|
|
991
|
+
loop = await this.reformatWithoutTools({
|
|
992
|
+
toolPhase: loop,
|
|
993
|
+
conversation,
|
|
994
|
+
schemaJson,
|
|
995
|
+
schema: options.schema,
|
|
996
|
+
runLoop,
|
|
997
|
+
modelId,
|
|
998
|
+
abortSignal: options.abortSignal,
|
|
999
|
+
});
|
|
954
1000
|
}
|
|
955
1001
|
const { text, finishReason, toolsUsed } = loop;
|
|
956
1002
|
const inputTokens = loop.inputTokens;
|
|
957
1003
|
const outputTokens = loop.outputTokens;
|
|
1004
|
+
// `loop.rawFinishReason` is where reformatWithoutTools's raw/unified
|
|
1005
|
+
// pairing lands — without forwarding it here, that pairing logic is
|
|
1006
|
+
// computed but never reaches the caller, and `result.rawFinishReason`
|
|
1007
|
+
// is silently undefined regardless of what the tool phase reported.
|
|
958
1008
|
// stopReason / stepsUsed parity with the other native loops (Vertex
|
|
959
1009
|
// Gemini / Claude / Bedrock) and with the ai-package path this replaced.
|
|
960
1010
|
// Without them a consumer cannot tell a completed turn from one the step
|
|
@@ -973,6 +1023,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
973
1023
|
provider: this.providerName,
|
|
974
1024
|
model: modelId,
|
|
975
1025
|
finishReason,
|
|
1026
|
+
...(loop.rawFinishReason
|
|
1027
|
+
? { rawFinishReason: loop.rawFinishReason }
|
|
1028
|
+
: {}),
|
|
976
1029
|
stopReason,
|
|
977
1030
|
stepsUsed,
|
|
978
1031
|
usage: {
|
|
@@ -999,6 +1052,253 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
999
1052
|
};
|
|
1000
1053
|
return this.finalizeNativeGenerate(enhanced, options, startTime);
|
|
1001
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Recover structured output on a turn where tools forced `response_format`
|
|
1057
|
+
* off the wire.
|
|
1058
|
+
*
|
|
1059
|
+
* Runs only after the tool turn is complete, and only when its answer does
|
|
1060
|
+
* not already satisfy the schema. The re-ask drops the tools, which is what
|
|
1061
|
+
* makes native `response_format` legal again on a vendor that refuses the two
|
|
1062
|
+
* together, so this is a formatting pass over an answer the model has already
|
|
1063
|
+
* produced rather than a second attempt at the question.
|
|
1064
|
+
*
|
|
1065
|
+
* The schema is deliberately NOT spelled into the tool turn's system prompt
|
|
1066
|
+
* instead. That was tried and reverted: a model handed a JSON Schema next to a
|
|
1067
|
+
* tool list can read it as another tool — Groq's llama-3.3 answers by calling
|
|
1068
|
+
* one named "json", which the server rejects — turning a wrong answer into a
|
|
1069
|
+
* hard error.
|
|
1070
|
+
*
|
|
1071
|
+
* Returns the merged result, or the untouched `toolPhase` whenever the re-ask
|
|
1072
|
+
* cannot be run or cannot be trusted, so it can improve an outcome but never
|
|
1073
|
+
* degrade one.
|
|
1074
|
+
*/
|
|
1075
|
+
async reformatWithoutTools(args) {
|
|
1076
|
+
const { toolPhase, conversation, schemaJson, schema, runLoop, modelId, abortSignal, } = args;
|
|
1077
|
+
let loop = toolPhase;
|
|
1078
|
+
// A function, not a captured boolean: the signal is read once before the
|
|
1079
|
+
// re-ask and again after it, and it can legitimately flip in between — a
|
|
1080
|
+
// const would let the compiler narrow the second read away.
|
|
1081
|
+
const isCancelled = () => abortSignal?.aborted === true;
|
|
1082
|
+
// The suppressed case, which is the ordinary one: tools rode along, so
|
|
1083
|
+
// `response_format` was dropped — and because BOTH branches above are keyed
|
|
1084
|
+
// on `responseFormat !== undefined`, the very condition that dropped it also
|
|
1085
|
+
// disabled the fallback that would have asked for the object in words. The
|
|
1086
|
+
// model was never asked for JSON in any form, answered in prose, and
|
|
1087
|
+
// `structuredData` came back unset, breaking the documented
|
|
1088
|
+
// `generate({ schema })` guarantee.
|
|
1089
|
+
//
|
|
1090
|
+
// The fix is NOT to spell the schema into the system prompt of the tool
|
|
1091
|
+
// turn. That collides with the tool-call channel: Groq's llama-3.3, handed
|
|
1092
|
+
// a JSON Schema while tools are declared, answers by trying to call a tool
|
|
1093
|
+
// literally named "json", and the server rejects the turn outright — a hard
|
|
1094
|
+
// error where there used to be a merely wrong answer.
|
|
1095
|
+
//
|
|
1096
|
+
// Instead, let the tool turn finish untouched and re-ask ONCE with the
|
|
1097
|
+
// tools removed, which is what makes native `response_format` legal again.
|
|
1098
|
+
// By then the tool phase is over and this is a pure formatting pass over an
|
|
1099
|
+
// answer the model has already produced.
|
|
1100
|
+
//
|
|
1101
|
+
// Wrapped so it can only ever improve the outcome: if the reformat pass
|
|
1102
|
+
// fails for any reason, the original tool-phase result stands and the turn
|
|
1103
|
+
// succeeds exactly as it did before.
|
|
1104
|
+
if (
|
|
1105
|
+
// Nothing to reformat. An empty completion has its own handling
|
|
1106
|
+
// downstream, and asking a model to restate a blank answer as JSON
|
|
1107
|
+
// invites it to invent one.
|
|
1108
|
+
loop.text.trim().length > 0 &&
|
|
1109
|
+
// Already cancelled: starting a second request here could only burn
|
|
1110
|
+
// quota on a turn nobody is waiting for.
|
|
1111
|
+
!isCancelled() &&
|
|
1112
|
+
!yieldsSchemaValidObject(loop.text, schema)) {
|
|
1113
|
+
logger.warn(`[${this.providerName}] tools suppressed response_format and the answer is not schema-valid — re-asking once without tools`, { provider: this.providerName, model: modelId });
|
|
1114
|
+
// Every request this block makes is billed, including the ones whose
|
|
1115
|
+
// ANSWER is discarded — a re-ask the vendor answered off-schema, and a
|
|
1116
|
+
// re-ask rejected below for still not being the object. Accumulate as
|
|
1117
|
+
// each response lands rather than reading the survivor at the end,
|
|
1118
|
+
// because the acceptance throw unwinds past the merge: reporting only
|
|
1119
|
+
// the kept response would bill a four-request turn as a two-request one.
|
|
1120
|
+
const billedReask = {
|
|
1121
|
+
inputTokens: 0,
|
|
1122
|
+
outputTokens: 0,
|
|
1123
|
+
cacheReadTokens: 0,
|
|
1124
|
+
cacheWriteTokens: 0,
|
|
1125
|
+
};
|
|
1126
|
+
const bill = (billed) => {
|
|
1127
|
+
billedReask.inputTokens += billed.inputTokens;
|
|
1128
|
+
billedReask.outputTokens += billed.outputTokens;
|
|
1129
|
+
billedReask.cacheReadTokens += billed.cacheReadTokens ?? 0;
|
|
1130
|
+
billedReask.cacheWriteTokens += billed.cacheWriteTokens ?? 0;
|
|
1131
|
+
};
|
|
1132
|
+
try {
|
|
1133
|
+
// `conversation` is load-bearing here, and its contents are not
|
|
1134
|
+
// obvious: runNativeGenerateLoop PUSHES each assistant tool-call turn
|
|
1135
|
+
// and each tool-result turn onto this very array, but breaks out before
|
|
1136
|
+
// pushing the final text answer. So it already carries the whole tool
|
|
1137
|
+
// exchange and is missing exactly one message — the answer — which is
|
|
1138
|
+
// what the assistant turn below supplies. That is what lets the
|
|
1139
|
+
// reformat restate tool-derived values instead of re-answering blind.
|
|
1140
|
+
// If that loop is ever changed to build a new array instead of pushing
|
|
1141
|
+
// onto its input, this call must be changed to take the loop's
|
|
1142
|
+
// conversation, or the reformat goes blind to the tools.
|
|
1143
|
+
const reaskConversation = [
|
|
1144
|
+
...conversation,
|
|
1145
|
+
{ role: "assistant", content: toolPhase.text },
|
|
1146
|
+
{
|
|
1147
|
+
role: "user",
|
|
1148
|
+
content:
|
|
1149
|
+
// "value", not "object": ValidationSchema also accepts array
|
|
1150
|
+
// and scalar roots (z.array(...), z.string()). Demanding an
|
|
1151
|
+
// object told a compliant model to emit something those roots
|
|
1152
|
+
// can never satisfy, so yieldsSchemaValidObject rejected every
|
|
1153
|
+
// such re-ask and the recovery silently never fired for them.
|
|
1154
|
+
"Return that same answer as a single JSON value conforming to " +
|
|
1155
|
+
"the required schema. No prose before or after it, and no " +
|
|
1156
|
+
"markdown code fence.",
|
|
1157
|
+
},
|
|
1158
|
+
];
|
|
1159
|
+
// Two strategies, native first. A vendor can reject the SCHEMA itself
|
|
1160
|
+
// rather than the request: Groq answers a non-object root with
|
|
1161
|
+
// "invalid JSON schema for response_format: schema must have type
|
|
1162
|
+
// 'object'", so every array- and scalar-root schema fails here. The
|
|
1163
|
+
// non-suppressed path already degrades to spelling the schema into the
|
|
1164
|
+
// prompt (its catch classifies that error via
|
|
1165
|
+
// isToolsSchemaConflictError); this path has to do the same or the
|
|
1166
|
+
// recovery is dead for exactly the roots the "value" wording was meant
|
|
1167
|
+
// to serve. Still tools-free either way, so response_format stays legal.
|
|
1168
|
+
let reformatted;
|
|
1169
|
+
let schemaSpelledOut = false;
|
|
1170
|
+
try {
|
|
1171
|
+
reformatted = await runLoop(reaskConversation, { type: "json", schema: schemaJson }, false);
|
|
1172
|
+
bill(reformatted);
|
|
1173
|
+
}
|
|
1174
|
+
catch (nativeFormatError) {
|
|
1175
|
+
if (isCancelled()) {
|
|
1176
|
+
throw nativeFormatError;
|
|
1177
|
+
}
|
|
1178
|
+
logger.warn(`[${this.providerName}] the re-ask's response_format was rejected — retrying with the schema in the prompt`, {
|
|
1179
|
+
provider: this.providerName,
|
|
1180
|
+
model: modelId,
|
|
1181
|
+
error: nativeFormatError instanceof Error
|
|
1182
|
+
? nativeFormatError.message
|
|
1183
|
+
: String(nativeFormatError),
|
|
1184
|
+
});
|
|
1185
|
+
reformatted = await runLoop(appendJsonSchemaInstruction(reaskConversation, schemaJson), undefined, false);
|
|
1186
|
+
bill(reformatted);
|
|
1187
|
+
schemaSpelledOut = true;
|
|
1188
|
+
}
|
|
1189
|
+
// A vendor whose `response_format` cannot carry a schema does not
|
|
1190
|
+
// reject the request — it accepts it and answers with JSON of a shape
|
|
1191
|
+
// it invented, because the schema never reached the model. DeepSeek is
|
|
1192
|
+
// the standing case: it supports only `json_object`, so the native
|
|
1193
|
+
// re-ask above returns well-formed JSON echoing the format envelope
|
|
1194
|
+
// (`{"type":"json_object","value":…}`) instead of the schema, and the
|
|
1195
|
+
// acceptance check below would keep the prose and count the recovery a
|
|
1196
|
+
// loss. The rejection branch already knows the remedy, so reach it by
|
|
1197
|
+
// OUTCOME as well as by exception: one more tools-free pass with the
|
|
1198
|
+
// schema spelled into the prompt. It costs an extra request only on a
|
|
1199
|
+
// turn that was already going to be thrown away.
|
|
1200
|
+
if (!schemaSpelledOut &&
|
|
1201
|
+
!isCancelled() &&
|
|
1202
|
+
!yieldsSchemaValidObject(reformatted.text, schema)) {
|
|
1203
|
+
logger.warn(`[${this.providerName}] the re-ask's response_format carried no schema — retrying with the schema in the prompt`, { provider: this.providerName, model: modelId });
|
|
1204
|
+
reformatted = await runLoop(appendJsonSchemaInstruction(reaskConversation, schemaJson), undefined, false);
|
|
1205
|
+
bill(reformatted);
|
|
1206
|
+
}
|
|
1207
|
+
// Accept the reformat ONLY if it actually produced the object. Sending
|
|
1208
|
+
// response_format is not the same as being obeyed: some vendors ignore
|
|
1209
|
+
// it outright (the branch above this one exists for precisely that),
|
|
1210
|
+
// and the pass can also be cut short by the same maxTokens cap the
|
|
1211
|
+
// prose answer just used. Without this check a substantive prose answer
|
|
1212
|
+
// could be replaced by a worse non-answer, which is the one thing this
|
|
1213
|
+
// fallback must never do.
|
|
1214
|
+
if (!yieldsSchemaValidObject(reformatted.text, schema)) {
|
|
1215
|
+
logger.warn(`[${this.providerName}] the tool-free re-ask did not yield a schema-valid object either; keeping the original answer`, { provider: this.providerName, model: modelId });
|
|
1216
|
+
throw new StructuredReformatRejected();
|
|
1217
|
+
}
|
|
1218
|
+
// Keep the tool phase's accounting. The reformat pass ran without
|
|
1219
|
+
// tools, so on its own it reports no tool use and only its own tokens —
|
|
1220
|
+
// publishing that verbatim would erase the tools the turn really did
|
|
1221
|
+
// call and under-report what the turn really did cost.
|
|
1222
|
+
loop = {
|
|
1223
|
+
...reformatted,
|
|
1224
|
+
toolsUsed: [
|
|
1225
|
+
...new Set([...toolPhase.toolsUsed, ...reformatted.toolsUsed]),
|
|
1226
|
+
],
|
|
1227
|
+
inputTokens: toolPhase.inputTokens + billedReask.inputTokens,
|
|
1228
|
+
outputTokens: toolPhase.outputTokens + billedReask.outputTokens,
|
|
1229
|
+
cacheReadTokens: (toolPhase.cacheReadTokens ?? 0) + billedReask.cacheReadTokens,
|
|
1230
|
+
cacheWriteTokens: (toolPhase.cacheWriteTokens ?? 0) + billedReask.cacheWriteTokens,
|
|
1231
|
+
// NOT summed. `steps` is the agentic-loop budget that `maxSteps`
|
|
1232
|
+
// bounds, and the reformat is not a tool step — it runs after the
|
|
1233
|
+
// loop, with no tools. Adding it lets `stepsUsed` exceed the very
|
|
1234
|
+
// bound it is measured against, and pushes a tool phase that stopped
|
|
1235
|
+
// one step short of the cap over the `stepsUsed >= maxSteps`
|
|
1236
|
+
// threshold below, reporting a "step-cap" that never happened.
|
|
1237
|
+
steps: toolPhase.steps,
|
|
1238
|
+
// Only the TOOL phase can end for an alarming reason — the reformat
|
|
1239
|
+
// runs without tools and stops on its own — so any tool-phase reason
|
|
1240
|
+
// other than a clean "stop" is a signal the caller must still get.
|
|
1241
|
+
// Taking the reformat's cheerful "stop" verbatim would erase it:
|
|
1242
|
+
// "tool-calls" is what the step-cap stopReason just below is derived
|
|
1243
|
+
// from, and "length" is what tells the truncation machinery the
|
|
1244
|
+
// underlying answer was cut off. A turn cut short would report itself
|
|
1245
|
+
// as a clean completion. Deferring to the reformat only on a clean
|
|
1246
|
+
// tool phase also lets a reformat that itself hit the cap surface its
|
|
1247
|
+
// own "length".
|
|
1248
|
+
//
|
|
1249
|
+
// `rawFinishReason` moves WITH it. It arrives via the spread, so
|
|
1250
|
+
// taking the tool phase's unified reason while leaving the reformat's
|
|
1251
|
+
// raw one would hand a consumer a contradictory pair — unified
|
|
1252
|
+
// "length" beside raw "stop".
|
|
1253
|
+
...(toolPhase.finishReason !== "stop"
|
|
1254
|
+
? {
|
|
1255
|
+
finishReason: toolPhase.finishReason,
|
|
1256
|
+
// Override unconditionally: even when the tool phase has no
|
|
1257
|
+
// raw reason of its own, the reformat's raw "stop" must not
|
|
1258
|
+
// survive alongside a non-"stop" unified reason — that
|
|
1259
|
+
// contradictory pairing is exactly what this block exists to
|
|
1260
|
+
// prevent.
|
|
1261
|
+
rawFinishReason: toolPhase.rawFinishReason,
|
|
1262
|
+
}
|
|
1263
|
+
: { finishReason: reformatted.finishReason }),
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
catch (reformatError) {
|
|
1267
|
+
// A caller who cancelled deserves their cancellation, but OUR OWN turn
|
|
1268
|
+
// deadline must not turn an answer they used to receive into an error:
|
|
1269
|
+
// withTurnTimeout races no timer, it only translates errors, so a
|
|
1270
|
+
// deadline reached DURING the reformat would do exactly that. The two
|
|
1271
|
+
// are distinguishable even though both arrive on one composed signal —
|
|
1272
|
+
// AbortSignal.any propagates the reason of whichever fired, and the
|
|
1273
|
+
// turn timer aborts with a TimeoutError. So: our deadline keeps the
|
|
1274
|
+
// tool-phase answer, a caller's abort propagates.
|
|
1275
|
+
if (isCancelled() && !(abortSignal?.reason instanceof TimeoutError)) {
|
|
1276
|
+
throw reformatError;
|
|
1277
|
+
}
|
|
1278
|
+
if (!(reformatError instanceof StructuredReformatRejected)) {
|
|
1279
|
+
logger.warn(`[${this.providerName}] the tool-free structured-output re-ask failed; keeping the original answer`, {
|
|
1280
|
+
provider: this.providerName,
|
|
1281
|
+
model: modelId,
|
|
1282
|
+
error: reformatError instanceof Error
|
|
1283
|
+
? reformatError.message
|
|
1284
|
+
: String(reformatError),
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
// The answer is the tool phase's, but the cost is not: the re-ask
|
|
1288
|
+
// requests above were charged for whether or not their answers were
|
|
1289
|
+
// used. `loop` is still the untouched tool phase here — the merge
|
|
1290
|
+
// above never ran — so carry the bill onto it.
|
|
1291
|
+
loop = {
|
|
1292
|
+
...loop,
|
|
1293
|
+
inputTokens: loop.inputTokens + billedReask.inputTokens,
|
|
1294
|
+
outputTokens: loop.outputTokens + billedReask.outputTokens,
|
|
1295
|
+
cacheReadTokens: (loop.cacheReadTokens ?? 0) + billedReask.cacheReadTokens,
|
|
1296
|
+
cacheWriteTokens: (loop.cacheWriteTokens ?? 0) + billedReask.cacheWriteTokens,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return loop;
|
|
1301
|
+
}
|
|
1002
1302
|
async executeStream(options, _analysisSchema) {
|
|
1003
1303
|
this.validateStreamOptions(options);
|
|
1004
1304
|
const startTime = Date.now();
|