mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(roo): add versioned settings support with minPluginVersion gating (#9934)
This commit is contained in:
parent
f89a6bef30
commit
83787a76ef
7 changed files with 608 additions and 3 deletions
|
|
@ -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({
|
||||
|
|
|
|||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -795,6 +795,9 @@ importers:
|
|||
say:
|
||||
specifier: ^0.16.0
|
||||
version: 0.16.0
|
||||
semver-compare:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
serialize-error:
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.0
|
||||
|
|
@ -898,6 +901,9 @@ importers:
|
|||
'@types/ps-tree':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6
|
||||
'@types/semver-compare':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@types/shell-quote':
|
||||
specifier: ^1.7.5
|
||||
version: 1.7.5
|
||||
|
|
@ -4160,6 +4166,9 @@ packages:
|
|||
'@types/retry@0.12.5':
|
||||
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
|
||||
|
||||
'@types/semver-compare@1.0.3':
|
||||
resolution: {integrity: sha512-mVZkB2QjXmZhh+MrtwMlJ8BqUnmbiSkpd88uOWskfwB8yitBT0tBRAKt+41VRgZD9zr9Sc+Xs02qGgvzd1Rq/Q==}
|
||||
|
||||
'@types/shell-quote@1.7.5':
|
||||
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
|
||||
|
||||
|
|
@ -8909,6 +8918,9 @@ packages:
|
|||
seed-random@2.2.0:
|
||||
resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==}
|
||||
|
||||
semver-compare@1.0.0:
|
||||
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
||||
|
||||
semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
|
|
@ -13898,6 +13910,8 @@ snapshots:
|
|||
|
||||
'@types/retry@0.12.5': {}
|
||||
|
||||
'@types/semver-compare@1.0.3': {}
|
||||
|
||||
'@types/shell-quote@1.7.5': {}
|
||||
|
||||
'@types/stack-utils@2.0.3': {}
|
||||
|
|
@ -14117,7 +14131,7 @@ snapshots:
|
|||
sirv: 3.0.1
|
||||
tinyglobby: 0.2.14
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
|
|
@ -19479,6 +19493,8 @@ snapshots:
|
|||
|
||||
seed-random@2.2.0: {}
|
||||
|
||||
semver-compare@1.0.0: {}
|
||||
|
||||
semver@5.7.2: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
263
src/api/providers/fetchers/__tests__/versionedSettings.spec.ts
Normal file
263
src/api/providers/fetchers/__tests__/versionedSettings.spec.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import {
|
||||
compareSemver,
|
||||
meetsMinimumVersion,
|
||||
findHighestMatchingVersion,
|
||||
resolveVersionedSettings,
|
||||
type VersionedSettings,
|
||||
} from "../versionedSettings"
|
||||
|
||||
describe("versionedSettings", () => {
|
||||
describe("compareSemver", () => {
|
||||
it("should return 0 for equal versions", () => {
|
||||
expect(compareSemver("1.0.0", "1.0.0")).toBe(0)
|
||||
expect(compareSemver("3.36.4", "3.36.4")).toBe(0)
|
||||
expect(compareSemver("0.0.1", "0.0.1")).toBe(0)
|
||||
})
|
||||
|
||||
it("should return positive when first version is greater", () => {
|
||||
expect(compareSemver("2.0.0", "1.0.0")).toBeGreaterThan(0)
|
||||
expect(compareSemver("1.1.0", "1.0.0")).toBeGreaterThan(0)
|
||||
expect(compareSemver("1.0.1", "1.0.0")).toBeGreaterThan(0)
|
||||
expect(compareSemver("3.36.5", "3.36.4")).toBeGreaterThan(0)
|
||||
expect(compareSemver("3.37.0", "3.36.4")).toBeGreaterThan(0)
|
||||
expect(compareSemver("4.0.0", "3.36.4")).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should return negative when first version is smaller", () => {
|
||||
expect(compareSemver("1.0.0", "2.0.0")).toBeLessThan(0)
|
||||
expect(compareSemver("1.0.0", "1.1.0")).toBeLessThan(0)
|
||||
expect(compareSemver("1.0.0", "1.0.1")).toBeLessThan(0)
|
||||
expect(compareSemver("3.36.3", "3.36.4")).toBeLessThan(0)
|
||||
expect(compareSemver("3.35.0", "3.36.4")).toBeLessThan(0)
|
||||
expect(compareSemver("2.0.0", "3.36.4")).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it("should handle pre-release versions by ignoring pre-release suffix", () => {
|
||||
expect(compareSemver("3.36.4-beta.1", "3.36.4")).toBe(0)
|
||||
expect(compareSemver("3.36.4-rc.2", "3.36.4")).toBe(0)
|
||||
expect(compareSemver("3.36.5-alpha", "3.36.4")).toBeGreaterThan(0)
|
||||
expect(compareSemver("3.36.3-beta", "3.36.4")).toBeLessThan(0)
|
||||
})
|
||||
|
||||
it("should handle edge cases", () => {
|
||||
expect(compareSemver("0.0.0", "0.0.0")).toBe(0)
|
||||
expect(compareSemver("10.20.30", "10.20.30")).toBe(0)
|
||||
expect(compareSemver("10.0.0", "9.99.99")).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("meetsMinimumVersion", () => {
|
||||
it("should return true when current version equals minimum", () => {
|
||||
expect(meetsMinimumVersion("3.36.4", "3.36.4")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true when current version exceeds minimum", () => {
|
||||
expect(meetsMinimumVersion("3.36.4", "3.36.5")).toBe(true)
|
||||
expect(meetsMinimumVersion("3.36.4", "3.37.0")).toBe(true)
|
||||
expect(meetsMinimumVersion("3.36.4", "4.0.0")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when current version is below minimum", () => {
|
||||
expect(meetsMinimumVersion("3.36.4", "3.36.3")).toBe(false)
|
||||
expect(meetsMinimumVersion("3.36.4", "3.35.0")).toBe(false)
|
||||
expect(meetsMinimumVersion("3.36.4", "2.0.0")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("findHighestMatchingVersion", () => {
|
||||
it("should return undefined when no versions match", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"4.0.0": { includedTools: ["apply_diff"] },
|
||||
"5.0.0": { includedTools: ["apply_diff", "search_replace"] },
|
||||
}
|
||||
|
||||
const result = findHighestMatchingVersion(versionedSettings, "3.36.4")
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return the exact version when it matches", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.36.4": { includedTools: ["apply_diff"] },
|
||||
"3.35.0": { includedTools: ["search_replace"] },
|
||||
}
|
||||
|
||||
const result = findHighestMatchingVersion(versionedSettings, "3.36.4")
|
||||
expect(result).toBe("3.36.4")
|
||||
})
|
||||
|
||||
it("should return the highest version that is <= current version", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.37.0": { includedTools: ["future_tool"] },
|
||||
"3.36.4": { includedTools: ["apply_diff"] },
|
||||
"3.35.0": { includedTools: ["search_replace"] },
|
||||
"3.34.0": { includedTools: ["basic_tool"] },
|
||||
}
|
||||
|
||||
// Current version is 3.36.5, should match 3.36.4 (highest <= 3.36.5)
|
||||
const result = findHighestMatchingVersion(versionedSettings, "3.36.5")
|
||||
expect(result).toBe("3.36.4")
|
||||
})
|
||||
|
||||
it("should handle single version", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.35.0": { includedTools: ["search_replace"] },
|
||||
}
|
||||
|
||||
expect(findHighestMatchingVersion(versionedSettings, "3.36.4")).toBe("3.35.0")
|
||||
expect(findHighestMatchingVersion(versionedSettings, "3.34.0")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle empty versionedSettings", () => {
|
||||
const versionedSettings: VersionedSettings = {}
|
||||
|
||||
const result = findHighestMatchingVersion(versionedSettings, "3.36.4")
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveVersionedSettings", () => {
|
||||
const currentVersion = "3.36.4"
|
||||
|
||||
it("should return settings for exact version match", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.36.4": {
|
||||
includedTools: ["search_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, currentVersion)
|
||||
|
||||
expect(resolved).toEqual({
|
||||
includedTools: ["search_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should return settings for highest matching version", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"4.0.0": {
|
||||
includedTools: ["future_tool"],
|
||||
},
|
||||
"3.36.0": {
|
||||
includedTools: ["search_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
},
|
||||
"3.35.0": {
|
||||
includedTools: ["old_tool"],
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, currentVersion)
|
||||
|
||||
expect(resolved).toEqual({
|
||||
includedTools: ["search_replace"],
|
||||
excludedTools: ["apply_diff"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should return empty object when no versions match", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"4.0.0": {
|
||||
includedTools: ["future_tool"],
|
||||
},
|
||||
"3.37.0": {
|
||||
includedTools: ["newer_tool"],
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, currentVersion)
|
||||
|
||||
expect(resolved).toEqual({})
|
||||
})
|
||||
|
||||
it("should handle empty versionedSettings", () => {
|
||||
const resolved = resolveVersionedSettings({}, currentVersion)
|
||||
expect(resolved).toEqual({})
|
||||
})
|
||||
|
||||
it("should handle versioned boolean values", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.36.0": {
|
||||
supportsNativeTools: true,
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, currentVersion)
|
||||
|
||||
expect(resolved).toEqual({
|
||||
supportsNativeTools: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle versioned null values", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.36.0": {
|
||||
defaultTemperature: null,
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, currentVersion)
|
||||
|
||||
expect(resolved).toEqual({
|
||||
defaultTemperature: null,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle versioned nested objects", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.36.0": {
|
||||
complexSetting: { nested: { deeply: true } },
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, currentVersion)
|
||||
|
||||
expect(resolved).toEqual({
|
||||
complexSetting: { nested: { deeply: true } },
|
||||
})
|
||||
})
|
||||
|
||||
it("should use all settings from the matching version", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.36.4": {
|
||||
includedTools: ["search_replace", "apply_diff"],
|
||||
excludedTools: ["write_to_file"],
|
||||
supportsReasoningEffort: true,
|
||||
description: "Updated model",
|
||||
},
|
||||
"3.35.0": {
|
||||
includedTools: ["search_replace"],
|
||||
description: "Old model",
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = resolveVersionedSettings(versionedSettings, "3.36.4")
|
||||
|
||||
expect(resolved).toEqual({
|
||||
includedTools: ["search_replace", "apply_diff"],
|
||||
excludedTools: ["write_to_file"],
|
||||
supportsReasoningEffort: true,
|
||||
description: "Updated model",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle multiple versions and select correct one", () => {
|
||||
const versionedSettings: VersionedSettings = {
|
||||
"3.38.0": { feature: "very_new" },
|
||||
"3.37.0": { feature: "new" },
|
||||
"3.36.0": { feature: "current" },
|
||||
"3.35.0": { feature: "old" },
|
||||
"3.34.0": { feature: "very_old" },
|
||||
}
|
||||
|
||||
// Test different current versions
|
||||
expect(resolveVersionedSettings(versionedSettings, "3.40.0")).toEqual({ feature: "very_new" })
|
||||
expect(resolveVersionedSettings(versionedSettings, "3.37.5")).toEqual({ feature: "new" })
|
||||
expect(resolveVersionedSettings(versionedSettings, "3.36.5")).toEqual({ feature: "current" })
|
||||
expect(resolveVersionedSettings(versionedSettings, "3.35.5")).toEqual({ feature: "old" })
|
||||
expect(resolveVersionedSettings(versionedSettings, "3.34.5")).toEqual({ feature: "very_old" })
|
||||
expect(resolveVersionedSettings(versionedSettings, "3.33.0")).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -4,6 +4,7 @@ import type { ModelRecord } from "../../../shared/api"
|
|||
import { parseApiPrice } from "../../../shared/cost"
|
||||
|
||||
import { DEFAULT_HEADERS } from "../constants"
|
||||
import { resolveVersionedSettings, type VersionedSettings } from "./versionedSettings"
|
||||
|
||||
/**
|
||||
* Fetches available models from the Roo Code Cloud provider
|
||||
|
|
@ -128,9 +129,37 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
// Apply API-provided settings on top of base model info
|
||||
// Settings allow the proxy to dynamically configure model-specific options
|
||||
// like includedTools, excludedTools, reasoningEffort, etc.
|
||||
const apiSettings = model.settings as Partial<ModelInfo> | undefined
|
||||
//
|
||||
// Two fields are used for backward compatibility:
|
||||
// - `settings`: Plain values that work with all client versions (e.g., { includedTools: ['search_replace'] })
|
||||
// - `versionedSettings`: Version-keyed settings (e.g., { '3.36.4': { includedTools: ['search_replace'] } })
|
||||
//
|
||||
// New clients check versionedSettings first - if a matching version is found, those settings are used.
|
||||
// Otherwise, falls back to plain `settings`. Old clients only see `settings`.
|
||||
const apiSettings = model.settings as Record<string, unknown> | undefined
|
||||
const apiVersionedSettings = model.versionedSettings as VersionedSettings | undefined
|
||||
|
||||
models[modelId] = apiSettings ? { ...baseModelInfo, ...apiSettings } : baseModelInfo
|
||||
// Start with base model info
|
||||
let modelInfo: ModelInfo = { ...baseModelInfo }
|
||||
|
||||
// Try to resolve versioned settings first (finds highest version <= current plugin version)
|
||||
// If versioned settings match, use them exclusively (they contain all necessary settings)
|
||||
// Otherwise fall back to plain settings for backward compatibility
|
||||
if (apiVersionedSettings) {
|
||||
const resolvedVersionedSettings = resolveVersionedSettings<Partial<ModelInfo>>(apiVersionedSettings)
|
||||
if (Object.keys(resolvedVersionedSettings).length > 0) {
|
||||
// Versioned settings found - use them exclusively
|
||||
modelInfo = { ...modelInfo, ...resolvedVersionedSettings }
|
||||
} else if (apiSettings) {
|
||||
// No matching versioned settings - fall back to plain settings
|
||||
modelInfo = { ...modelInfo, ...(apiSettings as Partial<ModelInfo>) }
|
||||
}
|
||||
} else if (apiSettings) {
|
||||
// No versioned settings at all - use plain settings
|
||||
modelInfo = { ...modelInfo, ...(apiSettings as Partial<ModelInfo>) }
|
||||
}
|
||||
|
||||
models[modelId] = modelInfo
|
||||
}
|
||||
|
||||
return models
|
||||
|
|
|
|||
113
src/api/providers/fetchers/versionedSettings.ts
Normal file
113
src/api/providers/fetchers/versionedSettings.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import cmp from "semver-compare"
|
||||
|
||||
import { Package } from "../../../shared/package"
|
||||
|
||||
/**
|
||||
* Type for versioned settings where the version is the key.
|
||||
* Each version key maps to a settings object that should be used
|
||||
* when the current plugin version is >= that version.
|
||||
*
|
||||
* Example API response:
|
||||
* ```
|
||||
* {
|
||||
* settings: {
|
||||
* includedTools: ['search_replace'] // Plain value for old clients
|
||||
* },
|
||||
* versionedSettings: {
|
||||
* '3.36.4': {
|
||||
* includedTools: ['search_replace', 'apply_diff'], // Enhanced value for 3.36.4+
|
||||
* excludedTools: ['write_to_file'],
|
||||
* },
|
||||
* '3.35.0': {
|
||||
* includedTools: ['search_replace'], // Value for 3.35.0 - 3.36.3
|
||||
* },
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The resolver will find the highest version key that is <= the current plugin version
|
||||
* and use those settings. If no version matches, falls back to plain `settings`.
|
||||
*/
|
||||
export type VersionedSettings = Record<string, Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings using semver-compare.
|
||||
*
|
||||
* @param version1 First version string (e.g., "3.36.4")
|
||||
* @param version2 Second version string (e.g., "3.36.0")
|
||||
* @returns -1 if version1 < version2, 0 if equal, 1 if version1 > version2
|
||||
*/
|
||||
export function compareSemver(version1: string, version2: string): number {
|
||||
// Handle pre-release versions by stripping the suffix
|
||||
// semver-compare doesn't handle pre-release properly
|
||||
const stripPrerelease = (v: string): string => v.split("-")[0]
|
||||
return cmp(stripPrerelease(version1), stripPrerelease(version2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current plugin version meets or exceeds the required minimum version.
|
||||
*
|
||||
* @param minPluginVersion The minimum required version
|
||||
* @param currentVersion The current plugin version (defaults to Package.version)
|
||||
* @returns true if current version >= minPluginVersion
|
||||
*/
|
||||
export function meetsMinimumVersion(minPluginVersion: string, currentVersion: string = Package.version): boolean {
|
||||
return compareSemver(currentVersion, minPluginVersion) >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the highest version from versionedSettings that is <= the current plugin version.
|
||||
*
|
||||
* @param versionedSettings The versioned settings object with version keys
|
||||
* @param currentVersion The current plugin version (defaults to Package.version)
|
||||
* @returns The highest matching version key, or undefined if none match
|
||||
*/
|
||||
export function findHighestMatchingVersion(
|
||||
versionedSettings: VersionedSettings,
|
||||
currentVersion: string = Package.version,
|
||||
): string | undefined {
|
||||
const versions = Object.keys(versionedSettings)
|
||||
|
||||
// Filter to versions that are <= currentVersion
|
||||
const matchingVersions = versions.filter((version) => meetsMinimumVersion(version, currentVersion))
|
||||
|
||||
if (matchingVersions.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Sort in descending order and return the highest
|
||||
matchingVersions.sort((a, b) => compareSemver(b, a))
|
||||
return matchingVersions[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves versioned settings by finding the highest version that is <= the current
|
||||
* plugin version and returning those settings.
|
||||
*
|
||||
* The versionedSettings structure uses version numbers as keys:
|
||||
* ```
|
||||
* versionedSettings: {
|
||||
* '3.36.4': { includedTools: ['search_replace'], excludedTools: ['apply_diff'] },
|
||||
* '3.35.0': { includedTools: ['search_replace'] },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This function finds the highest version key that is <= currentVersion and returns
|
||||
* the corresponding settings object. If no version matches, returns an empty object.
|
||||
*
|
||||
* @param versionedSettings The versioned settings object with version keys
|
||||
* @param currentVersion The current plugin version (defaults to Package.version)
|
||||
* @returns The settings object for the highest matching version, or empty object if none match
|
||||
*/
|
||||
export function resolveVersionedSettings<T extends Record<string, unknown>>(
|
||||
versionedSettings: VersionedSettings,
|
||||
currentVersion: string = Package.version,
|
||||
): Partial<T> {
|
||||
const matchingVersion = findHighestMatchingVersion(versionedSettings, currentVersion)
|
||||
|
||||
if (!matchingVersion) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return versionedSettings[matchingVersion] as Partial<T>
|
||||
}
|
||||
|
|
@ -488,6 +488,7 @@
|
|||
"safe-stable-stringify": "^2.5.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"say": "^0.16.0",
|
||||
"semver-compare": "^1.0.0",
|
||||
"serialize-error": "^12.0.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"simple-git": "^3.27.0",
|
||||
|
|
@ -524,6 +525,7 @@
|
|||
"@types/node-ipc": "^9.2.3",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/ps-tree": "^1.1.6",
|
||||
"@types/semver-compare": "^1.0.3",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/stream-json": "^1.7.8",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue