Skip to content

Commit ceb0be7

Browse files
committed
fix(roo): use separate versionedSettings field for backward compatibility
Address review feedback from @mrubens: Old clients expect plain values in the settings field (e.g., includedTools as array of strings). Changes: - Add new versionedSettings field to RooModelSchema for version-gated values - Keep settings field for plain values (backward compatible with old clients) - New clients read from both fields: settings first, then overlay versionedSettings - Update documentation to clarify the two-field approach - Add tests for versionedSettings overlay behavior
1 parent 43ed3ba commit ceb0be7

File tree

4 files changed

+179
-12
lines changed

4 files changed

+179
-12
lines changed

packages/types/src/providers/roo.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ export const RooModelSchema = z.object({
4141
default_temperature: z.number().optional(),
4242
// Dynamic settings that map directly to ModelInfo properties
4343
// Allows the API to configure model-specific defaults like includedTools, excludedTools, reasoningEffort, etc.
44+
// These are always direct values (e.g., includedTools: ['search_replace']) for backward compatibility with old clients.
4445
settings: z.record(z.string(), z.unknown()).optional(),
46+
// Versioned settings that are gated behind minimum plugin versions.
47+
// Each value is an object with { value: T, minPluginVersion: string }.
48+
// New clients check this field first and resolve based on current plugin version.
49+
// Old clients ignore this field and use plain values from `settings`.
50+
versionedSettings: z.record(z.string(), z.unknown()).optional(),
4551
})
4652

4753
export const RooModelsResponseSchema = z.object({

src/api/providers/fetchers/__tests__/roo.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,4 +801,140 @@ describe("getRooModels", () => {
801801
expect(model.anotherSetting).toBe(42)
802802
expect(model.nestedConfig).toEqual({ key: "value" })
803803
})
804+
805+
it("should apply versioned settings on top of plain settings", async () => {
806+
const mockResponse = {
807+
object: "list",
808+
data: [
809+
{
810+
id: "test/versioned-model",
811+
object: "model",
812+
created: 1234567890,
813+
owned_by: "test",
814+
name: "Model with Versioned Settings",
815+
description: "Model with versioned settings",
816+
context_window: 128000,
817+
max_tokens: 8192,
818+
type: "language",
819+
tags: ["tool-use"],
820+
pricing: {
821+
input: "0.0001",
822+
output: "0.0002",
823+
},
824+
// Plain settings for backward compatibility with old clients
825+
settings: {
826+
includedTools: ["apply_patch"],
827+
excludedTools: ["write_to_file"],
828+
},
829+
// Versioned settings for new clients (low version requirement, always met)
830+
versionedSettings: {
831+
includedTools: {
832+
value: ["apply_patch", "search_replace"],
833+
minPluginVersion: "1.0.0", // Very low version - always met
834+
},
835+
excludedTools: {
836+
value: ["apply_diff", "write_to_file"],
837+
minPluginVersion: "1.0.0", // Very low version - always met
838+
},
839+
},
840+
},
841+
],
842+
}
843+
844+
mockFetch.mockResolvedValueOnce({
845+
ok: true,
846+
json: async () => mockResponse,
847+
})
848+
849+
const models = await getRooModels(baseUrl, apiKey)
850+
851+
// Versioned settings should override plain settings
852+
expect(models["test/versioned-model"].includedTools).toEqual(["apply_patch", "search_replace"])
853+
expect(models["test/versioned-model"].excludedTools).toEqual(["apply_diff", "write_to_file"])
854+
})
855+
856+
it("should use plain settings when versioned settings version requirement is not met", async () => {
857+
const mockResponse = {
858+
object: "list",
859+
data: [
860+
{
861+
id: "test/old-version-model",
862+
object: "model",
863+
created: 1234567890,
864+
owned_by: "test",
865+
name: "Model for Old Version",
866+
description: "Model with versioned settings for newer version",
867+
context_window: 128000,
868+
max_tokens: 8192,
869+
type: "language",
870+
tags: ["tool-use"],
871+
pricing: {
872+
input: "0.0001",
873+
output: "0.0002",
874+
},
875+
settings: {
876+
includedTools: ["apply_patch"],
877+
},
878+
versionedSettings: {
879+
// Very high version requirement - never met
880+
includedTools: {
881+
value: ["apply_patch", "search_replace"],
882+
minPluginVersion: "99.0.0",
883+
},
884+
},
885+
},
886+
],
887+
}
888+
889+
mockFetch.mockResolvedValueOnce({
890+
ok: true,
891+
json: async () => mockResponse,
892+
})
893+
894+
const models = await getRooModels(baseUrl, apiKey)
895+
896+
// Should use plain settings since versioned requirement is not met
897+
expect(models["test/old-version-model"].includedTools).toEqual(["apply_patch"])
898+
})
899+
900+
it("should handle model with only versionedSettings and no plain settings", async () => {
901+
const mockResponse = {
902+
object: "list",
903+
data: [
904+
{
905+
id: "test/versioned-only-model",
906+
object: "model",
907+
created: 1234567890,
908+
owned_by: "test",
909+
name: "Model with Only Versioned Settings",
910+
description: "Model with only versioned settings",
911+
context_window: 128000,
912+
max_tokens: 8192,
913+
type: "language",
914+
tags: [],
915+
pricing: {
916+
input: "0.0001",
917+
output: "0.0002",
918+
},
919+
// No plain settings, only versionedSettings
920+
versionedSettings: {
921+
customFeature: {
922+
value: true,
923+
minPluginVersion: "1.0.0", // Low version, should always be met
924+
},
925+
},
926+
},
927+
],
928+
}
929+
930+
mockFetch.mockResolvedValueOnce({
931+
ok: true,
932+
json: async () => mockResponse,
933+
})
934+
935+
const models = await getRooModels(baseUrl, apiKey)
936+
const model = models["test/versioned-only-model"] as any
937+
938+
expect(model.customFeature).toBe(true)
939+
})
804940
})

src/api/providers/fetchers/roo.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,33 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
129129
// Apply API-provided settings on top of base model info
130130
// Settings allow the proxy to dynamically configure model-specific options
131131
// like includedTools, excludedTools, reasoningEffort, etc.
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' } }
132+
//
133+
// Two fields are used for backward compatibility:
134+
// - `settings`: Plain values that work with all client versions (e.g., { includedTools: ['search_replace'] })
135+
// - `versionedSettings`: Values gated by minPluginVersion (e.g., { includedTools: { value: ['search_replace'], minPluginVersion: '3.36.4' } })
136+
//
137+
// New clients process both fields - settings first, then overlay resolved versionedSettings.
138+
// Old clients only see `settings` and ignore `versionedSettings`.
135139
const apiSettings = model.settings as Record<string, unknown> | undefined
140+
const apiVersionedSettings = model.versionedSettings as Record<string, unknown> | undefined
136141

142+
// Start with base model info
143+
let modelInfo: ModelInfo = { ...baseModelInfo }
144+
145+
// Apply plain settings (backward compatible with old clients)
137146
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
147+
modelInfo = { ...modelInfo, ...(apiSettings as Partial<ModelInfo>) }
148+
}
149+
150+
// Apply versioned settings (new clients only - resolved based on plugin version)
151+
if (apiVersionedSettings) {
152+
const resolvedVersionedSettings = resolveVersionedSettings(
153+
apiVersionedSettings,
154+
) as Partial<ModelInfo>
155+
modelInfo = { ...modelInfo, ...resolvedVersionedSettings }
143156
}
157+
158+
models[modelId] = modelInfo
144159
}
145160

146161
return models

src/api/providers/fetchers/versionedSettings.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@ import { Package } from "../../../shared/package"
66
* Schema for a versioned setting value.
77
* Allows settings to be gated behind a minimum plugin version.
88
*
9-
* Example:
9+
* These values should be placed in the `versionedSettings` field of the API response,
10+
* separate from the plain `settings` field. This ensures backward compatibility:
11+
* - Old clients read from `settings` (plain values only)
12+
* - New clients read from both `settings` and `versionedSettings`
13+
*
14+
* Example API response:
1015
* ```
1116
* {
12-
* includedTools: {
13-
* value: ['search_replace'],
14-
* minPluginVersion: '3.36.4',
17+
* settings: {
18+
* includedTools: ['search_replace'] // Plain value for old clients
19+
* },
20+
* versionedSettings: {
21+
* includedTools: {
22+
* value: ['search_replace', 'apply_diff'], // Enhanced value for new clients
23+
* minPluginVersion: '3.36.4',
24+
* }
1525
* }
1626
* }
1727
* ```

0 commit comments

Comments
 (0)