From 34198f422c09a3a798f8864e2c7bf4451c6a42df Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 15 May 2026 02:48:01 +0530 Subject: [PATCH] refactor(provider): use runtime flag for experimental models (#27606) --- packages/core/src/flag/flag.ts | 1 - packages/opencode/src/effect/runtime-flags.ts | 1 + packages/opencode/src/provider/provider.ts | 24 ++++---- .../test/effect/runtime-flags.test.ts | 3 + .../opencode/test/provider/provider.test.ts | 60 ++++++++++++++++++- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index df276ff88a..3f2cbf1394 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -33,7 +33,6 @@ export const Flag = { OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"], OPENCODE_DISABLE_DEFAULT_PLUGINS: truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS"), OPENCODE_DISABLE_LSP_DOWNLOAD: truthy("OPENCODE_DISABLE_LSP_DOWNLOAD"), - OPENCODE_ENABLE_EXPERIMENTAL_MODELS: truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"), OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 4228a32f83..c01779e247 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -22,6 +22,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime enabled: bool("OPENCODE_ENABLE_PARALLEL"), legacy: bool("OPENCODE_EXPERIMENTAL_PARALLEL"), }).pipe(Config.map((flags) => flags.enabled || flags.legacy)), + enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"), enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"), experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"), experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6401518c7e..8f8a6fe8f3 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -12,7 +12,6 @@ import * as ModelsDev from "@opencode-ai/core/models" import { Auth } from "../auth" import { Env } from "../env" import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { Flag } from "@opencode-ai/core/flag/flag" import { iife } from "@/util/iife" import { Global } from "@opencode-ai/core/global" import path from "path" @@ -27,6 +26,7 @@ import { optionalOmitUndefined } from "@opencode-ai/core/schema" import * as ProviderTransform from "./transform" import { ModelID, ProviderID } from "./schema" import { ModelStatus } from "./model-status" +import { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "provider" }) @@ -1127,18 +1127,18 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { } } -function suggestionModelIDs(provider: Info | undefined) { +function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels: boolean) { if (!provider) return [] return Object.keys(provider.models).filter((id) => { const model = provider.models[id] if (model.status === "deprecated") return false - if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) return false + if (model.status === "alpha" && !enableExperimentalModels) return false return true }) } -function modelSuggestions(provider: Info | undefined, modelID: ModelID) { - const available = suggestionModelIDs(provider) +function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) { + const available = suggestionModelIDs(provider, enableExperimentalModels) const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target) if (fuzzy.length) return fuzzy const query = modelID @@ -1159,7 +1159,7 @@ function modelSuggestions(provider: Info | undefined, modelID: ModelID) { .map((item) => item.id) } -const layer = Layer.effect( +export const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* AppFileSystem.Service @@ -1168,6 +1168,7 @@ const layer = Layer.effect( const env = yield* Env.Service const plugin = yield* Plugin.Service const modelsDevSvc = yield* ModelsDev.Service + const runtimeFlags = yield* RuntimeFlags.Service const state = yield* InstanceState.make(() => Effect.gen(function* () { @@ -1460,7 +1461,7 @@ const layer = Layer.effect( (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") ) delete provider.models[modelID] - if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] + if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID] if (model.status === "deprecated") delete provider.models[modelID] if ( (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || @@ -1656,7 +1657,7 @@ const layer = Layer.effect( if (!provider) { const catalogProvider = s.catalog[providerID] const suggestions = catalogProvider - ? modelSuggestions(catalogProvider, modelID) + ? modelSuggestions(catalogProvider, modelID, runtimeFlags.enableExperimentalModels) : fuzzysort .go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 }) .map((m) => m.target) @@ -1665,8 +1666,10 @@ const layer = Layer.effect( const info = provider.models[modelID] if (!info) { - const current = modelSuggestions(provider, modelID) - const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID) + const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels) + const suggestions = current.length + ? current + : modelSuggestions(s.catalog[providerID], modelID, runtimeFlags.enableExperimentalModels) return yield* new ModelNotFoundError({ providerID, modelID, suggestions }) } return info @@ -1814,6 +1817,7 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer), Layer.provide(ModelsDev.defaultLayer), + Layer.provide(RuntimeFlags.defaultLayer), ), ) diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index aa611ee81d..3397adb018 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -19,6 +19,7 @@ describe("RuntimeFlags", () => { OPENCODE_EXPERIMENTAL: "true", OPENCODE_ENABLE_EXA: "true", OPENCODE_ENABLE_PARALLEL: "true", + OPENCODE_ENABLE_EXPERIMENTAL_MODELS: "true", OPENCODE_ENABLE_QUESTION_TOOL: "true", OPENCODE_CLIENT: "desktop", }), @@ -29,6 +30,7 @@ describe("RuntimeFlags", () => { expect(flags.disableDefaultPlugins).toBe(true) expect(flags.enableExa).toBe(true) expect(flags.enableParallel).toBe(true) + expect(flags.enableExperimentalModels).toBe(true) expect(flags.enableQuestionTool).toBe(true) expect(flags.experimentalScout).toBe(true) expect(flags.experimentalBackgroundSubagents).toBe(true) @@ -48,6 +50,7 @@ describe("RuntimeFlags", () => { expect(flags.disableDefaultPlugins).toBe(true) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) + expect(flags.enableExperimentalModels).toBe(false) expect(flags.client).toBe("cli") }), ) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 269064a600..18b0937efd 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -12,15 +12,30 @@ import { Provider } from "@/provider/provider" import { ProviderID, ModelID } from "../../src/provider/schema" import { Filesystem } from "@/util/filesystem" import { Env } from "../../src/env" -import { Effect } from "effect" +import { Effect, Layer } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" import { makeRuntime } from "../../src/effect/run-service" import { testEffect } from "../lib/effect" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Config } from "@/config/config" +import { Auth } from "@/auth" +import { RuntimeFlags } from "@/effect/runtime-flags" const env = makeRuntime(Env.Service, Env.defaultLayer) const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v)) const remove = (k: string) => env.runSync((svc) => svc.remove(k)) +const providerLayer = (flags: Partial = {}) => + Provider.layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(ModelsDev.defaultLayer), + Layer.provide(RuntimeFlags.layer(flags)), + ) + async function run(fn: (provider: Provider.Interface) => Effect.Effect) { return AppRuntime.runPromise( Effect.gen(function* () { @@ -73,6 +88,29 @@ function paid(providers: Awaited>) { } const it = testEffect(Provider.defaultLayer) +const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true })) + +const alphaProviderConfig = { + provider: { + "custom-provider": { + name: "Custom Provider", + npm: "@ai-sdk/openai-compatible", + api: "https://api.custom.com/v1", + models: { + "active-model": { + name: "Active Model", + }, + "alpha-model": { + name: "Alpha Model", + status: "alpha" as const, + }, + }, + options: { + apiKey: "custom-key", + }, + }, + }, +} test("provider loaded from env variable", async () => { await using tmp = await tmpdir({ @@ -305,6 +343,26 @@ test("custom provider with npm package", async () => { }) }) +it.instance( + "filters alpha provider models by default", + Effect.gen(function* () { + const providers = yield* Provider.Service.use((provider) => provider.list()) + expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined() + expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeUndefined() + }), + { config: alphaProviderConfig }, +) + +experimentalModels.instance( + "includes alpha provider models when experimental models are enabled", + Effect.gen(function* () { + const providers = yield* Provider.Service.use((provider) => provider.list()) + expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined() + expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeDefined() + }), + { config: alphaProviderConfig }, +) + test("custom DeepSeek openai-compatible model defaults interleaved reasoning field", async () => { await using tmp = await tmpdir({ init: async (dir) => {