refactor(provider): use runtime flag for experimental models (#27606)
This commit is contained in:
@@ -33,7 +33,6 @@ export const Flag = {
|
|||||||
OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"],
|
OPENCODE_PERMISSION: process.env["OPENCODE_PERMISSION"],
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
|
OPENCODE_DISABLE_DEFAULT_PLUGINS: truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD: truthy("OPENCODE_DISABLE_LSP_DOWNLOAD"),
|
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_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"),
|
||||||
OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"),
|
OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"),
|
||||||
OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"),
|
OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
|
|||||||
enabled: bool("OPENCODE_ENABLE_PARALLEL"),
|
enabled: bool("OPENCODE_ENABLE_PARALLEL"),
|
||||||
legacy: bool("OPENCODE_EXPERIMENTAL_PARALLEL"),
|
legacy: bool("OPENCODE_EXPERIMENTAL_PARALLEL"),
|
||||||
}).pipe(Config.map((flags) => flags.enabled || flags.legacy)),
|
}).pipe(Config.map((flags) => flags.enabled || flags.legacy)),
|
||||||
|
enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"),
|
||||||
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
|
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
|
||||||
experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"),
|
experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"),
|
||||||
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),
|
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import * as ModelsDev from "@opencode-ai/core/models"
|
|||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { Env } from "../env"
|
import { Env } from "../env"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -27,6 +26,7 @@ import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
|||||||
import * as ProviderTransform from "./transform"
|
import * as ProviderTransform from "./transform"
|
||||||
import { ModelID, ProviderID } from "./schema"
|
import { ModelID, ProviderID } from "./schema"
|
||||||
import { ModelStatus } from "./model-status"
|
import { ModelStatus } from "./model-status"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
|
||||||
const log = Log.create({ service: "provider" })
|
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 []
|
if (!provider) return []
|
||||||
return Object.keys(provider.models).filter((id) => {
|
return Object.keys(provider.models).filter((id) => {
|
||||||
const model = provider.models[id]
|
const model = provider.models[id]
|
||||||
if (model.status === "deprecated") return false
|
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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSuggestions(provider: Info | undefined, modelID: ModelID) {
|
function modelSuggestions(provider: Info | undefined, modelID: ModelID, enableExperimentalModels: boolean) {
|
||||||
const available = suggestionModelIDs(provider)
|
const available = suggestionModelIDs(provider, enableExperimentalModels)
|
||||||
const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
|
const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target)
|
||||||
if (fuzzy.length) return fuzzy
|
if (fuzzy.length) return fuzzy
|
||||||
const query = modelID
|
const query = modelID
|
||||||
@@ -1159,7 +1159,7 @@ function modelSuggestions(provider: Info | undefined, modelID: ModelID) {
|
|||||||
.map((item) => item.id)
|
.map((item) => item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
@@ -1168,6 +1168,7 @@ const layer = Layer.effect(
|
|||||||
const env = yield* Env.Service
|
const env = yield* Env.Service
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const modelsDevSvc = yield* ModelsDev.Service
|
const modelsDevSvc = yield* ModelsDev.Service
|
||||||
|
const runtimeFlags = yield* RuntimeFlags.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(() =>
|
const state = yield* InstanceState.make<State>(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -1460,7 +1461,7 @@ const layer = Layer.effect(
|
|||||||
(providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
|
(providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
|
||||||
)
|
)
|
||||||
delete provider.models[modelID]
|
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 (model.status === "deprecated") delete provider.models[modelID]
|
||||||
if (
|
if (
|
||||||
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
|
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
|
||||||
@@ -1656,7 +1657,7 @@ const layer = Layer.effect(
|
|||||||
if (!provider) {
|
if (!provider) {
|
||||||
const catalogProvider = s.catalog[providerID]
|
const catalogProvider = s.catalog[providerID]
|
||||||
const suggestions = catalogProvider
|
const suggestions = catalogProvider
|
||||||
? modelSuggestions(catalogProvider, modelID)
|
? modelSuggestions(catalogProvider, modelID, runtimeFlags.enableExperimentalModels)
|
||||||
: fuzzysort
|
: fuzzysort
|
||||||
.go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
|
.go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 })
|
||||||
.map((m) => m.target)
|
.map((m) => m.target)
|
||||||
@@ -1665,8 +1666,10 @@ const layer = Layer.effect(
|
|||||||
|
|
||||||
const info = provider.models[modelID]
|
const info = provider.models[modelID]
|
||||||
if (!info) {
|
if (!info) {
|
||||||
const current = modelSuggestions(provider, modelID)
|
const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels)
|
||||||
const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID)
|
const suggestions = current.length
|
||||||
|
? current
|
||||||
|
: modelSuggestions(s.catalog[providerID], modelID, runtimeFlags.enableExperimentalModels)
|
||||||
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
|
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
@@ -1814,6 +1817,7 @@ export const defaultLayer = Layer.suspend(() =>
|
|||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
Layer.provide(ModelsDev.defaultLayer),
|
Layer.provide(ModelsDev.defaultLayer),
|
||||||
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe("RuntimeFlags", () => {
|
|||||||
OPENCODE_EXPERIMENTAL: "true",
|
OPENCODE_EXPERIMENTAL: "true",
|
||||||
OPENCODE_ENABLE_EXA: "true",
|
OPENCODE_ENABLE_EXA: "true",
|
||||||
OPENCODE_ENABLE_PARALLEL: "true",
|
OPENCODE_ENABLE_PARALLEL: "true",
|
||||||
|
OPENCODE_ENABLE_EXPERIMENTAL_MODELS: "true",
|
||||||
OPENCODE_ENABLE_QUESTION_TOOL: "true",
|
OPENCODE_ENABLE_QUESTION_TOOL: "true",
|
||||||
OPENCODE_CLIENT: "desktop",
|
OPENCODE_CLIENT: "desktop",
|
||||||
}),
|
}),
|
||||||
@@ -29,6 +30,7 @@ describe("RuntimeFlags", () => {
|
|||||||
expect(flags.disableDefaultPlugins).toBe(true)
|
expect(flags.disableDefaultPlugins).toBe(true)
|
||||||
expect(flags.enableExa).toBe(true)
|
expect(flags.enableExa).toBe(true)
|
||||||
expect(flags.enableParallel).toBe(true)
|
expect(flags.enableParallel).toBe(true)
|
||||||
|
expect(flags.enableExperimentalModels).toBe(true)
|
||||||
expect(flags.enableQuestionTool).toBe(true)
|
expect(flags.enableQuestionTool).toBe(true)
|
||||||
expect(flags.experimentalScout).toBe(true)
|
expect(flags.experimentalScout).toBe(true)
|
||||||
expect(flags.experimentalBackgroundSubagents).toBe(true)
|
expect(flags.experimentalBackgroundSubagents).toBe(true)
|
||||||
@@ -48,6 +50,7 @@ describe("RuntimeFlags", () => {
|
|||||||
expect(flags.disableDefaultPlugins).toBe(true)
|
expect(flags.disableDefaultPlugins).toBe(true)
|
||||||
expect(flags.disableClaudeCodeSkills).toBe(false)
|
expect(flags.disableClaudeCodeSkills).toBe(false)
|
||||||
expect(flags.enableExa).toBe(false)
|
expect(flags.enableExa).toBe(false)
|
||||||
|
expect(flags.enableExperimentalModels).toBe(false)
|
||||||
expect(flags.client).toBe("cli")
|
expect(flags.client).toBe("cli")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,15 +12,30 @@ import { Provider } from "@/provider/provider"
|
|||||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Env } from "../../src/env"
|
import { Env } from "../../src/env"
|
||||||
import { Effect } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||||
import { makeRuntime } from "../../src/effect/run-service"
|
import { makeRuntime } from "../../src/effect/run-service"
|
||||||
import { testEffect } from "../lib/effect"
|
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 env = makeRuntime(Env.Service, Env.defaultLayer)
|
||||||
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
|
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
|
||||||
const remove = (k: string) => env.runSync((svc) => svc.remove(k))
|
const remove = (k: string) => env.runSync((svc) => svc.remove(k))
|
||||||
|
|
||||||
|
const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||||
|
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<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
|
async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
|
||||||
return AppRuntime.runPromise(
|
return AppRuntime.runPromise(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -73,6 +88,29 @@ function paid(providers: Awaited<ReturnType<typeof list>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = testEffect(Provider.defaultLayer)
|
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 () => {
|
test("provider loaded from env variable", async () => {
|
||||||
await using tmp = await tmpdir({
|
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 () => {
|
test("custom DeepSeek openai-compatible model defaults interleaved reasoning field", async () => {
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
init: async (dir) => {
|
init: async (dir) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user