@carlonicora/nestjs-neo4jsonapi 2.3.1 → 2.4.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/dist/agents/common/usage-attribution.d.ts +218 -0
- package/dist/agents/common/usage-attribution.d.ts.map +1 -0
- package/dist/agents/common/usage-attribution.js +235 -0
- package/dist/agents/common/usage-attribution.js.map +1 -0
- package/dist/agents/community.summariser/services/community.summariser.service.d.ts.map +1 -1
- package/dist/agents/community.summariser/services/community.summariser.service.js +13 -1
- package/dist/agents/community.summariser/services/community.summariser.service.js.map +1 -1
- package/dist/agents/contextualiser/contexts/contextualiser.context.d.ts +24 -1
- package/dist/agents/contextualiser/contexts/contextualiser.context.d.ts.map +1 -1
- package/dist/agents/contextualiser/contexts/contextualiser.context.js +32 -0
- package/dist/agents/contextualiser/contexts/contextualiser.context.js.map +1 -1
- package/dist/agents/contextualiser/factories/contextualiser.context.factory.d.ts +7 -0
- package/dist/agents/contextualiser/factories/contextualiser.context.factory.d.ts.map +1 -1
- package/dist/agents/contextualiser/factories/contextualiser.context.factory.js +5 -0
- package/dist/agents/contextualiser/factories/contextualiser.context.factory.js.map +1 -1
- package/dist/agents/contextualiser/nodes/atomicfacts.node.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/nodes/atomicfacts.node.service.js +4 -0
- package/dist/agents/contextualiser/nodes/atomicfacts.node.service.js.map +1 -1
- package/dist/agents/contextualiser/nodes/chunk.node.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/nodes/chunk.node.service.js +4 -0
- package/dist/agents/contextualiser/nodes/chunk.node.service.js.map +1 -1
- package/dist/agents/contextualiser/nodes/chunk.vector.node.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/nodes/chunk.vector.node.service.js +15 -0
- package/dist/agents/contextualiser/nodes/chunk.vector.node.service.js.map +1 -1
- package/dist/agents/contextualiser/nodes/keyconcepts.node.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/nodes/keyconcepts.node.service.js +11 -0
- package/dist/agents/contextualiser/nodes/keyconcepts.node.service.js.map +1 -1
- package/dist/agents/contextualiser/nodes/question.refiner.node.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/nodes/question.refiner.node.service.js +4 -0
- package/dist/agents/contextualiser/nodes/question.refiner.node.service.js.map +1 -1
- package/dist/agents/contextualiser/nodes/rational.node.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/nodes/rational.node.service.js +4 -0
- package/dist/agents/contextualiser/nodes/rational.node.service.js.map +1 -1
- package/dist/agents/contextualiser/services/contextualiser.service.d.ts +7 -0
- package/dist/agents/contextualiser/services/contextualiser.service.d.ts.map +1 -1
- package/dist/agents/contextualiser/services/contextualiser.service.js +17 -5
- package/dist/agents/contextualiser/services/contextualiser.service.js.map +1 -1
- package/dist/agents/drift/contexts/drift.context.d.ts +25 -1
- package/dist/agents/drift/contexts/drift.context.d.ts.map +1 -1
- package/dist/agents/drift/contexts/drift.context.js +32 -0
- package/dist/agents/drift/contexts/drift.context.js.map +1 -1
- package/dist/agents/drift/nodes/followup.node.service.d.ts.map +1 -1
- package/dist/agents/drift/nodes/followup.node.service.js +4 -0
- package/dist/agents/drift/nodes/followup.node.service.js.map +1 -1
- package/dist/agents/drift/nodes/hyde.node.service.d.ts.map +1 -1
- package/dist/agents/drift/nodes/hyde.node.service.js +9 -1
- package/dist/agents/drift/nodes/hyde.node.service.js.map +1 -1
- package/dist/agents/drift/nodes/primer.answer.node.service.d.ts.map +1 -1
- package/dist/agents/drift/nodes/primer.answer.node.service.js +4 -0
- package/dist/agents/drift/nodes/primer.answer.node.service.js.map +1 -1
- package/dist/agents/drift/nodes/synthesis.node.service.d.ts.map +1 -1
- package/dist/agents/drift/nodes/synthesis.node.service.js +4 -0
- package/dist/agents/drift/nodes/synthesis.node.service.js.map +1 -1
- package/dist/agents/drift/services/drift.search.service.d.ts +9 -0
- package/dist/agents/drift/services/drift.search.service.d.ts.map +1 -1
- package/dist/agents/drift/services/drift.search.service.js +22 -5
- package/dist/agents/drift/services/drift.search.service.js.map +1 -1
- package/dist/agents/graph/services/graph.search.service.d.ts.map +1 -1
- package/dist/agents/graph/services/graph.search.service.js +10 -1
- package/dist/agents/graph/services/graph.search.service.js.map +1 -1
- package/dist/agents/graph/tools/tool.factory.d.ts +6 -0
- package/dist/agents/graph/tools/tool.factory.d.ts.map +1 -1
- package/dist/agents/graph/tools/tool.factory.js.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/operator/contexts/operator.context.d.ts +37 -1
- package/dist/agents/operator/contexts/operator.context.d.ts.map +1 -1
- package/dist/agents/operator/contexts/operator.context.js +8 -0
- package/dist/agents/operator/contexts/operator.context.js.map +1 -1
- package/dist/agents/operator/interfaces/operator.tool.interface.d.ts +8 -0
- package/dist/agents/operator/interfaces/operator.tool.interface.d.ts.map +1 -1
- package/dist/agents/operator/interfaces/operator.tool.interface.js.map +1 -1
- package/dist/agents/operator/services/operator.service.d.ts +8 -0
- package/dist/agents/operator/services/operator.service.d.ts.map +1 -1
- package/dist/agents/operator/services/operator.service.js +29 -0
- package/dist/agents/operator/services/operator.service.js.map +1 -1
- package/dist/agents/operator/tools/operator.tool.registry.d.ts.map +1 -1
- package/dist/agents/operator/tools/operator.tool.registry.js +2 -1
- package/dist/agents/operator/tools/operator.tool.registry.js.map +1 -1
- package/dist/agents/operator/tools/search-communities.tool.d.ts +8 -2
- package/dist/agents/operator/tools/search-communities.tool.d.ts.map +1 -1
- package/dist/agents/operator/tools/search-communities.tool.js +16 -4
- package/dist/agents/operator/tools/search-communities.tool.js.map +1 -1
- package/dist/agents/operator/tools/search-documents.tool.d.ts.map +1 -1
- package/dist/agents/operator/tools/search-documents.tool.js +5 -0
- package/dist/agents/operator/tools/search-documents.tool.js.map +1 -1
- package/dist/agents/responder/contexts/responder.context.d.ts +31 -0
- package/dist/agents/responder/contexts/responder.context.d.ts.map +1 -1
- package/dist/agents/responder/contexts/responder.context.js +8 -0
- package/dist/agents/responder/contexts/responder.context.js.map +1 -1
- package/dist/agents/responder/factories/responder.context.factory.d.ts +4 -0
- package/dist/agents/responder/factories/responder.context.factory.d.ts.map +1 -1
- package/dist/agents/responder/factories/responder.context.factory.js +2 -0
- package/dist/agents/responder/factories/responder.context.factory.js.map +1 -1
- package/dist/agents/responder/nodes/graph.node.service.d.ts +6 -0
- package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/graph.node.service.js +20 -0
- package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
- package/dist/agents/responder/nodes/planner.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/planner.node.service.js +9 -0
- package/dist/agents/responder/nodes/planner.node.service.js.map +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.js +9 -0
- package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
- package/dist/agents/responder/services/responder.service.d.ts +16 -0
- package/dist/agents/responder/services/responder.service.d.ts.map +1 -1
- package/dist/agents/responder/services/responder.service.js +29 -5
- package/dist/agents/responder/services/responder.service.js.map +1 -1
- package/dist/agents/summariser/services/summariser.service.d.ts +7 -0
- package/dist/agents/summariser/services/summariser.service.d.ts.map +1 -1
- package/dist/agents/summariser/services/summariser.service.js +15 -3
- package/dist/agents/summariser/services/summariser.service.js.map +1 -1
- package/dist/common/helpers/index.d.ts +1 -0
- package/dist/common/helpers/index.d.ts.map +1 -1
- package/dist/common/helpers/index.js +1 -0
- package/dist/common/helpers/index.js.map +1 -1
- package/dist/common/helpers/map-with-concurrency.d.ts +23 -0
- package/dist/common/helpers/map-with-concurrency.d.ts.map +1 -0
- package/dist/common/helpers/map-with-concurrency.js +60 -0
- package/dist/common/helpers/map-with-concurrency.js.map +1 -0
- package/dist/common/tokens.d.ts +2 -1
- package/dist/common/tokens.d.ts.map +1 -1
- package/dist/common/tokens.js +2 -1
- package/dist/common/tokens.js.map +1 -1
- package/dist/config/base.config.d.ts +19 -0
- package/dist/config/base.config.d.ts.map +1 -1
- package/dist/config/base.config.js +30 -3
- package/dist/config/base.config.js.map +1 -1
- package/dist/config/interfaces/config.ai.interface.d.ts +10 -1
- package/dist/config/interfaces/config.ai.interface.d.ts.map +1 -1
- package/dist/config/interfaces/config.chunk.queues.interface.d.ts +13 -0
- package/dist/config/interfaces/config.chunk.queues.interface.d.ts.map +1 -1
- package/dist/config/interfaces/config.credits.interface.d.ts +1 -1
- package/dist/core/llm/services/audio.llm.service.d.ts +70 -1
- package/dist/core/llm/services/audio.llm.service.d.ts.map +1 -1
- package/dist/core/llm/services/audio.llm.service.js +118 -3
- package/dist/core/llm/services/audio.llm.service.js.map +1 -1
- package/dist/core/llm/services/document-ai.service.d.ts +6 -0
- package/dist/core/llm/services/document-ai.service.d.ts.map +1 -1
- package/dist/core/llm/services/document-ai.service.js +1 -0
- package/dist/core/llm/services/document-ai.service.js.map +1 -1
- package/dist/core/llm/services/embedder.service.d.ts +105 -0
- package/dist/core/llm/services/embedder.service.d.ts.map +1 -1
- package/dist/core/llm/services/embedder.service.js +237 -7
- package/dist/core/llm/services/embedder.service.js.map +1 -1
- package/dist/core/llm/services/llm.service.d.ts +103 -6
- package/dist/core/llm/services/llm.service.d.ts.map +1 -1
- package/dist/core/llm/services/llm.service.js +570 -148
- package/dist/core/llm/services/llm.service.js.map +1 -1
- package/dist/core/llm/services/model.service.d.ts +25 -0
- package/dist/core/llm/services/model.service.d.ts.map +1 -1
- package/dist/core/llm/services/model.service.js +76 -4
- package/dist/core/llm/services/model.service.js.map +1 -1
- package/dist/core/llm/utils/repair-truncated-json.d.ts +29 -0
- package/dist/core/llm/utils/repair-truncated-json.d.ts.map +1 -0
- package/dist/core/llm/utils/repair-truncated-json.js +111 -0
- package/dist/core/llm/utils/repair-truncated-json.js.map +1 -0
- package/dist/foundations/assistant/services/assistant.service.d.ts.map +1 -1
- package/dist/foundations/assistant/services/assistant.service.js +12 -0
- package/dist/foundations/assistant/services/assistant.service.js.map +1 -1
- package/dist/foundations/atomicfact/services/atomicfact.service.d.ts +7 -0
- package/dist/foundations/atomicfact/services/atomicfact.service.d.ts.map +1 -1
- package/dist/foundations/atomicfact/services/atomicfact.service.js +6 -0
- package/dist/foundations/atomicfact/services/atomicfact.service.js.map +1 -1
- package/dist/foundations/chunk/processors/chunk.processor.d.ts.map +1 -1
- package/dist/foundations/chunk/processors/chunk.processor.js +7 -1
- package/dist/foundations/chunk/processors/chunk.processor.js.map +1 -1
- package/dist/foundations/chunk/repositories/chunk.repository.d.ts +90 -2
- package/dist/foundations/chunk/repositories/chunk.repository.d.ts.map +1 -1
- package/dist/foundations/chunk/repositories/chunk.repository.js +205 -31
- package/dist/foundations/chunk/repositories/chunk.repository.js.map +1 -1
- package/dist/foundations/chunk/services/chunk.service.d.ts.map +1 -1
- package/dist/foundations/chunk/services/chunk.service.js +50 -1
- package/dist/foundations/chunk/services/chunk.service.js.map +1 -1
- package/dist/foundations/chunker/chunker.module.d.ts.map +1 -1
- package/dist/foundations/chunker/chunker.module.js +4 -1
- package/dist/foundations/chunker/chunker.module.js.map +1 -1
- package/dist/foundations/chunker/services/chunker.service.d.ts +57 -1
- package/dist/foundations/chunker/services/chunker.service.d.ts.map +1 -1
- package/dist/foundations/chunker/services/chunker.service.js +207 -16
- package/dist/foundations/chunker/services/chunker.service.js.map +1 -1
- package/dist/foundations/chunker/services/types/pdf.service.d.ts +42 -1
- package/dist/foundations/chunker/services/types/pdf.service.d.ts.map +1 -1
- package/dist/foundations/chunker/services/types/pdf.service.js +111 -22
- package/dist/foundations/chunker/services/types/pdf.service.js.map +1 -1
- package/dist/foundations/chunker/services/types/semanticsplitter.service.d.ts.map +1 -1
- package/dist/foundations/chunker/services/types/semanticsplitter.service.js +24 -3
- package/dist/foundations/chunker/services/types/semanticsplitter.service.js.map +1 -1
- package/dist/foundations/company/repositories/company.repository.d.ts +1 -1
- package/dist/foundations/company/repositories/company.repository.d.ts.map +1 -1
- package/dist/foundations/company/repositories/company.repository.js +31 -14
- package/dist/foundations/company/repositories/company.repository.js.map +1 -1
- package/dist/foundations/how-to/processors/how-to.processor.d.ts.map +1 -1
- package/dist/foundations/how-to/processors/how-to.processor.js +20 -7
- package/dist/foundations/how-to/processors/how-to.processor.js.map +1 -1
- package/dist/foundations/keyconcept/repositories/keyconcept.repository.d.ts +25 -1
- package/dist/foundations/keyconcept/repositories/keyconcept.repository.d.ts.map +1 -1
- package/dist/foundations/keyconcept/repositories/keyconcept.repository.js +38 -2
- package/dist/foundations/keyconcept/repositories/keyconcept.repository.js.map +1 -1
- package/dist/foundations/keyconcept/services/keyconcept.service.d.ts +5 -0
- package/dist/foundations/keyconcept/services/keyconcept.service.d.ts.map +1 -1
- package/dist/foundations/keyconcept/services/keyconcept.service.js +4 -0
- package/dist/foundations/keyconcept/services/keyconcept.service.js.map +1 -1
- package/dist/foundations/tokenusage/entities/tokenusage.js +1 -1
- package/dist/foundations/tokenusage/enums/tokenusage.type.d.ts +4 -1
- package/dist/foundations/tokenusage/enums/tokenusage.type.d.ts.map +1 -1
- package/dist/foundations/tokenusage/enums/tokenusage.type.js +3 -0
- package/dist/foundations/tokenusage/enums/tokenusage.type.js.map +1 -1
- package/dist/foundations/tokenusage/repositories/tokenusage.admin.repository.js +3 -3
- package/dist/foundations/tokenusage/repositories/tokenusage.repository.js +2 -2
- package/dist/foundations/tokenusage/services/tokenusage.service.d.ts +6 -1
- package/dist/foundations/tokenusage/services/tokenusage.service.d.ts.map +1 -1
- package/dist/foundations/tokenusage/services/tokenusage.service.js +7 -2
- package/dist/foundations/tokenusage/services/tokenusage.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -65,6 +65,7 @@ const llm_cache_service_1 = require("./llm-cache.service");
|
|
|
65
65
|
const model_service_1 = require("../../llm/services/model.service");
|
|
66
66
|
const schema_utils_1 = require("../../llm/utils/schema.utils");
|
|
67
67
|
const mock_from_zod_1 = require("../utils/mock-from-zod");
|
|
68
|
+
const repair_truncated_json_1 = require("../utils/repair-truncated-json");
|
|
68
69
|
const llm_raw_response_1 = require("../common/llm-raw-response");
|
|
69
70
|
const llm_call_dumper_service_1 = require("./llm-call-dumper.service");
|
|
70
71
|
const openrouter_fetch_1 = require("./openrouter-fetch");
|
|
@@ -86,6 +87,40 @@ const DEFAULT_REQUEST_WATCHDOG_MS = 30_000;
|
|
|
86
87
|
const DEFAULT_REQUEST_DEADLINE_ATTEMPTS = 3;
|
|
87
88
|
/** Grace on top of the budgeted attempts, covering backoff between retries. */
|
|
88
89
|
const DEADLINE_SLACK_MS = 15_000;
|
|
90
|
+
/**
|
|
91
|
+
* How long a FAILED stream may wait for its `usage` promise before giving up on
|
|
92
|
+
* it. Unlike `call()`, a stream's `result` promise runs under no outer deadline,
|
|
93
|
+
* so an unbounded await in its catch would replace a settled rejection with a
|
|
94
|
+
* caller that hangs forever. Two seconds is generous for a promise the SDK has
|
|
95
|
+
* normally already settled, and irrelevant to the happy path.
|
|
96
|
+
*/
|
|
97
|
+
const USAGE_SETTLE_TIMEOUT_MS = 2_000;
|
|
98
|
+
/**
|
|
99
|
+
* Waits before each EXTRA attempt of the transient-network retry, in ms — two
|
|
100
|
+
* extras, so three attempts in total.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately LONG. LangChain's `AsyncCaller` already retries roughly six times
|
|
103
|
+
* FAST underneath every `.invoke(...)`, so by the time a failure surfaces here
|
|
104
|
+
* the quick retries are spent and the fault has lasted seconds, not
|
|
105
|
+
* milliseconds — a DNS outage, a provider brown-out, a saturated egress NAT.
|
|
106
|
+
* Retrying fast again would burn both extra attempts inside the same bad second
|
|
107
|
+
* and change nothing. (Evidence: a crashed worker run with 28 `ENOTFOUND` DNS
|
|
108
|
+
* failures under load, every one of them already through LangChain's fast
|
|
109
|
+
* retries.)
|
|
110
|
+
*/
|
|
111
|
+
const TRANSIENT_RETRY_WAITS_MS = [5_000, 15_000];
|
|
112
|
+
/**
|
|
113
|
+
* Network-level error codes that say "the request never reached a working
|
|
114
|
+
* provider" — the class of failure another attempt can actually fix.
|
|
115
|
+
*/
|
|
116
|
+
const TRANSIENT_ERROR_CODES = new Set([
|
|
117
|
+
"ENOTFOUND",
|
|
118
|
+
"EAI_AGAIN",
|
|
119
|
+
"ECONNRESET",
|
|
120
|
+
"ETIMEDOUT",
|
|
121
|
+
"ECONNREFUSED",
|
|
122
|
+
"EPIPE",
|
|
123
|
+
]);
|
|
89
124
|
/**
|
|
90
125
|
* True for the abort a request timeout raises, whichever layer raised it — the
|
|
91
126
|
* OpenAI SDK's `APIConnectionTimeoutError`, undici's `TimeoutError`/
|
|
@@ -245,6 +280,37 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
245
280
|
attemptTimeoutMs(explicit) {
|
|
246
281
|
return explicit ?? this.config.get("ai")?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
247
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Reads a stream's `usage` promise WITHOUT ever hanging on it.
|
|
285
|
+
*
|
|
286
|
+
* Used only from the streaming error paths. `streamObject`/`streamText`
|
|
287
|
+
* usually settle `usage` even when `object`/`text` reject — a schema-invalid
|
|
288
|
+
* or aborted generation is still billed — so it is worth awaiting. But the
|
|
289
|
+
* `result` promise those catches belong to has no outer deadline (unlike
|
|
290
|
+
* `call()`, which runs under `runBounded`), so if the SDK ever rejected the
|
|
291
|
+
* content promise while leaving `usage` pending, an unbounded await would turn
|
|
292
|
+
* a prompt rejection into a caller that waits forever. Rejection is handled by
|
|
293
|
+
* `.catch`; PENDENCY is handled by the race. The loser's timer is cleared, so
|
|
294
|
+
* a settled call leaves no timer behind.
|
|
295
|
+
*
|
|
296
|
+
* @returns the usage object, or undefined if it rejected or did not settle in time
|
|
297
|
+
*/
|
|
298
|
+
async readUsageBounded(usage, timeoutMs = USAGE_SETTLE_TIMEOUT_MS) {
|
|
299
|
+
let timer;
|
|
300
|
+
try {
|
|
301
|
+
return await Promise.race([
|
|
302
|
+
Promise.resolve(usage).catch(() => undefined),
|
|
303
|
+
new Promise((resolve) => {
|
|
304
|
+
timer = setTimeout(() => resolve(undefined), timeoutMs);
|
|
305
|
+
timer.unref?.();
|
|
306
|
+
}),
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
if (timer)
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
248
314
|
/**
|
|
249
315
|
* Runs one provider call under a WATCHDOG and an ABSOLUTE DEADLINE.
|
|
250
316
|
*
|
|
@@ -305,6 +371,102 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
305
371
|
clearTimeout(deadline);
|
|
306
372
|
}
|
|
307
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* True for a failure that means the request never reached a working provider,
|
|
376
|
+
* and is therefore worth another attempt: a DNS/socket-level error code, an
|
|
377
|
+
* HTTP 429 or 5xx, or the message text either of those arrives as.
|
|
378
|
+
*
|
|
379
|
+
* All three are checked because the same failure wears different clothes per
|
|
380
|
+
* transport: undici puts the code on `error.cause.code` behind a bare
|
|
381
|
+
* `TypeError: fetch failed`, the OpenAI SDK puts the status on the error and
|
|
382
|
+
* the code in the message, and LangChain re-wraps both in a plain `Error`.
|
|
383
|
+
*
|
|
384
|
+
* Deliberately NOT transient: a stall that burned its whole deadline
|
|
385
|
+
* ({@link LLMTimeoutError}), a refusal (402/403), a malformed request (400) or
|
|
386
|
+
* a parse failure. Those either already had their retry (`call()` re-issues a
|
|
387
|
+
* timed-out attempt once, escalating the OpenRouter pin) or will fail
|
|
388
|
+
* identically forever. The 429 vocabulary matches the one
|
|
389
|
+
* `VisionLLMService.isRateLimitError` retries on, so the two agree on what a
|
|
390
|
+
* rate limit looks like.
|
|
391
|
+
*/
|
|
392
|
+
isTransientNetworkError(err) {
|
|
393
|
+
if (err === undefined || err === null)
|
|
394
|
+
return false;
|
|
395
|
+
const candidate = err;
|
|
396
|
+
const cause = candidate.cause;
|
|
397
|
+
const codes = [candidate.code, cause?.code];
|
|
398
|
+
if (codes.some((code) => typeof code === "string" && TRANSIENT_ERROR_CODES.has(code)))
|
|
399
|
+
return true;
|
|
400
|
+
const statuses = [candidate.status, candidate.statusCode, candidate.response?.status, cause?.status];
|
|
401
|
+
if (statuses.some((status) => typeof status === "number" && (status === 429 || (status >= 500 && status <= 599))))
|
|
402
|
+
return true;
|
|
403
|
+
const own = err instanceof Error ? err.message : String(err);
|
|
404
|
+
const causeMessage = cause instanceof Error ? cause.message : "";
|
|
405
|
+
const haystack = `${own} ${causeMessage}`.toLowerCase();
|
|
406
|
+
for (const code of TRANSIENT_ERROR_CODES)
|
|
407
|
+
if (haystack.includes(code.toLowerCase()))
|
|
408
|
+
return true;
|
|
409
|
+
return (haystack.includes("socket hang up") ||
|
|
410
|
+
haystack.includes("fetch failed") ||
|
|
411
|
+
haystack.includes("network error") ||
|
|
412
|
+
haystack.includes("econnaborted") ||
|
|
413
|
+
// 429 / rate limiting — matched as a whole word so a token count never
|
|
414
|
+
// reads as a status code.
|
|
415
|
+
/\b429\b/.test(haystack) ||
|
|
416
|
+
haystack.includes("rate limit") ||
|
|
417
|
+
haystack.includes("resource exhausted") ||
|
|
418
|
+
haystack.includes("too many requests") ||
|
|
419
|
+
// 5xx. Only the codes providers actually emit, again whole-word: a blanket
|
|
420
|
+
// /5\d\d/ would retry "context length 512 exceeded" forever.
|
|
421
|
+
/\b(500|502|503|504|529)\b/.test(haystack) ||
|
|
422
|
+
haystack.includes("internal server error") ||
|
|
423
|
+
haystack.includes("bad gateway") ||
|
|
424
|
+
haystack.includes("service unavailable") ||
|
|
425
|
+
haystack.includes("gateway timeout") ||
|
|
426
|
+
haystack.includes("overloaded"));
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Sleeps the jittered backoff for one transient retry and says so in the log.
|
|
430
|
+
* ±20% jitter so a fleet of workers knocked out by the same DNS blip does not
|
|
431
|
+
* come back in lockstep and knock it out again.
|
|
432
|
+
*/
|
|
433
|
+
async waitBeforeTransientRetry(label, attempt, error) {
|
|
434
|
+
const base = TRANSIENT_RETRY_WAITS_MS[attempt];
|
|
435
|
+
const waitMs = Math.round(base * (0.8 + Math.random() * 0.4));
|
|
436
|
+
this.logger.warn(`[${label}] transient network failure on attempt ${attempt + 1}/${TRANSIENT_RETRY_WAITS_MS.length + 1} — ` +
|
|
437
|
+
`retrying in ${Math.round(waitMs / 1000)}s: ${error instanceof Error ? error.message : String(error)}`);
|
|
438
|
+
await new Promise((resolve) => {
|
|
439
|
+
const timer = setTimeout(resolve, waitMs);
|
|
440
|
+
timer.unref?.();
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Runs a provider call under {@link runBounded}, retrying it when — and only
|
|
445
|
+
* when — it failed for a transient network reason
|
|
446
|
+
* ({@link isTransientNetworkError}). Two extra attempts, with the long
|
|
447
|
+
* jittered waits {@link TRANSIENT_RETRY_WAITS_MS} explains.
|
|
448
|
+
*
|
|
449
|
+
* Owns a FRESH AbortController per attempt: a controller that has already
|
|
450
|
+
* aborted stays aborted forever, so re-using one would make every retry abort
|
|
451
|
+
* before it sent a byte. `work` therefore receives the signal rather than
|
|
452
|
+
* capturing one from the caller.
|
|
453
|
+
*/
|
|
454
|
+
async runWithTransientRetry(label, attemptTimeoutMs, work) {
|
|
455
|
+
for (let attempt = 0;; attempt++) {
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
try {
|
|
458
|
+
return await this.runBounded(label, attemptTimeoutMs, controller, () => work(controller.signal));
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (attempt >= TRANSIENT_RETRY_WAITS_MS.length || !this.isTransientNetworkError(error))
|
|
462
|
+
throw error;
|
|
463
|
+
// The attempt is over; release whatever socket it may still hold before
|
|
464
|
+
// waiting out the backoff.
|
|
465
|
+
controller.abort();
|
|
466
|
+
await this.waitBeforeTransientRetry(label, attempt, error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
308
470
|
/**
|
|
309
471
|
* Records token usage for cost/observability attribution. Never throws —
|
|
310
472
|
* a persistence failure logs a warning and the LLM call continues, so
|
|
@@ -315,6 +477,18 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
315
477
|
* is the ONLY token-usage write inside the package; any future package caller
|
|
316
478
|
* MUST use the same token rather than injecting `TokenUsageService` directly
|
|
317
479
|
* (see the token's docblock for why).
|
|
480
|
+
*
|
|
481
|
+
* ZERO-TOKEN SUCCESS RULE: a call that SUCCEEDED but reported no usage at all
|
|
482
|
+
* (the provider omitted `usage_metadata`) is still recorded — the call really
|
|
483
|
+
* happened and must stay visible — but with `applyMinimum: false`, so it
|
|
484
|
+
* costs 0 credits instead of being floored to `minCreditsPerRecord`. Flooring
|
|
485
|
+
* exists to stop sub-cent REAL usage rounding to nothing, not to invent a
|
|
486
|
+
* charge for tokens nobody measured. A success carrying real counts keeps the
|
|
487
|
+
* floor exactly as before.
|
|
488
|
+
*
|
|
489
|
+
* This is deliberately NOT the same rule as the zero-token FAILURE rule in
|
|
490
|
+
* {@link persistUsageOnFailure}, which writes nothing at all: there the
|
|
491
|
+
* provider was never reached, so there is no call to make visible.
|
|
318
492
|
*/
|
|
319
493
|
async persistUsage(params, tokens) {
|
|
320
494
|
// Attribution is opt-in: the caller decides which entity this usage is
|
|
@@ -322,6 +496,7 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
322
496
|
// so we skip — the package stays domain-agnostic.
|
|
323
497
|
if (!params.relationshipId || !params.relationshipType)
|
|
324
498
|
return;
|
|
499
|
+
const measured = tokens.input + tokens.output + (tokens.cached ?? 0) > 0;
|
|
325
500
|
try {
|
|
326
501
|
await (this.tokenUsageRecorder ?? this.tokenUsageService).recordTokenUsage({
|
|
327
502
|
tokens,
|
|
@@ -329,12 +504,35 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
329
504
|
relationshipId: params.relationshipId,
|
|
330
505
|
relationshipType: params.relationshipType,
|
|
331
506
|
modelWeight: params.modelWeight,
|
|
507
|
+
...(measured ? {} : { applyMinimum: false }),
|
|
332
508
|
});
|
|
333
509
|
}
|
|
334
510
|
catch (err) {
|
|
335
511
|
this.logger.warn(`TokenUsage persistence failed — continuing: ${String(err)}`);
|
|
336
512
|
}
|
|
337
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Records what a FAILED call already burned. A failure is not a free call:
|
|
516
|
+
* the provider bills every round it served, so a tool loop that dies on its
|
|
517
|
+
* final structured invocation has already been charged for six figures of
|
|
518
|
+
* input tokens. Billing only successful calls understates real spend.
|
|
519
|
+
*
|
|
520
|
+
* ZERO-TOKEN RULE: a failure that consumed nothing (the provider was never
|
|
521
|
+
* reached — input validation, an unreachable host, an immediate abort) is NOT
|
|
522
|
+
* recorded. `recordTokenUsage` floors every record at `minCreditsPerRecord`,
|
|
523
|
+
* so writing a 0/0 row would invent a charge for tokens nobody spent; the
|
|
524
|
+
* floor exists to stop sub-cent REAL usage rounding to nothing, not to price
|
|
525
|
+
* a call that never happened. Such failures remain fully visible through the
|
|
526
|
+
* dump session, which closes with `finalStatus: "error"`.
|
|
527
|
+
*
|
|
528
|
+
* Never throws (delegates to {@link persistUsage}), so it can sit in a catch
|
|
529
|
+
* block without masking the original error.
|
|
530
|
+
*/
|
|
531
|
+
async persistUsageOnFailure(params, tokens) {
|
|
532
|
+
if (tokens.input + tokens.output + (tokens.cached ?? 0) === 0)
|
|
533
|
+
return;
|
|
534
|
+
await this.persistUsage(params, tokens);
|
|
535
|
+
}
|
|
338
536
|
/**
|
|
339
537
|
* Converts AgentMessageType to LangChain BaseMessage
|
|
340
538
|
*/
|
|
@@ -606,14 +804,15 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
606
804
|
// reports it while it is still open, and the deadline guarantees this
|
|
607
805
|
// promise settles even if the provider never answers.
|
|
608
806
|
const attemptTimeoutMs = this.attemptTimeoutMs(params.timeout);
|
|
609
|
-
const controller = new AbortController();
|
|
610
807
|
const label = `${params.metadata?.nodeName ?? "llm.call"}:${aiConfig.model}`;
|
|
611
808
|
try {
|
|
612
|
-
|
|
809
|
+
// The abort signal now comes from the retry wrapper, which owns a fresh
|
|
810
|
+
// controller per attempt — a reused one would abort every retry instantly.
|
|
811
|
+
const result = await this.runWithTransientRetry(label, attemptTimeoutMs, (signal) => this._invokeOriginal(params, session, (i, o, c) => {
|
|
613
812
|
totalInput += i;
|
|
614
813
|
totalOutput += o;
|
|
615
814
|
totalCached += c;
|
|
616
|
-
}, (kind) => parseFallbacks.push(kind), (w) => warnings.push(w), { attemptTimeoutMs, signal
|
|
815
|
+
}, (kind) => parseFallbacks.push(kind), (w) => warnings.push(w), { attemptTimeoutMs, signal, label }));
|
|
617
816
|
session.close({
|
|
618
817
|
finalStatus: "success",
|
|
619
818
|
totalTokens: { input: totalInput, output: totalOutput, cached: totalCached },
|
|
@@ -643,8 +842,26 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
643
842
|
warnings,
|
|
644
843
|
parseFallbacks,
|
|
645
844
|
});
|
|
845
|
+
// The provider charged for everything spent up to the failure — a timeout
|
|
846
|
+
// mid tool-loop can burn six figures of input tokens, all of them already
|
|
847
|
+
// reported through `addTokens` by the time we get here. Never throws, so
|
|
848
|
+
// this cannot mask the original error.
|
|
849
|
+
await this.persistUsageOnFailure({
|
|
850
|
+
tokenUsageType: params.tokenUsageType,
|
|
851
|
+
relationshipId: params.relationshipId,
|
|
852
|
+
relationshipType: params.relationshipType,
|
|
853
|
+
modelWeight,
|
|
854
|
+
}, { input: totalInput, output: totalOutput, cached: totalCached });
|
|
646
855
|
console.error("[LLMService] Error calling LLM:", error);
|
|
647
|
-
|
|
856
|
+
// The message text is load-bearing — callers match on "LLM service error:"
|
|
857
|
+
// — so it stays byte-for-byte identical, and the original error rides along
|
|
858
|
+
// as `cause`. Without it every network diagnosis stopped at this wrapper:
|
|
859
|
+
// the ENOTFOUND / status / stack that explained the failure was thrown
|
|
860
|
+
// away here. Assigned rather than passed as `new Error(msg, { cause })`
|
|
861
|
+
// because this package targets ES2021, whose Error takes one argument.
|
|
862
|
+
const wrapped = new Error(`LLM service error: ${message}`);
|
|
863
|
+
wrapped.cause = error;
|
|
864
|
+
throw wrapped;
|
|
648
865
|
}
|
|
649
866
|
}
|
|
650
867
|
async _invokeOriginal(params, session, addTokens, addParseFallback, addWarning,
|
|
@@ -785,12 +1002,21 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
785
1002
|
`in=${iterationUsage?.input_tokens ?? 0} out=${iterationUsage?.output_tokens ?? 0} ` +
|
|
786
1003
|
`reasoning=${iterationUsage?.output_token_details?.reasoning ?? 0} ` +
|
|
787
1004
|
`tools=[${requestedTools.join(",") || "none"}]`);
|
|
788
|
-
// Track token usage
|
|
1005
|
+
// Track token usage. Reported to the caller AS IT IS SPENT, not at the
|
|
1006
|
+
// end: this iteration is already billed by the provider, and a later
|
|
1007
|
+
// failure (a timeout on the final structured call, an unparseable
|
|
1008
|
+
// answer) must not make those tokens disappear from `call()`'s totals.
|
|
1009
|
+
// The final return therefore adds only ITS OWN delta — see `addTokens`
|
|
1010
|
+
// at the return sites below.
|
|
789
1011
|
const responseUsage = toolResponse.usage_metadata;
|
|
790
1012
|
if (responseUsage) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1013
|
+
const iterationInput = responseUsage.input_tokens ?? 0;
|
|
1014
|
+
const iterationOutput = responseUsage.output_tokens ?? 0;
|
|
1015
|
+
const iterationCached = responseUsage.input_token_details?.cache_read ?? 0;
|
|
1016
|
+
totalInputTokens += iterationInput;
|
|
1017
|
+
totalOutputTokens += iterationOutput;
|
|
1018
|
+
totalCachedTokens += iterationCached;
|
|
1019
|
+
addTokens(iterationInput, iterationOutput, iterationCached);
|
|
794
1020
|
}
|
|
795
1021
|
// Check for tool calls
|
|
796
1022
|
const toolCalls = toolResponse.tool_calls ?? [];
|
|
@@ -998,9 +1224,15 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
998
1224
|
},
|
|
999
1225
|
finishReason: raw?.response_metadata?.finish_reason,
|
|
1000
1226
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1227
|
+
// The final structured response's OWN usage. Reported separately from the
|
|
1228
|
+
// totals because the tool loop has already handed its share to `addTokens`;
|
|
1229
|
+
// re-reporting the sum would bill every tool iteration twice.
|
|
1230
|
+
const finalInput = raw?.usage_metadata?.input_tokens ?? 0;
|
|
1231
|
+
const finalOutput = raw?.usage_metadata?.output_tokens ?? 0;
|
|
1232
|
+
const finalCached = raw?.usage_metadata?.input_token_details?.cache_read ?? 0;
|
|
1233
|
+
const input = totalInputTokens + finalInput;
|
|
1234
|
+
const output = totalOutputTokens + finalOutput;
|
|
1235
|
+
const cached = totalCachedTokens + finalCached;
|
|
1004
1236
|
/**
|
|
1005
1237
|
* One summary line per completed call, emitted at EVERY return site — the
|
|
1006
1238
|
* degraded runs are the ones worth diagnosing, so a fallback-parsed call must
|
|
@@ -1047,7 +1279,7 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1047
1279
|
const validated = params.outputSchema.parse(normaliseStrictOutput(toolCallArgs));
|
|
1048
1280
|
console.warn("[LLMService] Fallback tool_calls parsing succeeded");
|
|
1049
1281
|
logCallSummary("fallback:tool_calls");
|
|
1050
|
-
addTokens(
|
|
1282
|
+
addTokens(finalInput, finalOutput, finalCached);
|
|
1051
1283
|
return {
|
|
1052
1284
|
...validated,
|
|
1053
1285
|
tokenUsage: { input, output, cached },
|
|
@@ -1084,7 +1316,7 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1084
1316
|
const validated = params.outputSchema.parse(cleanedArgs);
|
|
1085
1317
|
console.warn("[LLMService] Lenient tool_calls parsing succeeded");
|
|
1086
1318
|
logCallSummary("fallback:lenient");
|
|
1087
|
-
addTokens(
|
|
1319
|
+
addTokens(finalInput, finalOutput, finalCached);
|
|
1088
1320
|
return {
|
|
1089
1321
|
...validated,
|
|
1090
1322
|
tokenUsage: { input, output, cached },
|
|
@@ -1103,13 +1335,41 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1103
1335
|
const validated = params.outputSchema.parse(normaliseStrictOutput(manualParse));
|
|
1104
1336
|
console.warn("[LLMService] Fallback parsing succeeded");
|
|
1105
1337
|
logCallSummary("fallback:raw");
|
|
1106
|
-
addTokens(
|
|
1338
|
+
addTokens(finalInput, finalOutput, finalCached);
|
|
1107
1339
|
return {
|
|
1108
1340
|
...validated,
|
|
1109
1341
|
tokenUsage: { input, output, cached },
|
|
1110
1342
|
};
|
|
1111
1343
|
}
|
|
1112
1344
|
catch (fallbackError) {
|
|
1345
|
+
// Last rung. A MAX_TOKENS truncation (`finish_reason: "length"`) stops
|
|
1346
|
+
// the payload mid-value, so every rung above — all of which need the
|
|
1347
|
+
// whole document to parse — rejects a response whose completed elements
|
|
1348
|
+
// were perfectly good. Trim to the last complete value, close the open
|
|
1349
|
+
// containers, and validate exactly as the raw rung does.
|
|
1350
|
+
const repaired = (0, repair_truncated_json_1.repairTruncatedJson)(rawContent);
|
|
1351
|
+
if (repaired !== null) {
|
|
1352
|
+
try {
|
|
1353
|
+
const validated = params.outputSchema.parse(normaliseStrictOutput(JSON.parse(repaired)));
|
|
1354
|
+
this.logger.warn(`[${label}] parseFallback: "truncation-repair" — recovered a truncated payload ` +
|
|
1355
|
+
`(finishReason=${finishReason}, ${rawContent.length}→${repaired.length} chars)`);
|
|
1356
|
+
logCallSummary("fallback:truncation-repair");
|
|
1357
|
+
addTokens(finalInput, finalOutput, finalCached);
|
|
1358
|
+
return {
|
|
1359
|
+
...validated,
|
|
1360
|
+
tokenUsage: { input, output, cached },
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
// Repaired text still does not satisfy the schema — fall through to
|
|
1365
|
+
// the diagnostic below, which reports the ORIGINAL failure.
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
// Every salvage attempt failed, so this call is about to throw — but the
|
|
1369
|
+
// unparseable answer was generated and billed like any other. Report it
|
|
1370
|
+
// before unwinding, so `call()`'s catch can record it. No return site
|
|
1371
|
+
// follows this one, so nothing is counted twice.
|
|
1372
|
+
addTokens(finalInput, finalOutput, finalCached);
|
|
1113
1373
|
throw new Error(`LLM failed to return structured output. ` +
|
|
1114
1374
|
`Finish reason: ${finishReason}. ` +
|
|
1115
1375
|
`Raw content preview: ${rawContent.substring(0, 200)}...` +
|
|
@@ -1138,7 +1398,7 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1138
1398
|
return salvageParse();
|
|
1139
1399
|
}
|
|
1140
1400
|
logCallSummary("clean");
|
|
1141
|
-
addTokens(
|
|
1401
|
+
addTokens(finalInput, finalOutput, finalCached);
|
|
1142
1402
|
return {
|
|
1143
1403
|
...result,
|
|
1144
1404
|
tokenUsage: {
|
|
@@ -1261,77 +1521,132 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1261
1521
|
// final-structured response, just streamed instead of awaited atomically.
|
|
1262
1522
|
// Avoids widening the dumper's union type for a single call site.
|
|
1263
1523
|
session.startIteration("final-structured", []);
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1524
|
+
const attemptTimeoutMs = this.attemptTimeoutMs(params.timeout);
|
|
1525
|
+
const label = `${params.metadata?.nodeName ?? "llm.streamCall"}:${aiConfig.model}`;
|
|
1526
|
+
// One attempt = one abort controller + one whole-stream timeout, so a hung
|
|
1527
|
+
// connection can't pin the session open indefinitely. The bound is NOT
|
|
1528
|
+
// `runWithTransientRetry`'s per-attempt deadline: a stream outlives its
|
|
1529
|
+
// first byte, so its timer must stay armed until the stream finishes.
|
|
1530
|
+
//
|
|
1269
1531
|
// Schema cast: `streamObject`'s typing is a conditional union over the
|
|
1270
1532
|
// output mode (`object` / `enum` / `array` / `no-schema`). Our T is always
|
|
1271
1533
|
// a Zod object schema; the runtime call is correct.
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1534
|
+
const startAttempt = () => {
|
|
1535
|
+
const controller = new AbortController();
|
|
1536
|
+
const timeoutId = setTimeout(() => controller.abort(), attemptTimeoutMs);
|
|
1537
|
+
timeoutId.unref?.();
|
|
1538
|
+
return {
|
|
1539
|
+
controller,
|
|
1540
|
+
timeoutId,
|
|
1541
|
+
handle: streamObject({
|
|
1542
|
+
model,
|
|
1543
|
+
schema: params.outputSchema,
|
|
1544
|
+
system,
|
|
1545
|
+
prompt: finalInstructions,
|
|
1546
|
+
temperature: params.temperature,
|
|
1547
|
+
maxOutputTokens: params.maxTokens,
|
|
1548
|
+
maxRetries: 2,
|
|
1549
|
+
abortSignal: controller.signal,
|
|
1550
|
+
}),
|
|
1551
|
+
};
|
|
1552
|
+
};
|
|
1553
|
+
let attempt = startAttempt();
|
|
1554
|
+
// Set the instant the caller starts reading the stream. Past that point a
|
|
1555
|
+
// restart would REPLAY output the consumer has already seen, so the
|
|
1556
|
+
// transient retry below only ever fires while the consumer is still idle —
|
|
1557
|
+
// which is precisely the window in which a DNS/connect failure lands.
|
|
1558
|
+
let consumerStarted = false;
|
|
1282
1559
|
// Build the result Promise that closes the session once the stream finishes.
|
|
1283
1560
|
// This is awaitable independently of consuming the streams — `streamObject`
|
|
1284
1561
|
// internally tees the source, so consuming `textStream` (or not) doesn't
|
|
1285
1562
|
// affect `result` resolution.
|
|
1286
1563
|
const resultPromise = (async () => {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1564
|
+
for (let retry = 0;; retry++) {
|
|
1565
|
+
try {
|
|
1566
|
+
const finalObject = (await attempt.handle.object);
|
|
1567
|
+
const usage = await attempt.handle.usage;
|
|
1568
|
+
const input = usage?.inputTokens ?? 0;
|
|
1569
|
+
const output = usage?.outputTokens ?? 0;
|
|
1570
|
+
const cached = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
1571
|
+
session.recordResponse({
|
|
1572
|
+
content: JSON.stringify(finalObject),
|
|
1573
|
+
tokenUsage: { input, output },
|
|
1574
|
+
finishReason: String(await attempt.handle.finishReason),
|
|
1575
|
+
});
|
|
1576
|
+
session.close({
|
|
1577
|
+
finalStatus: "success",
|
|
1578
|
+
totalTokens: { input, output, cached },
|
|
1579
|
+
warnings: [],
|
|
1580
|
+
parseFallbacks: [],
|
|
1581
|
+
});
|
|
1582
|
+
clearTimeout(attempt.timeoutId);
|
|
1583
|
+
await this.persistUsage({
|
|
1584
|
+
tokenUsageType: params.tokenUsageType,
|
|
1585
|
+
relationshipId: params.relationshipId,
|
|
1586
|
+
relationshipType: params.relationshipType,
|
|
1587
|
+
modelWeight,
|
|
1588
|
+
}, { input, output, cached });
|
|
1589
|
+
return { ...finalObject, tokenUsage: { input, output }, modelWeight };
|
|
1590
|
+
}
|
|
1591
|
+
catch (error) {
|
|
1592
|
+
// Restartable only while nothing has been delivered — see
|
|
1593
|
+
// `consumerStarted`.
|
|
1594
|
+
if (!consumerStarted && retry < TRANSIENT_RETRY_WAITS_MS.length && this.isTransientNetworkError(error)) {
|
|
1595
|
+
clearTimeout(attempt.timeoutId);
|
|
1596
|
+
attempt.controller.abort();
|
|
1597
|
+
await this.waitBeforeTransientRetry(label, retry, error);
|
|
1598
|
+
attempt = startAttempt();
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
clearTimeout(attempt.timeoutId);
|
|
1602
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1603
|
+
const stack = error instanceof Error ? (error.stack ?? "").split("\n").slice(0, 10).join("\n") : undefined;
|
|
1604
|
+
// `streamObject` may have settled `usage` even though `object`
|
|
1605
|
+
// rejected (a schema-invalid payload is still a billed generation).
|
|
1606
|
+
// Read it defensively so the session and the ledger report the real
|
|
1607
|
+
// figures instead of the hard-coded 0/0 this used to close with.
|
|
1608
|
+
// Bounded: this promise has no outer deadline, so a `usage` that never
|
|
1609
|
+
// settles must not stop `result` from rejecting (see readUsageBounded).
|
|
1610
|
+
const usage = await this.readUsageBounded(attempt.handle.usage);
|
|
1611
|
+
const input = usage?.inputTokens ?? 0;
|
|
1612
|
+
const output = usage?.outputTokens ?? 0;
|
|
1613
|
+
const cached = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
1614
|
+
session.close({
|
|
1615
|
+
finalStatus: "error",
|
|
1616
|
+
errorMessage: message,
|
|
1617
|
+
errorStack: stack,
|
|
1618
|
+
totalTokens: { input, output, cached },
|
|
1619
|
+
warnings: [],
|
|
1620
|
+
parseFallbacks: [],
|
|
1621
|
+
});
|
|
1622
|
+
await this.persistUsageOnFailure({
|
|
1623
|
+
tokenUsageType: params.tokenUsageType,
|
|
1624
|
+
relationshipId: params.relationshipId,
|
|
1625
|
+
relationshipType: params.relationshipType,
|
|
1626
|
+
modelWeight,
|
|
1627
|
+
}, { input, output, cached });
|
|
1628
|
+
console.error("[LLMService.streamCall] Error:", error);
|
|
1629
|
+
const wrapped = new Error(`LLM streamCall error: ${message}`);
|
|
1630
|
+
wrapped.cause = error;
|
|
1631
|
+
throw wrapped;
|
|
1632
|
+
}
|
|
1328
1633
|
}
|
|
1329
1634
|
})();
|
|
1330
1635
|
// Surface (don't swallow) a rejected result even when the caller never
|
|
1331
1636
|
// awaits `result` — e.g. on abort/timeout or an unreachable provider.
|
|
1332
1637
|
resultPromise.catch((err) => this.logger.warn(`streamCall result rejected: ${String(err)}`));
|
|
1638
|
+
// Wrapped rather than handed over directly so that (a) the consumer's first
|
|
1639
|
+
// pull marks the stream unrestartable, and (b) the `partialObjectStream`
|
|
1640
|
+
// getter — one of three getters that lock the source on first access — is
|
|
1641
|
+
// touched only if the consumer actually reads, and on whichever attempt
|
|
1642
|
+
// finally connected.
|
|
1643
|
+
async function* partialObjects() {
|
|
1644
|
+
consumerStarted = true;
|
|
1645
|
+
for await (const partial of attempt.handle.partialObjectStream)
|
|
1646
|
+
yield partial;
|
|
1647
|
+
}
|
|
1333
1648
|
return {
|
|
1334
|
-
partialObjectStream:
|
|
1649
|
+
partialObjectStream: partialObjects(),
|
|
1335
1650
|
result: resultPromise,
|
|
1336
1651
|
};
|
|
1337
1652
|
}
|
|
@@ -1407,62 +1722,101 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1407
1722
|
});
|
|
1408
1723
|
const model = provider.chatModel(aiConfig.model);
|
|
1409
1724
|
session.startIteration("final-structured", []);
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1725
|
+
const attemptTimeoutMs = this.attemptTimeoutMs(params.timeout);
|
|
1726
|
+
const label = `${params.metadata?.nodeName ?? "llm.streamText"}:${aiConfig.model}`;
|
|
1727
|
+
// One attempt = one abort controller + one whole-stream timeout (see
|
|
1728
|
+
// streamCall). The AbortError surfaces as a rejection on the awaited
|
|
1729
|
+
// promises below (caught + logged).
|
|
1730
|
+
const startAttempt = () => {
|
|
1731
|
+
const controller = new AbortController();
|
|
1732
|
+
const timeoutId = setTimeout(() => controller.abort(), attemptTimeoutMs);
|
|
1733
|
+
timeoutId.unref?.();
|
|
1734
|
+
return {
|
|
1735
|
+
controller,
|
|
1736
|
+
timeoutId,
|
|
1737
|
+
handle: streamText({
|
|
1738
|
+
model,
|
|
1739
|
+
system,
|
|
1740
|
+
prompt: params.prompt,
|
|
1741
|
+
temperature: params.temperature,
|
|
1742
|
+
maxOutputTokens: params.maxTokens,
|
|
1743
|
+
maxRetries: 2,
|
|
1744
|
+
abortSignal: controller.signal,
|
|
1745
|
+
}),
|
|
1746
|
+
};
|
|
1747
|
+
};
|
|
1748
|
+
let attempt = startAttempt();
|
|
1749
|
+
// See streamCall: a stream may only be restarted while the consumer has not
|
|
1750
|
+
// yet read a single part.
|
|
1751
|
+
let consumerStarted = false;
|
|
1423
1752
|
const resultPromise = (async () => {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1753
|
+
for (let retry = 0;; retry++) {
|
|
1754
|
+
try {
|
|
1755
|
+
const text = await attempt.handle.text;
|
|
1756
|
+
const reasoning = (await attempt.handle.reasoningText) ?? "";
|
|
1757
|
+
const usage = await attempt.handle.usage;
|
|
1758
|
+
const input = usage?.inputTokens ?? 0;
|
|
1759
|
+
const output = usage?.outputTokens ?? 0;
|
|
1760
|
+
const cached = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
1761
|
+
session.recordResponse({
|
|
1762
|
+
content: text,
|
|
1763
|
+
tokenUsage: { input, output },
|
|
1764
|
+
finishReason: String(await attempt.handle.finishReason),
|
|
1765
|
+
});
|
|
1766
|
+
session.close({
|
|
1767
|
+
finalStatus: "success",
|
|
1768
|
+
totalTokens: { input, output, cached },
|
|
1769
|
+
warnings: [],
|
|
1770
|
+
parseFallbacks: [],
|
|
1771
|
+
});
|
|
1772
|
+
clearTimeout(attempt.timeoutId);
|
|
1773
|
+
await this.persistUsage({
|
|
1774
|
+
tokenUsageType: params.tokenUsageType,
|
|
1775
|
+
relationshipId: params.relationshipId,
|
|
1776
|
+
relationshipType: params.relationshipType,
|
|
1777
|
+
modelWeight,
|
|
1778
|
+
}, { input, output, cached });
|
|
1779
|
+
return { text, reasoning, tokenUsage: { input, output }, modelWeight };
|
|
1780
|
+
}
|
|
1781
|
+
catch (error) {
|
|
1782
|
+
// Restartable only while nothing has been delivered — see
|
|
1783
|
+
// `consumerStarted`.
|
|
1784
|
+
if (!consumerStarted && retry < TRANSIENT_RETRY_WAITS_MS.length && this.isTransientNetworkError(error)) {
|
|
1785
|
+
clearTimeout(attempt.timeoutId);
|
|
1786
|
+
attempt.controller.abort();
|
|
1787
|
+
await this.waitBeforeTransientRetry(label, retry, error);
|
|
1788
|
+
attempt = startAttempt();
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
clearTimeout(attempt.timeoutId);
|
|
1792
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1793
|
+
const stack = error instanceof Error ? (error.stack ?? "").split("\n").slice(0, 10).join("\n") : undefined;
|
|
1794
|
+
// A narration that streamed for 20s and then broke was still generated
|
|
1795
|
+
// and still billed — read whatever usage settled (see streamCall), under
|
|
1796
|
+
// the same bound so `result` always settles.
|
|
1797
|
+
const usage = await this.readUsageBounded(attempt.handle.usage);
|
|
1798
|
+
const input = usage?.inputTokens ?? 0;
|
|
1799
|
+
const output = usage?.outputTokens ?? 0;
|
|
1800
|
+
const cached = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
1801
|
+
session.close({
|
|
1802
|
+
finalStatus: "error",
|
|
1803
|
+
errorMessage: message,
|
|
1804
|
+
errorStack: stack,
|
|
1805
|
+
totalTokens: { input, output, cached },
|
|
1806
|
+
warnings: [],
|
|
1807
|
+
parseFallbacks: [],
|
|
1808
|
+
});
|
|
1809
|
+
await this.persistUsageOnFailure({
|
|
1810
|
+
tokenUsageType: params.tokenUsageType,
|
|
1811
|
+
relationshipId: params.relationshipId,
|
|
1812
|
+
relationshipType: params.relationshipType,
|
|
1813
|
+
modelWeight,
|
|
1814
|
+
}, { input, output, cached });
|
|
1815
|
+
console.error("[LLMService.streamText] Error:", error);
|
|
1816
|
+
const wrapped = new Error(`LLM streamText error: ${message}`);
|
|
1817
|
+
wrapped.cause = error;
|
|
1818
|
+
throw wrapped;
|
|
1819
|
+
}
|
|
1466
1820
|
}
|
|
1467
1821
|
})();
|
|
1468
1822
|
// Normalize the AI SDK `fullStream` to text/reasoning deltas. Consuming this
|
|
@@ -1470,7 +1824,11 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1470
1824
|
// resolve. Reasoning-capable models interleave `reasoning-delta` parts (e.g.
|
|
1471
1825
|
// Ollama emits the full thinking trace before answer content).
|
|
1472
1826
|
async function* normalizedStream() {
|
|
1473
|
-
|
|
1827
|
+
// The first pull marks the stream unrestartable (see `consumerStarted`),
|
|
1828
|
+
// and reads `attempt` late so a consumer that starts after a transient
|
|
1829
|
+
// retry gets the stream that actually connected.
|
|
1830
|
+
consumerStarted = true;
|
|
1831
|
+
for await (const part of attempt.handle.fullStream) {
|
|
1474
1832
|
if (part.type === "text-delta") {
|
|
1475
1833
|
yield { type: "text", delta: part.text };
|
|
1476
1834
|
}
|
|
@@ -1531,6 +1889,12 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1531
1889
|
outputSchemaName: params.tool.name,
|
|
1532
1890
|
});
|
|
1533
1891
|
const attemptTimeoutMs = this.attemptTimeoutMs(params.timeout);
|
|
1892
|
+
// Hoisted above the try so the catch can bill the attempts the provider
|
|
1893
|
+
// already served before the failure. Accumulated (not overwritten) across
|
|
1894
|
+
// attempts: the nudge retry is a SECOND request and is charged as one.
|
|
1895
|
+
let totalInputTokens = 0;
|
|
1896
|
+
let totalOutputTokens = 0;
|
|
1897
|
+
let totalCachedTokens = 0;
|
|
1534
1898
|
try {
|
|
1535
1899
|
const model = this.modelService.getLLM({
|
|
1536
1900
|
modelWeight,
|
|
@@ -1610,10 +1974,18 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1610
1974
|
return null;
|
|
1611
1975
|
};
|
|
1612
1976
|
// Each provider invocation is bounded independently — the nudge retry is a
|
|
1613
|
-
// second request and gets its own budget, not the leftovers of the first
|
|
1977
|
+
// second request and gets its own budget, not the leftovers of the first —
|
|
1978
|
+
// and each is retried on a transient network failure, with its own fresh
|
|
1979
|
+
// abort controller per attempt.
|
|
1614
1980
|
const invokeBounded = async (messages, attempt) => {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1981
|
+
const response = (await this.runWithTransientRetry(`extractViaTool:${params.tool.name}:${attempt}`, attemptTimeoutMs, (signal) => bound.invoke(messages, { signal })));
|
|
1982
|
+
// Bill as we go: an attempt that answered is charged whether or not its
|
|
1983
|
+
// payload turns out to be usable.
|
|
1984
|
+
const usage = response.usage_metadata;
|
|
1985
|
+
totalInputTokens += usage?.input_tokens ?? 0;
|
|
1986
|
+
totalOutputTokens += usage?.output_tokens ?? 0;
|
|
1987
|
+
totalCachedTokens += usage?.input_token_details?.cache_read ?? 0;
|
|
1988
|
+
return response;
|
|
1617
1989
|
};
|
|
1618
1990
|
let response = await invokeBounded(baseMessages, "attempt-1");
|
|
1619
1991
|
let parsed = tryExtract(response);
|
|
@@ -1629,9 +2001,11 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1629
2001
|
}
|
|
1630
2002
|
if (parsed === null)
|
|
1631
2003
|
throw new Error("extractViaTool: model did not call the tool");
|
|
2004
|
+
// The winning response's OWN usage — what the dump's response entry
|
|
2005
|
+
// describes. The session total and the ledger use the accumulated figures
|
|
2006
|
+
// instead, so a nudge retry bills both requests rather than only the last.
|
|
1632
2007
|
const inputTokens = response.usage_metadata?.input_tokens ?? 0;
|
|
1633
2008
|
const outputTokens = response.usage_metadata?.output_tokens ?? 0;
|
|
1634
|
-
const cachedTokens = response.usage_metadata?.input_token_details?.cache_read ?? 0;
|
|
1635
2009
|
session.recordResponse({
|
|
1636
2010
|
content: JSON.stringify(parsed),
|
|
1637
2011
|
tokenUsage: { input: inputTokens, output: outputTokens },
|
|
@@ -1639,7 +2013,7 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1639
2013
|
});
|
|
1640
2014
|
session.close({
|
|
1641
2015
|
finalStatus: "success",
|
|
1642
|
-
totalTokens: { input:
|
|
2016
|
+
totalTokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
|
|
1643
2017
|
warnings: [],
|
|
1644
2018
|
parseFallbacks: [],
|
|
1645
2019
|
});
|
|
@@ -1648,7 +2022,7 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1648
2022
|
relationshipId: params.relationshipId,
|
|
1649
2023
|
relationshipType: params.relationshipType,
|
|
1650
2024
|
modelWeight,
|
|
1651
|
-
}, { input:
|
|
2025
|
+
}, { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens });
|
|
1652
2026
|
// Write-through on a miss so the next identical cacheable call hits.
|
|
1653
2027
|
if (cacheKey && this.cache)
|
|
1654
2028
|
await this.cache.set(cacheKey, parsed);
|
|
@@ -1659,10 +2033,18 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1659
2033
|
session.close({
|
|
1660
2034
|
finalStatus: "error",
|
|
1661
2035
|
errorMessage: message,
|
|
1662
|
-
totalTokens: { input:
|
|
2036
|
+
totalTokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
|
|
1663
2037
|
warnings: [],
|
|
1664
2038
|
parseFallbacks: [],
|
|
1665
2039
|
});
|
|
2040
|
+
// Two refusals still cost two generations — bill what was served before
|
|
2041
|
+
// giving up. Never throws, so the original error survives untouched.
|
|
2042
|
+
await this.persistUsageOnFailure({
|
|
2043
|
+
tokenUsageType: params.tokenUsageType,
|
|
2044
|
+
relationshipId: params.relationshipId,
|
|
2045
|
+
relationshipType: params.relationshipType,
|
|
2046
|
+
modelWeight,
|
|
2047
|
+
}, { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens });
|
|
1666
2048
|
console.error("[LLMService.extractViaTool] Error:", error);
|
|
1667
2049
|
throw error instanceof Error ? error : new Error(message);
|
|
1668
2050
|
}
|
|
@@ -1670,14 +2052,22 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1670
2052
|
/**
|
|
1671
2053
|
* Single-step model invocation with tools bound — the durable-checkpointing
|
|
1672
2054
|
* counterpart to {@link call}'s internal tool loop. Performs exactly ONE
|
|
1673
|
-
* model
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
2055
|
+
* model STEP (no tool execution, no loop, no structured output) and returns
|
|
2056
|
+
* the raw AIMessage with any `tool_calls` untouched, so the caller (e.g. the
|
|
2057
|
+
* operator agent) can checkpoint state and execute the tool calls itself.
|
|
2058
|
+
*
|
|
2059
|
+
* "One step" is not "one socket": the step is bounded like every other call
|
|
2060
|
+
* (per-attempt budget, watchdog, deadline) and re-issued on a transient
|
|
2061
|
+
* network failure. What it never does is re-run the AGENT — the caller's
|
|
2062
|
+
* checkpointed state is untouched either way.
|
|
1677
2063
|
*
|
|
1678
2064
|
* Reuses {@link call}'s model construction (`modelService.getLLM` +
|
|
1679
|
-
* `bindTools`) and `LLMCallDumper` hooks.
|
|
1680
|
-
*
|
|
2065
|
+
* `bindTools`) and `LLMCallDumper` hooks. The step's token usage is still
|
|
2066
|
+
* RETURNED to the caller (so an agent can keep its own running total), and —
|
|
2067
|
+
* when `relationshipId`/`relationshipType` are supplied — is now also
|
|
2068
|
+
* persisted here, exactly like every other provider call in this service.
|
|
2069
|
+
* A caller that omits the attribution gets the previous behaviour: nothing is
|
|
2070
|
+
* written. Each step is billed once, by whichever path completes it.
|
|
1681
2071
|
*
|
|
1682
2072
|
* @param params.systemPrompts - System prompts, prepended (in order) as
|
|
1683
2073
|
* SystemMessages before `messages`
|
|
@@ -1685,6 +2075,9 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1685
2075
|
* @param params.tools - Tools to bind (NOT executed by this method)
|
|
1686
2076
|
* @param params.temperature - Optional temperature override
|
|
1687
2077
|
* @param params.metadata - Optional metadata for dump-session tracking
|
|
2078
|
+
* @param params.tokenUsageType - Optional usage type for the recorded row
|
|
2079
|
+
* @param params.relationshipId - Optional entity this usage is attributed to
|
|
2080
|
+
* @param params.relationshipType - Optional entity type for the attribution
|
|
1688
2081
|
*
|
|
1689
2082
|
* @returns The raw AIMessage (tool_calls intact) plus this call's token usage
|
|
1690
2083
|
*/
|
|
@@ -1709,10 +2102,28 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1709
2102
|
})),
|
|
1710
2103
|
outputSchemaName: "callStep",
|
|
1711
2104
|
});
|
|
2105
|
+
// This method used to be the ONE provider call in the service with no bound
|
|
2106
|
+
// of any kind: no per-attempt budget on the model, no watchdog, no deadline.
|
|
2107
|
+
// A stalled operator step therefore hung its durable run forever, which is
|
|
2108
|
+
// exactly the failure `runBounded` exists to end — so it gets the same
|
|
2109
|
+
// treatment as `call()`, plus the transient-network retry.
|
|
2110
|
+
const attemptTimeoutMs = this.attemptTimeoutMs();
|
|
2111
|
+
const label = `llm.callStep:${aiConfig.model}`;
|
|
2112
|
+
// Hoisted above the try so the catch reports and bills whatever the step
|
|
2113
|
+
// managed to consume before it threw.
|
|
2114
|
+
let input = 0;
|
|
2115
|
+
let output = 0;
|
|
2116
|
+
// Prompt-cache hits, priced at `cachedInputCostPer1MTokens` by `computeCost`.
|
|
2117
|
+
// The operator re-sends its whole conversation every step, so a cache hit is
|
|
2118
|
+
// the NORM here: dropping this would price the entire prompt at the uncached
|
|
2119
|
+
// rate and over-bill every operator row. Credited exactly as `call()` and
|
|
2120
|
+
// `streamCall` credit theirs.
|
|
2121
|
+
let cached = 0;
|
|
1712
2122
|
try {
|
|
1713
2123
|
const baseModel = this.modelService.getLLM({
|
|
1714
2124
|
temperature: params.temperature,
|
|
1715
2125
|
modelWeight,
|
|
2126
|
+
timeoutMs: attemptTimeoutMs,
|
|
1716
2127
|
});
|
|
1717
2128
|
const modelWithTools = params.tools.length > 0 ? baseModel.bindTools(params.tools) : baseModel;
|
|
1718
2129
|
const conversationMessages = [
|
|
@@ -1720,12 +2131,11 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1720
2131
|
...params.messages,
|
|
1721
2132
|
];
|
|
1722
2133
|
session.startIteration("tool-loop", conversationMessages);
|
|
1723
|
-
const response = (await (params.metadata
|
|
1724
|
-
? modelWithTools.invoke(conversationMessages, { metadata: params.metadata })
|
|
1725
|
-
: modelWithTools.invoke(conversationMessages)));
|
|
2134
|
+
const response = (await this.runWithTransientRetry(label, attemptTimeoutMs, (signal) => modelWithTools.invoke(conversationMessages, params.metadata ? { metadata: params.metadata, signal } : { signal })));
|
|
1726
2135
|
const raw = response;
|
|
1727
|
-
|
|
1728
|
-
|
|
2136
|
+
input = raw.usage_metadata?.input_tokens ?? 0;
|
|
2137
|
+
output = raw.usage_metadata?.output_tokens ?? 0;
|
|
2138
|
+
cached = raw.usage_metadata?.input_token_details?.cache_read ?? 0;
|
|
1729
2139
|
session.recordResponse({
|
|
1730
2140
|
content: typeof response.content === "string" ? response.content : "",
|
|
1731
2141
|
toolCalls: (response.tool_calls ?? []).map((c) => ({
|
|
@@ -1738,10 +2148,16 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1738
2148
|
});
|
|
1739
2149
|
session.close({
|
|
1740
2150
|
finalStatus: "success",
|
|
1741
|
-
totalTokens: { input, output },
|
|
2151
|
+
totalTokens: { input, output, cached },
|
|
1742
2152
|
warnings: [],
|
|
1743
2153
|
parseFallbacks: [],
|
|
1744
2154
|
});
|
|
2155
|
+
await this.persistUsage({
|
|
2156
|
+
tokenUsageType: params.tokenUsageType,
|
|
2157
|
+
relationshipId: params.relationshipId,
|
|
2158
|
+
relationshipType: params.relationshipType,
|
|
2159
|
+
modelWeight,
|
|
2160
|
+
}, { input, output, cached });
|
|
1745
2161
|
return { message: response, tokenUsage: { input, output } };
|
|
1746
2162
|
}
|
|
1747
2163
|
catch (error) {
|
|
@@ -1751,10 +2167,16 @@ let LLMService = LLMService_1 = class LLMService {
|
|
|
1751
2167
|
finalStatus: "error",
|
|
1752
2168
|
errorMessage: message,
|
|
1753
2169
|
errorStack: stack,
|
|
1754
|
-
totalTokens: { input
|
|
2170
|
+
totalTokens: { input, output, cached },
|
|
1755
2171
|
warnings: [],
|
|
1756
2172
|
parseFallbacks: [],
|
|
1757
2173
|
});
|
|
2174
|
+
await this.persistUsageOnFailure({
|
|
2175
|
+
tokenUsageType: params.tokenUsageType,
|
|
2176
|
+
relationshipId: params.relationshipId,
|
|
2177
|
+
relationshipType: params.relationshipType,
|
|
2178
|
+
modelWeight,
|
|
2179
|
+
}, { input, output, cached });
|
|
1758
2180
|
console.error("[LLMService.callStep] Error:", error);
|
|
1759
2181
|
throw error instanceof Error ? error : new Error(message);
|
|
1760
2182
|
}
|