Skip to content

Commit 43ed3ba

Browse files
committed
feat(roo): add versioned settings support with minPluginVersion gating
Add ability to gate model settings behind minimum plugin versions. Settings from the Roo API can now be either direct values or wrapped with { value: T, minPluginVersion: string } to conditionally apply based on the extension version. - Add versionedSettings.ts with semver comparison utilities - Add resolveVersionedSettings() to process versioned values - Update getRooModels() to resolve versioned settings - Add comprehensive tests for all utility functions
1 parent 5bde2e5 commit 43ed3ba

File tree

3 files changed

+383
-3
lines changed

3 files changed

+383
-3
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import {
2+
compareSemver,
3+
meetsMinimumVersion,
4+
resolveVersionedSettings,
5+
isVersionedValue,
6+
type VersionedValue,
7+
} from "../versionedSettings"
8+
9+
describe("versionedSettings", () => {
10+
describe("isVersionedValue", () => {
11+
it("should return true for valid versioned value objects", () => {
12+
const versionedValue: VersionedValue<string[]> = {
13+
value: ["search_replace"],
14+
minPluginVersion: "3.36.4",
15+
}
16+
expect(isVersionedValue(versionedValue)).toBe(true)
17+
})
18+
19+
it("should return true for versioned value with any value type", () => {
20+
expect(isVersionedValue({ value: true, minPluginVersion: "1.0.0" })).toBe(true)
21+
expect(isVersionedValue({ value: 42, minPluginVersion: "1.0.0" })).toBe(true)
22+
expect(isVersionedValue({ value: "string", minPluginVersion: "1.0.0" })).toBe(true)
23+
expect(isVersionedValue({ value: null, minPluginVersion: "1.0.0" })).toBe(true)
24+
expect(isVersionedValue({ value: { nested: "object" }, minPluginVersion: "1.0.0" })).toBe(true)
25+
})
26+
27+
it("should return false for non-versioned values", () => {
28+
expect(isVersionedValue(null)).toBe(false)
29+
expect(isVersionedValue(undefined)).toBe(false)
30+
expect(isVersionedValue("string")).toBe(false)
31+
expect(isVersionedValue(123)).toBe(false)
32+
expect(isVersionedValue(["array"])).toBe(false)
33+
expect(isVersionedValue({ value: "only value" })).toBe(false)
34+
expect(isVersionedValue({ minPluginVersion: "1.0.0" })).toBe(false)
35+
expect(isVersionedValue({ value: "test", minPluginVersion: 123 })).toBe(false) // version must be string
36+
})
37+
})
38+
39+
describe("compareSemver", () => {
40+
it("should return 0 for equal versions", () => {
41+
expect(compareSemver("1.0.0", "1.0.0")).toBe(0)
42+
expect(compareSemver("3.36.4", "3.36.4")).toBe(0)
43+
expect(compareSemver("0.0.1", "0.0.1")).toBe(0)
44+
})
45+
46+
it("should return positive when first version is greater", () => {
47+
expect(compareSemver("2.0.0", "1.0.0")).toBeGreaterThan(0)
48+
expect(compareSemver("1.1.0", "1.0.0")).toBeGreaterThan(0)
49+
expect(compareSemver("1.0.1", "1.0.0")).toBeGreaterThan(0)
50+
expect(compareSemver("3.36.5", "3.36.4")).toBeGreaterThan(0)
51+
expect(compareSemver("3.37.0", "3.36.4")).toBeGreaterThan(0)
52+
expect(compareSemver("4.0.0", "3.36.4")).toBeGreaterThan(0)
53+
})
54+
55+
it("should return negative when first version is smaller", () => {
56+
expect(compareSemver("1.0.0", "2.0.0")).toBeLessThan(0)
57+
expect(compareSemver("1.0.0", "1.1.0")).toBeLessThan(0)
58+
expect(compareSemver("1.0.0", "1.0.1")).toBeLessThan(0)
59+
expect(compareSemver("3.36.3", "3.36.4")).toBeLessThan(0)
60+
expect(compareSemver("3.35.0", "3.36.4")).toBeLessThan(0)
61+
expect(compareSemver("2.0.0", "3.36.4")).toBeLessThan(0)
62+
})
63+
64+
it("should handle versions with different segment counts", () => {
65+
expect(compareSemver("1.0", "1.0.0")).toBe(0)
66+
expect(compareSemver("1", "1.0.0")).toBe(0)
67+
expect(compareSemver("1.0.0.0", "1.0.0")).toBe(0)
68+
expect(compareSemver("1.0.1", "1.0")).toBeGreaterThan(0)
69+
expect(compareSemver("1.0", "1.0.1")).toBeLessThan(0)
70+
})
71+
72+
it("should handle pre-release versions by ignoring pre-release suffix", () => {
73+
expect(compareSemver("3.36.4-beta.1", "3.36.4")).toBe(0)
74+
expect(compareSemver("3.36.4-rc.2", "3.36.4")).toBe(0)
75+
expect(compareSemver("3.36.5-alpha", "3.36.4")).toBeGreaterThan(0)
76+
expect(compareSemver("3.36.3-beta", "3.36.4")).toBeLessThan(0)
77+
})
78+
79+
it("should handle edge cases", () => {
80+
expect(compareSemver("0.0.0", "0.0.0")).toBe(0)
81+
expect(compareSemver("10.20.30", "10.20.30")).toBe(0)
82+
expect(compareSemver("10.0.0", "9.99.99")).toBeGreaterThan(0)
83+
})
84+
})
85+
86+
describe("meetsMinimumVersion", () => {
87+
it("should return true when current version equals minimum", () => {
88+
expect(meetsMinimumVersion("3.36.4", "3.36.4")).toBe(true)
89+
})
90+
91+
it("should return true when current version exceeds minimum", () => {
92+
expect(meetsMinimumVersion("3.36.4", "3.36.5")).toBe(true)
93+
expect(meetsMinimumVersion("3.36.4", "3.37.0")).toBe(true)
94+
expect(meetsMinimumVersion("3.36.4", "4.0.0")).toBe(true)
95+
})
96+
97+
it("should return false when current version is below minimum", () => {
98+
expect(meetsMinimumVersion("3.36.4", "3.36.3")).toBe(false)
99+
expect(meetsMinimumVersion("3.36.4", "3.35.0")).toBe(false)
100+
expect(meetsMinimumVersion("3.36.4", "2.0.0")).toBe(false)
101+
})
102+
})
103+
104+
describe("resolveVersionedSettings", () => {
105+
const currentVersion = "3.36.4"
106+
107+
it("should pass through non-versioned settings unchanged", () => {
108+
const settings = {
109+
includedTools: ["search_replace"],
110+
excludedTools: ["apply_diff"],
111+
supportsReasoningEffort: false,
112+
}
113+
114+
const resolved = resolveVersionedSettings(settings, currentVersion)
115+
116+
expect(resolved).toEqual(settings)
117+
})
118+
119+
it("should include versioned settings when version requirement is met", () => {
120+
const settings = {
121+
includedTools: {
122+
value: ["search_replace"],
123+
minPluginVersion: "3.36.4",
124+
},
125+
excludedTools: {
126+
value: ["apply_diff"],
127+
minPluginVersion: "3.36.0",
128+
},
129+
}
130+
131+
const resolved = resolveVersionedSettings(settings, currentVersion)
132+
133+
expect(resolved).toEqual({
134+
includedTools: ["search_replace"],
135+
excludedTools: ["apply_diff"],
136+
})
137+
})
138+
139+
it("should exclude versioned settings when version requirement is not met", () => {
140+
const settings = {
141+
includedTools: {
142+
value: ["search_replace"],
143+
minPluginVersion: "3.36.5", // Higher than current
144+
},
145+
excludedTools: {
146+
value: ["apply_diff"],
147+
minPluginVersion: "4.0.0", // Higher than current
148+
},
149+
}
150+
151+
const resolved = resolveVersionedSettings(settings, currentVersion)
152+
153+
expect(resolved).toEqual({})
154+
})
155+
156+
it("should handle mixed versioned and non-versioned settings", () => {
157+
const settings = {
158+
supportsReasoningEffort: false, // Non-versioned, should be included
159+
includedTools: {
160+
value: ["search_replace"],
161+
minPluginVersion: "3.36.4", // Met, should be included
162+
},
163+
excludedTools: {
164+
value: ["apply_diff"],
165+
minPluginVersion: "4.0.0", // Not met, should be excluded
166+
},
167+
description: "A test model", // Non-versioned, should be included
168+
}
169+
170+
const resolved = resolveVersionedSettings(settings, currentVersion)
171+
172+
expect(resolved).toEqual({
173+
supportsReasoningEffort: false,
174+
includedTools: ["search_replace"],
175+
description: "A test model",
176+
})
177+
})
178+
179+
it("should handle empty settings object", () => {
180+
const resolved = resolveVersionedSettings({}, currentVersion)
181+
expect(resolved).toEqual({})
182+
})
183+
184+
it("should handle versioned boolean values", () => {
185+
const settings = {
186+
supportsNativeTools: {
187+
value: true,
188+
minPluginVersion: "3.36.0",
189+
},
190+
}
191+
192+
const resolved = resolveVersionedSettings(settings, currentVersion)
193+
194+
expect(resolved).toEqual({
195+
supportsNativeTools: true,
196+
})
197+
})
198+
199+
it("should handle versioned null values", () => {
200+
const settings = {
201+
defaultTemperature: {
202+
value: null,
203+
minPluginVersion: "3.36.0",
204+
},
205+
}
206+
207+
const resolved = resolveVersionedSettings(settings, currentVersion)
208+
209+
expect(resolved).toEqual({
210+
defaultTemperature: null,
211+
})
212+
})
213+
214+
it("should handle versioned nested objects", () => {
215+
const settings = {
216+
complexSetting: {
217+
value: { nested: { deeply: true } },
218+
minPluginVersion: "3.36.0",
219+
},
220+
}
221+
222+
const resolved = resolveVersionedSettings(settings, currentVersion)
223+
224+
expect(resolved).toEqual({
225+
complexSetting: { nested: { deeply: true } },
226+
})
227+
})
228+
229+
it("should correctly resolve settings with exact version match", () => {
230+
const settings = {
231+
feature: {
232+
value: "enabled",
233+
minPluginVersion: "3.36.4", // Exact match
234+
},
235+
}
236+
237+
const resolved = resolveVersionedSettings(settings, "3.36.4")
238+
239+
expect(resolved).toEqual({
240+
feature: "enabled",
241+
})
242+
})
243+
})
244+
})

src/api/providers/fetchers/roo.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ModelRecord } from "../../../shared/api"
44
import { parseApiPrice } from "../../../shared/cost"
55

66
import { DEFAULT_HEADERS } from "../constants"
7+
import { resolveVersionedSettings } from "./versionedSettings"
78

89
/**
910
* Fetches available models from the Roo Code Cloud provider
@@ -128,9 +129,18 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
128129
// Apply API-provided settings on top of base model info
129130
// Settings allow the proxy to dynamically configure model-specific options
130131
// like includedTools, excludedTools, reasoningEffort, etc.
131-
const apiSettings = model.settings as Partial<ModelInfo> | undefined
132-
133-
models[modelId] = apiSettings ? { ...baseModelInfo, ...apiSettings } : baseModelInfo
132+
// Settings can be versioned with minPluginVersion to gate features by plugin version:
133+
// - Direct values: { includedTools: ['search_replace'] }
134+
// - Versioned values: { includedTools: { value: ['search_replace'], minPluginVersion: '3.36.4' } }
135+
const apiSettings = model.settings as Record<string, unknown> | undefined
136+
137+
if (apiSettings) {
138+
// Resolve versioned settings based on current plugin version
139+
const resolvedSettings = resolveVersionedSettings(apiSettings) as Partial<ModelInfo>
140+
models[modelId] = { ...baseModelInfo, ...resolvedSettings }
141+
} else {
142+
models[modelId] = baseModelInfo
143+
}
134144
}
135145

136146
return models

0 commit comments

Comments
 (0)