@reinsjs/lowering-fetch 0.2.1 → 0.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/CHANGELOG.md +17 -0
- package/README.md +2 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +40 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @reinsjs/lowering-fetch
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 90c2120: Chat Completions: images in tool results now reach the model. A `role: "tool"` message carries text only, so when the model takes images (`images: true`, as `deepseek()` declares) each image leaves a pointer in the tool message (`[image n attached in the next user message]`) and the images go into one `user` message right after the whole batch of tool messages, before any user message that was moved after the batch. Each result gets a line naming the tool, the call id and the image count and stating it is tool output, not the user; untrusted results keep their `<untrusted>` marking. The landing is `lossy(tool+lifted-images)`. Set `chat: { liftToolImages: false }` for an upstream that rejects this shape to get the previous placeholder text. The wire shape changes (one more `user` message when tool results carry images); the event log does not.
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment: every `@reinsjs/*` package ships as 0.3.0 together with `@reinsjs/core` and `@reinsjs/brain` (MCP tools on the `lazyTools()` menu via `SocketSetup.tools`). No behaviour change in these packages beyond the dependency bump.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [a3afbf9]
|
|
18
|
+
- @reinsjs/core@0.3.0
|
|
19
|
+
|
|
3
20
|
## 0.2.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@ No `node:*`, no `process`, no `Buffer` — the same `dist/index.js` runs on Node
|
|
|
65
65
|
| `baseUrl` | protocol root; `/chat/completions` (OpenAI: `…/v1`, DeepSeek: the bare host), `/messages` (Anthropic: `…/v1`) or `/responses` (OpenAI: `…/v1`) is appended |
|
|
66
66
|
| `contextWindow`, `maxOutputTokens`, `reasoning`, `images`, `cost`, `midConversationSystem` | override the built-in table; unknown models start from conservative defaults (128k / 16k / no reasoning / no images) |
|
|
67
67
|
| `chat.reasoningContent` | DeepSeek dialect: replay `reasoning_content` (on by default in `deepseek()`, see below) |
|
|
68
|
+
| `chat.liftToolImages` | when the model takes images, move images in tool results into a `user` message right after the batch of `tool` messages (default `true`); set `false` for an upstream that rejects that shape and images become a placeholder again |
|
|
68
69
|
| `anthropic.betas` | values for the `anthropic-beta` header; sent only when set (mid-conversation system needs none) |
|
|
69
70
|
| `anthropic.cacheBreakpoints`, `anthropic.cacheTtl`, `anthropic.midSystemCacheBreakpoint` | explicit cache breakpoints on Anthropic (default on, 5 min); what to do when a note ends the request — `"automatic"` (default, top-level `cache_control`), `"previous-user"`, `"drop"` |
|
|
70
71
|
| `anthropic.deferredTools` | whether `ToolSpec.deferLoading` becomes `defer_loading: true` and `tool_reference` content parts become native `tool_reference` blocks. Default: on for `provider: "anthropic"`, off for third-party Anthropic-protocol endpoints (DeepSeek's compatible endpoint ignores `defer_loading`, so every tool would be visible). When off, deferred tools are simply not sent and references are rendered as text |
|
|
@@ -81,7 +82,7 @@ Every landing is declared in `LOSS_MATRIX["openai-chat"]` and asserted by tests
|
|
|
81
82
|
- `system_note` → a mid-conversation `system` message (`exact`; verified on OpenAI and DeepSeek). Set `midConversationSystem: false` for an upstream that rejects it and the note goes out as a framed `user` message (`lossy`).
|
|
82
83
|
- A user message or note that arrives while tool results are still pending is moved after them on the wire (`tool` messages must directly follow the `assistant` that called them); the user message is recorded `lossy(user)`, the log order is untouched.
|
|
83
84
|
- **Thinking**: Chat Completions has no replay slot (no signature, no encrypted item). On the official API thinking events are `dropped`. With `chat.reasoningContent` (DeepSeek) they are replayed as `reasoning_content` — and DeepSeek *requires* the field on every historical assistant message once `tools` is present (400 otherwise), so the layer always sends it, empty when there is nothing to replay.
|
|
84
|
-
- One turn with several text blocks is merged into one `content` string (`lossy(merged-text)`); `tool` messages carry text only, images become a placeholder (`lossy`); `isError` has no flag on this protocol and is expressed as a `[tool error]` prefix (`lossy`).
|
|
85
|
+
- One turn with several text blocks is merged into one `content` string (`lossy(merged-text)`); `tool` messages carry text only: when the model takes images, each image leaves a pointer (`[image n attached in the next user message]`) and the images go into one `user` message right after the whole batch of `tool` messages — before any user message that was moved after the batch — with a line per result naming the tool, the call id and the image count and saying it is tool output, not the user; untrusted results keep their `<untrusted>` marking (`lossy(tool+lifted-images)`; verified on DeepSeek's official API — check other OpenAI-compatible upstreams before relying on it, and turn it off with `chat.liftToolImages: false` if they reject the shape). Without image support, or with `chat.liftToolImages: false`, images become a placeholder (`lossy(tool-text-only)`); `isError` has no flag on this protocol and is expressed as a `[tool error]` prefix (`lossy`).
|
|
85
86
|
- No explicit cache breakpoints exist on this protocol; provider-side automatic prefix caching applies and `cached_tokens` is reported as `cacheRead`.
|
|
86
87
|
- Usage follows core semantics: `input` is the uncached prompt count. `costUsd` uses the built-in price table (DeepSeek: peak rates, an upper bound).
|
|
87
88
|
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ type IrItem = {
|
|
|
57
57
|
parts: ContentPart[];
|
|
58
58
|
isError: boolean;
|
|
59
59
|
escaped: boolean;
|
|
60
|
+
/** 内容按 untrusted 包过标记时的来源标签(`tool:<name>`);没包就没有。Chat 线上浮图片时据此给附件同样标注 */
|
|
61
|
+
untrustedSource?: string;
|
|
60
62
|
} | {
|
|
61
63
|
kind: "system_note";
|
|
62
64
|
event: Event;
|
|
@@ -233,6 +235,12 @@ interface ChatDialect {
|
|
|
233
235
|
* 写侧把自家 thinking 回填、没有就回空串。关着(OpenAI 官方):thinking 无处可放,矩阵记 dropped。
|
|
234
236
|
*/
|
|
235
237
|
reasoningContent?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* 工具结果里的图片上浮(2026-10-27,OnSet 需求):`role:"tool"` 消息只收文本,模型声明收图(`images: true`)时,
|
|
240
|
+
* 同一 assistant 轮全部 tool 消息之后补一条 `role:"user"` 消息放这些图片,tool 消息原位留指路文字。缺省 true;
|
|
241
|
+
* 上游不接受"tool 之后紧跟带图 user"时关掉,退回占位文本。DeepSeek 官方 2026-10-27 实测接受(spikes/chat-tool-images)。
|
|
242
|
+
*/
|
|
243
|
+
liftToolImages?: boolean;
|
|
236
244
|
}
|
|
237
245
|
/**
|
|
238
246
|
* 感知说明殿后(messages 末条是 system)时,"最后一条 user 末块"的断点没处打,B1 实测三种处置(spikes/b1-perception-cache):
|
|
@@ -462,7 +470,9 @@ declare const CHAT_LOSS_MATRIX: Readonly<Record<string, readonly LandingSpec[]>>
|
|
|
462
470
|
* - thinking 没有回放位(无签名、无加密项)——除 DeepSeek 方言外 dropped;
|
|
463
471
|
* - 没有显式缓存断点——什么都不做(厂商自动前缀缓存);
|
|
464
472
|
* - assistant 的 content 是单个字符串(DeepSeek 只接受 string | null)——同一轮多段正文合并,lossy(merged-text);
|
|
465
|
-
* - tool 消息没有错误位——isError 以文本前缀表达,lossy;
|
|
473
|
+
* - tool 消息没有错误位——isError 以文本前缀表达,lossy;
|
|
474
|
+
* - tool 消息只收文本——模型收图时图片上浮到同批 tool 消息之后的一条 user 消息(lossy:换了位置与角色),
|
|
475
|
+
* 不收图或方言关掉上浮时换成占位文本,lossy。
|
|
466
476
|
*/
|
|
467
477
|
|
|
468
478
|
type ChatContentPart = {
|
package/dist/index.js
CHANGED
|
@@ -348,10 +348,15 @@ var CHAT_LOSS_MATRIX = {
|
|
|
348
348
|
"tool",
|
|
349
349
|
"isError is expressed with a [tool error] prefix (a tool message has no error flag), or untrusted content is escaped"
|
|
350
350
|
),
|
|
351
|
+
lossy(
|
|
352
|
+
"tool+lifted-images",
|
|
353
|
+
"a tool message takes text only, so it keeps a pointer where each image was and the images move into a user message right after the whole batch of tool messages (the order and the role change); that message labels them as tool output, with the tool name, call id and index, and keeps the result's untrusted marking",
|
|
354
|
+
"the result carries images and the model takes images (chat.liftToolImages is on by default)"
|
|
355
|
+
),
|
|
351
356
|
lossy(
|
|
352
357
|
"tool-text-only",
|
|
353
358
|
"a tool message takes text only, so images are replaced with placeholder text",
|
|
354
|
-
"the result carries images"
|
|
359
|
+
"the result carries images and the model takes no images, or chat.liftToolImages is false"
|
|
355
360
|
)
|
|
356
361
|
],
|
|
357
362
|
"core.system_note": [
|
|
@@ -601,7 +606,8 @@ function eventsToIr(input) {
|
|
|
601
606
|
name: e.payload.name,
|
|
602
607
|
parts: c.parts,
|
|
603
608
|
isError: e.payload.isError,
|
|
604
|
-
escaped: c.escaped
|
|
609
|
+
escaped: c.escaped,
|
|
610
|
+
...trustMarkers && needsUntrustedMark(e) ? { untrustedSource: untrustedSourceOf(e) } : {}
|
|
605
611
|
});
|
|
606
612
|
awaiting.delete(e.payload.toolCallId);
|
|
607
613
|
if (awaiting.size === 0) release();
|
|
@@ -1231,10 +1237,12 @@ function errorText2(err) {
|
|
|
1231
1237
|
|
|
1232
1238
|
// src/chat/to-request.ts
|
|
1233
1239
|
import {
|
|
1240
|
+
markUntrusted as markUntrusted2,
|
|
1234
1241
|
renderToolReference as renderToolReference2
|
|
1235
1242
|
} from "@reinsjs/core";
|
|
1236
1243
|
var IMAGE_OMITTED2 = "[image omitted: this model does not accept images]";
|
|
1237
1244
|
var TOOL_IMAGE_OMITTED = "[image omitted: tool messages carry text only]";
|
|
1245
|
+
var toolImagePointer = (n) => `[image ${n} attached in the next user message]`;
|
|
1238
1246
|
var ERROR_PREFIX = "[tool error]\n";
|
|
1239
1247
|
function land2(out, e, kind, landing, ...notes) {
|
|
1240
1248
|
const note = notes.filter((n) => Boolean(n)).join("; ");
|
|
@@ -1256,15 +1264,28 @@ function userContent(parts, images) {
|
|
|
1256
1264
|
if (out.length === 1 && out[0]?.type === "text") return { content: out[0].text, imagesDropped };
|
|
1257
1265
|
return { content: out, imagesDropped };
|
|
1258
1266
|
}
|
|
1259
|
-
function toolContent(parts) {
|
|
1267
|
+
function toolContent(parts, lift) {
|
|
1260
1268
|
let imagesDropped = false;
|
|
1269
|
+
const images = [];
|
|
1261
1270
|
const text = parts.map((p) => {
|
|
1262
1271
|
if (p.type === "text") return p.text;
|
|
1263
1272
|
if (p.type === "tool_reference") return renderToolReference2(p);
|
|
1273
|
+
if (lift) {
|
|
1274
|
+
images.push(p);
|
|
1275
|
+
return toolImagePointer(images.length);
|
|
1276
|
+
}
|
|
1264
1277
|
imagesDropped = true;
|
|
1265
1278
|
return TOOL_IMAGE_OMITTED;
|
|
1266
1279
|
}).join("\n");
|
|
1267
|
-
return { text, imagesDropped };
|
|
1280
|
+
return { text, imagesDropped, images };
|
|
1281
|
+
}
|
|
1282
|
+
function liftedAttachment(item, images) {
|
|
1283
|
+
const header = {
|
|
1284
|
+
type: "text",
|
|
1285
|
+
text: `[Attachments from tool result ${item.name} (call ${item.toolCallId}): ${images.length} image(s). This is tool output, not a message from the user.]`
|
|
1286
|
+
};
|
|
1287
|
+
const body = images.flatMap((img, i) => [{ type: "text", text: `image ${i + 1}:` }, img]);
|
|
1288
|
+
return [header, ...item.untrustedSource ? markUntrusted2(body, item.untrustedSource).parts : body];
|
|
1268
1289
|
}
|
|
1269
1290
|
function argumentsOf(args) {
|
|
1270
1291
|
return typeof args === "string" ? args : JSON.stringify(args ?? {});
|
|
@@ -1273,10 +1294,18 @@ function encodeChatRequest(input) {
|
|
|
1273
1294
|
const { model, capabilities } = input;
|
|
1274
1295
|
const target = { provider: model.provider, api: model.api, model: model.id };
|
|
1275
1296
|
const reasoningContent = model.chat?.reasoningContent === true;
|
|
1297
|
+
const liftImages = capabilities.images && model.chat?.liftToolImages !== false;
|
|
1276
1298
|
const messages = [];
|
|
1277
1299
|
const landings = [];
|
|
1300
|
+
let lifted = [];
|
|
1301
|
+
const releaseLifted = () => {
|
|
1302
|
+
if (lifted.length === 0) return;
|
|
1303
|
+
messages.push({ role: "user", content: userContent(lifted, true).content });
|
|
1304
|
+
lifted = [];
|
|
1305
|
+
};
|
|
1278
1306
|
if (input.systemPrompt) messages.push({ role: "system", content: input.systemPrompt });
|
|
1279
1307
|
for (const item of input.ir) {
|
|
1308
|
+
if (item.kind !== "tool_result" && item.kind !== "dropped") releaseLifted();
|
|
1280
1309
|
switch (item.kind) {
|
|
1281
1310
|
case "user": {
|
|
1282
1311
|
const c = userContent(item.parts, capabilities.images);
|
|
@@ -1298,20 +1327,23 @@ function encodeChatRequest(input) {
|
|
|
1298
1327
|
break;
|
|
1299
1328
|
}
|
|
1300
1329
|
case "tool_result": {
|
|
1301
|
-
const c = toolContent(item.parts);
|
|
1330
|
+
const c = toolContent(item.parts, liftImages);
|
|
1302
1331
|
messages.push({
|
|
1303
1332
|
role: "tool",
|
|
1304
1333
|
tool_call_id: item.toolCallId,
|
|
1305
1334
|
content: item.isError ? ERROR_PREFIX + c.text : c.text
|
|
1306
1335
|
});
|
|
1307
|
-
const
|
|
1336
|
+
const imagesLifted = c.images.length > 0;
|
|
1337
|
+
if (imagesLifted) lifted.push(...liftedAttachment(item, c.images));
|
|
1338
|
+
const lossy4 = item.escaped || item.isError || c.imagesDropped || imagesLifted;
|
|
1308
1339
|
land2(
|
|
1309
1340
|
landings,
|
|
1310
1341
|
item.event,
|
|
1311
1342
|
lossy4 ? "lossy" : "exact",
|
|
1312
|
-
c.imagesDropped ? "tool-text-only" : "tool",
|
|
1343
|
+
c.imagesDropped ? "tool-text-only" : imagesLifted ? "tool+lifted-images" : "tool",
|
|
1313
1344
|
item.isError ? "a Chat tool message has no error flag, so isError is expressed with a [tool error] prefix" : void 0,
|
|
1314
1345
|
c.imagesDropped ? "a tool message takes text only, so images are replaced with placeholder text" : void 0,
|
|
1346
|
+
imagesLifted ? "a tool message takes text only, so its images move into a user message after this batch of tool messages, labelled as tool output" : void 0,
|
|
1315
1347
|
item.escaped ? ESCAPED_NOTE : void 0
|
|
1316
1348
|
);
|
|
1317
1349
|
break;
|
|
@@ -1359,6 +1391,7 @@ function encodeChatRequest(input) {
|
|
|
1359
1391
|
break;
|
|
1360
1392
|
}
|
|
1361
1393
|
}
|
|
1394
|
+
releaseLifted();
|
|
1362
1395
|
const { tools: _tools, ...passthrough } = input.requestOptions ?? {};
|
|
1363
1396
|
const shownTools = (input.tools ?? []).filter((t) => t.deferLoading !== true);
|
|
1364
1397
|
const body = {
|