refactor: isolate legacy flags

This commit is contained in:
Dax Raad 2026-07-20 21:42:02 -04:00
commit 43c08387f1
65 changed files with 282 additions and 147 deletions

View file

@ -155,6 +155,8 @@ export interface Interface {
export const Options = Schema.Struct({
project: Schema.optional(Schema.Boolean),
file: Schema.optional(Schema.String),
content: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
@ -293,14 +295,39 @@ export const layer = (options?: Options) => Layer.effect(
Effect.map((entries) => entries.flat()),
)
const file = options?.file
const explicit = file
? yield* loadFile(path.resolve(file)).pipe(
Effect.map((config) => [
...(config ? [config] : []),
new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }),
]),
Effect.orDie,
)
: []
const content = options?.content
? yield* ConfigVariable.substitute({
type: "virtual",
source: "OPENCODE_CONFIG_CONTENT",
dir: location.directory,
text: options.content,
}).pipe(
Effect.map(parseInfo),
Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])),
Effect.orDie,
)
: []
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
return [
...claude,
...agents,
...(supplementary[0] ?? []),
...explicit,
...direct,
...supplementary.slice(1).flat(),
...(yield* loadWellknown().pipe(Effect.orDie)),
...content,
]
})

View file

@ -1,84 +0,0 @@
import { Config } from "effect"
export function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"]
const fff = process.env["OPENCODE_DISABLE_FFF"]
function enabledByExperimental(key: string) {
return process.env[key] === undefined ? truthy("OPENCODE_EXPERIMENTAL") : truthy(key)
}
export const Flag = {
// V2: ServerOptions.observability.endpoint
OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
// V2: ServerOptions.observability.headers
OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"],
OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"),
// V2: ServerOptions.windows.gitbash
OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"],
OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"],
OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"],
OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"),
OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"),
OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"),
OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"),
OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"),
OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"),
// V2: ServerOptions.models.fetch
OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"),
OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"),
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
// V2: ServerOptions.fs.fff
OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"),
// V2: ServerOptions.fs.filewatcher
OPENCODE_DISABLE_FILEWATCHER: truthy("OPENCODE_DISABLE_FILEWATCHER"),
// Experimental
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT:
copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"),
// V2: ServerOptions.models.url
OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
// V2: ServerOptions.models.file
OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"],
// V2: ServerOptions.database.path
OPENCODE_DB: process.env["OPENCODE_DB"],
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
// Evaluated at access time (not module load) because tests, the CLI, and
// external tooling set these env vars at runtime.
// V2: ServerOptions.config.project
get OPENCODE_DISABLE_PROJECT_CONFIG() {
return truthy("OPENCODE_DISABLE_PROJECT_CONFIG")
},
get OPENCODE_EXPERIMENTAL_REFERENCES() {
return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES")
},
get OPENCODE_TUI_CONFIG() {
return process.env["OPENCODE_TUI_CONFIG"]
},
// V2: ServerOptions.config.directory
get OPENCODE_CONFIG_DIR() {
return process.env["OPENCODE_CONFIG_DIR"]
},
get OPENCODE_PURE() {
return truthy("OPENCODE_PURE")
},
get OPENCODE_PERMISSION() {
return process.env["OPENCODE_PERMISSION"]
},
get OPENCODE_PLUGIN_META_FILE() {
return process.env["OPENCODE_PLUGIN_META_FILE"]
},
get OPENCODE_CLIENT() {
return process.env["OPENCODE_CLIENT"] ?? "cli"
},
}

View file

@ -4,7 +4,6 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Money } from "@opencode-ai/schema/money"
import { Global } from "./global"
import { Flag } from "./flag/flag"
import { Flock } from "./util/flock"
import { Hash } from "./util/hash"
import { FSUtil } from "./fs-util"
@ -18,8 +17,6 @@ import { ProviderV2 } from "./provider"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
type Cost = {
readonly input: Money.USDPerMillionTokens
readonly output: Money.USDPerMillionTokens
@ -534,6 +531,7 @@ export const Options = Schema.Struct({
url: Schema.optional(Schema.String),
file: Schema.optional(Schema.String),
fetch: Schema.optional(Schema.Boolean),
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
@ -556,6 +554,7 @@ export const layer = (options?: Options) => Layer.effect(
const source = options?.url ?? "https://models.dev"
const fetch = options?.fetch ?? true
const userAgent = `opencode/${InstallationChannel}/${InstallationVersion}/${options?.client ?? "cli"}`
const filepath = path.join(
Global.Path.cache,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
@ -572,7 +571,7 @@ export const layer = (options?: Options) => Layer.effect(
const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () {
return yield* HttpClientRequest.get(`${source}/api.json`).pipe(
HttpClientRequest.setHeader("User-Agent", USER_AGENT),
HttpClientRequest.setHeader("User-Agent", userAgent),
http.execute,
Effect.flatMap((res) => res.text),
Effect.timeout("10 seconds"),

View file

@ -11,6 +11,7 @@ import { Otlp } from "./observability/otlp"
export const Options = Schema.Struct({
endpoint: Schema.optional(Schema.String),
headers: Schema.optional(Schema.String),
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
@ -18,6 +19,7 @@ export function layer(
options: Options = {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
client: process.env.OPENCODE_CLIENT ?? "cli",
},
) {
const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(

View file

@ -1,12 +1,12 @@
import { Layer } from "effect"
import { OtlpLogger } from "effect/unstable/observability"
import { Flag } from "../flag/flag"
import { InstallationChannel, InstallationVersion } from "../installation/version"
import { runID } from "./shared"
export interface Options {
readonly endpoint?: string
readonly headers?: string
readonly client?: string
}
function parseHeaders(value?: string) {
@ -38,14 +38,14 @@ function resourceAttributes() {
}
}
export function resource(): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } {
export function resource(client = "cli"): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } {
return {
serviceName: "opencode",
serviceVersion: InstallationVersion,
attributes: {
...resourceAttributes(),
"deployment.environment.name": InstallationChannel,
"opencode.client": Flag.OPENCODE_CLIENT,
"opencode.client": client,
"opencode.run": runID,
"service.instance.id": runID,
},
@ -55,7 +55,11 @@ export function resource(): { serviceName: string; serviceVersion: string; attri
export function loggers(options?: Options) {
if (!options?.endpoint) return []
return [
OtlpLogger.make({ url: `${options.endpoint}/v1/logs`, resource: resource(), headers: parseHeaders(options.headers) }),
OtlpLogger.make({
url: `${options.endpoint}/v1/logs`,
resource: resource(options.client),
headers: parseHeaders(options.headers),
}),
]
}
@ -73,7 +77,7 @@ export async function tracingLayer(options?: Options) {
context.setGlobalContextManager(manager)
return NodeSdk.layer(() => ({
resource: resource(),
resource: resource(options.client),
spanProcessor: new SdkBase.BatchSpanProcessor(
new OTLP.OTLPTraceExporter({
url: `${options.endpoint}/v1/traces`,

View file

@ -60,6 +60,7 @@ type Settings = {
}
type Dependencies = {
readonly headers?: SessionModelHeaders.Options
readonly events: EventV2.Interface
readonly llm: {
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
@ -258,7 +259,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: plan.model,
http: { headers: SessionModelHeaders.make(plan.session) },
http: { headers: SessionModelHeaders.make(plan.session, dependencies.headers) },
messages: [Message.user(plan.prompt)],
tools: [],
}),
@ -390,19 +391,23 @@ const make = (dependencies: Dependencies) => {
})
}
export const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const llm = yield* LLMClient.Service
const config = yield* Config.Service
const models = yield* SessionRunnerModel.Service
return make({ events, llm, models, config: settings(yield* config.entries()) })
return make({ events, llm, models, config: settings(yield* config.entries()), headers: options })
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node],
})
}
export const node = configured()

View file

@ -14,7 +14,7 @@ import { SessionRunnerModel } from "./runner/model"
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
import { toLLMMessages } from "./runner/to-llm-message"
const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
SessionGenerate.Service,
Effect.gen(function* () {
const context = yield* SessionContext.Service
@ -49,7 +49,7 @@ const layer = Layer.effect(
return (yield* llm.generate(
LLM.request({
model: model.model,
http: { headers: SessionModelHeaders.make(selection.session) },
http: { headers: SessionModelHeaders.make(selection.session, options) },
providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system,
messages: contextEvent.messages,
@ -62,8 +62,12 @@ const layer = Layer.effect(
}),
)
export const node = makeLocationNode({
service: SessionGenerate.Service,
layer,
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({
service: SessionGenerate.Service,
layer: layer(options),
deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient],
})
}
export const node = configured()

View file

@ -1,15 +1,23 @@
export * as SessionModelHeaders from "./model-headers"
import { Flag } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { SessionSchema } from "./schema"
import { Schema } from "effect"
export const make = (session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">) => ({
export const Options = Schema.Struct({
client: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
export const make = (
session: Pick<SessionSchema.Info, "id" | "parentID" | "projectID">,
options?: Options,
) => ({
"x-session-affinity": session.id,
"X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
"User-Agent": `opencode/${InstallationVersion}`,
"x-opencode-project": session.projectID,
"x-opencode-session": session.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
"x-opencode-client": options?.client ?? "cli",
})

View file

@ -39,7 +39,7 @@ export interface Interface {
/** Location-scoped outbound model-request preparation. */
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionModelRequest") {}
const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const hooks = yield* PluginHooks.Service
@ -81,7 +81,7 @@ const layer = Layer.effect(
const request = LLM.request({
model,
http: {
headers: SessionModelHeaders.make(session),
headers: SessionModelHeaders.make(session, options),
},
providerOptions: { openai: { promptCacheKey } },
system: contextEvent.system,
@ -112,8 +112,8 @@ const layer = Layer.effect(
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [PluginHooks.node, ToolRegistry.node],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({ service: Service, layer: layer(options), deps: [PluginHooks.node, ToolRegistry.node] })
}
export const node = configured()

View file

@ -17,6 +17,7 @@ import { SessionUsage } from "./usage"
const MAX_LENGTH = 100
type Dependencies = {
readonly headers?: SessionModelHeaders.Options
readonly events: EventV2.Interface
readonly llm: {
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
@ -66,7 +67,7 @@ const make = (dependencies: Dependencies) => {
.stream(
LLM.request({
model: resolved.model,
http: { headers: SessionModelHeaders.make(session) },
http: { headers: SessionModelHeaders.make(session, dependencies.headers) },
system: agent.system,
messages: [Message.user(firstUser.text)],
tools: [],
@ -102,7 +103,7 @@ const make = (dependencies: Dependencies) => {
return { generateForFirstPrompt }
}
export const layer = Layer.effect(
export const layer = (options?: SessionModelHeaders.Options) => Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
@ -110,15 +111,19 @@ export const layer = Layer.effect(
const agents = yield* AgentV2.Service
const models = yield* SessionRunnerModel.Service
const database = yield* Database.Service
const title = make({ events, llm, agents, models })
const title = make({ events, llm, agents, models, headers: options })
return Service.of({
generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session),
})
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node],
})
export function configured(options?: SessionModelHeaders.Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node],
})
}
export const node = configured()

View file

@ -5,7 +5,6 @@ import { ToolFailure } from "@opencode-ai/ai"
import { Context, Duration, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { makeLocationNode } from "../effect/app-node"
import { truthy } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { PositiveInt } from "../schema"
import { PermissionV2 } from "../permission"
@ -74,8 +73,13 @@ export const defaultConfigLayer = Layer.sync(ConfigService, () =>
process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
? process.env.OPENCODE_WEBSEARCH_PROVIDER
: undefined,
enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"),
enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
enableExa:
["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL?.toLowerCase() ?? "") ||
["1", "true"].includes(process.env.OPENCODE_ENABLE_EXA?.toLowerCase() ?? "") ||
["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_EXA?.toLowerCase() ?? ""),
enableParallel:
["1", "true"].includes(process.env.OPENCODE_ENABLE_PARALLEL?.toLowerCase() ?? "") ||
["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_PARALLEL?.toLowerCase() ?? ""),
exaApiKey: process.env.EXA_API_KEY,
parallelApiKey: process.env.PARALLEL_API_KEY,
}),