mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(coderd/taskname): parse task name JSON with trailing text (#25005)
Anthropic task name responses can include valid JSON followed by a closing fence or extra text, which made `json.Unmarshal` fail with trailing-character errors and forced fallback naming. This updates task name JSON extraction to accept the first JSON value after optional fences and adds regression coverage for fenced and bare JSON with trailing content.
This commit is contained in:
+13
-14
@@ -94,26 +94,25 @@ Do not include any additional keys, explanations, or text outside the JSON.`
|
||||
var (
|
||||
ErrNoAPIKey = xerrors.New("no api key provided")
|
||||
ErrNoNameGenerated = xerrors.New("no task name generated")
|
||||
|
||||
markdownCodeFenceRE = regexp.MustCompile("(?s)^```[^\n]*\n(.*?)(?:\n```.*|```\\s*)?$")
|
||||
)
|
||||
|
||||
// extractJSON strips optional markdown code fences (```json or
|
||||
// ```) that LLMs sometimes wrap around JSON output, returning
|
||||
// only the inner JSON string. Only well-formed fences with a
|
||||
// newline after the opening backticks are stripped; malformed
|
||||
// fences are left untouched so that json.Unmarshal fails
|
||||
// cleanly and the caller can fall back to other strategies.
|
||||
// extractJSON strips optional markdown code fences (```json or ```) that
|
||||
// LLMs sometimes wrap around JSON output, returning only the inner JSON
|
||||
// string. If the response starts with JSON, it returns the first JSON value so
|
||||
// trailing commentary or dangling fences do not break parsing.
|
||||
func extractJSON(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "```") {
|
||||
// Only strip when there is a newline separating the
|
||||
// fence line from the body. Without one we cannot
|
||||
// reliably tell the fence from the content.
|
||||
if idx := strings.Index(s, "\n"); idx != -1 {
|
||||
s = s[idx+1:]
|
||||
s = strings.TrimSuffix(s, "```")
|
||||
s = strings.TrimSpace(s)
|
||||
if matches := markdownCodeFenceRE.FindStringSubmatch(s); matches != nil {
|
||||
s = strings.TrimSpace(matches[1])
|
||||
}
|
||||
|
||||
var raw json.RawMessage
|
||||
if err := json.NewDecoder(strings.NewReader(s)).Decode(&raw); err == nil {
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,21 @@ func TestExtractJSON(t *testing.T) {
|
||||
input: "```json\n{\n \"display_name\": \"Fix bug\",\n \"task_name\": \"fix-bug\"\n}\n```",
|
||||
expected: "{\n \"display_name\": \"Fix bug\",\n \"task_name\": \"fix-bug\"\n}",
|
||||
},
|
||||
{
|
||||
name: "FencedJSONWithTrailingText",
|
||||
input: "```json\n{\"display_name\": \"Fix bug\", \"task_name\": \"fix-bug\"}\n```\n\nDone.",
|
||||
expected: `{"display_name": "Fix bug", "task_name": "fix-bug"}`,
|
||||
},
|
||||
{
|
||||
name: "BareJSONWithTrailingFence",
|
||||
input: "{\"display_name\": \"Fix bug\", \"task_name\": \"fix-bug\"}\n```",
|
||||
expected: `{"display_name": "Fix bug", "task_name": "fix-bug"}`,
|
||||
},
|
||||
{
|
||||
name: "BareJSONWithTrailingText",
|
||||
input: "{\"display_name\": \"Fix bug\", \"task_name\": \"fix-bug\"}\n\nDone.",
|
||||
expected: `{"display_name": "Fix bug", "task_name": "fix-bug"}`,
|
||||
},
|
||||
{
|
||||
name: "FencedNoNewlinePassthrough",
|
||||
input: "```json{\"display_name\": \"Fix bug\", \"task_name\": \"fix-bug\"}```",
|
||||
@@ -235,6 +250,18 @@ func TestGenerateFromAnthropicMock(t *testing.T) {
|
||||
expectedDisplayName: "Setup CI",
|
||||
expectedNamePrefix: "setup-ci-",
|
||||
},
|
||||
{
|
||||
name: "FencedJSONWithTrailingText",
|
||||
responseText: "```json\n{\"display_name\": \"Debug auth\", \"task_name\": \"debug-auth\"}\n```\n\nDone.",
|
||||
expectedDisplayName: "Debug auth",
|
||||
expectedNamePrefix: "debug-auth-",
|
||||
},
|
||||
{
|
||||
name: "BareJSONWithTrailingFence",
|
||||
responseText: "{\"display_name\": \"Setup CI\", \"task_name\": \"setup-ci\"}\n```",
|
||||
expectedDisplayName: "Setup CI",
|
||||
expectedNamePrefix: "setup-ci-",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
Reference in New Issue
Block a user