Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
Brendan Allan
c2f6135daf
Merge branch 'dev' into brendan/effect-env 2026-04-14 09:24:51 +08:00
Brendan Allan
3c74c0db30
Merge branch 'dev' into brendan/effect-env 2026-04-14 09:24:18 +08:00
Brendan Allan
1fa55dcca7
Merge branch 'dev' into brendan/effect-env 2026-04-14 08:25:37 +08:00
Brendan Allan
ad0ff9b651
formatting 2026-04-14 08:25:11 +08:00
Brendan Allan
9ef9fac6db
formatting 2026-04-14 08:10:18 +08:00
Brendan Allan
ef2faaac4c
convert wrappers to use sync runtime 2026-04-14 08:07:25 +08:00
Brendan Allan
e74c99320f
core: effectify Env service 2026-04-14 08:00:58 +08:00
7 changed files with 1103 additions and 1074 deletions

View file

@ -1161,13 +1161,17 @@ export namespace Config {
}), }),
) )
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Auth.Service | Account.Service> = export const layer: Layer.Layer<
Layer.effect( Service,
never,
AppFileSystem.Service | Auth.Service | Account.Service | Env.Service
> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
const authSvc = yield* Auth.Service const authSvc = yield* Auth.Service
const accountSvc = yield* Account.Service const accountSvc = yield* Account.Service
const env = yield* Env.Service
const readConfigFile = Effect.fnUntraced(function* (filepath: string) { const readConfigFile = Effect.fnUntraced(function* (filepath: string) {
return yield* fs.readFileString(filepath).pipe( return yield* fs.readFileString(filepath).pipe(
@ -1187,10 +1191,7 @@ export namespace Config {
const source = "path" in options ? options.path : options.source const source = "path" in options ? options.path : options.source
const isFile = "path" in options const isFile = "path" in options
const data = yield* Effect.promise(() => const data = yield* Effect.promise(() =>
ConfigPaths.parseText( ConfigPaths.parseText(text, "path" in options ? options.path : { source: options.source, dir: options.dir }),
text,
"path" in options ? options.path : { source: options.source, dir: options.dir },
),
) )
const normalized = (() => { const normalized = (() => {
@ -1358,11 +1359,7 @@ export namespace Config {
return "global" return "global"
}) })
const track = Effect.fnUntraced(function* ( const track = Effect.fnUntraced(function* (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) {
source: string,
list: PluginSpec[] | undefined,
kind?: PluginScope,
) {
if (!list?.length) return if (!list?.length) return
const hit = kind ?? (yield* scope(source)) const hit = kind ?? (yield* scope(source))
const plugins = deduplicatePluginOrigins([ const plugins = deduplicatePluginOrigins([
@ -1482,7 +1479,7 @@ export namespace Config {
) )
if (Option.isSome(tokenOpt)) { if (Option.isSome(tokenOpt)) {
process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
Env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
} }
activeOrgName = activeOrg.org.name activeOrgName = activeOrg.org.name
@ -1659,5 +1656,6 @@ export namespace Config {
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Auth.defaultLayer), Layer.provide(Auth.defaultLayer),
Layer.provide(Account.defaultLayer), Layer.provide(Account.defaultLayer),
Layer.provide(Env.defaultLayer),
) )
} }

View file

@ -1,28 +1,54 @@
import { Instance } from "../project/instance" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Context, Effect, Layer } from "effect"
export namespace Env { export namespace Env {
const state = Instance.state(() => { type State = Record<string, string | undefined>
export interface Interface {
readonly get: (key: string) => Effect.Effect<string | undefined>
readonly all: () => Effect.Effect<State>
readonly set: (key: string, value: string) => Effect.Effect<void>
readonly remove: (key: string) => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Env") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const state = yield* InstanceState.make<State>(
Effect.fn("Env.state")(() =>
Effect.succeed(
// Create a shallow copy to isolate environment per instance // Create a shallow copy to isolate environment per instance
// Prevents parallel tests from interfering with each other's env vars // Prevents parallel tests from interfering with each other's env vars
return { ...process.env } as Record<string, string | undefined> { ...process.env } as State,
),
),
)
const get = Effect.fn("Env.get")((key: string) => InstanceState.use(state, (env) => env[key]))
const all = Effect.fn("Env.all")(() => InstanceState.get(state))
const set = Effect.fn("Env.set")(function* (key: string, value: string) {
const env = yield* InstanceState.get(state)
env[key] = value
}) })
export function get(key: string) { const remove = Effect.fn("Env.remove")(function* (key: string) {
const env = state() const env = yield* InstanceState.get(state)
return env[key] delete env[key]
} })
export function all() { return Service.of({ get, all, set, remove })
return state() }),
} )
export const defaultLayer = layer
const rt = makeRuntime(Service, layer)
export function set(key: string, value: string) { export function set(key: string, value: string) {
const env = state() return rt.runSync((svc) => svc.set(key, value))
env[key] = value
}
export function remove(key: string) {
const env = state()
delete env[key]
} }
} }

View file

@ -116,8 +116,8 @@ export namespace Provider {
}) })
} }
function e2eURL() { function e2eURL(env: Record<string, string | undefined>) {
const url = Env.get("OPENCODE_E2E_LLM_URL") const url = env["OPENCODE_E2E_LLM_URL"]
if (typeof url !== "string" || url === "") return if (typeof url !== "string" || url === "") return
return url return url
} }
@ -172,7 +172,7 @@ export namespace Provider {
return sdk.responses === undefined && sdk.chat === undefined return sdk.responses === undefined && sdk.chat === undefined
} }
function custom(dep: CustomDep): Record<string, CustomLoader> { function custom(dep: CustomDep, env: Env.Interface): Record<string, CustomLoader> {
return { return {
anthropic: () => anthropic: () =>
Effect.succeed({ Effect.succeed({
@ -184,9 +184,9 @@ export namespace Provider {
}, },
}), }),
opencode: Effect.fnUntraced(function* (input: Info) { opencode: Effect.fnUntraced(function* (input: Info) {
const env = Env.all() const vals = yield* env.all()
const hasKey = iife(() => { const hasKey = iife(() => {
if (input.env.some((item) => env[item])) return true if (input.env.some((item) => vals[item])) return true
return false return false
}) })
const ok = const ok =
@ -231,14 +231,13 @@ export namespace Provider {
}, },
options: {}, options: {},
}), }),
azure: (provider) => { azure: Effect.fnUntraced(function* (provider: Info) {
const resource = iife(() => { const resource =
const name = provider.options?.resourceName typeof provider.options?.resourceName === "string" && provider.options.resourceName.trim() !== ""
if (typeof name === "string" && name.trim() !== "") return name ? provider.options.resourceName
return Env.get("AZURE_RESOURCE_NAME") : yield* env.get("AZURE_RESOURCE_NAME")
})
return Effect.succeed({ return {
autoload: false, autoload: false,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) { async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
if (useLanguageModel(sdk)) return sdk.languageModel(modelID) if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
@ -254,11 +253,11 @@ export namespace Provider {
...(resource && { AZURE_RESOURCE_NAME: resource }), ...(resource && { AZURE_RESOURCE_NAME: resource }),
} }
}, },
}) }
}, }),
"azure-cognitive-services": () => { "azure-cognitive-services": Effect.fnUntraced(function* () {
const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") const resource = yield* env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
return Effect.succeed({ return {
autoload: false, autoload: false,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) { async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
if (useLanguageModel(sdk)) return sdk.languageModel(modelID) if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
@ -269,25 +268,25 @@ export namespace Provider {
} }
}, },
options: { options: {
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, baseURL: resource ? `https://${resource}.cognitiveservices.azure.com/openai` : undefined,
},
})
}, },
}
}),
"amazon-bedrock": Effect.fnUntraced(function* () { "amazon-bedrock": Effect.fnUntraced(function* () {
const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"] const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"]
const auth = yield* dep.auth("amazon-bedrock") const auth = yield* dep.auth("amazon-bedrock")
// Region precedence: 1) config file, 2) env var, 3) default // Region precedence: 1) config file, 2) env var, 3) default
const configRegion = providerConfig?.options?.region const configRegion = providerConfig?.options?.region
const envRegion = Env.get("AWS_REGION") const envRegion = yield* env.get("AWS_REGION")
const defaultRegion = configRegion ?? envRegion ?? "us-east-1" const defaultRegion = configRegion ?? envRegion ?? "us-east-1"
// Profile: config file takes precedence over env var // Profile: config file takes precedence over env var
const configProfile = providerConfig?.options?.profile const configProfile = providerConfig?.options?.profile
const envProfile = Env.get("AWS_PROFILE") const envProfile = yield* env.get("AWS_PROFILE")
const profile = configProfile ?? envProfile const profile = configProfile ?? envProfile
const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID") const awsAccessKeyId = yield* env.get("AWS_ACCESS_KEY_ID")
// TODO: Using process.env directly because Env.set only updates a process.env shallow copy, // TODO: Using process.env directly because Env.set only updates a process.env shallow copy,
// until the scope of the Env API is clarified (test only or runtime?) // until the scope of the Env API is clarified (test only or runtime?)
@ -301,7 +300,7 @@ export namespace Provider {
return undefined return undefined
}) })
const awsWebIdentityTokenFile = Env.get("AWS_WEB_IDENTITY_TOKEN_FILE") const awsWebIdentityTokenFile = yield* env.get("AWS_WEB_IDENTITY_TOKEN_FILE")
const containerCreds = Boolean( const containerCreds = Boolean(
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
@ -439,24 +438,24 @@ export namespace Provider {
}, },
}, },
}), }),
"google-vertex": (provider) => { "google-vertex": Effect.fnUntraced(function* (provider: Info) {
const project = const project =
provider.options?.project ?? provider.options?.project ??
Env.get("GOOGLE_CLOUD_PROJECT") ?? (yield* env.get("GOOGLE_CLOUD_PROJECT")) ??
Env.get("GCP_PROJECT") ?? (yield* env.get("GCP_PROJECT")) ??
Env.get("GCLOUD_PROJECT") (yield* env.get("GCLOUD_PROJECT"))
const location = String( const location = String(
provider.options?.location ?? provider.options?.location ??
Env.get("GOOGLE_VERTEX_LOCATION") ?? (yield* env.get("GOOGLE_VERTEX_LOCATION")) ??
Env.get("GOOGLE_CLOUD_LOCATION") ?? (yield* env.get("GOOGLE_CLOUD_LOCATION")) ??
Env.get("VERTEX_LOCATION") ?? (yield* env.get("VERTEX_LOCATION")) ??
"us-central1", "us-central1",
) )
const autoload = Boolean(project) const autoload = Boolean(project)
if (!autoload) return Effect.succeed({ autoload: false }) if (!autoload) return { autoload: false }
return Effect.succeed({ return {
autoload: true, autoload: true,
vars(_options: Record<string, any>) { vars(_options: Record<string, any>) {
const endpoint = const endpoint =
@ -485,14 +484,17 @@ export namespace Provider {
const id = String(modelID).trim() const id = String(modelID).trim()
return sdk.languageModel(id) return sdk.languageModel(id)
}, },
}) }
}, }),
"google-vertex-anthropic": () => { "google-vertex-anthropic": Effect.fnUntraced(function* () {
const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT") const project =
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global" (yield* env.get("GOOGLE_CLOUD_PROJECT")) ??
(yield* env.get("GCP_PROJECT")) ??
(yield* env.get("GCLOUD_PROJECT"))
const location = (yield* env.get("GOOGLE_CLOUD_LOCATION")) ?? (yield* env.get("VERTEX_LOCATION")) ?? "global"
const autoload = Boolean(project) const autoload = Boolean(project)
if (!autoload) return Effect.succeed({ autoload: false }) if (!autoload) return { autoload: false }
return Effect.succeed({ return {
autoload: true, autoload: true,
options: { options: {
project, project,
@ -502,8 +504,8 @@ export namespace Provider {
const id = String(modelID).trim() const id = String(modelID).trim()
return sdk.languageModel(id) return sdk.languageModel(id)
}, },
}) }
}, }),
"sap-ai-core": Effect.fnUntraced(function* () { "sap-ai-core": Effect.fnUntraced(function* () {
const auth = yield* dep.auth("sap-ai-core") const auth = yield* dep.auth("sap-ai-core")
// TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env), // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env),
@ -539,14 +541,11 @@ export namespace Provider {
}, },
}), }),
gitlab: Effect.fnUntraced(function* (input: Info) { gitlab: Effect.fnUntraced(function* (input: Info) {
const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com" const instanceUrl = (yield* env.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
const auth = yield* dep.auth(input.id) const auth = yield* dep.auth(input.id)
const apiKey = yield* Effect.sync(() => { const apiKey =
if (auth?.type === "oauth") return auth.access auth?.type === "oauth" ? auth.access : auth?.type === "api" ? auth.key : yield* env.get("GITLAB_TOKEN")
if (auth?.type === "api") return auth.key
return Env.get("GITLAB_TOKEN")
})
const providerConfig = (yield* dep.config()).provider?.["gitlab"] const providerConfig = (yield* dep.config()).provider?.["gitlab"]
@ -682,7 +681,7 @@ export namespace Provider {
const auth = yield* dep.auth(input.id) const auth = yield* dep.auth(input.id)
const accountId = const accountId =
Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined) (yield* env.get("CLOUDFLARE_ACCOUNT_ID")) || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
if (!accountId) if (!accountId)
return { return {
autoload: false, autoload: false,
@ -694,7 +693,7 @@ export namespace Provider {
} }
const apiKey = yield* Effect.gen(function* () { const apiKey = yield* Effect.gen(function* () {
const envToken = Env.get("CLOUDFLARE_API_KEY") const envToken = yield* env.get("CLOUDFLARE_API_KEY")
if (envToken) return envToken if (envToken) return envToken
if (auth?.type === "api") return auth.key if (auth?.type === "api") return auth.key
return undefined return undefined
@ -724,9 +723,9 @@ export namespace Provider {
const auth = yield* dep.auth(input.id) const auth = yield* dep.auth(input.id)
const accountId = const accountId =
Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined) (yield* env.get("CLOUDFLARE_ACCOUNT_ID")) || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
const gateway = const gateway =
Env.get("CLOUDFLARE_GATEWAY_ID") || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) (yield* env.get("CLOUDFLARE_GATEWAY_ID")) || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
if (!accountId || !gateway) { if (!accountId || !gateway) {
const missing = [ const missing = [
@ -745,7 +744,7 @@ export namespace Provider {
// Get API token from env or auth - required for authenticated gateways // Get API token from env or auth - required for authenticated gateways
const apiToken = yield* Effect.gen(function* () { const apiToken = yield* Effect.gen(function* () {
const envToken = Env.get("CLOUDFLARE_API_TOKEN") || Env.get("CF_AIG_TOKEN") const envToken = (yield* env.get("CLOUDFLARE_API_TOKEN")) || (yield* env.get("CF_AIG_TOKEN"))
if (envToken) return envToken if (envToken) return envToken
if (auth?.type === "api") return auth.key if (auth?.type === "api") return auth.key
return undefined return undefined
@ -1030,14 +1029,18 @@ export namespace Provider {
} }
} }
const layer: Layer.Layer<Service, never, Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service> = const layer: Layer.Layer<
Layer.effect( Service,
never,
Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service
> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
const config = yield* Config.Service const config = yield* Config.Service
const auth = yield* Auth.Service const auth = yield* Auth.Service
const plugin = yield* Plugin.Service const plugin = yield* Plugin.Service
const env = yield* Env.Service
const state = yield* InstanceState.make<State>(() => const state = yield* InstanceState.make<State>(() =>
Effect.gen(function* () { Effect.gen(function* () {
@ -1142,20 +1145,13 @@ export namespace Provider {
pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
}, },
output: { output: {
text: text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
audio: audio:
model.modalities?.output?.includes("audio") ?? model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
existingModel?.capabilities.output.audio ??
false,
image: image:
model.modalities?.output?.includes("image") ?? model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
existingModel?.capabilities.output.image ??
false,
video: video:
model.modalities?.output?.includes("video") ?? model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
existingModel?.capabilities.output.video ??
false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
}, },
interleaved: model.interleaved ?? false, interleaved: model.interleaved ?? false,
@ -1190,11 +1186,11 @@ export namespace Provider {
} }
// load env // load env
const env = Env.all() const vals = yield* env.all()
for (const [id, provider] of Object.entries(database)) { for (const [id, provider] of Object.entries(database)) {
const providerID = ProviderID.make(id) const providerID = ProviderID.make(id)
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const apiKey = provider.env.map((item) => env[item]).find(Boolean) const apiKey = provider.env.map((item) => vals[item]).find(Boolean)
if (!apiKey) continue if (!apiKey) continue
mergeProvider(providerID, { mergeProvider(providerID, {
source: "env", source: "env",
@ -1228,20 +1224,16 @@ export namespace Provider {
const options = yield* Effect.promise(() => const options = yield* Effect.promise(() =>
plugin.auth!.loader!( plugin.auth!.loader!(
() => () =>
Effect.runPromise( Effect.runPromise(auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer))) as any,
auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer)),
) as any,
database[plugin.auth!.provider], database[plugin.auth!.provider],
), ),
) )
const opts = options ?? {} const opts = options ?? {}
const patch: Partial<Info> = providers[providerID] const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
? { options: opts }
: { source: "custom", options: opts }
mergeProvider(providerID, patch) mergeProvider(providerID, patch)
} }
for (const [id, fn] of Object.entries(custom(dep))) { for (const [id, fn] of Object.entries(custom(dep, env))) {
const providerID = ProviderID.make(id) const providerID = ProviderID.make(id)
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const data = database[providerID] const data = database[providerID]
@ -1331,8 +1323,7 @@ export namespace Provider {
(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) if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
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)) ||
@ -1372,7 +1363,7 @@ export namespace Provider {
const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers)) const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
async function resolveSDK(model: Model, s: State) { async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
try { try {
using _ = log.time("getSDK", { using _ = log.time("getSDK", {
providerID: model.providerID, providerID: model.providerID,
@ -1403,7 +1394,7 @@ export namespace Provider {
} }
url = url.replace(/\$\{([^}]+)\}/g, (item, key) => { url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
const val = Env.get(String(key)) const val = envs[String(key)]
return val ?? item return val ?? item
}) })
return url return url
@ -1443,8 +1434,7 @@ export namespace Provider {
if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
signals.push(AbortSignal.timeout(options["timeout"])) signals.push(AbortSignal.timeout(options["timeout"]))
const combined = const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
if (combined) opts.signal = combined if (combined) opts.signal = combined
// Strip openai itemId metadata following what codex does // Strip openai itemId metadata following what codex does
@ -1536,9 +1526,10 @@ export namespace Provider {
const s = yield* InstanceState.get(state) const s = yield* InstanceState.get(state)
const key = `${model.providerID}/${model.id}` const key = `${model.providerID}/${model.id}`
if (s.models.has(key)) return s.models.get(key)! if (s.models.has(key)) return s.models.get(key)!
const vals = yield* env.all()
return yield* Effect.promise(async () => { return yield* Effect.promise(async () => {
const url = e2eURL() const url = e2eURL(vals)
if (url) { if (url) {
const language = createOpenAICompatible({ const language = createOpenAICompatible({
name: model.providerID, name: model.providerID,
@ -1550,7 +1541,7 @@ export namespace Provider {
} }
const provider = s.providers[model.providerID] const provider = s.providers[model.providerID]
const sdk = await resolveSDK(model, s) const sdk = await resolveSDK(model, s, vals)
try { try {
const language = s.modelLoaders[model.providerID] const language = s.modelLoaders[model.providerID]
@ -1688,6 +1679,7 @@ export namespace Provider {
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Config.defaultLayer), Layer.provide(Config.defaultLayer),
Layer.provide(Auth.defaultLayer), Layer.provide(Auth.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Plugin.defaultLayer), Layer.provide(Plugin.defaultLayer),
), ),
) )

View file

@ -95,6 +95,7 @@ export namespace ToolRegistry {
| Ripgrep.Service | Ripgrep.Service
| Format.Service | Format.Service
| Truncate.Service | Truncate.Service
| Env.Service
> = Layer.effect( > = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
@ -103,6 +104,7 @@ export namespace ToolRegistry {
const agents = yield* Agent.Service const agents = yield* Agent.Service
const skill = yield* Skill.Service const skill = yield* Skill.Service
const truncate = yield* Truncate.Service const truncate = yield* Truncate.Service
const env = yield* Env.Service
const invalid = yield* InvalidTool const invalid = yield* InvalidTool
const task = yield* TaskTool const task = yield* TaskTool
@ -272,13 +274,14 @@ export namespace ToolRegistry {
}) })
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) { const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
const e2e = !!(yield* env.get("OPENCODE_E2E_LLM_URL"))
const filtered = (yield* all()).filter((tool) => { const filtered = (yield* all()).filter((tool) => {
if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) { if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) {
return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
} }
const usePatch = const usePatch =
!!Env.get("OPENCODE_E2E_LLM_URL") || e2e ||
(input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")) (input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4"))
if (tool.id === ApplyPatchTool.id) return usePatch if (tool.id === ApplyPatchTool.id) return usePatch
if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
@ -342,6 +345,7 @@ export namespace ToolRegistry {
Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Ripgrep.defaultLayer), Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Truncate.defaultLayer), Layer.provide(Truncate.defaultLayer),
Layer.provide(Env.defaultLayer),
), ),
) )
} }

View file

@ -2,6 +2,7 @@ import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:
import { Deferred, Effect, Fiber, Layer, Option } from "effect" import { Deferred, Effect, Fiber, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node" import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "../../src/config/config" import { Config } from "../../src/config/config"
import { Env } from "../../src/env"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { Auth } from "../../src/auth" import { Auth } from "../../src/auth"
import { AccessToken, Account, AccountID, OrgID } from "../../src/account" import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
@ -37,6 +38,7 @@ const layer = Config.layer.pipe(
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(emptyAuth), Layer.provide(emptyAuth),
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provide(Env.defaultLayer),
Layer.provideMerge(infra), Layer.provideMerge(infra),
) )
@ -334,6 +336,7 @@ test("resolves env templates in account config with account token", async () =>
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(emptyAuth), Layer.provide(emptyAuth),
Layer.provide(fakeAccount), Layer.provide(fakeAccount),
Layer.provide(Env.defaultLayer),
Layer.provideMerge(infra), Layer.provideMerge(infra),
) )
@ -1826,6 +1829,7 @@ test("project config overrides remote well-known config", async () => {
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(fakeAuth), Layer.provide(fakeAuth),
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provide(Env.defaultLayer),
Layer.provideMerge(infra), Layer.provideMerge(infra),
) )
@ -1881,6 +1885,7 @@ test("wellknown URL with trailing slash is normalized", async () => {
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(fakeAuth), Layer.provide(fakeAuth),
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provide(Env.defaultLayer),
Layer.provideMerge(infra), Layer.provideMerge(infra),
) )

View file

@ -8,6 +8,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Command } from "../../src/command" import { Command } from "../../src/command"
import { Config } from "../../src/config/config" import { Config } from "../../src/config/config"
import { Env } from "../../src/env"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp" import { MCP } from "../../src/mcp"
@ -183,6 +184,7 @@ function makeHttp() {
const todo = Todo.layer.pipe(Layer.provideMerge(deps)) const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = ToolRegistry.layer.pipe( const registry = ToolRegistry.layer.pipe(
Layer.provide(Skill.defaultLayer), Layer.provide(Skill.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Ripgrep.defaultLayer), Layer.provide(Ripgrep.defaultLayer),

View file

@ -33,6 +33,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Command } from "../../src/command" import { Command } from "../../src/command"
import { Config } from "../../src/config/config" import { Config } from "../../src/config/config"
import { Env } from "../../src/env"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp" import { MCP } from "../../src/mcp"
@ -137,6 +138,7 @@ function makeHttp() {
const todo = Todo.layer.pipe(Layer.provideMerge(deps)) const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = ToolRegistry.layer.pipe( const registry = ToolRegistry.layer.pipe(
Layer.provide(Skill.defaultLayer), Layer.provide(Skill.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Ripgrep.defaultLayer), Layer.provide(Ripgrep.defaultLayer),