Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/types/src/providers/roo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ export const RooModelSchema = z.object({
default_temperature: z.number().optional(),
// Dynamic settings that map directly to ModelInfo properties
// Allows the API to configure model-specific defaults like includedTools, excludedTools, reasoningEffort, etc.
// These are always direct values (e.g., includedTools: ['search_replace']) for backward compatibility with old clients.
settings: z.record(z.string(), z.unknown()).optional(),
// Versioned settings keyed by version number (e.g., '3.36.4').
// Each version key maps to a settings object that is used when plugin version >= that version.
// New clients find the highest version key <= current version and use those settings.
// Old clients ignore this field and use plain values from `settings`.
// Example: { '3.36.4': { includedTools: ['search_replace'] }, '3.35.0': { ... } }
versionedSettings: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
})

export const RooModelsResponseSchema = z.object({
Expand Down
18 changes: 17 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

175 changes: 175 additions & 0 deletions src/api/providers/fetchers/__tests__/roo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,4 +801,179 @@ describe("getRooModels", () => {
expect(model.anotherSetting).toBe(42)
expect(model.nestedConfig).toEqual({ key: "value" })
})

it("should apply versioned settings when version matches", async () => {
const mockResponse = {
object: "list",
data: [
{
id: "test/versioned-model",
object: "model",
created: 1234567890,
owned_by: "test",
name: "Model with Versioned Settings",
description: "Model with versioned settings",
context_window: 128000,
max_tokens: 8192,
type: "language",
tags: ["tool-use"],
pricing: {
input: "0.0001",
output: "0.0002",
},
// Plain settings for backward compatibility with old clients
settings: {
includedTools: ["apply_patch"],
excludedTools: ["write_to_file"],
},
// Versioned settings keyed by version number (low version - always met)
versionedSettings: {
"1.0.0": {
includedTools: ["apply_patch", "search_replace"],
excludedTools: ["apply_diff", "write_to_file"],
},
},
},
],
}

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})

const models = await getRooModels(baseUrl, apiKey)

// Versioned settings should be used instead of plain settings
expect(models["test/versioned-model"].includedTools).toEqual(["apply_patch", "search_replace"])
expect(models["test/versioned-model"].excludedTools).toEqual(["apply_diff", "write_to_file"])
})

it("should use plain settings when no versioned settings version matches", async () => {
const mockResponse = {
object: "list",
data: [
{
id: "test/old-version-model",
object: "model",
created: 1234567890,
owned_by: "test",
name: "Model for Old Version",
description: "Model with versioned settings for newer version",
context_window: 128000,
max_tokens: 8192,
type: "language",
tags: ["tool-use"],
pricing: {
input: "0.0001",
output: "0.0002",
},
settings: {
includedTools: ["apply_patch"],
},
// Versioned settings keyed by very high version - never met
versionedSettings: {
"99.0.0": {
includedTools: ["apply_patch", "search_replace"],
},
},
},
],
}

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})

const models = await getRooModels(baseUrl, apiKey)

// Should use plain settings since no versioned settings match current version
expect(models["test/old-version-model"].includedTools).toEqual(["apply_patch"])
})

it("should handle model with only versionedSettings and no plain settings", async () => {
const mockResponse = {
object: "list",
data: [
{
id: "test/versioned-only-model",
object: "model",
created: 1234567890,
owned_by: "test",
name: "Model with Only Versioned Settings",
description: "Model with only versioned settings",
context_window: 128000,
max_tokens: 8192,
type: "language",
tags: [],
pricing: {
input: "0.0001",
output: "0.0002",
},
// No plain settings, only versionedSettings keyed by version
versionedSettings: {
"1.0.0": {
customFeature: true,
},
},
},
],
}

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})

const models = await getRooModels(baseUrl, apiKey)
const model = models["test/versioned-only-model"] as Record<string, unknown>

expect(model.customFeature).toBe(true)
})

it("should select highest matching version from versionedSettings", async () => {
const mockResponse = {
object: "list",
data: [
{
id: "test/multi-version-model",
object: "model",
created: 1234567890,
owned_by: "test",
name: "Model with Multiple Versions",
description: "Model with multiple version settings",
context_window: 128000,
max_tokens: 8192,
type: "language",
tags: [],
pricing: {
input: "0.0001",
output: "0.0002",
},
settings: {
feature: "default",
},
// Multiple version keys - should use highest one <= current version
versionedSettings: {
"99.0.0": { feature: "future" },
"3.0.0": { feature: "current" },
"2.0.0": { feature: "old" },
"1.0.0": { feature: "very_old" },
},
},
],
}

mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})

const models = await getRooModels(baseUrl, apiKey)
const model = models["test/multi-version-model"] as Record<string, unknown>

// Should use 3.0.0 version settings (highest that's <= current plugin version)
expect(model.feature).toBe("current")
})
})
Loading
Loading