From 1bd8bb76cbeddb698a3ad46496ca468117b29988 Mon Sep 17 00:00:00 2001 From: Romil Bijarnia Date: Wed, 11 Nov 2026 19:16:09 +1100 Subject: [PATCH] fix(client): bound delimiter scans while parsing model references --- .../src/composerModelMentions.test.ts | 14 +++++++++---- .../src/composerModelMentions.ts | 21 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/client-runtime/src/composerModelMentions.test.ts b/packages/client-runtime/src/composerModelMentions.test.ts index b6a51a71301fe..dfd0f6ac9acb5 100644 --- a/packages/client-runtime/src/composerModelMentions.test.ts +++ b/packages/client-runtime/src/composerModelMentions.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; import { collectComposerModelMentions, @@ -85,10 +85,16 @@ describe("collectComposerModelMentions", () => { ["model:codex/gpt-6.1,model:claudeAgent/sonnet", ["codex/gpt-6.1", "claudeAgent/sonnet"]], ["model:codex/gpt-6.1's output", ["codex/gpt-6.1"]], ["model:opencode/foo(bar)", ["opencode/foo(bar)"]], + ["(model:codex/GPT-6.1-Sol[1m]).", ["codex/GPT-6.1-Sol[1m]"]], + ["model:codex/gpt-6.1" + ")".repeat(128), ["codex/gpt-6.1"]], ])("bounds the reference in %s", (text, expected) => { - expect( - collectComposerModelMentions(text).map(({ instanceId, model }) => `${instanceId}/${model}`), - ).toEqual(expected); + const { mentions, rescans } = (() => { + using split = vi.spyOn(String.prototype, "split"); + const mentions = collectComposerModelMentions(text); + return { mentions, rescans: split.mock.calls.length }; + })(); + expect(mentions.map(({ instanceId, model }) => `${instanceId}/${model}`)).toEqual(expected); + expect(rescans).toBeLessThanOrEqual(expected.length * 4); }); it.each(["remodel:codex/gpt", "model:codex", "model:/gpt", "model:codex/.", "model:1x/gpt"])( diff --git a/packages/client-runtime/src/composerModelMentions.ts b/packages/client-runtime/src/composerModelMentions.ts index 29af1b8deb73b..140786d778ee2 100644 --- a/packages/client-runtime/src/composerModelMentions.ts +++ b/packages/client-runtime/src/composerModelMentions.ts @@ -68,15 +68,22 @@ function countOf(text: string, char: string): number { // Slugs like `foo(bar)` or `GPT-6.1-Sol[1m]` keep balanced brackets; prose wrapping them does not. function trimModelSlug(slug: string): string { - let trimmed = slug; - while (trimmed.length > 0) { - const last = trimmed.at(-1)!; + let end = slug.length; + const unmatched = new Map(); + while (end > 0) { + const last = slug[end - 1]!; + if (SENTENCE_PUNCTUATION.includes(last)) { + end -= 1; + continue; + } const opening = CLOSING_BRACKETS[last]; - const unbalanced = opening !== undefined && countOf(trimmed, opening) < countOf(trimmed, last); - if (!SENTENCE_PUNCTUATION.includes(last) && !unbalanced) break; - trimmed = trimmed.slice(0, -1); + if (opening === undefined) break; + const excess = unmatched.get(last) ?? countOf(slug, last) - countOf(slug, opening); + if (excess <= 0) break; + unmatched.set(last, excess - 1); + end -= 1; } - return trimmed; + return slug.slice(0, end); } /** Finds the `model:/` references that `formatComposerModelMention` writes. */