Procházet zdrojové kódy

fix(core): expose discovered lm studio models

Shoubhit Dash před 3 dny
rodič
revize
c02bdd3a14

+ 11 - 1
packages/core/src/plugin/provider/lmstudio.ts

@@ -36,12 +36,21 @@ export function make(origin = "http://127.0.0.1:1234", interval: Duration.Input
       const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
       const loaded = { models: [] as (typeof RemoteModel.Type)[], hash: "[]" }
 
+      yield* ctx.integration.transform((integrations) => {
+        if (loaded.models.length === 0) return
+        integrations.remove(providerID)
+      })
+
       yield* ctx.catalog.transform((catalog) => {
         if (loaded.models.length === 0) return
+        for (const model of catalog.provider.get(providerID)?.models.values() ?? []) {
+          catalog.model.remove(providerID, model.id)
+        }
         catalog.provider.update(providerID, (provider) => {
           provider.name = "LM Studio"
           provider.package = "@opencode-ai/ai/providers/openai-compatible"
-          provider.settings = { baseURL, provider: providerID }
+          provider.settings = { baseURL, provider: providerID, apiKey: "" }
+          provider.integrationID = undefined
         })
         for (const item of loaded.models) {
           catalog.model.update(providerID, item.key, (model) => {
@@ -89,6 +98,7 @@ export function make(origin = "http://127.0.0.1:1234", interval: Duration.Input
         if (hash === loaded.hash) return
         loaded.models = models
         loaded.hash = hash
+        yield* ctx.integration.reload()
         yield* ctx.catalog.reload()
       })
 

+ 72 - 1
packages/core/test/plugin/provider-lmstudio.test.ts

@@ -1,4 +1,5 @@
 import { Catalog } from "@opencode-ai/core/catalog"
+import { Integration } from "@opencode-ai/core/integration"
 import { Model } from "@opencode-ai/core/model"
 import { Plugin } from "@opencode-ai/core/plugin"
 import { PluginHost } from "@opencode-ai/core/plugin/host"
@@ -91,7 +92,7 @@ describe("LMStudioPlugin", () => {
             id: providerID,
             name: "LM Studio",
             package: "@opencode-ai/ai/providers/openai-compatible",
-            settings: { baseURL: `${server.url.origin}/v1`, provider: "lmstudio" },
+            settings: { baseURL: `${server.url.origin}/v1`, provider: "lmstudio", apiKey: "" },
           })
           expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
           expect(gemma).toMatchObject({
@@ -188,4 +189,74 @@ describe("LMStudioPlugin", () => {
       ({ server }) => Effect.promise(() => server.stop(true)),
     ),
   )
+
+  it.live("replaces the credential-gated Models.dev catalog when discovery succeeds", () =>
+    Effect.acquireUseRelease(
+      Effect.sync(() => {
+        const models = [
+          {
+            type: "llm",
+            key: "discovered-model",
+            display_name: "Discovered Model",
+            loaded_instances: [],
+            max_context_length: 32_768,
+          },
+        ]
+        return { models, server: Bun.serve({ port: 0, fetch: () => Response.json({ models }) }) }
+      }),
+      ({ models, server }) =>
+        Effect.gen(function* () {
+          const catalog = yield* Catalog.Service
+          const integrations = yield* Integration.Service
+          const providerID = Provider.ID.make("lmstudio")
+          yield* integrations.transform((draft) => {
+            draft.update(Integration.ID.make("lmstudio"), (integration) => {
+              integration.name = "LMStudio"
+            })
+            draft.method.update({
+              integrationID: Integration.ID.make("lmstudio"),
+              method: { type: "env", names: ["LMSTUDIO_API_KEY"] },
+            })
+          })
+          yield* catalog.transform((draft) => {
+            draft.provider.update(providerID, (provider) => {
+              provider.name = "LMStudio"
+              provider.package = "aisdk:@ai-sdk/openai-compatible"
+              provider.integrationID = Integration.ID.make("lmstudio")
+            })
+            draft.model.update(providerID, Model.ID.make("static-model"), () => {})
+          })
+
+          expect((yield* catalog.provider.available()).map((provider) => provider.id)).not.toContain(providerID)
+          yield* addPlugin(server.url.origin, "5 millis")
+          yield* eventually(
+            catalog.model.get(providerID, Model.ID.make("discovered-model")),
+            (model) => model !== undefined,
+          )
+
+          expect(yield* integrations.get(Integration.ID.make("lmstudio"))).toBeUndefined()
+          expect((yield* catalog.provider.get(providerID))?.integrationID).toBeUndefined()
+          expect(yield* catalog.model.get(providerID, Model.ID.make("static-model"))).toBeUndefined()
+          expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
+
+          yield* integrations.transform((draft) => {
+            draft.update(Integration.ID.make("lmstudio"), (integration) => {
+              integration.name = "Configured LM Studio"
+            })
+            draft.method.update({ integrationID: Integration.ID.make("lmstudio"), method: { type: "key" } })
+          })
+          expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
+
+          models.splice(0)
+          yield* eventually(
+            catalog.model.get(providerID, Model.ID.make("static-model")),
+            (model) => model !== undefined,
+          )
+          expect(yield* catalog.model.get(providerID, Model.ID.make("discovered-model"))).toBeUndefined()
+          expect(yield* integrations.get(Integration.ID.make("lmstudio"))).toBeDefined()
+          expect((yield* catalog.provider.get(providerID))?.integrationID).toBe(Integration.ID.make("lmstudio"))
+        }),
+      ({ server }) => Effect.promise(() => server.stop(true)),
+    ),
+  )
 })