Skip to content

Commit 33c5c10

Browse files
fix: frontmatter was adding newlines in some cases causing invalid model ids (#11095)
Co-authored-by: aptdnfapt <[email protected]>
1 parent 0fabdcc commit 33c5c10

File tree

4 files changed

+82
-17
lines changed

4 files changed

+82
-17
lines changed

packages/opencode/src/config/markdown.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export namespace ConfigMarkdown {
1414
return Array.from(template.matchAll(SHELL_REGEX))
1515
}
1616

17-
export function preprocessFrontmatter(content: string): string {
17+
// other coding agents like claude code allow invalid yaml in their
18+
// frontmatter, we need to fallback to a more permissive parser for those cases
19+
export function fallbackSanitization(content: string): string {
1820
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
1921
if (!match) return content
2022

@@ -53,7 +55,7 @@ export namespace ConfigMarkdown {
5355

5456
// if value contains a colon, convert to block scalar
5557
if (value.includes(":")) {
56-
result.push(`${key}: |`)
58+
result.push(`${key}: |-`)
5759
result.push(` ${value}`)
5860
continue
5961
}
@@ -66,20 +68,23 @@ export namespace ConfigMarkdown {
6668
}
6769

6870
export async function parse(filePath: string) {
69-
const raw = await Bun.file(filePath).text()
70-
const template = preprocessFrontmatter(raw)
71+
const template = await Bun.file(filePath).text()
7172

7273
try {
7374
const md = matter(template)
7475
return md
75-
} catch (err) {
76-
throw new FrontmatterError(
77-
{
78-
path: filePath,
79-
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
80-
},
81-
{ cause: err },
82-
)
76+
} catch {
77+
try {
78+
return matter(fallbackSanitization(template))
79+
} catch (err) {
80+
throw new FrontmatterError(
81+
{
82+
path: filePath,
83+
message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
84+
},
85+
{ cause: err },
86+
)
87+
}
8388
}
8489
}
8590

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Response Formatting Requirements
2+
3+
Always structure your responses using clear markdown formatting:
4+
5+
- By default don't put information into tables for questions (but do put information into tables when creating or updating files)
6+
- Use headings (##, ###) to organise sections, always
7+
- Use bullet points or numbered lists for multiple items
8+
- Use code blocks with language tags for any code
9+
- Use **bold** for key terms and emphasis
10+
- Use tables when comparing options or listing structured data
11+
- Break long responses into logical sections with headings
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
description: General coding and planning agent
3+
mode: subagent
4+
model: synthetic/hf:zai-org/GLM-4.7
5+
tools:
6+
write: true
7+
read: true
8+
edit: true
9+
stuff: >
10+
This is some stuff
11+
---
12+
13+
Strictly follow da rules

packages/opencode/test/config/markdown.test.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe("ConfigMarkdown: frontmatter parsing", async () => {
104104
})
105105

106106
test("should extract occupation field with colon in value", () => {
107-
expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer\n")
107+
expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer")
108108
})
109109

110110
test("should extract title field with single quotes", () => {
@@ -128,15 +128,15 @@ describe("ConfigMarkdown: frontmatter parsing", async () => {
128128
})
129129

130130
test("should extract URL with port", () => {
131-
expect(parsed.data.url).toBe("https://example.com:8080/path?query=value\n")
131+
expect(parsed.data.url).toBe("https://example.com:8080/path?query=value")
132132
})
133133

134134
test("should extract time with colons", () => {
135-
expect(parsed.data.time).toBe("The time is 12:30:00 PM\n")
135+
expect(parsed.data.time).toBe("The time is 12:30:00 PM")
136136
})
137137

138138
test("should extract value with multiple colons", () => {
139-
expect(parsed.data.nested).toBe("First: Second: Third: Fourth\n")
139+
expect(parsed.data.nested).toBe("First: Second: Third: Fourth")
140140
})
141141

142142
test("should preserve already double-quoted values with colons", () => {
@@ -148,7 +148,7 @@ describe("ConfigMarkdown: frontmatter parsing", async () => {
148148
})
149149

150150
test("should extract value with quotes and colons mixed", () => {
151-
expect(parsed.data.mixed).toBe('He said "hello: world" and then left\n')
151+
expect(parsed.data.mixed).toBe('He said "hello: world" and then left')
152152
})
153153

154154
test("should handle empty values", () => {
@@ -190,3 +190,39 @@ describe("ConfigMarkdown: frontmatter parsing w/ no frontmatter", async () => {
190190
expect(result.content.trim()).toBe("Content")
191191
})
192192
})
193+
194+
describe("ConfigMarkdown: frontmatter parsing w/ Markdown header", async () => {
195+
const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/markdown-header.md")
196+
197+
test("should parse and match", () => {
198+
expect(result).toBeDefined()
199+
expect(result.data).toEqual({})
200+
expect(result.content.trim()).toBe(`# Response Formatting Requirements
201+
202+
Always structure your responses using clear markdown formatting:
203+
204+
- By default don't put information into tables for questions (but do put information into tables when creating or updating files)
205+
- Use headings (##, ###) to organise sections, always
206+
- Use bullet points or numbered lists for multiple items
207+
- Use code blocks with language tags for any code
208+
- Use **bold** for key terms and emphasis
209+
- Use tables when comparing options or listing structured data
210+
- Break long responses into logical sections with headings`)
211+
})
212+
})
213+
214+
describe("ConfigMarkdown: frontmatter has weird model id", async () => {
215+
const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/weird-model-id.md")
216+
217+
test("should parse and match", () => {
218+
expect(result).toBeDefined()
219+
expect(result.data["description"]).toEqual("General coding and planning agent")
220+
expect(result.data["mode"]).toEqual("subagent")
221+
expect(result.data["model"]).toEqual("synthetic/hf:zai-org/GLM-4.7")
222+
expect(result.data["tools"]["write"]).toBeTrue()
223+
expect(result.data["tools"]["read"]).toBeTrue()
224+
expect(result.data["stuff"]).toBe("This is some stuff\n")
225+
226+
expect(result.content.trim()).toBe("Strictly follow da rules")
227+
})
228+
})

0 commit comments

Comments
 (0)