Skip to content

Commit 2f3bc90

Browse files
authored
fix(glm): stabilize reasoning and context reporting (#3615)
1 parent 9222c50 commit 2f3bc90

12 files changed

Lines changed: 434 additions & 84 deletions

File tree

packages/agent/src/adapters/claude/claude-agent.streamed-text.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,17 @@ function textDelta(sessionId: string, text: string) {
149149
};
150150
}
151151

152-
function assistantMessage(sessionId: string, apiId: string, text: string) {
152+
function assistantMessage(
153+
sessionId: string,
154+
apiId: string,
155+
text: string,
156+
usage?: {
157+
input_tokens: number;
158+
output_tokens: number;
159+
cache_read_input_tokens: number;
160+
cache_creation_input_tokens: number;
161+
},
162+
) {
153163
return {
154164
type: "assistant",
155165
parent_tool_use_id: null,
@@ -159,6 +169,20 @@ function assistantMessage(sessionId: string, apiId: string, text: string) {
159169
id: apiId,
160170
role: "assistant",
161171
content: [{ type: "text", text }],
172+
...(usage ? { usage } : {}),
173+
},
174+
};
175+
}
176+
177+
function compactBoundary(sessionId: string) {
178+
return {
179+
type: "system",
180+
subtype: "compact_boundary",
181+
session_id: sessionId,
182+
uuid: "compact-1",
183+
compact_metadata: {
184+
trigger: "auto",
185+
pre_tokens: 434_000,
162186
},
163187
};
164188
}
@@ -192,6 +216,23 @@ function messageChunkTexts(
192216
.map((update) => update?.content?.text ?? "");
193217
}
194218

219+
function usageUpdates(
220+
calls: ClientMocks["sessionUpdate"]["mock"]["calls"],
221+
): Array<{ used: number; size: number }> {
222+
return calls.flatMap(([call]) => {
223+
const update = (
224+
call as {
225+
update?: { sessionUpdate?: string; used?: number; size?: number };
226+
}
227+
).update;
228+
return update?.sessionUpdate === "usage_update" &&
229+
typeof update.used === "number" &&
230+
typeof update.size === "number"
231+
? [{ used: update.used, size: update.size }]
232+
: [];
233+
});
234+
}
235+
195236
describe("ClaudeAcpAgent.prompt — streamed assistant text wiring", () => {
196237
beforeEach(() => {
197238
vi.clearAllMocks();
@@ -243,6 +284,48 @@ describe("ClaudeAcpAgent.prompt — streamed assistant text wiring", () => {
243284
]);
244285
});
245286

287+
it("does not replace known context usage with incomplete gateway snapshots", async () => {
288+
const { agent, client } = makeAgent();
289+
const sessionId = "s-context-usage";
290+
const { query, input } = installFakeSession(agent, sessionId);
291+
const session = (
292+
agent as unknown as {
293+
session: { contextUsed?: number; lastContextWindowSize?: number };
294+
}
295+
).session;
296+
session.contextUsed = 434_000;
297+
session.lastContextWindowSize = 1_000_000;
298+
vi.mocked(query.getContextUsage).mockRejectedValue(
299+
new Error("context usage unavailable"),
300+
);
301+
302+
const promptPromise = agent.prompt({
303+
sessionId,
304+
prompt: [{ type: "text", text: "continue" }],
305+
});
306+
await tick();
307+
308+
await echoUserMessage(query, input);
309+
await send(query, messageStart(sessionId, "msg-context"));
310+
await send(query, compactBoundary(sessionId));
311+
await send(
312+
query,
313+
assistantMessage(sessionId, "msg-context", "done", {
314+
input_tokens: 440_000,
315+
output_tokens: 100,
316+
cache_read_input_tokens: 0,
317+
cache_creation_input_tokens: 0,
318+
}),
319+
);
320+
await send(query, resultSuccess(sessionId));
321+
await promptPromise;
322+
323+
const updates = usageUpdates(client.sessionUpdate.mock.calls);
324+
expect(updates.length).toBeGreaterThan(0);
325+
expect(updates.every(({ used }) => used >= 434_000)).toBe(true);
326+
expect(updates.every(({ size }) => size === 1_000_000)).toBe(true);
327+
});
328+
246329
it("keeps the original turn open until a pending steer is consumed", async () => {
247330
const { agent, client } = makeAgent();
248331
const sessionId = "s-steer-ordering";

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,21 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
651651
};
652652
};
653653

654+
const recordContextUsage = (nextTotal: number): boolean => {
655+
if (nextTotal <= 0 || nextTotal === lastAssistantTotalUsage) {
656+
return false;
657+
}
658+
const knownTotal = Math.max(
659+
lastAssistantTotalUsage ?? 0,
660+
session.contextUsed ?? 0,
661+
);
662+
if (nextTotal < knownTotal) {
663+
return false;
664+
}
665+
lastAssistantTotalUsage = nextTotal;
666+
return true;
667+
};
668+
654669
const resetTurnScratch = () => {
655670
lastAssistantTotalUsage = null;
656671
lastRefusalExplanation = null;
@@ -832,16 +847,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
832847
fetchContextUsedTokens(query, this.logger),
833848
cancelController.signal,
834849
);
835-
lastAssistantTotalUsage =
836-
usedTokens.result === "success" ? (usedTokens.value ?? 0) : 0;
837-
await this.client.sessionUpdate({
838-
sessionId,
839-
update: {
840-
sessionUpdate: "usage_update",
841-
used: lastAssistantTotalUsage,
842-
size: windowSize(),
843-
},
844-
});
850+
if (usedTokens.result === "success" && usedTokens.value != null) {
851+
lastAssistantTotalUsage = usedTokens.value;
852+
session.contextUsed = usedTokens.value;
853+
session.contextSize = windowSize();
854+
await this.client.sessionUpdate({
855+
sessionId,
856+
update: {
857+
sessionUpdate: "usage_update",
858+
used: lastAssistantTotalUsage,
859+
size: windowSize(),
860+
},
861+
});
862+
}
845863
}
846864
if (message.subtype === "commands_changed") {
847865
session.knownSlashCommands = collectKnownSlashCommands(
@@ -1201,8 +1219,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
12011219
lastStreamUsage.cache_read_input_tokens +
12021220
lastStreamUsage.cache_creation_input_tokens;
12031221

1204-
if (nextTotal !== lastAssistantTotalUsage) {
1205-
lastAssistantTotalUsage = nextTotal;
1222+
if (recordContextUsage(nextTotal)) {
12061223
await this.client.sessionUpdate({
12071224
sessionId,
12081225
update: {
@@ -1297,21 +1314,23 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
12971314
cache_read_input_tokens: number | null;
12981315
cache_creation_input_tokens: number | null;
12991316
};
1300-
lastAssistantTotalUsage =
1317+
const nextTotal =
13011318
(usage.input_tokens ?? 0) +
13021319
(usage.output_tokens ?? 0) +
13031320
(usage.cache_read_input_tokens ?? 0) +
13041321
(usage.cache_creation_input_tokens ?? 0);
13051322

1306-
await this.client.sessionUpdate({
1307-
sessionId,
1308-
update: {
1309-
sessionUpdate: "usage_update",
1310-
used: lastAssistantTotalUsage,
1311-
size: windowSize(),
1312-
cost: null,
1313-
},
1314-
});
1323+
if (recordContextUsage(nextTotal)) {
1324+
await this.client.sessionUpdate({
1325+
sessionId,
1326+
update: {
1327+
sessionUpdate: "usage_update",
1328+
used: nextTotal,
1329+
size: windowSize(),
1330+
cost: null,
1331+
},
1332+
});
1333+
}
13151334
}
13161335

13171336
const result = await handleUserAssistantMessage(message, context);

packages/agent/src/adapters/claude/session/models.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ describe("model capability flags", () => {
9595
xhighEffort: false,
9696
mcpInjection: false,
9797
},
98+
{
99+
modelId: "@cf/zai-org/glm-5.2",
100+
oneMContext: false,
101+
effort: true,
102+
xhighEffort: false,
103+
mcpInjection: true,
104+
},
98105
])(
99106
"$modelId capability flags",
100107
({ modelId, oneMContext, effort, xhighEffort, mcpInjection }) => {
@@ -118,6 +125,7 @@ describe("resolveEffortForModel", () => {
118125
["claude-opus-4-7", undefined, "high"],
119126
["claude-sonnet-4-6", undefined, "high"],
120127
["claude-sonnet-5", undefined, "high"],
128+
["@cf/zai-org/glm-5.2", undefined, "high"],
121129
// Models without effort support stay unset (SDK disables thinking).
122130
["claude-haiku-4-5", undefined, undefined],
123131
["claude-opus-4-6", undefined, undefined],
@@ -139,20 +147,12 @@ describe("getEffortOptions", () => {
139147
expect(getEffortOptions("claude-opus-4-6")).toBeNull();
140148
});
141149

142-
it("returns low/medium/high for effort-supporting models", () => {
143-
const opts = getEffortOptions("claude-sonnet-4-6");
144-
expect(opts?.map((o) => o.value)).toEqual(["low", "medium", "high"]);
145-
});
146-
147-
it("appends xhigh and max for xhigh-supporting models", () => {
148-
const opts = getEffortOptions("claude-opus-4-7");
149-
expect(opts?.map((o) => o.value)).toEqual([
150-
"low",
151-
"medium",
152-
"high",
153-
"xhigh",
154-
"max",
155-
]);
150+
it.each([
151+
["claude-sonnet-4-6", ["low", "medium", "high"]],
152+
["claude-opus-4-7", ["low", "medium", "high", "xhigh", "max"]],
153+
["@cf/zai-org/glm-5.2", ["high", "max"]],
154+
])("returns the exact effort levels for %s", (modelId, expected) => {
155+
expect(getEffortOptions(modelId)?.map((o) => o.value)).toEqual(expected);
156156
});
157157
});
158158

packages/agent/src/adapters/claude/session/models.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,27 @@ export function supports1MContext(modelId: string): boolean {
3333
return MODELS_WITH_1M_CONTEXT.has(modelId);
3434
}
3535

36-
const MODELS_WITH_EFFORT = new Set([
37-
"claude-opus-4-7",
38-
"claude-opus-4-8",
39-
"claude-sonnet-4-6",
40-
"claude-sonnet-5",
41-
"claude-fable-5",
42-
]);
43-
44-
const MODELS_WITH_XHIGH_EFFORT = new Set([
45-
"claude-opus-4-7",
46-
"claude-opus-4-8",
47-
"claude-sonnet-5",
48-
"claude-fable-5",
49-
]);
36+
const STANDARD_EFFORT_LEVELS: readonly EffortLevel[] = [
37+
"low",
38+
"medium",
39+
"high",
40+
];
41+
const EXTENDED_EFFORT_LEVELS: readonly EffortLevel[] = [
42+
...STANDARD_EFFORT_LEVELS,
43+
"xhigh",
44+
"max",
45+
];
46+
const MODEL_EFFORT_LEVELS: Readonly<Record<string, readonly EffortLevel[]>> = {
47+
"claude-opus-4-7": EXTENDED_EFFORT_LEVELS,
48+
"claude-opus-4-8": EXTENDED_EFFORT_LEVELS,
49+
"claude-sonnet-4-6": STANDARD_EFFORT_LEVELS,
50+
"claude-sonnet-5": EXTENDED_EFFORT_LEVELS,
51+
"claude-fable-5": EXTENDED_EFFORT_LEVELS,
52+
"@cf/zai-org/glm-5.2": ["high", "max"],
53+
};
5054

5155
export function supportsEffort(modelId: string): boolean {
52-
return MODELS_WITH_EFFORT.has(modelId);
56+
return MODEL_EFFORT_LEVELS[modelId] !== undefined;
5357
}
5458

5559
export function resolveEffortForModel(
@@ -61,7 +65,7 @@ export function resolveEffortForModel(
6165
}
6266

6367
export function supportsXhighEffort(modelId: string): boolean {
64-
return MODELS_WITH_XHIGH_EFFORT.has(modelId);
68+
return MODEL_EFFORT_LEVELS[modelId]?.includes("xhigh") ?? false;
6569
}
6670

6771
const MODELS_TO_EXCLUDE_MCP_TOOLS = new Set(["claude-haiku-4-5"]);
@@ -82,27 +86,23 @@ export function fastModeStateEnabled(state: string | undefined): boolean {
8286
}
8387

8488
interface EffortOption {
85-
value: string;
89+
value: EffortLevel;
8690
name: string;
8791
}
8892

89-
export function getEffortOptions(modelId: string): EffortOption[] | null {
90-
if (!supportsEffort(modelId)) return null;
91-
92-
const options: EffortOption[] = [
93-
{ value: "low", name: "Low" },
94-
{ value: "medium", name: "Medium" },
95-
{ value: "high", name: "High" },
96-
];
97-
98-
if (supportsXhighEffort(modelId)) {
99-
options.push(
100-
{ value: "xhigh", name: "Extra High" },
101-
{ value: "max", name: "Max" },
102-
);
103-
}
93+
const EFFORT_LABELS: Record<EffortLevel, string> = {
94+
low: "Low",
95+
medium: "Medium",
96+
high: "High",
97+
xhigh: "Extra High",
98+
max: "Max",
99+
};
104100

105-
return options;
101+
export function getEffortOptions(modelId: string): EffortOption[] | null {
102+
const levels = MODEL_EFFORT_LEVELS[modelId];
103+
return (
104+
levels?.map((value) => ({ value, name: EFFORT_LABELS[value] })) ?? null
105+
);
106106
}
107107

108108
// Model alias resolution — lets callers use human-friendly aliases like

packages/agent/src/adapters/reasoning-effort.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,16 @@ describe("isSupportedReasoningEffort", () => {
3939
isSupportedReasoningEffort("claude", "claude-sonnet-4-6", "xhigh"),
4040
).toBe(false);
4141
});
42+
43+
it.each([
44+
["high", true],
45+
["max", true],
46+
["low", false],
47+
["medium", false],
48+
["xhigh", false],
49+
])("validates GLM 5.2 effort %s", (effort, expected) => {
50+
expect(
51+
isSupportedReasoningEffort("claude", "@cf/zai-org/glm-5.2", effort),
52+
).toBe(expected);
53+
});
4254
});

0 commit comments

Comments
 (0)