core: add provider test coverage for upcoming refactor
Some checks are pending
deploy / deploy (push) Waiting to run
format / format (push) Waiting to run
snapshot / publish (push) Waiting to run
test / test (push) Waiting to run

Add comprehensive test suite for Provider module to ensure safe
refactoring of provider internals. Tests cover:
- Provider loading from env vars and config
- Provider filtering (disabled_providers, enabled_providers)
- Model whitelist/blacklist
- Model aliasing and custom providers
- getModel, getProvider, closest, defaultModel functions

Also adds Env module for instance-scoped environment variable access,
enabling isolated test environments without global state pollution.
This commit is contained in:
Dax Raad 2025-12-03 18:30:42 -05:00
parent 7a4aa68706
commit ee4437ff32
4 changed files with 1805 additions and 11 deletions

26
packages/opencode/src/env/index.ts vendored Normal file
View file

@ -0,0 +1,26 @@
import { Instance } from "../project/instance"
export namespace Env {
const state = Instance.state(() => {
return { ...process.env } as Record<string, string | undefined>
})
export function get(key: string) {
const env = state()
return env[key]
}
export function all() {
return state()
}
export function set(key: string, value: string) {
const env = state()
env[key] = value
}
export function remove(key: string) {
const env = state()
delete env[key]
}
}

View file

@ -9,6 +9,7 @@ import { Plugin } from "../plugin"
import { ModelsDev } from "./models"
import { NamedError } from "@opencode-ai/util/error"
import { Auth } from "../auth"
import { Env } from "../env"
import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { iife } from "@/util/iife"
@ -64,7 +65,8 @@ export namespace Provider {
},
async opencode(input) {
const hasKey = await (async () => {
if (input.env.some((item) => process.env[item])) return true
const env = Env.all()
if (input.env.some((item) => env[item])) return true
if (await Auth.get(input.id)) return true
return false
})()
@ -128,7 +130,7 @@ export namespace Provider {
}
},
"azure-cognitive-services": async () => {
const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
return {
autoload: false,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
@ -144,10 +146,15 @@ export namespace Provider {
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
return { autoload: false }
const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
Env.get("AWS_PROFILE"),
Env.get("AWS_ACCESS_KEY_ID"),
Env.get("AWS_BEARER_TOKEN_BEDROCK"),
Env.get("AWS_REGION"),
])
if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
const region = process.env["AWS_REGION"] ?? "us-east-1"
const region = awsRegion ?? "us-east-1"
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
return {
@ -246,8 +253,8 @@ export namespace Provider {
}
},
"google-vertex": async () => {
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "us-east5"
const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "us-east5"
const autoload = Boolean(project)
if (!autoload) return { autoload: false }
return {
@ -263,8 +270,8 @@ export namespace Provider {
}
},
"google-vertex-anthropic": async () => {
const project = process.env["GOOGLE_CLOUD_PROJECT"] ?? process.env["GCP_PROJECT"] ?? process.env["GCLOUD_PROJECT"]
const location = process.env["GOOGLE_CLOUD_LOCATION"] ?? process.env["VERTEX_LOCATION"] ?? "global"
const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT")
const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global"
const autoload = Boolean(project)
if (!autoload) return { autoload: false }
return {
@ -435,9 +442,10 @@ export namespace Provider {
}
// load env
const env = Env.all()
for (const [providerID, provider] of Object.entries(database)) {
if (disabled.has(providerID)) continue
const apiKey = provider.env.map((item) => process.env[item]).at(0)
const apiKey = provider.env.map((item) => env[item]).find(Boolean)
if (!apiKey) continue
mergeProvider(
providerID,

View file

@ -1,4 +1,35 @@
import { Log } from "../src/util/log"
// IMPORTANT: Set env vars BEFORE any imports from src/ directory
// xdg-basedir reads env vars at import time, so we must set these first
import os from "os"
import path from "path"
const testDataDir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
process.env["XDG_DATA_HOME"] = testDataDir
process.env["XDG_CACHE_HOME"] = path.join(testDataDir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(testDataDir, "config")
process.env["XDG_STATE_HOME"] = path.join(testDataDir, "state")
// Clear provider env vars to ensure clean test state
delete process.env["ANTHROPIC_API_KEY"]
delete process.env["OPENAI_API_KEY"]
delete process.env["GOOGLE_API_KEY"]
delete process.env["GOOGLE_GENERATIVE_AI_API_KEY"]
delete process.env["AZURE_OPENAI_API_KEY"]
delete process.env["AWS_ACCESS_KEY_ID"]
delete process.env["AWS_PROFILE"]
delete process.env["OPENROUTER_API_KEY"]
delete process.env["GROQ_API_KEY"]
delete process.env["MISTRAL_API_KEY"]
delete process.env["PERPLEXITY_API_KEY"]
delete process.env["TOGETHER_API_KEY"]
delete process.env["XAI_API_KEY"]
delete process.env["DEEPSEEK_API_KEY"]
delete process.env["FIREWORKS_API_KEY"]
delete process.env["CEREBRAS_API_KEY"]
delete process.env["SAMBANOVA_API_KEY"]
// Now safe to import from src/
const { Log } = await import("../src/util/log")
Log.init({
print: false,

File diff suppressed because it is too large Load diff