Skip to content

Commit 34198f4

Browse files
authored
refactor(provider): use runtime flag for experimental models (anomalyco#27606)
1 parent cccdeef commit 34198f4

5 files changed

Lines changed: 77 additions & 12 deletions

File tree

packages/core/src/flag/flag.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export const Flag = {
3333
OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"],
3434
OPENCODE_DISABLE_DEFAULT_PLUGINS: truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
3535
OPENCODE_DISABLE_LSP_DOWNLOAD: truthy("OPENCODE_DISABLE_LSP_DOWNLOAD"),
36-
OPENCODE_ENABLE_EXPERIMENTAL_MODELS: truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"),
3736
OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"),
3837
OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"),
3938
OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"),

packages/opencode/src/effect/runtime-flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
2222
enabled: bool("OPENCODE_ENABLE_PARALLEL"),
2323
legacy: bool("OPENCODE_EXPERIMENTAL_PARALLEL"),
2424
}).pipe(Config.map((flags) => flags.enabled || flags.legacy)),
25+
enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"),
2526
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
2627
experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"),
2728
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),

packages/opencode/src/provider/provider.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import * as ModelsDev from "@opencode-ai/core/models"
1212
import { Auth } from "../auth"
1313
import { Env } from "../env"
1414
import { InstallationVersion } from "@opencode-ai/core/installation/version"
15-
import { Flag } from "@opencode-ai/core/flag/flag"
1615
import { iife } from "@/util/iife"
1716
import { Global } from "@opencode-ai/core/global"
1817
import path from "path"
@@ -27,6 +26,7 @@ import { optionalOmitUndefined } from "@opencode-ai/core/schema"
2726
import * as ProviderTransform from "./transform"
2827
import { ModelID, ProviderID } from "./schema"
2928
import { ModelStatus } from "./model-status"
29+
import { RuntimeFlags } from "@/effect/runtime-flags"
3030

3131
const log = Log.create({ service: "provider" })
3232

@@ -1127,18 +1127,18 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
11271127
}
11281128
}
11291129

1130-
function suggestionModelIDs(provider: Info | undefined) {
1130+
function suggestionModelIDs(provider: Info | undefined, enableExperimentalModels: boolean) {
11311131
if (!provider) return []
11321132
return Object.keys(provider.models).filter((id) => {
11331133
const model = provider.models[id]
11341134
if (model.status === "deprecated") return false
1135-
if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) return false
1135+
if (model.status === "alpha" && !enableExperimentalModels) return false
11361136
return true
11371137
})
11381138
}
11391139

1140-
function modelSuggestions(provider: Info | undefined, modelID: ModelID) {
1141-
const available = suggestionModelIDs(provider)
1140+
function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) {
1141+
const available = suggestionModelIDs(provider, enableExperimentalModels)
11421142
const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
11431143
if (fuzzy.length) return fuzzy
11441144
const query = modelID
@@ -1159,7 +1159,7 @@ function modelSuggestions(provider: Info | undefined, modelID: ModelID) {
11591159
.map((item) => item.id)
11601160
}
11611161

1162-
const layer = Layer.effect(
1162+
export const layer = Layer.effect(
11631163
Service,
11641164
Effect.gen(function* () {
11651165
const fs = yield* AppFileSystem.Service
@@ -1168,6 +1168,7 @@ const layer = Layer.effect(
11681168
const env = yield* Env.Service
11691169
const plugin = yield* Plugin.Service
11701170
const modelsDevSvc = yield* ModelsDev.Service
1171+
const runtimeFlags = yield* RuntimeFlags.Service
11711172

11721173
const state = yield* InstanceState.make<State>(() =>
11731174
Effect.gen(function* () {
@@ -1460,7 +1461,7 @@ const layer = Layer.effect(
14601461
(providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
14611462
)
14621463
delete provider.models[modelID]
1463-
if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
1464+
if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID]
14641465
if (model.status === "deprecated") delete provider.models[modelID]
14651466
if (
14661467
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
@@ -1656,7 +1657,7 @@ const layer = Layer.effect(
16561657
if (!provider) {
16571658
const catalogProvider = s.catalog[providerID]
16581659
const suggestions = catalogProvider
1659-
? modelSuggestions(catalogProvider, modelID)
1660+
? modelSuggestions(catalogProvider, modelID, runtimeFlags.enableExperimentalModels)
16601661
: fuzzysort
16611662
.go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
16621663
.map((m) => m.target)
@@ -1665,8 +1666,10 @@ const layer = Layer.effect(
16651666

16661667
const info = provider.models[modelID]
16671668
if (!info) {
1668-
const current = modelSuggestions(provider, modelID)
1669-
const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID)
1669+
const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels)
1670+
const suggestions = current.length
1671+
? current
1672+
: modelSuggestions(s.catalog[providerID], modelID, runtimeFlags.enableExperimentalModels)
16701673
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
16711674
}
16721675
return info
@@ -1814,6 +1817,7 @@ export const defaultLayer = Layer.suspend(() =>
18141817
Layer.provide(Auth.defaultLayer),
18151818
Layer.provide(Plugin.defaultLayer),
18161819
Layer.provide(ModelsDev.defaultLayer),
1820+
Layer.provide(RuntimeFlags.defaultLayer),
18171821
),
18181822
)
18191823

packages/opencode/test/effect/runtime-flags.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe("RuntimeFlags", () => {
1919
OPENCODE_EXPERIMENTAL: "true",
2020
OPENCODE_ENABLE_EXA: "true",
2121
OPENCODE_ENABLE_PARALLEL: "true",
22+
OPENCODE_ENABLE_EXPERIMENTAL_MODELS: "true",
2223
OPENCODE_ENABLE_QUESTION_TOOL: "true",
2324
OPENCODE_CLIENT: "desktop",
2425
}),
@@ -29,6 +30,7 @@ describe("RuntimeFlags", () => {
2930
expect(flags.disableDefaultPlugins).toBe(true)
3031
expect(flags.enableExa).toBe(true)
3132
expect(flags.enableParallel).toBe(true)
33+
expect(flags.enableExperimentalModels).toBe(true)
3234
expect(flags.enableQuestionTool).toBe(true)
3335
expect(flags.experimentalScout).toBe(true)
3436
expect(flags.experimentalBackgroundSubagents).toBe(true)
@@ -48,6 +50,7 @@ describe("RuntimeFlags", () => {
4850
expect(flags.disableDefaultPlugins).toBe(true)
4951
expect(flags.disableClaudeCodeSkills).toBe(false)
5052
expect(flags.enableExa).toBe(false)
53+
expect(flags.enableExperimentalModels).toBe(false)
5154
expect(flags.client).toBe("cli")
5255
}),
5356
)

packages/opencode/test/provider/provider.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,30 @@ import { Provider } from "@/provider/provider"
1212
import { ProviderID, ModelID } from "../../src/provider/schema"
1313
import { Filesystem } from "@/util/filesystem"
1414
import { Env } from "../../src/env"
15-
import { Effect } from "effect"
15+
import { Effect, Layer } from "effect"
1616
import { AppRuntime } from "../../src/effect/app-runtime"
1717
import { makeRuntime } from "../../src/effect/run-service"
1818
import { testEffect } from "../lib/effect"
19+
import { AppFileSystem } from "@opencode-ai/core/filesystem"
20+
import { Config } from "@/config/config"
21+
import { Auth } from "@/auth"
22+
import { RuntimeFlags } from "@/effect/runtime-flags"
1923

2024
const env = makeRuntime(Env.Service, Env.defaultLayer)
2125
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
2226
const remove = (k: string) => env.runSync((svc) => svc.remove(k))
2327

28+
const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
29+
Provider.layer.pipe(
30+
Layer.provide(AppFileSystem.defaultLayer),
31+
Layer.provide(Env.defaultLayer),
32+
Layer.provide(Config.defaultLayer),
33+
Layer.provide(Auth.defaultLayer),
34+
Layer.provide(Plugin.defaultLayer),
35+
Layer.provide(ModelsDev.defaultLayer),
36+
Layer.provide(RuntimeFlags.layer(flags)),
37+
)
38+
2439
async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
2540
return AppRuntime.runPromise(
2641
Effect.gen(function* () {
@@ -73,6 +88,29 @@ function paid(providers: Awaited<ReturnType<typeof list>>) {
7388
}
7489

7590
const it = testEffect(Provider.defaultLayer)
91+
const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
92+
93+
const alphaProviderConfig = {
94+
provider: {
95+
"custom-provider": {
96+
name: "Custom Provider",
97+
npm: "@ai-sdk/openai-compatible",
98+
api: "https://api.custom.com/v1",
99+
models: {
100+
"active-model": {
101+
name: "Active Model",
102+
},
103+
"alpha-model": {
104+
name: "Alpha Model",
105+
status: "alpha" as const,
106+
},
107+
},
108+
options: {
109+
apiKey: "custom-key",
110+
},
111+
},
112+
},
113+
}
76114

77115
test("provider loaded from env variable", async () => {
78116
await using tmp = await tmpdir({
@@ -305,6 +343,26 @@ test("custom provider with npm package", async () => {
305343
})
306344
})
307345

346+
it.instance(
347+
"filters alpha provider models by default",
348+
Effect.gen(function* () {
349+
const providers = yield* Provider.Service.use((provider) => provider.list())
350+
expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined()
351+
expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeUndefined()
352+
}),
353+
{ config: alphaProviderConfig },
354+
)
355+
356+
experimentalModels.instance(
357+
"includes alpha provider models when experimental models are enabled",
358+
Effect.gen(function* () {
359+
const providers = yield* Provider.Service.use((provider) => provider.list())
360+
expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined()
361+
expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeDefined()
362+
}),
363+
{ config: alphaProviderConfig },
364+
)
365+
308366
test("custom DeepSeek openai-compatible model defaults interleaved reasoning field", async () => {
309367
await using tmp = await tmpdir({
310368
init: async (dir) => {

0 commit comments

Comments
 (0)