mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-15 09:02:35 +00:00
fix(provider): isolate plugin model mutations (#26561)
Co-authored-by: Developer <temp@example.com>
This commit is contained in:
@@ -1162,7 +1162,7 @@ const layer: Layer.Layer<
|
||||
const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
|
||||
|
||||
provider.models = yield* Effect.promise(async () => {
|
||||
const next = await models(provider, { auth: pluginAuth })
|
||||
const next = await models(toPublicInfo(provider), { auth: pluginAuth })
|
||||
return Object.fromEntries(
|
||||
Object.entries(next).map(([id, model]) => [
|
||||
id,
|
||||
|
||||
@@ -37,6 +37,38 @@ function hasProviderWithFetch(input: unknown, key: "all" | "providers") {
|
||||
return "providers" in input && providerListHasFetch(input.providers)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function providerList(input: unknown, key: "all" | "providers") {
|
||||
if (!isRecord(input)) return []
|
||||
if (!Array.isArray(input[key])) return []
|
||||
return input[key]
|
||||
}
|
||||
|
||||
function providerByID(input: unknown, key: "all" | "providers", id: string) {
|
||||
return providerList(input, key).find((provider) => isRecord(provider) && provider.id === id)
|
||||
}
|
||||
|
||||
function hasNonZeroModelCost(input: unknown, key: "all" | "providers", id: string) {
|
||||
const provider = providerByID(input, key, id)
|
||||
if (!isRecord(provider) || !isRecord(provider.models)) return false
|
||||
return Object.values(provider.models).some((model) => {
|
||||
if (!isRecord(model) || !isRecord(model.cost) || !isRecord(model.cost.cache)) return false
|
||||
return [model.cost.input, model.cost.output, model.cost.cache.read, model.cost.cache.write].some(
|
||||
(cost) => typeof cost === "number" && cost > 0,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function hasProviderMutationMarker(input: unknown, key: "all" | "providers", id: string) {
|
||||
const provider = providerByID(input, key, id)
|
||||
if (!isRecord(provider)) return false
|
||||
if (provider.name === "mutated-provider") return true
|
||||
return isRecord(provider.options) && provider.options.mutatedByPlugin === true
|
||||
}
|
||||
|
||||
function requestAuthorize(input: {
|
||||
app: ReturnType<typeof app>
|
||||
providerID: string
|
||||
@@ -125,6 +157,40 @@ function writeFunctionOptionsPlugin(dir: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function writeProviderModelsMutationPlugin(dir: string) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
|
||||
yield* fs.makeDirectory(path.join(dir, ".opencode", "plugin"), { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, ".opencode", "plugin", "provider-models-mutation.ts"),
|
||||
[
|
||||
"export default {",
|
||||
' id: "test.provider-models-mutation",',
|
||||
" server: async () => ({",
|
||||
" provider: {",
|
||||
' id: "google",',
|
||||
" models: async (provider) => {",
|
||||
" const models = Object.fromEntries(",
|
||||
" Object.entries(provider.models ?? {}).map(([id, model]) => [id, { ...model }]),",
|
||||
" )",
|
||||
' provider.name = "mutated-provider"',
|
||||
" provider.options = { ...provider.options, mutatedByPlugin: true }",
|
||||
" for (const model of Object.values(provider.models ?? {})) {",
|
||||
" model.cost = { input: 0, output: 0 }",
|
||||
" }",
|
||||
" return models",
|
||||
" },",
|
||||
" },",
|
||||
" }),",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
@@ -222,6 +288,37 @@ describe("provider HttpApi", () => {
|
||||
const configBody = yield* Effect.promise(() => configResponse.json())
|
||||
expect(hasProviderWithFetch(providerBody, "all")).toBe(false)
|
||||
expect(hasProviderWithFetch(configBody, "providers")).toBe(false)
|
||||
expect(hasNonZeroModelCost(providerBody, "all", "google")).toBe(true)
|
||||
expect(hasNonZeroModelCost(configBody, "providers", "google")).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps provider.models hook input mutations out of provider state", () =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
|
||||
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
|
||||
)
|
||||
yield* writeProviderModelsMutationPlugin(dir)
|
||||
|
||||
const headers = { "x-opencode-directory": dir }
|
||||
const providerResponse = yield* Effect.promise(() => Promise.resolve(app().request("/provider", { headers })))
|
||||
const configResponse = yield* Effect.promise(() =>
|
||||
Promise.resolve(app().request("/config/providers", { headers })),
|
||||
)
|
||||
|
||||
expect(providerResponse.status).toBe(200)
|
||||
expect(configResponse.status).toBe(200)
|
||||
|
||||
const providerBody = yield* Effect.promise(() => providerResponse.json())
|
||||
const configBody = yield* Effect.promise(() => configResponse.json())
|
||||
expect(hasProviderMutationMarker(providerBody, "all", "google")).toBe(false)
|
||||
expect(hasProviderMutationMarker(configBody, "providers", "google")).toBe(false)
|
||||
expect(hasNonZeroModelCost(providerBody, "all", "google")).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user