Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- Added the GLM 5.2 prompt preset with automatic model detection.

### Fixed

- Fixed inherited Anthropic same-model replay to preserve provider-native server tool blocks around signed thinking, avoiding `thinking` / `redacted_thinking` modification errors on follow-up tool-result requests.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type BuildDynamicSystemPromptOptions, buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.ts";

function buildGlm52Tuning(): string {
return `You are running on GLM 5.2: Opus 4.6-class agent behavior tuned toward Fable 5 decisiveness and GPT 5.5 outcome-first coding. Apply literal scopes literally - "every", "all", and "for each" mean the full set. Prefer sufficient context over exhaustive context, pick minor decisions and note them, and use matching tools or skills immediately instead of under-reaching.

Calibrate deliberation. Use extended reasoning only for genuine multi-step uncertainty; routine classification, file edits, and lookups should be decided directly. A cheap tool call beats long internal debate: act, inspect evidence, and verify.

Code toward the destination: define the outcome, constraints, and stopping condition, then work without mechanical step-by-step recitation. In ultrawork mode, maintain absolute certainty discipline: preserve the goal, prove completion with evidence, and do not deliver partial work.

The intent gate routing line is non-optional every turn. For non-trivial tasks, call todowrite with atomic items before starting, keep exactly one item in progress, and complete each item immediately when done.`;
}

export function buildGlm52Prompt(options: BuildDynamicSystemPromptOptions): string {
return buildDynamicSystemPrompt({ ...options, tuningSection: buildGlm52Tuning() });
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { BuildDynamicSystemPromptOptions } from "../../../dynamic-prompt/bu
import { buildClaudeOpus45Prompt } from "./claude-opus-4-5.ts";
import { buildClaudeOpus46Prompt } from "./claude-opus-4-6.ts";
import { buildClaudeOpus47Prompt } from "./claude-opus-4-7.ts";
import { buildGlm52Prompt } from "./glm-5-2.ts";
import { buildGpt52Prompt } from "./gpt-5.2.ts";
import { buildGpt53CodexPrompt } from "./gpt-5.3-codex.ts";
import { buildGpt54Prompt } from "./gpt-5.4.ts";
Expand Down Expand Up @@ -64,6 +65,14 @@ function isKimiK27Model(model: ModelWithPromptPresetMetadata): boolean {
return hasKimiK27Signal(model.id) || (model.name !== undefined && hasKimiK27Signal(model.name));
}

function hasGlm52Signal(value: string): boolean {
return /(?:^|[/@._-])glm(?:[._-]|p)5(?:[._-]|p)2(?:$|[/@._:-])/.test(normalizeModelId(value));
}

function isGlm52Model(model: ModelWithPromptPresetMetadata): boolean {
return hasGlm52Signal(model.id) || (model.name !== undefined && hasGlm52Signal(model.name));
}

type ClaudeOpusVersion = "claude-opus-4-7" | "claude-opus-4-6" | "claude-opus-4-5";

function extractClaudeOpusVersion(modelId: string): ClaudeOpusVersion | undefined {
Expand Down Expand Up @@ -107,6 +116,9 @@ export function resolvePresetName(
if (claudeVersion) {
return claudeVersion;
}
if (isGlm52Model(model)) {
return "glm-5.2";
}
return undefined;
}

Expand All @@ -122,6 +134,8 @@ function buildPreset(name: ResolvedPresetName, options: BuildDynamicSystemPrompt
return { name, prompt: buildGpt52Prompt(options) };
case "gpt-5":
return { name, prompt: buildGpt5Prompt(options) };
case "glm-5.2":
return { name, prompt: buildGlm52Prompt(options) };
case "kimi-k2-7":
return { name, prompt: buildKimiK27Prompt(options) };
case "kimi-k2-6":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type PromptPresetName =
| "claude-opus-4-7"
| "claude-opus-4-6"
| "claude-opus-4-5"
| "glm-5.2"
| "kimi-k2-7"
| "kimi-k2-6"
| "gpt-5"
Expand All @@ -24,6 +25,7 @@ const VALID_PRESETS: ReadonlySet<string> = new Set<PromptPresetName>([
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-5",
"glm-5.2",
"kimi-k2-7",
"kimi-k2-6",
"gpt-5",
Expand Down
107 changes: 107 additions & 0 deletions packages/coding-agent/test/suite/prompt-presets-glm-5-2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { type Api, getModels, getProviders, type Model } from "@earendil-works/pi-ai";
import { describe, expect, it } from "vitest";
import {
type PromptPresetSettings,
resolvePreset,
resolvePresetName,
} from "../../src/core/extensions/builtin/prompt-preset/presets.ts";

function createModel(id: string, provider: string, api: Api = "openai-responses"): Model<Api> {
return {
id,
name: id,
api,
provider,
baseUrl: "https://example.com/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 16_384,
};
}

function hasGlm52CatalogSignal(model: Model<Api>): boolean {
const searchable = `${model.id} ${model.name}`.toLowerCase().replace(/\s+/g, "-");
return /(?:^|[/@._-])glm(?:[._-]|p)5(?:[._-]|p)2(?:$|[/@._:-])/.test(searchable);
}

function getGlm52CatalogModels(): Model<Api>[] {
return getProviders().flatMap((provider) => (getModels(provider) as Model<Api>[]).filter(hasGlm52CatalogSignal));
}

describe("GLM 5.2 prompt preset", () => {
it.each([
"zai-org/glm-5.2",
"glm-5.2",
"GLM 5.2",
"zai-org/glm-5p2",
"zai-org/glm_5_2:thinking",
])("resolves %s to the glm-5.2 preset", (modelId) => {
// given
const settings: PromptPresetSettings = { promptPreset: "auto" };
const model = createModel(modelId, "openrouter", "openai-responses");

// when
const preset = resolvePreset(model, settings);

// then
expect(preset?.name).toBe("glm-5.2");
expect(preset?.prompt).toContain("running on GLM 5.2");
expect(preset?.prompt).toContain("absolute certainty");
expect(preset?.prompt).toContain("todowrite");
expect(preset?.prompt).not.toContain("apply_patch");
});

it.each([
"glm-4.6",
"zai-org/glm-4.5",
"some-glm-compatible-router",
])("does not route %s to the glm-5.2 preset", (modelId) => {
// given
const settings: PromptPresetSettings = { promptPreset: "auto" };
const model = createModel(modelId, "openrouter", "openai-responses");

// when
const preset = resolvePreset(model, settings);

// then
expect(preset).toBeUndefined();
});

it("allows settings.json to force glm-5.2 regardless of model id", () => {
// given
const settings: PromptPresetSettings = { promptPreset: "glm-5.2" };
const model = createModel("some-random-model", "custom", "openai-responses");

// when
const preset = resolvePreset(model, settings);

// then
expect(preset?.name).toBe("glm-5.2");
expect(preset?.prompt).toContain("running on GLM 5.2");
});

it("returns glm-5.2 preset for every GLM 5.2 built-in catalog model", () => {
// given
const settings: PromptPresetSettings = { promptPreset: "auto" };
const catalogModels = getGlm52CatalogModels();
const catalogModelIds = catalogModels.map((model) => `${model.provider}/${model.id}`);

// when
const misses = catalogModels
.filter((model) => resolvePresetName(model, settings) !== "glm-5.2")
.map((model) => `${model.provider}/${model.id}`);

// then
expect(catalogModelIds).toEqual(
expect.arrayContaining([
"cloudflare-workers-ai/@cf/zai-org/glm-5.2",
"fireworks/accounts/fireworks/models/glm-5p2",
"openrouter/z-ai/glm-5.2",
"zai/glm-5.2",
]),
);
expect(misses).toEqual([]);
});
});