@mars-sea/dsh-commandcode-provider 0.10.5 → 0.10.6
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 +15 -0
- package/README.md +5 -4
- package/README.zh-CN.md +3 -3
- package/lib/client.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +170 -19
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.10.6] - 2026-09-12
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Thinking models no longer fail every tool-loop turn with `The reasoning_content in the thinking mode must be passed back to the API`** ([#34](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/34)). On the `/alpha/generate` transport the adapter ported the pi plugin's behavior of dropping historical reasoning blocks, on the theory that private reasoning must not leak into later turns — but the official CLI does the opposite (`toWireMessages`, command-code@1.53.1, converts every `thinking` block into a `{ type: 'reasoning', text }` part of the assistant message), and the provider rebuilds its DeepSeek request from those parts: an assistant turn whose tool calls arrive without their reasoning is rejected outright. The failure was the whole turn, on every iteration after the first tool call, so a Go-plan or fallback-transport session was unusable with a thinking model. Reasoning is now replayed in content order on the CLI transport, and the Provider API transport keeps passing it back as `reasoning_content` as before.
|
|
11
|
+
|
|
12
|
+
- **A tool whose parameter schema has no object root no longer kills the turn** ([#35](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/35)). The gateway validates the root of every function schema and refuses the entire request otherwise (`Invalid schema for function 'x': schema must be a JSON Schema of 'type: "object"', got 'type: null'`). The harness's own tool builder always declares that root, so the offending schema comes from a tool registered outside it — a plugin's or MCP bridge's hand-written schema (a type-less `{ properties, required }`), an empty one, or a generator's root `$ref`. Choosing a strict model (e.g. `deepseek/deepseek-v4.1-flash`) then failed the turn no matter which tool the model wanted to call: one badly declared tool poisoned the request for all of them. Both bodies now pass every schema through `toolParametersSchema()`: an object-rooted schema is untouched, a type-less object-shaped one gains `type: 'object'`, a `["object","null"]` union is collapsed, a root `$ref` is inlined from its local `$defs`/`definitions`, an `allOf`/`anyOf`/`oneOf` root is flattened into one object (branch properties unioned; `required` kept only where every alternative demands it), and a non-object root degrades to a permissive free-form object rather than failing the request. Only the root is rewritten and every path returns a copy, so the caller's (possibly deep-frozen) schema is never mutated.
|
|
13
|
+
|
|
14
|
+
- **Reading several images in one turn no longer makes the conversation unusable** ([#33](https://github.com/Mars-Sea/dsh-commandcode-provider/pull/33)). When one assistant turn issued parallel tool calls whose results each carried an image (two `read_image` calls, say), both transports emitted each image carrier as a user message directly after its own tool result, so a user message sat between the two tool messages. The gateway requires an assistant's tool blocks to be answered consecutively and rejected the request with HTTP 400 (`An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'`); because that history is replayed on every later turn, the conversation could not continue on this provider at all — a retry did not help and only switching provider or model worked. The carriers are now buffered and flushed once the whole tool group has been emitted, so parallel results stay adjacent (`assistant → tool → tool → user → user`) on both the CLI and the Provider API transport. A single-call turn keeps the shape from [#30](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/30). Because those carriers now travel as a group, each one names the tool call it came out of (`Attached image(s) from tool result (call-a):`) — every `read_image` result renders the same envelope text, so without that the notes would be identical and the only thing tying an image to its own result would be position.
|
|
15
|
+
|
|
16
|
+
- **A user message that converts to nothing is now dropped by both transports.** The CLI transport emitted it as `{ role: 'user', content: [] }` while the Provider API transport skipped it, so the same conversation could be assembled two different ways — and an empty content array is a shape the gateway has no reason to accept. Both now skip a user message that produced no text and no image (including one whose only blocks are ones this adapter does not put on the wire). An image carried out of a tool result is still flushed before the skip, so a pending image is never lost with the message.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Synced with the official command-code@1.53.1 CLI** (2026-09-12, four CLI-local items: default compaction model set to DeepSeek V4.1 Flash in `/config`, a BYOK reasoning-effort fix, and two `/usage` summary-line changes — Extra Credits shown separately, the `$X left` field removed). `COMMAND_CODE_CLI_VERSION` is now `1.53.1`. Static inspection of both bundles finds no data or protocol drift: the `/alpha/generate` request body, both endpoints, the per-model effort map, the subscription plan maps, every plan tier, every deal, and the peak/off-peak membership and schedule are unchanged, and the catalog still serves the same 69 models (`gpt-6-astra` remains a CLI/pricing/docs-only model, absent from the public Provider catalog, as before). The one bundle difference is upstream-internal: `gpt-5.6-terra` and `gpt-5.6-luna` are now routed through vercel-ai-gateway instead of openrouter, with the same efforts, modalities and 1.05M context. The BYOK fix renames the CLI's own `providers.json` option key from `reasoning_effort` to `reasoningEffort` — a CLI-side BYOK channel, not the Command Code API contract this plugin speaks.
|
|
20
|
+
- **`npm run typecheck` now checks the test suite too.** `tsconfig.json` included only `src`, so `tests/` was never type-checked — `tsx` strips types without checking them, which is why a test file could reference an undeclared type for months and still pass. Every test file type-checks now, and CI's existing `typecheck` step covers them from here on. No shipped behavior changed: this is the quality gate catching up with the suite.
|
|
21
|
+
|
|
7
22
|
## [0.10.5] - 2026-09-11
|
|
8
23
|
|
|
9
24
|
### Added
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Pick the release line that matches your DeepSeek Harness version:
|
|
|
52
52
|
**pnpm 11 holds back new releases.** Its `minimumReleaseAge` defaults to 1440 minutes, so a version published less than a day ago is skipped and `@latest` resolves to the *previous* release — silently, with a success exit code. To install a release from the last 24 hours, name it exactly:
|
|
53
53
|
|
|
54
54
|
```sh
|
|
55
|
-
dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.10.
|
|
55
|
+
dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.10.6
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
The same applies to every profile you install into, including the terminal UI below.
|
|
@@ -71,7 +71,7 @@ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 #
|
|
|
71
71
|
Each profile updates separately — the terminal UI owns its own plugin list (see below):
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
|
-
dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.10.
|
|
74
|
+
dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.10.6
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
To move to a version published less than 24 hours ago, name it exactly as in Install above; pnpm 11's age gate resolves `@latest` to the previous release instead.
|
|
@@ -100,7 +100,7 @@ After restart, enter your API key in **Settings → Command Code** and save; **S
|
|
|
100
100
|
The plugin also works under a terminal front door. **Each dsh profile owns its own plugin list**, so the web install above does not reach the terminal — add the plugin to the `dsh-tui` profile as well:
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.10.
|
|
103
|
+
dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.10.6
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Pin the exact version here. For the first 24 hours after a release, a bare package name (or `@latest`) is silently resolved to the previous one: the install succeeds, but the profile gets the older build — which is how a fresh terminal install ends up with no **`/settings` → Command Code** page and no `commandcode` models at all.
|
|
@@ -217,7 +217,8 @@ When your deployment's dsh shell mounts the web capability (`@deepseek-ai/dsh-we
|
|
|
217
217
|
- **Image input is model-gated** — only Vision models accept images; text-only models refuse them.
|
|
218
218
|
- Switching to a text-only model in an image-bearing session is rejected by dsh — pick a model marked *`Image`* or remove the images first.
|
|
219
219
|
- **No `stop` sequences** — requests carrying one fail.
|
|
220
|
-
-
|
|
220
|
+
- Historical reasoning is replayed on both transports so a tool-calling loop keeps its chain of thought: `/provider/v1/chat/completions` passes it back as `reasoning_content`, and `/alpha/generate` carries it as a `reasoning` block inside the rebuilt assistant turn (the shape the official CLI sends) — a DeepSeek thinking-mode tool loop is rejected without it. Only tool calls with a paired tool result are replayed on either transport.
|
|
221
|
+
- Every tool's parameter schema is normalized to the object root the provider requires, so a tool declared without one (a third-party plugin's hand-written schema, an MCP server's own `inputSchema`, a generated root `$ref`) no longer fails the whole turn with `schema must be a JSON Schema of 'type: "object"'`.
|
|
221
222
|
- The model catalog is browsable without a key; chat requests need one.
|
|
222
223
|
|
|
223
224
|
## Permissions & privacy
|
package/README.zh-CN.md
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
**pnpm 11 会拦下刚发布的新版本。** 它的 `minimumReleaseAge` 默认为 1440 分钟,发布不足一天的版本会被跳过,`@latest` 解析到**上一个**版本 —— 而且是静默的,命令照样以成功退出。想装 24 小时内发布的版本,必须写精确版本号:
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
|
-
dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.10.
|
|
53
|
+
dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.10.6
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
这一点对每个 profile 都成立,包括下面的终端界面。
|
|
@@ -69,7 +69,7 @@ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 #
|
|
|
69
69
|
每个 profile 各自更新 —— 终端界面有独立的插件列表(见下文):
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
|
-
dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.10.
|
|
72
|
+
dsh plugin --profile dsh-tui update @mars-sea/dsh-commandcode-provider@0.10.6
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
要更新到发布不足 24 小时的版本,和上面的安装一样写精确版本号;pnpm 11 的年龄门禁会把 `@latest` 解析成上一个版本。
|
|
@@ -98,7 +98,7 @@ cmd login # macOS/Linux;Windows 原生版:cmdc login
|
|
|
98
98
|
插件同样支持终端前端。**每个 dsh profile 有独立的插件列表**,所以上面那条 Web 安装命令不会装到终端里 —— 还要把插件装进 `dsh-tui` profile:
|
|
99
99
|
|
|
100
100
|
```sh
|
|
101
|
-
dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.10.
|
|
101
|
+
dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider@0.10.6
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
这里请写精确版本号。新版本发布后的 24 小时内,只写包名(或 `@latest`)会被静默解析到上一个版本 —— 安装命令照样成功,但 profile 里拿到的是旧版本,结果就是全新的终端安装里既没有 **`/settings` → Command Code** 页面,也看不到任何 `commandcode` 模型。
|
package/lib/client.js
CHANGED
|
@@ -2051,7 +2051,7 @@ window.__ModuleLoader__.load({
|
|
|
2051
2051
|
}
|
|
2052
2052
|
//#endregion
|
|
2053
2053
|
//#region package.json
|
|
2054
|
-
var version = "0.10.
|
|
2054
|
+
var version = "0.10.6";
|
|
2055
2055
|
var repository = {
|
|
2056
2056
|
"type": "git",
|
|
2057
2057
|
"url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
|
package/lib/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Context } from "@deepseek-ai/cordis";
|
|
|
7
7
|
import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
8
8
|
import { CommandDefinition } from "@deepseek-ai/dsh-commands";
|
|
9
9
|
//#region src/adapter.d.ts
|
|
10
|
-
declare const COMMAND_CODE_CLI_VERSION = "1.53.
|
|
10
|
+
declare const COMMAND_CODE_CLI_VERSION = "1.53.1";
|
|
11
11
|
declare const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
12
12
|
declare const DEFAULT_GENERATE_MAX_TOKENS = 64000;
|
|
13
13
|
declare const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
package/lib/index.js
CHANGED
|
@@ -1024,7 +1024,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
1024
1024
|
* and API key or subscription, and Command Code's terms apply.
|
|
1025
1025
|
*
|
|
1026
1026
|
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
|
|
1027
|
-
* re-verified against command-code@1.53.
|
|
1027
|
+
* re-verified against command-code@1.53.1 — endpoints, request shape, and
|
|
1028
1028
|
* stream events unchanged):
|
|
1029
1029
|
* POST {apiBase}/alpha/generate
|
|
1030
1030
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
@@ -1037,7 +1037,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
1037
1037
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
1038
1038
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
1039
1039
|
*/
|
|
1040
|
-
const COMMAND_CODE_CLI_VERSION = "1.53.
|
|
1040
|
+
const COMMAND_CODE_CLI_VERSION = "1.53.1";
|
|
1041
1041
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
1042
1042
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
1043
1043
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -1105,6 +1105,124 @@ function recordOrEmpty(value) {
|
|
|
1105
1105
|
} catch {}
|
|
1106
1106
|
return {};
|
|
1107
1107
|
}
|
|
1108
|
+
/** How deep a root `$ref` / combinator chain is followed while normalizing. */
|
|
1109
|
+
const SCHEMA_NORMALIZE_MAX_DEPTH = 4;
|
|
1110
|
+
/**
|
|
1111
|
+
* Whether a type-less node is object-shaped enough to be one with the type
|
|
1112
|
+
* declared: `properties`/`required`/`additionalProperties` only make sense on
|
|
1113
|
+
* an object, and a schema carrying them was meant to be one.
|
|
1114
|
+
*/
|
|
1115
|
+
function isObjectShaped(node) {
|
|
1116
|
+
return isRecord(node.properties) || isRecord(node.patternProperties) || Array.isArray(node.required) || node.additionalProperties !== void 0;
|
|
1117
|
+
}
|
|
1118
|
+
/** The `required` names of one schema node, ignoring malformed entries. */
|
|
1119
|
+
function requiredNames(node) {
|
|
1120
|
+
return Array.isArray(node.required) ? node.required.filter((name) => typeof name === "string") : [];
|
|
1121
|
+
}
|
|
1122
|
+
/** Resolve a local `#/...` pointer inside the schema that carried it. */
|
|
1123
|
+
function resolveLocalRef(root, ref) {
|
|
1124
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
1125
|
+
let node = root;
|
|
1126
|
+
for (const rawSegment of ref.slice(2).split("/")) {
|
|
1127
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1128
|
+
if (!isRecord(node)) return void 0;
|
|
1129
|
+
node = node[segment];
|
|
1130
|
+
}
|
|
1131
|
+
return isRecord(node) ? node : void 0;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Flatten a type-less combinator node into one object schema, so the model
|
|
1135
|
+
* still sees the branch fields instead of an argument-free tool. `allOf`
|
|
1136
|
+
* branches must all hold, so their `required` entries are all kept; `anyOf` /
|
|
1137
|
+
* `oneOf` branches are alternatives, so only a name required by every branch
|
|
1138
|
+
* survives. Returns undefined when no branch describes an object.
|
|
1139
|
+
*/
|
|
1140
|
+
function mergeCombinatorBranches(node, depth) {
|
|
1141
|
+
if (depth >= SCHEMA_NORMALIZE_MAX_DEPTH) return void 0;
|
|
1142
|
+
const properties = {};
|
|
1143
|
+
const required = /* @__PURE__ */ new Set();
|
|
1144
|
+
const alternatives = [];
|
|
1145
|
+
let sawBranch = false;
|
|
1146
|
+
for (const key of [
|
|
1147
|
+
"allOf",
|
|
1148
|
+
"anyOf",
|
|
1149
|
+
"oneOf"
|
|
1150
|
+
]) {
|
|
1151
|
+
const branches = node[key];
|
|
1152
|
+
if (!Array.isArray(branches)) continue;
|
|
1153
|
+
const objectBranches = [];
|
|
1154
|
+
for (const branch of branches) {
|
|
1155
|
+
if (!isRecord(branch)) continue;
|
|
1156
|
+
objectBranches.push(toolParametersSchema(branch, depth + 1));
|
|
1157
|
+
}
|
|
1158
|
+
if (objectBranches.length === 0) continue;
|
|
1159
|
+
sawBranch = true;
|
|
1160
|
+
const names = objectBranches.map(requiredNames);
|
|
1161
|
+
if (key === "allOf") for (const name of names.flat()) required.add(name);
|
|
1162
|
+
else if (names.length > 0) alternatives.push(names[0].filter((name) => names.every((list) => list.includes(name))));
|
|
1163
|
+
for (const branch of objectBranches) {
|
|
1164
|
+
const branchProperties = isRecord(branch.properties) ? branch.properties : {};
|
|
1165
|
+
for (const [name, schema] of Object.entries(branchProperties)) if (!(name in properties)) properties[name] = schema;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (!sawBranch) return void 0;
|
|
1169
|
+
for (const group of alternatives) for (const name of group) required.add(name);
|
|
1170
|
+
const merged = {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties
|
|
1173
|
+
};
|
|
1174
|
+
if (required.size > 0) merged.required = [...required];
|
|
1175
|
+
if (node.additionalProperties !== void 0) merged.additionalProperties = node.additionalProperties;
|
|
1176
|
+
for (const key of ["description", "title"]) if (node[key] !== void 0) merged[key] = node[key];
|
|
1177
|
+
return merged;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Normalize one tool's parameters schema to an object-rooted JSON Schema.
|
|
1181
|
+
* A schema that already declares an object root is passed through untouched;
|
|
1182
|
+
* a type-less object-shaped one gains the type; a root `$ref` or combinator is
|
|
1183
|
+
* resolved/merged; anything else (an array/scalar root, or no schema at all)
|
|
1184
|
+
* degrades to a permissive free-form object, because a request the provider
|
|
1185
|
+
* refuses helps no one and the tool's own description is still in the prompt.
|
|
1186
|
+
*/
|
|
1187
|
+
function toolParametersSchema(parameters, depth = 0) {
|
|
1188
|
+
if (!isRecord(parameters)) return {
|
|
1189
|
+
type: "object",
|
|
1190
|
+
properties: {},
|
|
1191
|
+
additionalProperties: true
|
|
1192
|
+
};
|
|
1193
|
+
if (parameters.type === "object") return parameters;
|
|
1194
|
+
if (Array.isArray(parameters.type) && parameters.type.includes("object")) return {
|
|
1195
|
+
...parameters,
|
|
1196
|
+
type: "object"
|
|
1197
|
+
};
|
|
1198
|
+
if (parameters.type === void 0 || parameters.type === null) {
|
|
1199
|
+
if (typeof parameters.$ref === "string" && depth < SCHEMA_NORMALIZE_MAX_DEPTH) {
|
|
1200
|
+
const target = resolveLocalRef(parameters, parameters.$ref);
|
|
1201
|
+
if (target !== void 0) {
|
|
1202
|
+
const { $ref: _ref, ...rest } = parameters;
|
|
1203
|
+
return toolParametersSchema({
|
|
1204
|
+
...target,
|
|
1205
|
+
...rest
|
|
1206
|
+
}, depth + 1);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (isObjectShaped(parameters)) return {
|
|
1210
|
+
...parameters,
|
|
1211
|
+
type: "object"
|
|
1212
|
+
};
|
|
1213
|
+
const merged = mergeCombinatorBranches(parameters, depth);
|
|
1214
|
+
if (merged !== void 0) return merged;
|
|
1215
|
+
if (typeof parameters.$ref === "string") return {
|
|
1216
|
+
...parameters,
|
|
1217
|
+
type: "object"
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
type: "object",
|
|
1222
|
+
properties: {},
|
|
1223
|
+
additionalProperties: true
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1108
1226
|
function projectSlugFromPath(pathName) {
|
|
1109
1227
|
return pathName.toLowerCase().replace(/^[a-z]:/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|(?<!-)-+$/g, "") || "project";
|
|
1110
1228
|
}
|
|
@@ -1281,23 +1399,33 @@ function toolResultTextForWire(media) {
|
|
|
1281
1399
|
return media.text || (media.images.length > 0 ? "(image returned; see the attached image)" : "");
|
|
1282
1400
|
}
|
|
1283
1401
|
/** Leading line of the user message that carries a tool result's images. */
|
|
1284
|
-
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result
|
|
1402
|
+
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result";
|
|
1285
1403
|
/**
|
|
1286
1404
|
* The model-visible note introducing the images carried out of one tool
|
|
1287
1405
|
* result. Neither transport merges them back into the tool result, so this
|
|
1288
1406
|
* line is what tells the model the image it is about to see belongs to the
|
|
1289
1407
|
* tool it just ran rather than to the user; it also leads the part list,
|
|
1290
|
-
* because an image-first content array is what some gateways reject.
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1408
|
+
* because an image-first content array is what some gateways reject.
|
|
1409
|
+
*
|
|
1410
|
+
* The note names the tool call the image came out of, because a turn's
|
|
1411
|
+
* carriers are emitted as a group after the whole tool group (issue #33):
|
|
1412
|
+
* position alone would have to carry the association, and every `read_image`
|
|
1413
|
+
* result renders the same envelope text, so two parallel calls would produce
|
|
1414
|
+
* two identical notes. The id is the WIRE id — the one the model saw on the
|
|
1415
|
+
* `tool-call` it issued and on the tool message just above — so an overlong
|
|
1416
|
+
* cross-provider id is named by the alias that replaced it, not by the
|
|
1417
|
+
* harness-side id the model never saw.
|
|
1418
|
+
*
|
|
1419
|
+
* Count and pixel dimensions are appended when the tool's own text does not
|
|
1420
|
+
* already state them (a result that returns the image and nothing else).
|
|
1293
1421
|
*/
|
|
1294
|
-
function toolResultImageNote(media) {
|
|
1422
|
+
function toolResultImageNote(media, toolCallId) {
|
|
1423
|
+
const lead = `${TOOL_RESULT_IMAGE_TEXT} (${toolCallId}):`;
|
|
1295
1424
|
const first = media.images[0];
|
|
1296
|
-
if (first === void 0) return
|
|
1425
|
+
if (first === void 0) return lead;
|
|
1297
1426
|
const dimensions = `${first.width}x${first.height} px`;
|
|
1298
|
-
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return
|
|
1299
|
-
|
|
1300
|
-
return `${TOOL_RESULT_IMAGE_TEXT} ${count}${dimensions}`;
|
|
1427
|
+
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return lead;
|
|
1428
|
+
return `${lead} ${media.images.length > 1 ? `${media.images.length} images, ` : ""}${dimensions}`;
|
|
1301
1429
|
}
|
|
1302
1430
|
function hasImageContent(message) {
|
|
1303
1431
|
const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
|
|
@@ -1324,9 +1452,15 @@ async function messagesToCC(messages, readImage) {
|
|
|
1324
1452
|
const out = [];
|
|
1325
1453
|
const { ids: paired, names: toolNames } = pairedToolCalls(messages);
|
|
1326
1454
|
const wireIds = wireToolCallIds(paired);
|
|
1455
|
+
const pendingImages = [];
|
|
1456
|
+
const flushPendingImages = () => {
|
|
1457
|
+
for (const message of pendingImages) out.push(message);
|
|
1458
|
+
pendingImages.length = 0;
|
|
1459
|
+
};
|
|
1327
1460
|
for (const message of messages) {
|
|
1328
1461
|
if (message.role === "system") continue;
|
|
1329
1462
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
1463
|
+
flushPendingImages();
|
|
1330
1464
|
const parts = [];
|
|
1331
1465
|
for (const block of message.content) {
|
|
1332
1466
|
if (block.type === "text") parts.push({
|
|
@@ -1338,6 +1472,7 @@ async function messagesToCC(messages, readImage) {
|
|
|
1338
1472
|
parts.push(await imageToCommandCode(block.attachment, readImage));
|
|
1339
1473
|
}
|
|
1340
1474
|
}
|
|
1475
|
+
if (parts.length === 0) continue;
|
|
1341
1476
|
out.push({
|
|
1342
1477
|
role: "user",
|
|
1343
1478
|
content: parts
|
|
@@ -1345,11 +1480,16 @@ async function messagesToCC(messages, readImage) {
|
|
|
1345
1480
|
continue;
|
|
1346
1481
|
}
|
|
1347
1482
|
if (message.role === "assistant") {
|
|
1483
|
+
flushPendingImages();
|
|
1348
1484
|
const parts = [];
|
|
1349
1485
|
for (const block of message.content) if (block.type === "text") parts.push({
|
|
1350
1486
|
type: "text",
|
|
1351
1487
|
text: block.text
|
|
1352
1488
|
});
|
|
1489
|
+
else if (block.type === "reasoning") parts.push({
|
|
1490
|
+
type: "reasoning",
|
|
1491
|
+
text: block.text
|
|
1492
|
+
});
|
|
1353
1493
|
else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
|
|
1354
1494
|
type: "tool-call",
|
|
1355
1495
|
toolCallId: wireIds.get(block.id) ?? block.id,
|
|
@@ -1366,11 +1506,12 @@ async function messagesToCC(messages, readImage) {
|
|
|
1366
1506
|
const block = message.content[0];
|
|
1367
1507
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1368
1508
|
const media = toolResultMedia(block);
|
|
1509
|
+
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
1369
1510
|
out.push({
|
|
1370
1511
|
role: "tool",
|
|
1371
1512
|
content: [{
|
|
1372
1513
|
type: "tool-result",
|
|
1373
|
-
toolCallId:
|
|
1514
|
+
toolCallId: wireToolCallId,
|
|
1374
1515
|
toolName: toolNames.get(block.toolCallId) || "unknown",
|
|
1375
1516
|
output: block.isError ? {
|
|
1376
1517
|
type: "error-text",
|
|
@@ -1385,16 +1526,17 @@ async function messagesToCC(messages, readImage) {
|
|
|
1385
1526
|
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1386
1527
|
const carried = [{
|
|
1387
1528
|
type: "text",
|
|
1388
|
-
text: toolResultImageNote(media)
|
|
1529
|
+
text: toolResultImageNote(media, wireToolCallId)
|
|
1389
1530
|
}];
|
|
1390
1531
|
for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
|
|
1391
|
-
|
|
1532
|
+
pendingImages.push({
|
|
1392
1533
|
role: "user",
|
|
1393
1534
|
content: carried
|
|
1394
1535
|
});
|
|
1395
1536
|
}
|
|
1396
1537
|
}
|
|
1397
1538
|
}
|
|
1539
|
+
flushPendingImages();
|
|
1398
1540
|
return out;
|
|
1399
1541
|
}
|
|
1400
1542
|
/**
|
|
@@ -1422,9 +1564,15 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1422
1564
|
const out = [];
|
|
1423
1565
|
const { ids: paired } = pairedToolCalls(messages);
|
|
1424
1566
|
const wireIds = wireToolCallIds(paired);
|
|
1567
|
+
const pendingImages = [];
|
|
1568
|
+
const flushPendingImages = () => {
|
|
1569
|
+
for (const message of pendingImages) out.push(message);
|
|
1570
|
+
pendingImages.length = 0;
|
|
1571
|
+
};
|
|
1425
1572
|
for (const message of messages) {
|
|
1426
1573
|
if (message.role === "system") continue;
|
|
1427
1574
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
1575
|
+
flushPendingImages();
|
|
1428
1576
|
const parts = [];
|
|
1429
1577
|
for (const block of message.content) {
|
|
1430
1578
|
if (block.type === "text") parts.push({
|
|
@@ -1448,6 +1596,7 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1448
1596
|
continue;
|
|
1449
1597
|
}
|
|
1450
1598
|
if (message.role === "assistant") {
|
|
1599
|
+
flushPendingImages();
|
|
1451
1600
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1452
1601
|
const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
1453
1602
|
const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
|
|
@@ -1472,25 +1621,27 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1472
1621
|
const block = message.content[0];
|
|
1473
1622
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1474
1623
|
const media = toolResultMedia(block);
|
|
1624
|
+
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
1475
1625
|
out.push({
|
|
1476
1626
|
role: "tool",
|
|
1477
|
-
tool_call_id:
|
|
1627
|
+
tool_call_id: wireToolCallId,
|
|
1478
1628
|
content: toolResultTextForWire(media)
|
|
1479
1629
|
});
|
|
1480
1630
|
if (media.images.length > 0) {
|
|
1481
1631
|
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1482
1632
|
const carried = [{
|
|
1483
1633
|
type: "text",
|
|
1484
|
-
text: toolResultImageNote(media)
|
|
1634
|
+
text: toolResultImageNote(media, wireToolCallId)
|
|
1485
1635
|
}];
|
|
1486
1636
|
for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
|
|
1487
|
-
|
|
1637
|
+
pendingImages.push({
|
|
1488
1638
|
role: "user",
|
|
1489
1639
|
content: carried
|
|
1490
1640
|
});
|
|
1491
1641
|
}
|
|
1492
1642
|
}
|
|
1493
1643
|
}
|
|
1644
|
+
flushPendingImages();
|
|
1494
1645
|
return out;
|
|
1495
1646
|
}
|
|
1496
1647
|
/** Account endpoints fetched by one `getUsage()` run (see the classification there). */
|
|
@@ -1595,7 +1746,7 @@ async function buildCliBody(options, connection, facts) {
|
|
|
1595
1746
|
type: "function",
|
|
1596
1747
|
name: tool.name,
|
|
1597
1748
|
description: tool.description,
|
|
1598
|
-
input_schema: tool.parameters
|
|
1749
|
+
input_schema: toolParametersSchema(tool.parameters)
|
|
1599
1750
|
})),
|
|
1600
1751
|
system: facts.systemText,
|
|
1601
1752
|
max_tokens: facts.maxTokens,
|
|
@@ -1617,7 +1768,7 @@ async function buildOpenAIBody(options, facts) {
|
|
|
1617
1768
|
function: {
|
|
1618
1769
|
name: tool.name,
|
|
1619
1770
|
description: tool.description,
|
|
1620
|
-
parameters: tool.parameters
|
|
1771
|
+
parameters: toolParametersSchema(tool.parameters)
|
|
1621
1772
|
}
|
|
1622
1773
|
}));
|
|
1623
1774
|
return {
|