kit/env instance state (#22383)
This commit is contained in:
@@ -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
|
||||||
@@ -1657,6 +1654,7 @@ export namespace Config {
|
|||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Account.defaultLayer),
|
Layer.provide(Account.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+40
-12
@@ -1,28 +1,56 @@
|
|||||||
import { Instance } from "../project/instance"
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
export namespace Env {
|
export namespace Env {
|
||||||
const state = Instance.state(() => {
|
type State = Record<string, string | undefined>
|
||||||
// Create a shallow copy to isolate environment per instance
|
|
||||||
// Prevents parallel tests from interfering with each other's env vars
|
export interface Interface {
|
||||||
return { ...process.env } as Record<string, string | undefined>
|
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({ ...process.env })))
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
const remove = Effect.fn("Env.remove")(function* (key: string) {
|
||||||
|
const env = yield* InstanceState.get(state)
|
||||||
|
delete env[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return Service.of({ get, all, set, remove })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer
|
||||||
|
|
||||||
|
const rt = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
export function get(key: string) {
|
export function get(key: string) {
|
||||||
const env = state()
|
return rt.runSync((svc) => svc.get(key))
|
||||||
return env[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function all() {
|
export function all() {
|
||||||
return state()
|
return rt.runSync((svc) => svc.all())
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
export function remove(key: string) {
|
||||||
const env = state()
|
return rt.runSync((svc) => svc.remove(key))
|
||||||
delete env[key]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,12 +116,6 @@ export namespace Provider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function e2eURL() {
|
|
||||||
const url = Env.get("OPENCODE_E2E_LLM_URL")
|
|
||||||
if (typeof url !== "string" || url === "") return
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
type BundledSDK = {
|
type BundledSDK = {
|
||||||
languageModel(modelId: string): LanguageModelV3
|
languageModel(modelId: string): LanguageModelV3
|
||||||
}
|
}
|
||||||
@@ -166,6 +160,8 @@ export namespace Provider {
|
|||||||
type CustomDep = {
|
type CustomDep = {
|
||||||
auth: (id: string) => Effect.Effect<Auth.Info | undefined>
|
auth: (id: string) => Effect.Effect<Auth.Info | undefined>
|
||||||
config: () => Effect.Effect<Config.Info>
|
config: () => Effect.Effect<Config.Info>
|
||||||
|
env: () => Effect.Effect<Record<string, string | undefined>>
|
||||||
|
get: (key: string) => Effect.Effect<string | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLanguageModel(sdk: any) {
|
function useLanguageModel(sdk: any) {
|
||||||
@@ -184,7 +180,7 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
opencode: Effect.fnUntraced(function* (input: Info) {
|
opencode: Effect.fnUntraced(function* (input: Info) {
|
||||||
const env = Env.all()
|
const env = yield* dep.env()
|
||||||
const hasKey = iife(() => {
|
const hasKey = iife(() => {
|
||||||
if (input.env.some((item) => env[item])) return true
|
if (input.env.some((item) => env[item])) return true
|
||||||
return false
|
return false
|
||||||
@@ -231,14 +227,15 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
options: {},
|
options: {},
|
||||||
}),
|
}),
|
||||||
azure: (provider) => {
|
azure: Effect.fnUntraced(function* (provider: Info) {
|
||||||
|
const env = yield* dep.env()
|
||||||
const resource = iife(() => {
|
const resource = iife(() => {
|
||||||
const name = provider.options?.resourceName
|
const name = provider.options?.resourceName
|
||||||
if (typeof name === "string" && name.trim() !== "") return name
|
if (typeof name === "string" && name.trim() !== "") return name
|
||||||
return Env.get("AZURE_RESOURCE_NAME")
|
return env["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 +251,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 resourceName = yield* dep.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)
|
||||||
@@ -271,23 +268,24 @@ export namespace Provider {
|
|||||||
options: {
|
options: {
|
||||||
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
baseURL: resourceName ? `https://${resourceName}.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")
|
||||||
|
const env = yield* dep.env()
|
||||||
|
|
||||||
// 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 = env["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 = env["AWS_PROFILE"]
|
||||||
const profile = configProfile ?? envProfile
|
const profile = configProfile ?? envProfile
|
||||||
|
|
||||||
const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")
|
const awsAccessKeyId = env["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 +299,7 @@ export namespace Provider {
|
|||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const awsWebIdentityTokenFile = Env.get("AWS_WEB_IDENTITY_TOKEN_FILE")
|
const awsWebIdentityTokenFile = env["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 +437,22 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
"google-vertex": (provider) => {
|
"google-vertex": Effect.fnUntraced(function* (provider: Info) {
|
||||||
|
const env = yield* dep.env()
|
||||||
const project =
|
const project =
|
||||||
provider.options?.project ??
|
provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
||||||
Env.get("GOOGLE_CLOUD_PROJECT") ??
|
|
||||||
Env.get("GCP_PROJECT") ??
|
|
||||||
Env.get("GCLOUD_PROJECT")
|
|
||||||
|
|
||||||
const location = String(
|
const location = String(
|
||||||
provider.options?.location ??
|
provider.options?.location ??
|
||||||
Env.get("GOOGLE_VERTEX_LOCATION") ??
|
env["GOOGLE_VERTEX_LOCATION"] ??
|
||||||
Env.get("GOOGLE_CLOUD_LOCATION") ??
|
env["GOOGLE_CLOUD_LOCATION"] ??
|
||||||
Env.get("VERTEX_LOCATION") ??
|
env["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 +481,15 @@ 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 env = yield* dep.env()
|
||||||
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"
|
const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"]
|
||||||
|
const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["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 +499,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 +536,15 @@ 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* dep.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 = yield* Effect.sync(() => {
|
||||||
if (auth?.type === "oauth") return auth.access
|
if (auth?.type === "oauth") return auth.access
|
||||||
if (auth?.type === "api") return auth.key
|
if (auth?.type === "api") return auth.key
|
||||||
return Env.get("GITLAB_TOKEN")
|
return undefined
|
||||||
})
|
})
|
||||||
|
const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
|
||||||
|
|
||||||
const providerConfig = (yield* dep.config()).provider?.["gitlab"]
|
const providerConfig = (yield* dep.config()).provider?.["gitlab"]
|
||||||
|
|
||||||
@@ -563,10 +561,10 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
autoload: !!apiKey,
|
autoload: !!token,
|
||||||
options: {
|
options: {
|
||||||
instanceUrl,
|
instanceUrl,
|
||||||
apiKey,
|
apiKey: token,
|
||||||
aiGatewayHeaders,
|
aiGatewayHeaders,
|
||||||
featureFlags,
|
featureFlags,
|
||||||
},
|
},
|
||||||
@@ -681,8 +679,8 @@ export namespace Provider {
|
|||||||
if (input.options?.baseURL) return { autoload: false }
|
if (input.options?.baseURL) return { autoload: false }
|
||||||
|
|
||||||
const auth = yield* dep.auth(input.id)
|
const auth = yield* dep.auth(input.id)
|
||||||
const accountId =
|
const env = yield* dep.env()
|
||||||
Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
||||||
if (!accountId)
|
if (!accountId)
|
||||||
return {
|
return {
|
||||||
autoload: false,
|
autoload: false,
|
||||||
@@ -694,7 +692,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 = env["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
|
||||||
@@ -723,10 +721,9 @@ export namespace Provider {
|
|||||||
if (input.options?.baseURL) return { autoload: false }
|
if (input.options?.baseURL) return { autoload: false }
|
||||||
|
|
||||||
const auth = yield* dep.auth(input.id)
|
const auth = yield* dep.auth(input.id)
|
||||||
const accountId =
|
const env = yield* dep.env()
|
||||||
Env.get("CLOUDFLARE_ACCOUNT_ID") || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined)
|
||||||
const gateway =
|
const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
|
||||||
Env.get("CLOUDFLARE_GATEWAY_ID") || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined)
|
|
||||||
|
|
||||||
if (!accountId || !gateway) {
|
if (!accountId || !gateway) {
|
||||||
const missing = [
|
const missing = [
|
||||||
@@ -745,7 +742,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 = env["CLOUDFLARE_API_TOKEN"] || env["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,13 +1027,17 @@ 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 env = yield* Env.Service
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(() =>
|
const state = yield* InstanceState.make<State>(() =>
|
||||||
@@ -1061,6 +1062,8 @@ export namespace Provider {
|
|||||||
const dep = {
|
const dep = {
|
||||||
auth: (id: string) => auth.get(id).pipe(Effect.orDie),
|
auth: (id: string) => auth.get(id).pipe(Effect.orDie),
|
||||||
config: () => config.get(),
|
config: () => config.get(),
|
||||||
|
env: () => env.all(),
|
||||||
|
get: (key: string) => env.get(key),
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("init")
|
log.info("init")
|
||||||
@@ -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 envs = 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) => envs[item]).find(Boolean)
|
||||||
if (!apiKey) continue
|
if (!apiKey) continue
|
||||||
mergeProvider(providerID, {
|
mergeProvider(providerID, {
|
||||||
source: "env",
|
source: "env",
|
||||||
@@ -1228,16 +1224,12 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -1534,11 +1524,16 @@ export namespace Provider {
|
|||||||
|
|
||||||
const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
|
const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
|
||||||
const s = yield* InstanceState.get(state)
|
const s = yield* InstanceState.get(state)
|
||||||
|
const envs = yield* env.all()
|
||||||
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)!
|
||||||
|
|
||||||
return yield* Effect.promise(async () => {
|
return yield* Effect.promise(async () => {
|
||||||
const url = e2eURL()
|
const url = (() => {
|
||||||
|
const item = envs["OPENCODE_E2E_LLM_URL"]
|
||||||
|
if (typeof item !== "string" || item === "") return
|
||||||
|
return item
|
||||||
|
})()
|
||||||
if (url) {
|
if (url) {
|
||||||
const language = createOpenAICompatible({
|
const language = createOpenAICompatible({
|
||||||
name: model.providerID,
|
name: model.providerID,
|
||||||
@@ -1550,7 +1545,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, envs)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const language = s.modelLoaders[model.providerID]
|
const language = s.modelLoaders[model.providerID]
|
||||||
@@ -1686,6 +1681,7 @@ export namespace Provider {
|
|||||||
export const defaultLayer = Layer.suspend(() =>
|
export const defaultLayer = Layer.suspend(() =>
|
||||||
layer.pipe(
|
layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export namespace ToolRegistry {
|
|||||||
Service,
|
Service,
|
||||||
never,
|
never,
|
||||||
| Config.Service
|
| Config.Service
|
||||||
|
| Env.Service
|
||||||
| Plugin.Service
|
| Plugin.Service
|
||||||
| Question.Service
|
| Question.Service
|
||||||
| Todo.Service
|
| Todo.Service
|
||||||
@@ -99,6 +100,7 @@ export namespace ToolRegistry {
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
const env = yield* Env.Service
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
@@ -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
|
||||||
@@ -325,6 +328,7 @@ export namespace ToolRegistry {
|
|||||||
export const defaultLayer = Layer.suspend(() =>
|
export const defaultLayer = Layer.suspend(() =>
|
||||||
layer.pipe(
|
layer.pipe(
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(Plugin.defaultLayer),
|
Layer.provide(Plugin.defaultLayer),
|
||||||
Layer.provide(Question.defaultLayer),
|
Layer.provide(Question.defaultLayer),
|
||||||
Layer.provide(Todo.defaultLayer),
|
Layer.provide(Todo.defaultLayer),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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"
|
||||||
import { AppFileSystem } from "../../src/filesystem"
|
import { AppFileSystem } from "../../src/filesystem"
|
||||||
|
import { Env } from "../../src/env"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { tmpdir, tmpdirScoped } from "../fixture/fixture"
|
import { tmpdir, tmpdirScoped } from "../fixture/fixture"
|
||||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||||
@@ -35,6 +36,7 @@ const emptyAuth = Layer.mock(Auth.Service)({
|
|||||||
|
|
||||||
const layer = Config.layer.pipe(
|
const layer = Config.layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(emptyAuth),
|
Layer.provide(emptyAuth),
|
||||||
Layer.provide(emptyAccount),
|
Layer.provide(emptyAccount),
|
||||||
Layer.provideMerge(infra),
|
Layer.provideMerge(infra),
|
||||||
@@ -332,6 +334,7 @@ test("resolves env templates in account config with account token", async () =>
|
|||||||
|
|
||||||
const layer = Config.layer.pipe(
|
const layer = Config.layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(emptyAuth),
|
Layer.provide(emptyAuth),
|
||||||
Layer.provide(fakeAccount),
|
Layer.provide(fakeAccount),
|
||||||
Layer.provideMerge(infra),
|
Layer.provideMerge(infra),
|
||||||
@@ -1824,6 +1827,7 @@ test("project config overrides remote well-known config", async () => {
|
|||||||
|
|
||||||
const layer = Config.layer.pipe(
|
const layer = Config.layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(fakeAuth),
|
Layer.provide(fakeAuth),
|
||||||
Layer.provide(emptyAccount),
|
Layer.provide(emptyAccount),
|
||||||
Layer.provideMerge(infra),
|
Layer.provideMerge(infra),
|
||||||
@@ -1879,6 +1883,7 @@ test("wellknown URL with trailing slash is normalized", async () => {
|
|||||||
|
|
||||||
const layer = Config.layer.pipe(
|
const layer = Config.layer.pipe(
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(Env.defaultLayer),
|
||||||
Layer.provide(fakeAuth),
|
Layer.provide(fakeAuth),
|
||||||
Layer.provide(emptyAccount),
|
Layer.provide(emptyAccount),
|
||||||
Layer.provideMerge(infra),
|
Layer.provideMerge(infra),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { MCP } from "../../src/mcp"
|
|||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
import { Plugin } from "../../src/plugin"
|
import { Plugin } from "../../src/plugin"
|
||||||
import { Provider as ProviderSvc } from "../../src/provider/provider"
|
import { Provider as ProviderSvc } from "../../src/provider/provider"
|
||||||
|
import { Env } from "../../src/env"
|
||||||
import type { Provider } from "../../src/provider/provider"
|
import type { Provider } from "../../src/provider/provider"
|
||||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
import { Question } from "../../src/question"
|
import { Question } from "../../src/question"
|
||||||
@@ -167,6 +168,7 @@ function makeHttp() {
|
|||||||
Session.defaultLayer,
|
Session.defaultLayer,
|
||||||
Snapshot.defaultLayer,
|
Snapshot.defaultLayer,
|
||||||
LLM.defaultLayer,
|
LLM.defaultLayer,
|
||||||
|
Env.defaultLayer,
|
||||||
AgentSvc.defaultLayer,
|
AgentSvc.defaultLayer,
|
||||||
Command.defaultLayer,
|
Command.defaultLayer,
|
||||||
Permission.defaultLayer,
|
Permission.defaultLayer,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { MCP } from "../../src/mcp"
|
|||||||
import { Permission } from "../../src/permission"
|
import { Permission } from "../../src/permission"
|
||||||
import { Plugin } from "../../src/plugin"
|
import { Plugin } from "../../src/plugin"
|
||||||
import { Provider as ProviderSvc } from "../../src/provider/provider"
|
import { Provider as ProviderSvc } from "../../src/provider/provider"
|
||||||
|
import { Env } from "../../src/env"
|
||||||
import { Question } from "../../src/question"
|
import { Question } from "../../src/question"
|
||||||
import { Skill } from "../../src/skill"
|
import { Skill } from "../../src/skill"
|
||||||
import { SystemPrompt } from "../../src/session/system"
|
import { SystemPrompt } from "../../src/session/system"
|
||||||
@@ -121,6 +122,7 @@ function makeHttp() {
|
|||||||
Session.defaultLayer,
|
Session.defaultLayer,
|
||||||
Snapshot.defaultLayer,
|
Snapshot.defaultLayer,
|
||||||
LLM.defaultLayer,
|
LLM.defaultLayer,
|
||||||
|
Env.defaultLayer,
|
||||||
AgentSvc.defaultLayer,
|
AgentSvc.defaultLayer,
|
||||||
Command.defaultLayer,
|
Command.defaultLayer,
|
||||||
Permission.defaultLayer,
|
Permission.defaultLayer,
|
||||||
|
|||||||
Reference in New Issue
Block a user