teapot-coding-agent 0.37.78 → 0.37.79
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/agent/agent.js +19 -1
- package/dist/agent/llm.js +51 -5
- package/dist/master.js +3 -0
- package/dist/model-meta.js +4 -0
- package/package.json +1 -1
- package/public/assets/{index-DYA1sY0b.js → index-CmaAIX0k.js} +4 -4
- package/public/assets/index-CmaAIX0k.js.map +1 -0
- package/public/assets/{shiki-vS_Tti6t.js → shiki-CWqNrHE6.js} +2 -2
- package/public/assets/{shiki-vS_Tti6t.js.map → shiki-CWqNrHE6.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-DYA1sY0b.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -1807,6 +1807,15 @@ export class Agent {
|
|
|
1807
1807
|
await this.log.append("usage", this.currentSession, this.currentBranch, res.usage);
|
|
1808
1808
|
}
|
|
1809
1809
|
const m = res.message;
|
|
1810
|
+
/**
|
|
1811
|
+
* Carry the thinking on the message itself, not only in the log.
|
|
1812
|
+
*
|
|
1813
|
+
* `res.reasoning` was written to `chat.jsonl` and then dropped: `this.messages.push(m)` stored a
|
|
1814
|
+
* message with no reasoning, so the NEXT request sent back an assistant turn with its thinking
|
|
1815
|
+
* missing. Providers that require it (Beam, #187) answered 400; the rest simply lost the thread.
|
|
1816
|
+
*/
|
|
1817
|
+
if (res.reasoning && !m.reasoning)
|
|
1818
|
+
m.reasoning = res.reasoning;
|
|
1810
1819
|
const isPlaceholder = m.content === "(tool call)" || m.content === "(no content)";
|
|
1811
1820
|
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
1812
1821
|
role: "assistant",
|
|
@@ -2334,6 +2343,9 @@ export class Agent {
|
|
|
2334
2343
|
resetToolCatalog(this.toolCtx);
|
|
2335
2344
|
const res = await this.llmCall(this.buildMessages(), allToolSpecs());
|
|
2336
2345
|
const calls = res.message.tool_calls ?? [];
|
|
2346
|
+
// same rule as the main loop: the thinking belongs to the message it came with (#187)
|
|
2347
|
+
if (res.reasoning && !res.message.reasoning)
|
|
2348
|
+
res.message.reasoning = res.reasoning;
|
|
2337
2349
|
this.messages.push(res.message);
|
|
2338
2350
|
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
2339
2351
|
role: "assistant",
|
|
@@ -2993,7 +3005,13 @@ async function rebuildMessagesFrom(list, imagesDir) {
|
|
|
2993
3005
|
const hasCalls = Array.isArray(d.toolCalls) && d.toolCalls.length > 0;
|
|
2994
3006
|
if (role === "assistant" && !content && !hasCalls)
|
|
2995
3007
|
continue;
|
|
2996
|
-
const m = {
|
|
3008
|
+
const m = {
|
|
3009
|
+
role,
|
|
3010
|
+
content,
|
|
3011
|
+
...(role === "assistant" && typeof d.reasoning === "string" && d.reasoning
|
|
3012
|
+
? { reasoning: d.reasoning }
|
|
3013
|
+
: {}),
|
|
3014
|
+
};
|
|
2997
3015
|
if (hasCalls) {
|
|
2998
3016
|
m.tool_calls = d.toolCalls.map((c) => ({
|
|
2999
3017
|
id: c.id,
|
package/dist/agent/llm.js
CHANGED
|
@@ -188,7 +188,11 @@ function issuedBefore(messages, result) {
|
|
|
188
188
|
}
|
|
189
189
|
return false;
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
/** the field name this provider uses for a turn's thinking */
|
|
192
|
+
export function reasoningSpelling(supported) {
|
|
193
|
+
return supported?.includes("reasoning") ? "plain" : "content";
|
|
194
|
+
}
|
|
195
|
+
function sanitize(messages, supported) {
|
|
192
196
|
return repairToolPairing(messages).map((m) => {
|
|
193
197
|
if (m.content_parts?.length) {
|
|
194
198
|
const parts = m.content_parts.map((p) => p.type === "image_url"
|
|
@@ -208,7 +212,7 @@ function sanitize(messages) {
|
|
|
208
212
|
// `content_parts` is OUR field, carrying the parts into `content`; sending it upstream is an
|
|
209
213
|
// unknown property (a strict server rejects the request), so it is dropped here.
|
|
210
214
|
const { content_parts: _parts, ...rest } = m;
|
|
211
|
-
return { ...rest, content: parts };
|
|
215
|
+
return withReasoning({ ...rest, content: parts }, reasoningSpelling(supported));
|
|
212
216
|
}
|
|
213
217
|
if ((m.role === "user" || m.role === "assistant") && !m.content) {
|
|
214
218
|
return { ...m, content: m.tool_calls?.length ? "(tool call)" : "(no content)" };
|
|
@@ -216,9 +220,51 @@ function sanitize(messages) {
|
|
|
216
220
|
if (m.role === "tool" && typeof m.content !== "string") {
|
|
217
221
|
return { ...m, content: String(m.content ?? "(no output)") };
|
|
218
222
|
}
|
|
219
|
-
return { ...m };
|
|
223
|
+
return withReasoning({ ...m }, reasoningSpelling(supported));
|
|
220
224
|
});
|
|
221
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Send an assistant turn's thinking back as `reasoning_content`.
|
|
228
|
+
*
|
|
229
|
+
* WHY THIS EXISTS AT ALL: providers disagree on the spelling. MEASURED (commit 880d054, #153):
|
|
230
|
+
*
|
|
231
|
+
* OpenRouter (nemotron) -> `reasoning`
|
|
232
|
+
* commandcode (deepseek) -> `reasoning_content`
|
|
233
|
+
*
|
|
234
|
+
* so the READ side accepts either (`reasoningTextOf`). The WRITE side cannot guess: OpenRouter rejects
|
|
235
|
+
* a field it does not know, and a reasoning model reached through Command Code REQUIRES
|
|
236
|
+
* `reasoning_content` on EVERY earlier assistant turn (#187).
|
|
237
|
+
*
|
|
238
|
+
* WHY THE EMPTY-STRING CASE: a turn whose thinking the provider never returned (or that was compacted
|
|
239
|
+
* away) still has to carry the field, because these providers check PRESENCE across the whole history,
|
|
240
|
+
* not just the turns that had thinking. MEASURED — this is what `ClawRouter` does after hitting the
|
|
241
|
+
* same wall: patch EVERY assistant message with `reasoning_content: ""` when it is absent, because
|
|
242
|
+
* "reasoning models require it on every assistant turn in history, not just tool-call turns".
|
|
243
|
+
*
|
|
244
|
+
* Only assistant messages get it: a user prompt or a tool result has no thinking, and a stray field
|
|
245
|
+
* there is noise.
|
|
246
|
+
*/
|
|
247
|
+
function withReasoning(m, spell) {
|
|
248
|
+
if (m.role !== "assistant")
|
|
249
|
+
return m;
|
|
250
|
+
const r = typeof m.reasoning === "string" ? m.reasoning : undefined;
|
|
251
|
+
// `reasoning` is OUR field; it travels under the provider's spelling, so the original is dropped
|
|
252
|
+
const { reasoning: _r, ...rest } = m;
|
|
253
|
+
/**
|
|
254
|
+
* WHICH SPELLING is decided by what the provider ADVERTISES, and that is measurable.
|
|
255
|
+
*
|
|
256
|
+
* MEASURED on `/models`:
|
|
257
|
+
* OpenRouter -> `supported_parameters` includes `reasoning` and `include_reasoning`
|
|
258
|
+
* CommandCode -> no `supported_parameters` at all
|
|
259
|
+
*
|
|
260
|
+
* `reasoning` is what OpenRouter understands (and it rejects a key it does not know), while Command
|
|
261
|
+
* Code / DeepSeek requires `reasoning_content`. So: advertised `reasoning` → use `reasoning`; silence
|
|
262
|
+
* → `reasoning_content`, the DeepSeek-family spelling that is far more common among OpenAI-compatible
|
|
263
|
+
* servers.
|
|
264
|
+
*/
|
|
265
|
+
const key = spell === "plain" ? "reasoning" : "reasoning_content";
|
|
266
|
+
return { ...rest, [key]: r ?? "" };
|
|
267
|
+
}
|
|
222
268
|
function reasoningTextOf(m) {
|
|
223
269
|
for (const key of ["reasoning", "reasoning_content"]) {
|
|
224
270
|
const v = m?.[key];
|
|
@@ -270,7 +316,7 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
|
|
|
270
316
|
try {
|
|
271
317
|
const { value: res } = await withKeyRotation(cfg, (c) => client(c).chat.completions.create({
|
|
272
318
|
model: c.model,
|
|
273
|
-
messages: sanitize(messages),
|
|
319
|
+
messages: sanitize(messages, cfg.supportedParameters),
|
|
274
320
|
...(tools.length ? { tools } : {}),
|
|
275
321
|
...effortForRequest(c),
|
|
276
322
|
...cacheControlForRequest(c),
|
|
@@ -346,7 +392,7 @@ export async function chatStream(cfg, messages, tools, signal, onDelta) {
|
|
|
346
392
|
try {
|
|
347
393
|
const { value: stream } = await withKeyRotation(cfg, (c) => client(c).chat.completions.create({
|
|
348
394
|
model: c.model,
|
|
349
|
-
messages: sanitize(messages),
|
|
395
|
+
messages: sanitize(messages, cfg.supportedParameters),
|
|
350
396
|
...(tools.length ? { tools } : {}),
|
|
351
397
|
...effortForRequest(c),
|
|
352
398
|
...cacheControlForRequest(c),
|
package/dist/master.js
CHANGED
|
@@ -664,6 +664,8 @@ export class Master {
|
|
|
664
664
|
...(ac.chatFn ? { chatFn: ac.chatFn } : {}),
|
|
665
665
|
...(ac.reasoningEffort ? { reasoningEffort: ac.reasoningEffort } : {}),
|
|
666
666
|
...(modelMeta?.supportedEfforts?.length ? { supportedEfforts: modelMeta.supportedEfforts } : {}),
|
|
667
|
+
// decides the spelling of a turn's thinking on the wire (#187)
|
|
668
|
+
...(modelMeta?.supportedParameters?.length ? { supportedParameters: modelMeta.supportedParameters } : {}),
|
|
667
669
|
});
|
|
668
670
|
void fetchModelList(llm.baseUrl, llm.apiKey)
|
|
669
671
|
.then((list) => agent.setModelPricing(list.find((m) => m.id === llm.model)?.pricing))
|
|
@@ -1457,6 +1459,7 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
1457
1459
|
cfgEntry.reasoningEffort = opts.opts.reasoningEffort || undefined;
|
|
1458
1460
|
}
|
|
1459
1461
|
opts.opts.supportedEfforts = meta?.supportedEfforts;
|
|
1462
|
+
opts.opts.supportedParameters = meta?.supportedParameters;
|
|
1460
1463
|
if (!llm.model)
|
|
1461
1464
|
throw new Error("no model resolved (pass model or set one on the provider)");
|
|
1462
1465
|
if (!llm.baseUrl)
|
package/dist/model-meta.js
CHANGED
|
@@ -231,6 +231,7 @@ async function fetchProviderModels(baseUrl, apiKey) {
|
|
|
231
231
|
...(ctx !== undefined ? { contextLength: ctx } : {}),
|
|
232
232
|
...(input ? { modalities: { input, output: output ?? ["text"] } } : {}),
|
|
233
233
|
...(params?.some((p) => p === "reasoning" || p === "reasoning_effort") ? { reasoning: true } : {}),
|
|
234
|
+
...(params ? { supportedParameters: params } : {}),
|
|
234
235
|
});
|
|
235
236
|
}
|
|
236
237
|
return out.length ? out : null;
|
|
@@ -251,6 +252,9 @@ function mergeMeta(own, db) {
|
|
|
251
252
|
* effort LEVELS, only that reasoning exists. `own.reasoning` still wins for the yes/no question.
|
|
252
253
|
*/
|
|
253
254
|
...(db.supportedEfforts ? { supportedEfforts: db.supportedEfforts } : {}),
|
|
255
|
+
...(own.supportedParameters ?? db.supportedParameters
|
|
256
|
+
? { supportedParameters: own.supportedParameters ?? db.supportedParameters }
|
|
257
|
+
: {}),
|
|
254
258
|
...(own.reasoning ?? db.reasoning ? { reasoning: own.reasoning ?? db.reasoning } : {}),
|
|
255
259
|
...(db.pricing ? { pricing: db.pricing } : {}),
|
|
256
260
|
};
|