Explorar el Código

fix(core): harden model selection edges (#30987)

Kit Langton hace 3 meses
padre
commit
3151e2246a

+ 8 - 6
packages/core/src/catalog.ts

@@ -199,6 +199,8 @@ export const layer = Layer.effect(
         }
       }),
     })
+    const available = (model: ModelV2.Info) =>
+      state.get().providers.get(model.providerID)?.provider.enabled !== false && model.enabled
 
     yield* events.subscribe(PluginV2.Event.Added).pipe(
       // Plugin registries are location scoped even though the event bus is process scoped.
@@ -250,17 +252,17 @@ export const layer = Layer.effect(
         }),
 
         available: Effect.fn("CatalogV2.model.available")(function* () {
-          return (yield* result.model.all()).filter((model) => {
-            const record = state.get().providers.get(model.providerID)
-            return record?.provider.enabled !== false && model.enabled
-          })
+          return (yield* result.model.all()).filter(available)
         }),
 
         default: Effect.fn("CatalogV2.model.default")(function* () {
           const defaultModel = state.get().defaultModel
           if (defaultModel) {
-            const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
-            if (Option.isSome(model) && model.value.enabled) return model
+            const provider = state.get().providers.get(defaultModel.providerID)?.provider
+            if (provider?.enabled !== false) {
+              const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
+              if (Option.isSome(model) && available(model.value)) return model
+            }
           }
 
           return pipe(

+ 2 - 8
packages/core/src/session.ts

@@ -88,7 +88,7 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Ses
 export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
   "Session.OperationUnavailableError",
   {
-    operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "switchModel", "compact", "wait"]),
+    operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]),
   },
 ) {}
 
@@ -386,13 +386,7 @@ export const layer = Layer.effect(
         return yield* new OperationUnavailableError({ operation: "switchAgent" })
       }),
       switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
-        const session = yield* result.get(input.sessionID)
-        if (
-          session.model?.providerID === input.model.providerID &&
-          session.model.id === input.model.id &&
-          (session.model.variant ?? "default") === (input.model.variant ?? "default")
-        )
-          return
+        yield* result.get(input.sessionID)
         yield* events.publish(SessionEvent.ModelSwitched, {
           sessionID: input.sessionID,
           messageID: SessionMessage.ID.create(),

+ 28 - 0
packages/core/test/catalog.test.ts

@@ -281,6 +281,34 @@ describe("CatalogV2", () => {
     }),
   )
 
+  it.effect("ignores a configured default on a disabled provider", () =>
+    Effect.gen(function* () {
+      const catalog = yield* Catalog.Service
+      const disabledProvider = ProviderV2.ID.make("disabled")
+      const enabledProvider = ProviderV2.ID.make("enabled")
+      const disabledModel = ModelV2.ID.make("configured")
+      const fallbackModel = ModelV2.ID.make("fallback")
+      const transform = yield* catalog.transform()
+
+      yield* transform((catalog) => {
+        catalog.provider.update(disabledProvider, (provider) => {
+          provider.enabled = false
+        })
+        catalog.model.update(disabledProvider, disabledModel, () => {})
+        catalog.provider.update(enabledProvider, (provider) => {
+          provider.enabled = { via: "custom", data: {} }
+        })
+        catalog.model.update(enabledProvider, fallbackModel, () => {})
+        catalog.model.default.set(disabledProvider, disabledModel)
+      })
+
+      expect(Option.getOrUndefined(yield* catalog.model.default())).toMatchObject({
+        providerID: enabledProvider,
+        id: fallbackModel,
+      })
+    }),
+  )
+
   it.effect("small model prefers small keyword candidates before cost scoring", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service

+ 12 - 1
packages/core/test/session-create.test.ts

@@ -356,12 +356,23 @@ describe("SessionV2.create", () => {
       expect(
         Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)),
       ).toMatchObject([{ event: { type: "session.next.model.switched", data: { model } } }])
+    }),
+  )
 
+  it.effect("persists repeated switches as distinct durable Session events", () =>
+    Effect.gen(function* () {
+      const session = yield* SessionV2.Service
+      const created = yield* session.create({ location })
+      const model = ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic })
+
+      yield* session.switchModel({ sessionID: created.id, model })
       yield* session.switchModel({ sessionID: created.id, model })
+
       const { db } = yield* Database.Service
       expect(
         yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie),
-      ).toHaveLength(2)
+      ).toHaveLength(3)
+      expect(yield* session.get(created.id)).toMatchObject({ model })
     }),
   )