@rei-standard/amsg-shared 0.4.0-next.2 → 0.4.0-next.5
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/README.md +98 -12
- package/dist/index.cjs +12 -1
- package/dist/index.d.cts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.mjs +12 -1
- package/dist/llm-call.d.cts +5 -0
- package/dist/llm-call.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,10 +41,24 @@ origin** (`'instant'` for `amsg-instant`, `'scheduled'` for any
|
|
|
41
41
|
| `messageType` | `MessageType` | Dispatch axis. |
|
|
42
42
|
| `source` | `'instant' \| 'scheduled'` | Routing origin. |
|
|
43
43
|
| `messageId` | `string` | Unique per push. Format owned by the producer. |
|
|
44
|
-
| `sessionId` | `string` | **Shared across all pushes from the same LLM round** (reasoning + content), and across all iterations of a single agentic-loop request. |
|
|
44
|
+
| `sessionId` | `string` | **Shared across all pushes from the same LLM round** (reasoning + content), and across all iterations of a single agentic-loop request. Opaque — do not parse it for task identity, read the scheduling fields below. |
|
|
45
45
|
| `timestamp` | `string` (ISO 8601) | Producer-side wall clock. |
|
|
46
46
|
| `messageSubtype` | `string?` | Caller's business namespace. Defaults to `'chat'` at producers. |
|
|
47
47
|
| `metadata` | `object?` | **Caller passthrough.** Packages MUST NOT write here. |
|
|
48
|
+
| `taskId` | `number \| string \| null?` | Scheduled task row id. |
|
|
49
|
+
| `taskUuid` | `string \| null?` | Scheduled task uuid (the id the scheduling side chose). |
|
|
50
|
+
| `recurrenceType` | `'none' \| 'daily' \| 'weekly'?` | Whether that task fires again. |
|
|
51
|
+
| `occurrenceMs` | `number \| null?` | Nominal fire time of this occurrence (epoch ms). |
|
|
52
|
+
|
|
53
|
+
### Scheduling identity
|
|
54
|
+
|
|
55
|
+
`taskId` / `taskUuid` / `recurrenceType` / `occurrenceMs` are stamped by
|
|
56
|
+
`@rei-standard/amsg-server` on every push that came out of a scheduled task
|
|
57
|
+
row. They tell the client which task this is, whether the task will come
|
|
58
|
+
back, and which nominal fire time produced this burst — so a task the client
|
|
59
|
+
never created (one the character scheduled for itself at fire time) still
|
|
60
|
+
arrives fully identified. Pushes with no task behind them (`amsg-instant`)
|
|
61
|
+
omit all four.
|
|
48
62
|
|
|
49
63
|
---
|
|
50
64
|
|
|
@@ -85,7 +99,6 @@ fields above are validated by the builders when present.
|
|
|
85
99
|
| `title` | `string?` | Notification title. |
|
|
86
100
|
| `contactName` | `string?` | Sender display name. |
|
|
87
101
|
| `avatarUrl` | `string \| null?` | Sender avatar URL (`https:` only — `data:` is rejected upstream). |
|
|
88
|
-
| `taskId` | `string \| null?` | Scheduled task ID (server only). |
|
|
89
102
|
|
|
90
103
|
### `ReasoningPush` — LLM meta-thinking
|
|
91
104
|
|
|
@@ -96,11 +109,17 @@ fields above are validated by the builders when present.
|
|
|
96
109
|
| `title` | `string?` | |
|
|
97
110
|
| `contactName` | `string?` | |
|
|
98
111
|
| `avatarUrl` | `string \| null?` | |
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
| `messageIndex` | `number?` | 1-based part index when a producer sentence-splits reasoning into a burst. Omit for singletons. |
|
|
113
|
+
| `totalMessages` | `number?` | Total parts of that split. Omit for singletons. |
|
|
114
|
+
| `chunkIndex` | `number?` | Transport-only slice index when a single segment exceeds the Web Push payload limit (see `chunkReasoningByUtf8Bytes`). |
|
|
115
|
+
| `totalChunks` | `number?` | Total transport slices. Omit when not sliced. |
|
|
116
|
+
|
|
117
|
+
Both multi-part axes are **omitted from the wire when the part count
|
|
118
|
+
is 1**, so single-shot reasoning stays byte-for-byte identical to
|
|
119
|
+
older payloads. Current producers emit one `ReasoningPush` per LLM
|
|
120
|
+
round and set neither — oversized reasoning rides the generic
|
|
121
|
+
multipart transport instead. The two axes can coexist when a
|
|
122
|
+
sentence-split segment is itself oversized.
|
|
104
123
|
|
|
105
124
|
Emitted **before** the matching `ContentPush` burst when the LLM
|
|
106
125
|
response carried a non-empty `reasoning_content`.
|
|
@@ -116,8 +135,11 @@ response carried a non-empty `reasoning_content`.
|
|
|
116
135
|
| `message` | `string?` | Optional human-readable tag for the request. |
|
|
117
136
|
|
|
118
137
|
Emitted by an agentic-loop hook returning
|
|
119
|
-
`{ decision: 'tool-request',
|
|
120
|
-
|
|
138
|
+
`{ decision: 'tool-request', pushPayloads }`. In the default
|
|
139
|
+
(amsg-instant) flavor the client executes the tools and resumes via
|
|
140
|
+
`/continue`; amsg-server's fire-time loop runs the tools in-process
|
|
141
|
+
instead and may carry a `toolCalls` array directly — see
|
|
142
|
+
`assertValidDecision` below.
|
|
121
143
|
|
|
122
144
|
### `ErrorPush` — producer-level error
|
|
123
145
|
|
|
@@ -221,7 +243,7 @@ const error = buildErrorPush({
|
|
|
221
243
|
### Type guards
|
|
222
244
|
|
|
223
245
|
```js
|
|
224
|
-
import { isContentPush, isReasoningPush, isErrorPush } from '@rei-standard/amsg-shared';
|
|
246
|
+
import { isContentPush, isReasoningPush, isToolRequestPush, isErrorPush } from '@rei-standard/amsg-shared';
|
|
225
247
|
|
|
226
248
|
if (isContentPush(push)) {
|
|
227
249
|
// push.message is `string`
|
|
@@ -251,6 +273,68 @@ PUSH_SOURCE.SCHEDULED; // 'scheduled'
|
|
|
251
273
|
|
|
252
274
|
---
|
|
253
275
|
|
|
276
|
+
## Shared building blocks
|
|
277
|
+
|
|
278
|
+
Besides the push schema, this package is the single source of truth
|
|
279
|
+
for helpers that `amsg-instant`, `amsg-server`, and `amsg-sw` used to
|
|
280
|
+
each keep a copy of. All are exported from the package root; the
|
|
281
|
+
one-liners below are just a map — see the JSDoc on each export for
|
|
282
|
+
the full contract.
|
|
283
|
+
|
|
284
|
+
### Bytes / encoding / crypto
|
|
285
|
+
|
|
286
|
+
`toUint8` · `concatBytes` · `utf8` · `utf8Decode` · `bytesToBase64` ·
|
|
287
|
+
`bytesToBase64Url` · `base64UrlToBytes` · `jsonToBase64Url` ·
|
|
288
|
+
`bytesToHex` · `hexToBytes` · `randomBytes` · `hmacSha256` ·
|
|
289
|
+
`timingSafeEqualBytes` — WebCrypto-friendly byte/encoding helpers for
|
|
290
|
+
base64url, hex, HMAC, and constant-time comparison.
|
|
291
|
+
|
|
292
|
+
### LLM call
|
|
293
|
+
|
|
294
|
+
| Export | What it is |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `callLlm(...)` | Call an OpenAI-compatible chat-completions endpoint with timeout / abort handling (default 300 000 ms, injectable `fetch`). |
|
|
297
|
+
| `buildLlmRequestBody(...)` | Build the request body for prompt mode or `messages` mode. |
|
|
298
|
+
| `normalizeAiApiUrl(apiUrl)` | Normalize a base URL to its chat-completions endpoint without doubling `/v1`; unrecognized paths pass through unchanged. |
|
|
299
|
+
| `validateLlmMessagesShape(...)` | Validate a `messages` array, including assistant `tool_calls` and `role: 'tool'` entries. |
|
|
300
|
+
| `LLM_MESSAGES_ERROR` | Stable error codes emitted by that validation. |
|
|
301
|
+
|
|
302
|
+
### Web Push
|
|
303
|
+
|
|
304
|
+
| Export | What it is |
|
|
305
|
+
|---|---|
|
|
306
|
+
| `sendWebPush(...)` | RFC 8291 payload encryption + RFC 8292 VAPID auth + POST to the push service (`err.code = 'PUSH_SEND_FAILED'` on failure). |
|
|
307
|
+
| `buildVapidJwt(...)` / `verifyVapidJwt(...)` | Build / verify the ES256 VAPID JWT. |
|
|
308
|
+
| `normalizeVapidSubject(...)` | Normalize the VAPID subject (e.g. add the `mailto:` prefix). |
|
|
309
|
+
|
|
310
|
+
### Wire-protocol constants
|
|
311
|
+
|
|
312
|
+
| Export | What it is |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `MULTIPART_MESSAGE_KIND` / `MULTIPART_ENCODING` / `MULTIPART_VERSION` | The generic multipart chunk envelope (`'_multipart'`) produced by `amsg-instant` and reassembled by `amsg-sw`. |
|
|
315
|
+
| `DEFAULT_MULTIPART_TTL_MS` / `DEFAULT_MULTIPART_MAX_CHUNKS` / `DEFAULT_MULTIPART_MAX_TOTAL_BYTES` | Multipart reassembly budget defaults. |
|
|
316
|
+
| `REI_AMSG_POSTMESSAGE_TYPE` / `REI_SW_EVENT` / `REI_SW_MESSAGE_TYPE` / `REI_AMSG_DELIVER_MESSAGE_TYPE` | The window ⇄ Service Worker `postMessage` protocol strings — import these instead of hard-coding the literals. |
|
|
317
|
+
|
|
318
|
+
### Agentic-loop contract
|
|
319
|
+
|
|
320
|
+
| Export | What it is |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `assertValidDecision(decision, options?)` | Runtime-validate an `onLLMOutput` hook decision (`finish` / `tool-request` / `continue` / `skip-push`); `{ inlineToolCalls: true }` enables the amsg-server flavor. |
|
|
323
|
+
| `extractToolCallsFromDecision(...)` | Pull tool calls out of either decision flavor (inline `toolCalls` or tool-request `pushPayloads`). |
|
|
324
|
+
| `buildSessionContext(...)` | Build the frozen, credential-free context object handed to agentic hooks. |
|
|
325
|
+
| `extractAssistantMessage(...)` | Safely read `choices[0].message` off an LLM response (never throws). |
|
|
326
|
+
| `readReasoningContent(...)` | Read `reasoning_content` off an assistant message; empty string means "none". |
|
|
327
|
+
| `stripReasoningTags(...)` | Strip reasoning that leaked into `message.content` so it doesn't ship inside the `ContentPush` burst. |
|
|
328
|
+
| `chunkReasoningByUtf8Bytes(text, maxBytes)` | Split reasoning text on safe UTF-8 edges for payload-limited transports. |
|
|
329
|
+
|
|
330
|
+
### Validation misc
|
|
331
|
+
|
|
332
|
+
`isValidUrl` · `validateAvatarUrl` · `AVATAR_URL_MAX_LENGTH` — the
|
|
333
|
+
avatar-URL soft-strip rule shared by client / instant / server
|
|
334
|
+
(standards §6.2).
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
254
338
|
## Invariants
|
|
255
339
|
|
|
256
340
|
1. **`messageKind` is a literal-type discriminator.** Producers must
|
|
@@ -260,8 +344,10 @@ PUSH_SOURCE.SCHEDULED; // 'scheduled'
|
|
|
260
344
|
`ReasoningPush` and the `ContentPush`(es) it precedes share the
|
|
261
345
|
same `sessionId`. Agentic-loop multi-iteration runs reuse the
|
|
262
346
|
same `sessionId` across iterations.
|
|
263
|
-
3.
|
|
264
|
-
|
|
347
|
+
3. **Multi-part fields are omitted for singletons.** On `ContentPush`
|
|
348
|
+
and `ReasoningPush` alike, `messageIndex` / `totalMessages` (and
|
|
349
|
+
reasoning's `chunkIndex` / `totalChunks`) only appear on genuine
|
|
350
|
+
multi-part bursts — never as a redundant `1 / 1`.
|
|
265
351
|
4. **`metadata` is caller-owned.** Packages must add protocol-level
|
|
266
352
|
data as top-level fields, never inside `metadata`.
|
|
267
353
|
5. **`source` is the routing origin, not the dispatch type.**
|
package/dist/index.cjs
CHANGED
|
@@ -264,7 +264,10 @@ async function callLlm(payload, options = {}) {
|
|
|
264
264
|
}
|
|
265
265
|
function buildLlmRequestBody(payload, options = {}) {
|
|
266
266
|
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
267
|
+
const extraBody = payload.llmExtraBody && typeof payload.llmExtraBody === "object" && !Array.isArray(payload.llmExtraBody) ? payload.llmExtraBody : null;
|
|
267
268
|
const requestBody = {
|
|
269
|
+
// 先展开 extra body,核心字段随后写入(撞键时核心字段赢)。
|
|
270
|
+
...extraBody || {},
|
|
268
271
|
model: payload.primaryModel,
|
|
269
272
|
messages: llmMessages
|
|
270
273
|
};
|
|
@@ -511,7 +514,7 @@ async function verifyVapidJwt(jwt, publicKey) {
|
|
|
511
514
|
utf8(`${h}.${p}`)
|
|
512
515
|
);
|
|
513
516
|
if (!ok) throw new Error("VAPID JWT: signature mismatch");
|
|
514
|
-
const payload = JSON.parse(
|
|
517
|
+
const payload = JSON.parse(utf8Decode(base64UrlToBytes(p)));
|
|
515
518
|
if (!payload.exp || payload.exp <= Math.floor(Date.now() / 1e3)) {
|
|
516
519
|
throw new Error("VAPID JWT: expired");
|
|
517
520
|
}
|
|
@@ -836,12 +839,20 @@ function buildSessionContext({
|
|
|
836
839
|
scratch
|
|
837
840
|
}) {
|
|
838
841
|
const llmOutputText = readLlmOutputText(llmResponse);
|
|
842
|
+
const usage = llmResponse && typeof llmResponse === "object" && /** @type {any} */
|
|
843
|
+
llmResponse.usage && typeof /** @type {any} */
|
|
844
|
+
llmResponse.usage === "object" ? (
|
|
845
|
+
/** @type {Record<string, unknown>} */
|
|
846
|
+
/** @type {any} */
|
|
847
|
+
llmResponse.usage
|
|
848
|
+
) : null;
|
|
839
849
|
const ctx = {
|
|
840
850
|
sessionId,
|
|
841
851
|
charId,
|
|
842
852
|
messages,
|
|
843
853
|
llmResponse,
|
|
844
854
|
llmOutputText,
|
|
855
|
+
usage,
|
|
845
856
|
iteration,
|
|
846
857
|
metadata: metadata && typeof metadata === "object" ? metadata : {},
|
|
847
858
|
contactName,
|
package/dist/index.d.cts
CHANGED
|
@@ -296,6 +296,7 @@ export function stripReasoningTags(content: string): string;
|
|
|
296
296
|
* @property {ChatMessage[]} messages - Including the just-appended assistant turn.
|
|
297
297
|
* @property {unknown} llmResponse - Full LLM response (choices, usage, …).
|
|
298
298
|
* @property {string} llmOutputText - May be '' for pure tool-call responses.
|
|
299
|
+
* @property {Record<string, unknown>|null} usage - `llmResponse.usage` 的直接引用(prompt/completion/total tokens)。响应没带 usage → null。llmResponse 里本来就有,单独提出来是让「记本轮用量」不必依赖响应对象的具体形状。
|
|
299
300
|
* @property {number} iteration - 0-indexed: the round that just finished.
|
|
300
301
|
* @property {Record<string, unknown>} metadata
|
|
301
302
|
* @property {string} contactName
|
|
@@ -460,6 +461,14 @@ export const AVATAR_URL_MAX_LENGTH: 2048;
|
|
|
460
461
|
* `metadata` is a passthrough namespace owned by the caller. Packages
|
|
461
462
|
* are forbidden from writing their own fields into `metadata` — any
|
|
462
463
|
* protocol-level data goes on top-level fields.
|
|
464
|
+
*
|
|
465
|
+
* Scheduling identity (`taskId` / `taskUuid` / `recurrenceType` /
|
|
466
|
+
* `occurrenceMs`) is stamped by `@rei-standard/amsg-server` on every push
|
|
467
|
+
* that came out of a scheduled task row. It tells the client which task
|
|
468
|
+
* this is, whether the task will come back, and which nominal fire time
|
|
469
|
+
* produced this burst — so a task the client never created (one the
|
|
470
|
+
* character scheduled for itself at fire time) still arrives fully
|
|
471
|
+
* identified. Pushes with no task behind them (amsg-instant) omit all four.
|
|
463
472
|
*/
|
|
464
473
|
export type AmsgPushCommon = {
|
|
465
474
|
/**
|
|
@@ -475,7 +484,7 @@ export type AmsgPushCommon = {
|
|
|
475
484
|
*/
|
|
476
485
|
messageId: string;
|
|
477
486
|
/**
|
|
478
|
-
* - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request.
|
|
487
|
+
* - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request. Opaque id — do not parse it for task identity, read the fields below.
|
|
479
488
|
*/
|
|
480
489
|
sessionId: string;
|
|
481
490
|
/**
|
|
@@ -494,6 +503,22 @@ export type AmsgPushCommon = {
|
|
|
494
503
|
* - SW notification strategy.
|
|
495
504
|
*/
|
|
496
505
|
notification?: NotificationDirective;
|
|
506
|
+
/**
|
|
507
|
+
* - Scheduled task row id.
|
|
508
|
+
*/
|
|
509
|
+
taskId?: number | string | null;
|
|
510
|
+
/**
|
|
511
|
+
* - Scheduled task uuid (the id the scheduling side chose).
|
|
512
|
+
*/
|
|
513
|
+
taskUuid?: string | null;
|
|
514
|
+
/**
|
|
515
|
+
* - Whether that task fires again.
|
|
516
|
+
*/
|
|
517
|
+
recurrenceType?: "none" | "daily" | "weekly";
|
|
518
|
+
/**
|
|
519
|
+
* - Nominal fire time of this occurrence (epoch ms).
|
|
520
|
+
*/
|
|
521
|
+
occurrenceMs?: number | null;
|
|
497
522
|
};
|
|
498
523
|
/**
|
|
499
524
|
* SW-rendering directive. Mirrors the fields that `amsg-sw`'s
|
|
@@ -568,7 +593,6 @@ export type ContentPush = AmsgPushCommon & {
|
|
|
568
593
|
avatarUrl?: string | null;
|
|
569
594
|
messageIndex?: number;
|
|
570
595
|
totalMessages?: number;
|
|
571
|
-
taskId?: string | null;
|
|
572
596
|
};
|
|
573
597
|
/**
|
|
574
598
|
* LLM "meta-thinking" — `choices[0].message.reasoning_content` lifted
|
|
@@ -667,6 +691,10 @@ export type SessionContext = {
|
|
|
667
691
|
* - May be '' for pure tool-call responses.
|
|
668
692
|
*/
|
|
669
693
|
llmOutputText: string;
|
|
694
|
+
/**
|
|
695
|
+
* - `llmResponse.usage` 的直接引用(prompt/completion/total tokens)。响应没带 usage → null。llmResponse 里本来就有,单独提出来是让「记本轮用量」不必依赖响应对象的具体形状。
|
|
696
|
+
*/
|
|
697
|
+
usage: Record<string, unknown> | null;
|
|
670
698
|
/**
|
|
671
699
|
* - 0-indexed: the round that just finished.
|
|
672
700
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -296,6 +296,7 @@ export function stripReasoningTags(content: string): string;
|
|
|
296
296
|
* @property {ChatMessage[]} messages - Including the just-appended assistant turn.
|
|
297
297
|
* @property {unknown} llmResponse - Full LLM response (choices, usage, …).
|
|
298
298
|
* @property {string} llmOutputText - May be '' for pure tool-call responses.
|
|
299
|
+
* @property {Record<string, unknown>|null} usage - `llmResponse.usage` 的直接引用(prompt/completion/total tokens)。响应没带 usage → null。llmResponse 里本来就有,单独提出来是让「记本轮用量」不必依赖响应对象的具体形状。
|
|
299
300
|
* @property {number} iteration - 0-indexed: the round that just finished.
|
|
300
301
|
* @property {Record<string, unknown>} metadata
|
|
301
302
|
* @property {string} contactName
|
|
@@ -460,6 +461,14 @@ export const AVATAR_URL_MAX_LENGTH: 2048;
|
|
|
460
461
|
* `metadata` is a passthrough namespace owned by the caller. Packages
|
|
461
462
|
* are forbidden from writing their own fields into `metadata` — any
|
|
462
463
|
* protocol-level data goes on top-level fields.
|
|
464
|
+
*
|
|
465
|
+
* Scheduling identity (`taskId` / `taskUuid` / `recurrenceType` /
|
|
466
|
+
* `occurrenceMs`) is stamped by `@rei-standard/amsg-server` on every push
|
|
467
|
+
* that came out of a scheduled task row. It tells the client which task
|
|
468
|
+
* this is, whether the task will come back, and which nominal fire time
|
|
469
|
+
* produced this burst — so a task the client never created (one the
|
|
470
|
+
* character scheduled for itself at fire time) still arrives fully
|
|
471
|
+
* identified. Pushes with no task behind them (amsg-instant) omit all four.
|
|
463
472
|
*/
|
|
464
473
|
export type AmsgPushCommon = {
|
|
465
474
|
/**
|
|
@@ -475,7 +484,7 @@ export type AmsgPushCommon = {
|
|
|
475
484
|
*/
|
|
476
485
|
messageId: string;
|
|
477
486
|
/**
|
|
478
|
-
* - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request.
|
|
487
|
+
* - Shared across all pushes from one LLM round (reasoning + content) and across iterations of a single agentic-loop request. Opaque id — do not parse it for task identity, read the fields below.
|
|
479
488
|
*/
|
|
480
489
|
sessionId: string;
|
|
481
490
|
/**
|
|
@@ -494,6 +503,22 @@ export type AmsgPushCommon = {
|
|
|
494
503
|
* - SW notification strategy.
|
|
495
504
|
*/
|
|
496
505
|
notification?: NotificationDirective;
|
|
506
|
+
/**
|
|
507
|
+
* - Scheduled task row id.
|
|
508
|
+
*/
|
|
509
|
+
taskId?: number | string | null;
|
|
510
|
+
/**
|
|
511
|
+
* - Scheduled task uuid (the id the scheduling side chose).
|
|
512
|
+
*/
|
|
513
|
+
taskUuid?: string | null;
|
|
514
|
+
/**
|
|
515
|
+
* - Whether that task fires again.
|
|
516
|
+
*/
|
|
517
|
+
recurrenceType?: "none" | "daily" | "weekly";
|
|
518
|
+
/**
|
|
519
|
+
* - Nominal fire time of this occurrence (epoch ms).
|
|
520
|
+
*/
|
|
521
|
+
occurrenceMs?: number | null;
|
|
497
522
|
};
|
|
498
523
|
/**
|
|
499
524
|
* SW-rendering directive. Mirrors the fields that `amsg-sw`'s
|
|
@@ -568,7 +593,6 @@ export type ContentPush = AmsgPushCommon & {
|
|
|
568
593
|
avatarUrl?: string | null;
|
|
569
594
|
messageIndex?: number;
|
|
570
595
|
totalMessages?: number;
|
|
571
|
-
taskId?: string | null;
|
|
572
596
|
};
|
|
573
597
|
/**
|
|
574
598
|
* LLM "meta-thinking" — `choices[0].message.reasoning_content` lifted
|
|
@@ -667,6 +691,10 @@ export type SessionContext = {
|
|
|
667
691
|
* - May be '' for pure tool-call responses.
|
|
668
692
|
*/
|
|
669
693
|
llmOutputText: string;
|
|
694
|
+
/**
|
|
695
|
+
* - `llmResponse.usage` 的直接引用(prompt/completion/total tokens)。响应没带 usage → null。llmResponse 里本来就有,单独提出来是让「记本轮用量」不必依赖响应对象的具体形状。
|
|
696
|
+
*/
|
|
697
|
+
usage: Record<string, unknown> | null;
|
|
670
698
|
/**
|
|
671
699
|
* - 0-indexed: the round that just finished.
|
|
672
700
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -186,7 +186,10 @@ async function callLlm(payload, options = {}) {
|
|
|
186
186
|
}
|
|
187
187
|
function buildLlmRequestBody(payload, options = {}) {
|
|
188
188
|
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
189
|
+
const extraBody = payload.llmExtraBody && typeof payload.llmExtraBody === "object" && !Array.isArray(payload.llmExtraBody) ? payload.llmExtraBody : null;
|
|
189
190
|
const requestBody = {
|
|
191
|
+
// 先展开 extra body,核心字段随后写入(撞键时核心字段赢)。
|
|
192
|
+
...extraBody || {},
|
|
190
193
|
model: payload.primaryModel,
|
|
191
194
|
messages: llmMessages
|
|
192
195
|
};
|
|
@@ -433,7 +436,7 @@ async function verifyVapidJwt(jwt, publicKey) {
|
|
|
433
436
|
utf8(`${h}.${p}`)
|
|
434
437
|
);
|
|
435
438
|
if (!ok) throw new Error("VAPID JWT: signature mismatch");
|
|
436
|
-
const payload = JSON.parse(
|
|
439
|
+
const payload = JSON.parse(utf8Decode(base64UrlToBytes(p)));
|
|
437
440
|
if (!payload.exp || payload.exp <= Math.floor(Date.now() / 1e3)) {
|
|
438
441
|
throw new Error("VAPID JWT: expired");
|
|
439
442
|
}
|
|
@@ -758,12 +761,20 @@ function buildSessionContext({
|
|
|
758
761
|
scratch
|
|
759
762
|
}) {
|
|
760
763
|
const llmOutputText = readLlmOutputText(llmResponse);
|
|
764
|
+
const usage = llmResponse && typeof llmResponse === "object" && /** @type {any} */
|
|
765
|
+
llmResponse.usage && typeof /** @type {any} */
|
|
766
|
+
llmResponse.usage === "object" ? (
|
|
767
|
+
/** @type {Record<string, unknown>} */
|
|
768
|
+
/** @type {any} */
|
|
769
|
+
llmResponse.usage
|
|
770
|
+
) : null;
|
|
761
771
|
const ctx = {
|
|
762
772
|
sessionId,
|
|
763
773
|
charId,
|
|
764
774
|
messages,
|
|
765
775
|
llmResponse,
|
|
766
776
|
llmOutputText,
|
|
777
|
+
usage,
|
|
767
778
|
iteration,
|
|
768
779
|
metadata: metadata && typeof metadata === "object" ? metadata : {},
|
|
769
780
|
contactName,
|
package/dist/llm-call.d.cts
CHANGED
|
@@ -79,6 +79,11 @@ export function callLlm(payload: any, options?: {
|
|
|
79
79
|
* non-empty payload.tools. An empty array is treated as "no tools"
|
|
80
80
|
* because some OpenAI-compatible relays reject `tools: []`.
|
|
81
81
|
*
|
|
82
|
+
* `payload.llmExtraBody`(可选,普通对象):原样展开进请求体,给上游中转的
|
|
83
|
+
* 非标准参数用(thinking / reasoning_effort 之类库不认识也不该认识的字段)。
|
|
84
|
+
* 先展开它、再写核心字段——model / messages / temperature / max_tokens /
|
|
85
|
+
* tools 永远以库的口径为准,extra body 撞了这些键也盖不掉。
|
|
86
|
+
*
|
|
82
87
|
* @param {Object} payload
|
|
83
88
|
* @param {{ stream?: boolean, forwardTools?: boolean }} [options]
|
|
84
89
|
* stream — set to include an explicit `stream` field in the body
|
package/dist/llm-call.d.ts
CHANGED
|
@@ -79,6 +79,11 @@ export function callLlm(payload: any, options?: {
|
|
|
79
79
|
* non-empty payload.tools. An empty array is treated as "no tools"
|
|
80
80
|
* because some OpenAI-compatible relays reject `tools: []`.
|
|
81
81
|
*
|
|
82
|
+
* `payload.llmExtraBody`(可选,普通对象):原样展开进请求体,给上游中转的
|
|
83
|
+
* 非标准参数用(thinking / reasoning_effort 之类库不认识也不该认识的字段)。
|
|
84
|
+
* 先展开它、再写核心字段——model / messages / temperature / max_tokens /
|
|
85
|
+
* tools 永远以库的口径为准,extra body 撞了这些键也盖不掉。
|
|
86
|
+
*
|
|
82
87
|
* @param {Object} payload
|
|
83
88
|
* @param {{ stream?: boolean, forwardTools?: boolean }} [options]
|
|
84
89
|
* stream — set to include an explicit `stream` field in the body
|
package/package.json
CHANGED