Просмотр исходного кода

refactor(core): model provider activation explicitly (#42791)

Co-authored-by: Michael Hart <mhart@cloudflare.com>
Dax 2 дней назад
Родитель
Сommit
0d7904c91f

+ 1 - 1
packages/client/src/promise/generated/types.ts

@@ -192,7 +192,7 @@ export type ProviderInfo = {
   id: string
   id: string
   integrationID?: string
   integrationID?: string
   name: string
   name: string
-  disabled?: boolean
+  activation: "auto" | "enabled" | "disabled"
   package: string
   package: string
   settings?: { [x: string]: any }
   settings?: { [x: string]: any }
   headers?: { [x: string]: string }
   headers?: { [x: string]: string }

+ 2 - 2
packages/core/src/catalog.ts

@@ -65,8 +65,8 @@ const layer = Layer.effect(
     const integrations = yield* Integration.Service
     const integrations = yield* Integration.Service
 
 
     const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
     const available = (provider: Provider.Info, integration: Integration.Info | undefined) => {
-      if (provider.disabled) return false
-      if (typeof provider.settings?.apiKey === "string") return true
+      if (provider.activation === "disabled") return false
+      if (provider.activation === "enabled") return true
       if (integration?.connections.length) return true
       if (integration?.connections.length) return true
       return provider.integrationID === undefined && !integration
       return provider.integrationID === undefined && !integration
     }
     }

+ 1 - 0
packages/core/src/config/plugin/provider.ts

@@ -40,6 +40,7 @@ export const Plugin = define({
       for (const [id, item] of configuredProviders(loaded.entries)) {
       for (const [id, item] of configuredProviders(loaded.entries)) {
         const providerID = id
         const providerID = id
         catalog.provider.update(providerID, (provider) => {
         catalog.provider.update(providerID, (provider) => {
+          provider.activation = "enabled"
           if (item.name !== undefined) provider.name = item.name
           if (item.name !== undefined) provider.name = item.name
           if (item.package !== undefined) provider.package = item.package
           if (item.package !== undefined) provider.package = item.package
           if (item.settings !== undefined) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
           if (item.settings !== undefined) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)

+ 43 - 10
packages/core/src/model-resolver.ts

@@ -358,17 +358,21 @@ export const layer = Layer.effect(
       const connection = yield* integrations.connection.active(
       const connection = yield* integrations.connection.active(
         provider?.integrationID ?? Integration.ID.make(selected.providerID),
         provider?.integrationID ?? Integration.ID.make(selected.providerID),
       )
       )
-      const model = yield* resolveModel(
-        selected,
-        variant,
-        connection ? yield* integrations.connection.resolve(connection) : undefined,
-        {
-          loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
-          loadAISDK: (model) => aisdk.model(model),
-        },
-      )
+      const credential = connection ? yield* integrations.connection.resolve(connection) : undefined
+      const runtimeInfo = yield* withVariant(selected, variant)
+      const model = yield* fromCatalogModel(runtimeInfo, credential, {
+        loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
+        loadAISDK: (model) => aisdk.model(model),
+      })
+      const runtime =
+        provider?.activation === "enabled" &&
+        credential === undefined &&
+        !hasConfiguredAuth(runtimeInfo) &&
+        usesAPIKeyAuth(runtimeInfo.package)
+          ? LanguageModel.update(model, { route: model.route.with({ auth: Auth.none }) })
+          : model
       return {
       return {
-        model,
+        model: runtime,
         ref: Ref.make({
         ref: Ref.make({
           id: selected.id,
           id: selected.id,
           providerID: selected.providerID,
           providerID: selected.providerID,
@@ -399,6 +403,35 @@ export const layer = Layer.effect(
   }),
   }),
 )
 )
 
 
+function hasConfiguredAuth(model: Info) {
+  return [model.settings?.apiKey, model.settings?.authToken, model.settings?.accessToken].some(
+    (value) => typeof value === "string" && value !== "",
+  )
+}
+
+function usesAPIKeyAuth(packageName: string | undefined) {
+  const name = Provider.packageName(packageName)
+  return (
+    name === "@ai-sdk/openai" ||
+    name === "@ai-sdk/anthropic" ||
+    name === "@ai-sdk/openai-compatible" ||
+    name === "@ai-sdk/google" ||
+    name === "@ai-sdk/xai" ||
+    name === "@openrouter/ai-sdk-provider" ||
+    name === "@ai-sdk/azure" ||
+    name === "@opencode-ai/ai/providers/openai" ||
+    name?.startsWith("@opencode-ai/ai/providers/openai/") === true ||
+    name === "@opencode-ai/ai/providers/anthropic" ||
+    name === "@opencode-ai/ai/providers/anthropic-compatible" ||
+    name === "@opencode-ai/ai/providers/openai-compatible" ||
+    name === "@opencode-ai/ai/providers/google" ||
+    name === "@opencode-ai/ai/providers/xai" ||
+    name === "@opencode-ai/ai/providers/openrouter" ||
+    name === "@opencode-ai/ai/providers/azure" ||
+    name?.startsWith("@opencode-ai/ai/providers/azure/") === true
+  )
+}
+
 export const node = makeLocationNode({
 export const node = makeLocationNode({
   service: Service,
   service: Service,
   layer,
   layer,

+ 1 - 0
packages/core/src/models-dev.ts

@@ -86,6 +86,7 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
     const info = {
     const info = {
       id: providerID,
       id: providerID,
       name: item.name,
       name: item.name,
+      activation: "auto",
       package: Provider.aisdk(item.npm),
       package: Provider.aisdk(item.npm),
       ...(item.api ? { settings: { baseURL: item.api } } : {}),
       ...(item.api ? { settings: { baseURL: item.api } } : {}),
     } satisfies Provider.Info
     } satisfies Provider.Info

+ 1 - 1
packages/core/src/plugin/provider/llmgateway.ts

@@ -10,7 +10,7 @@ export const LLMGatewayPlugin = define({
     const configured = new Set((yield* integrations.list()).map((integration) => integration.id))
     const configured = new Set((yield* integrations.list()).map((integration) => integration.id))
     yield* ctx.catalog.transform((evt) => {
     yield* ctx.catalog.transform((evt) => {
       for (const item of evt.provider.list()) {
       for (const item of evt.provider.list()) {
-        if (item.provider.disabled) continue
+        if (item.provider.activation === "disabled") continue
         if (!Provider.isAISDK(item.provider.package)) continue
         if (!Provider.isAISDK(item.provider.package)) continue
         if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
         if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
         if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
         if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue

+ 4 - 1
packages/core/src/plugin/provider/opencode.ts

@@ -178,7 +178,10 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
       if (!item) return
       if (!item) return
       const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
       const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
       catalog.provider.update(item.provider.id, (provider) => {
       catalog.provider.update(item.provider.id, (provider) => {
-        if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
+        if (!hasKey) {
+          provider.activation = "enabled"
+          provider.settings = { ...provider.settings, apiKey: "public" }
+        }
       })
       })
       if (hasKey) return
       if (hasKey) return
       for (const model of item.models.values()) {
       for (const model of item.models.values()) {

+ 30 - 1
packages/core/test/catalog.test.ts

@@ -102,6 +102,35 @@ describe("Catalog", () => {
     }).pipe(Effect.provide(localCatalogLayer))
     }).pipe(Effect.provide(localCatalogLayer))
   })
   })
 
 
+  it.effect("makes an explicitly enabled provider available without a connection", () => {
+    const integrationID = Integration.ID.make("gateway")
+    const providerID = Provider.ID.make("remote")
+    const localCatalogLayer = Layer.fresh(
+      AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [
+        [Location.node, locationLayer],
+      ]),
+    )
+
+    return Effect.gen(function* () {
+      const catalog = yield* Catalog.Service
+      yield* (yield* Integration.Service).transform((editor) => editor.update(integrationID, () => {}))
+      yield* catalog.transform((editor) =>
+        editor.provider.update(providerID, (provider) => {
+          provider.integrationID = integrationID
+          provider.settings = { baseURL: "https://gateway.example.com/v1" }
+        }),
+      )
+      expect(yield* catalog.provider.available()).toEqual([])
+
+      yield* catalog.transform((editor) =>
+        editor.provider.update(providerID, (provider) => {
+          provider.activation = "enabled"
+        }),
+      )
+      expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID])
+    }).pipe(Effect.provide(localCatalogLayer))
+  })
+
   it.effect("projects environment connections without a catalog plugin", () =>
   it.effect("projects environment connections without a catalog plugin", () =>
     Effect.acquireUseRelease(
     Effect.acquireUseRelease(
       Effect.sync(() => {
       Effect.sync(() => {
@@ -278,7 +307,7 @@ describe("Catalog", () => {
       const fallbackModel = Model.ID.make("fallback")
       const fallbackModel = Model.ID.make("fallback")
       yield* catalog.transform((catalog) => {
       yield* catalog.transform((catalog) => {
         catalog.provider.update(disabledProvider, (provider) => {
         catalog.provider.update(disabledProvider, (provider) => {
-          provider.disabled = true
+          provider.activation = "disabled"
         })
         })
         catalog.model.update(disabledProvider, disabledModel, () => {})
         catalog.model.update(disabledProvider, disabledModel, () => {})
         catalog.provider.update(enabledProvider, () => {})
         catalog.provider.update(enabledProvider, () => {})

+ 1 - 1
packages/core/test/config/provider.test.ts

@@ -342,7 +342,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
           names: ["CUSTOM_API_KEY"],
           names: ["CUSTOM_API_KEY"],
         })
         })
         expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
         expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
-        expect(provider.disabled).toBeUndefined()
+        expect(provider.activation).toBe("enabled")
         expect(provider.package).toBe("aisdk:custom-sdk")
         expect(provider.package).toBe("aisdk:custom-sdk")
         expect(provider.settings).toEqual({ baseURL: "https://example.test" })
         expect(provider.settings).toEqual({ baseURL: "https://example.test" })
         expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })
         expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })

+ 107 - 1
packages/core/test/model-resolver.test.ts

@@ -2,13 +2,16 @@ import { describe, expect } from "bun:test"
 import { LLM, LanguageModel } from "@opencode-ai/ai"
 import { LLM, LanguageModel } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { compileRequest } from "@opencode-ai/ai/route/client"
 import { compileRequest } from "@opencode-ai/ai/route/client"
-import { Effect } from "effect"
+import { Effect, Layer } from "effect"
 import { Headers } from "effect/unstable/http"
 import { Headers } from "effect/unstable/http"
 import { Credential } from "@opencode-ai/core/credential"
 import { Credential } from "@opencode-ai/core/credential"
 import { Integration } from "@opencode-ai/core/integration"
 import { Integration } from "@opencode-ai/core/integration"
 import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model"
 import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model"
 import { Provider } from "@opencode-ai/core/provider"
 import { Provider } from "@opencode-ai/core/provider"
 import { ModelResolver } from "@opencode-ai/core/model-resolver"
 import { ModelResolver } from "@opencode-ai/core/model-resolver"
+import { Catalog } from "@opencode-ai/core/catalog"
+import { AISDK } from "@opencode-ai/core/aisdk"
+import { Npm } from "@opencode-ai/util/npm"
 import { it } from "./lib/effect"
 import { it } from "./lib/effect"
 
 
 interface ModelOptions {
 interface ModelOptions {
@@ -269,6 +272,109 @@ describe("ModelResolver", () => {
     }),
     }),
   )
   )
 
 
+  it.effect("uses no native API-key auth for an explicitly enabled provider without credentials", () => {
+    const selected = model(Provider.aisdk("@ai-sdk/google"), {
+      providerID: Provider.ID.make("gateway"),
+      settings: { baseURL: "https://gateway.example.com/v1" },
+      headers: { "cf-access-token": "access-token" },
+    })
+    const provider = Provider.Info.make({
+      ...Provider.Info.empty(selected.providerID),
+      activation: "enabled",
+      package: selected.package ?? "",
+      settings: selected.settings,
+      headers: selected.headers,
+    })
+    const catalog = Layer.mock(Catalog.Service, {
+      provider: {
+        get: () => Effect.succeed(provider),
+        all: () => Effect.die("unused"),
+        available: () => Effect.die("unused"),
+      },
+      model: {
+        get: () => Effect.succeed(selected),
+        all: () => Effect.die("unused"),
+        available: () => Effect.die("unused"),
+        default: () => Effect.die("unused"),
+        small: () => Effect.die("unused"),
+      },
+    })
+    const integrations = Layer.mock(Integration.Service, {
+      connection: {
+        active: () => Effect.succeed(undefined),
+        resolve: () => Effect.die("unused"),
+        key: () => Effect.die("unused"),
+        update: () => Effect.die("unused"),
+        remove: () => Effect.die("unused"),
+      },
+      oauth: {
+        connect: () => Effect.die("unused"),
+        status: () => Effect.die("unused"),
+        complete: () => Effect.die("unused"),
+        cancel: () => Effect.die("unused"),
+      },
+      command: {
+        connect: () => Effect.die("unused"),
+        status: () => Effect.die("unused"),
+        cancel: () => Effect.die("unused"),
+      },
+    })
+    const npm = Layer.mock(Npm.Service, {
+      add: () => Effect.die("unused"),
+      which: () => Effect.die("unused"),
+    })
+    const aisdk = Layer.mock(AISDK.Service, {
+      hook: {
+        sdk: () => Effect.die("unused"),
+        language: () => Effect.die("unused"),
+      },
+      model: () => Effect.die("unused"),
+    })
+    const layer = ModelResolver.layer.pipe(Layer.provide(Layer.mergeAll(catalog, integrations, npm, aisdk)))
+
+    return withEnv({ GOOGLE_GENERATIVE_AI_API_KEY: undefined }, () =>
+      Effect.gen(function* () {
+        const resolver = yield* ModelResolver.Service
+        const resolved = yield* resolver.resolveModel(selected)
+
+        const headers = yield* resolved.model.route.auth.apply({
+          request: LLM.request({ model: resolved.model, prompt: "Hello" }),
+          method: "POST",
+          url: "https://gateway.example.com/v1",
+          body: "{}",
+          headers: Headers.fromInput(resolved.model.route.defaults.headers),
+        })
+
+        expect(headers["cf-access-token"]).toBe("access-token")
+        expect(headers.authorization).toBeUndefined()
+        expect(headers["x-goog-api-key"]).toBeUndefined()
+      }).pipe(Effect.provide(layer)),
+    )
+  })
+
+  it.effect("keeps native provider environment auth strict when no API key is configured", () =>
+    withEnv({ GOOGLE_GENERATIVE_AI_API_KEY: undefined }, () =>
+      Effect.gen(function* () {
+        const resolved = yield* ModelResolver.fromCatalogModel(
+          model(Provider.aisdk("@ai-sdk/google"), {
+            settings: { baseURL: "https://google.example.com/v1" },
+          }),
+        )
+        const exit = yield* Effect.exit(
+          resolved.route.auth.apply({
+            request: LLM.request({ model: resolved, prompt: "Hello" }),
+            method: "POST",
+            url: "https://google.example.com/v1",
+            body: "{}",
+            headers: Headers.empty,
+          }),
+        )
+
+        expect(exit._tag).toBe("Failure")
+      }),
+    ),
+  )
+
   it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
   it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const resolved = yield* ModelResolver.fromCatalogModel(
       const resolved = yield* ModelResolver.fromCatalogModel(

+ 2 - 0
packages/core/test/models.test.ts

@@ -47,6 +47,7 @@ const fixtureSnapshot = [
     info: {
     info: {
       id: Provider.ID.make("acme"),
       id: Provider.ID.make("acme"),
       name: "Acme",
       name: "Acme",
+      activation: "auto",
       package: Provider.aisdk("@ai-sdk/openai-compatible"),
       package: Provider.aisdk("@ai-sdk/openai-compatible"),
     },
     },
     models: [
     models: [
@@ -109,6 +110,7 @@ const fixture2Snapshot = [
     info: {
     info: {
       id: Provider.ID.make("beta"),
       id: Provider.ID.make("beta"),
       name: "Beta",
       name: "Beta",
+      activation: "auto",
       package: Provider.aisdk("@ai-sdk/openai-compatible"),
       package: Provider.aisdk("@ai-sdk/openai-compatible"),
     },
     },
     models: [
     models: [

+ 4 - 0
packages/core/test/plugin/models-dev.test.ts

@@ -64,6 +64,7 @@ describe("ModelsDevPlugin", () => {
               info: {
               info: {
                 id: providerID,
                 id: providerID,
                 name: "Acme",
                 name: "Acme",
+                activation: "auto",
                 package: Provider.aisdk("@ai-sdk/openai-compatible"),
                 package: Provider.aisdk("@ai-sdk/openai-compatible"),
                 settings: { baseURL: "https://api.acme.test/v1" },
                 settings: { baseURL: "https://api.acme.test/v1" },
               },
               },
@@ -239,6 +240,7 @@ describe("ModelsDevPlugin", () => {
           info: {
           info: {
             id: providerID,
             id: providerID,
             name: "Acme",
             name: "Acme",
+            activation: "auto",
             package: Provider.aisdk("@ai-sdk/openai-compatible"),
             package: Provider.aisdk("@ai-sdk/openai-compatible"),
           },
           },
           environment: [],
           environment: [],
@@ -330,6 +332,7 @@ describe("ModelsDevPlugin", () => {
                       info: {
                       info: {
                         id: providerID,
                         id: providerID,
                         name: "Acme",
                         name: "Acme",
+                        activation: "auto",
                         package: Provider.aisdk("@ai-sdk/openai-compatible"),
                         package: Provider.aisdk("@ai-sdk/openai-compatible"),
                         settings: { baseURL: "https://${ACME_HOST}/${UNDECLARED_HOST}/v1" },
                         settings: { baseURL: "https://${ACME_HOST}/${UNDECLARED_HOST}/v1" },
                       },
                       },
@@ -385,6 +388,7 @@ describe("ModelsDevPlugin", () => {
         info: {
         info: {
           id: Provider.ID.make(id),
           id: Provider.ID.make(id),
           name,
           name,
+          activation: "auto",
           package: Provider.aisdk(packageName),
           package: Provider.aisdk(packageName),
         },
         },
         environment: id === "azure" ? ["AZURE_RESOURCE_NAME", environment] : [environment],
         environment: id === "azure" ? ["AZURE_RESOURCE_NAME", environment] : [environment],

+ 2 - 2
packages/core/test/plugin/provider-llmgateway.test.ts

@@ -60,14 +60,14 @@ describe("LLMGatewayPlugin", () => {
       })
       })
       yield* catalog.transform((catalog) => {
       yield* catalog.transform((catalog) => {
         catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => {
         catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => {
-          provider.disabled = true
+          provider.activation = "disabled"
           provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
           provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
           provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
           provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
         })
         })
       })
       })
       yield* addPlugin()
       yield* addPlugin()
 
 
-      expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.disabled).toBe(true)
+      expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.activation).toBe("disabled")
       expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toBeUndefined()
       expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toBeUndefined()
     }),
     }),
   )
   )

+ 2 - 0
packages/core/test/plugin/provider-opencode.test.ts

@@ -347,6 +347,8 @@ describe("OpencodePlugin", () => {
         })
         })
         yield* addPlugin()
         yield* addPlugin()
         expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public")
         expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public")
+        expect(required(yield* catalog.provider.get(Provider.ID.opencode)).activation).toBe("enabled")
+        expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(Provider.ID.opencode)
         expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("free"))).enabled).toBe(true)
         expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("free"))).enabled).toBe(true)
       }),
       }),
     ),
     ),

+ 5 - 2
packages/schema/src/provider.ts

@@ -25,6 +25,9 @@ export type ID = typeof ID.Type
 export const Package = Schema.String
 export const Package = Schema.String
 export type Package = typeof Package.Type
 export type Package = typeof Package.Type
 
 
+export const Activation = Schema.Literals(["auto", "enabled", "disabled"])
+export type Activation = typeof Activation.Type
+
 export const Overlays = {
 export const Overlays = {
   settings: Schema.Record(Schema.String, Schema.Any).pipe(optional),
   settings: Schema.Record(Schema.String, Schema.Any).pipe(optional),
   headers: Schema.Record(Schema.String, Schema.String).pipe(optional),
   headers: Schema.Record(Schema.String, Schema.String).pipe(optional),
@@ -46,13 +49,13 @@ export const Info = Schema.Struct({
   id: ID,
   id: ID,
   integrationID: Integration.ID.pipe(optional),
   integrationID: Integration.ID.pipe(optional),
   name: Schema.String,
   name: Schema.String,
-  disabled: Schema.Boolean.pipe(optional),
+  activation: Activation,
   package: Package,
   package: Package,
   ...Overlays,
   ...Overlays,
 })
 })
   .annotate({ identifier: "Provider.Info" })
   .annotate({ identifier: "Provider.Info" })
   .pipe(
   .pipe(
     statics(() => ({
     statics(() => ({
-      empty: (id: ID): Info => ({ id, name: id, package: "" }),
+      empty: (id: ID): Info => ({ id, name: id, activation: "auto", package: "" }),
     })),
     })),
   )
   )

+ 2 - 0
packages/schema/test/contract-hygiene.test.ts

@@ -120,10 +120,12 @@ describe("contract hygiene", () => {
   test("model defaults and provider overlays preserve public invariants", () => {
   test("model defaults and provider overlays preserve public invariants", () => {
     const id = Model.ID.make("model")
     const id = Model.ID.make("model")
     expect(Model.Info.default(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] })
     expect(Model.Info.default(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] })
+    expect(Provider.Info.empty(Provider.ID.make("provider")).activation).toBe("auto")
     expect(
     expect(
       Schema.decodeUnknownSync(Provider.Info)({
       Schema.decodeUnknownSync(Provider.Info)({
         id: "provider",
         id: "provider",
         name: "Provider",
         name: "Provider",
+        activation: "auto",
         package: "native",
         package: "native",
         settings: { arbitrary: 1n },
         settings: { arbitrary: 1n },
       }).settings,
       }).settings,

+ 1 - 0
packages/tui/test/mini/fixture/catalog.ts

@@ -5,6 +5,7 @@ export function catalogProvider(id: string, name: string): ProviderListOutput["d
   return {
   return {
     id,
     id,
     name,
     name,
+    activation: "auto",
     package: "",
     package: "",
   }
   }
 }
 }