Skip to content

Commit 265c951

Browse files
committed
add env var config overrides
1 parent e4788e5 commit 265c951

File tree

3 files changed

+401
-1
lines changed

3 files changed

+401
-1
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest"
2+
import { applyEnvOverrides, ENV_OVERRIDES } from "../env-overrides.js"
3+
import type { CLIConfig } from "../types.js"
4+
5+
describe("env-overrides", () => {
6+
const originalEnv = process.env
7+
let testConfig: CLIConfig
8+
9+
beforeEach(() => {
10+
// Reset environment variables before each test
11+
process.env = { ...originalEnv }
12+
13+
// Create a test config
14+
testConfig = {
15+
version: "1.0.0",
16+
mode: "code",
17+
telemetry: true,
18+
provider: "default",
19+
providers: [
20+
{
21+
id: "default",
22+
provider: "kilocode",
23+
kilocodeToken: "test-token",
24+
kilocodeModel: "anthropic/claude-sonnet-4.5",
25+
kilocodeOrganizationId: "original-org-id",
26+
},
27+
{
28+
id: "anthropic-provider",
29+
provider: "anthropic",
30+
apiKey: "test-key",
31+
apiModelId: "claude-3-5-sonnet-20241022",
32+
},
33+
],
34+
autoApproval: {
35+
enabled: true,
36+
},
37+
theme: "dark",
38+
customThemes: {},
39+
}
40+
})
41+
42+
afterEach(() => {
43+
// Restore original environment
44+
process.env = originalEnv
45+
})
46+
47+
describe("KILO_PROVIDER override", () => {
48+
it("should override provider when KILO_PROVIDER is set and provider exists", () => {
49+
process.env[ENV_OVERRIDES.PROVIDER] = "anthropic-provider"
50+
51+
const result = applyEnvOverrides(testConfig)
52+
53+
expect(result.provider).toBe("anthropic-provider")
54+
})
55+
56+
it("should not override provider when KILO_PROVIDER provider does not exist", () => {
57+
process.env[ENV_OVERRIDES.PROVIDER] = "nonexistent-provider"
58+
59+
const result = applyEnvOverrides(testConfig)
60+
61+
expect(result.provider).toBe("default")
62+
})
63+
64+
it("should not override provider when KILO_PROVIDER is not set", () => {
65+
const result = applyEnvOverrides(testConfig)
66+
67+
expect(result.provider).toBe("default")
68+
})
69+
})
70+
71+
describe("KILO_MODEL override", () => {
72+
it("should override kilocodeModel for kilocode provider", () => {
73+
process.env[ENV_OVERRIDES.MODEL] = "anthropic/claude-opus-4.0"
74+
75+
const result = applyEnvOverrides(testConfig)
76+
77+
const provider = result.providers.find((p) => p.id === "default")
78+
expect(provider?.kilocodeModel).toBe("anthropic/claude-opus-4.0")
79+
})
80+
81+
it("should override apiModelId for anthropic provider", () => {
82+
testConfig.provider = "anthropic-provider"
83+
process.env[ENV_OVERRIDES.MODEL] = "claude-3-opus-20240229"
84+
85+
const result = applyEnvOverrides(testConfig)
86+
87+
const provider = result.providers.find((p) => p.id === "anthropic-provider")
88+
expect(provider?.apiModelId).toBe("claude-3-opus-20240229")
89+
})
90+
91+
it("should not modify original config object", () => {
92+
process.env[ENV_OVERRIDES.MODEL] = "new-model"
93+
94+
const result = applyEnvOverrides(testConfig)
95+
96+
const originalProvider = testConfig.providers.find((p) => p.id === "default")
97+
const resultProvider = result.providers.find((p) => p.id === "default")
98+
99+
expect(originalProvider?.kilocodeModel).toBe("anthropic/claude-sonnet-4.5")
100+
expect(resultProvider?.kilocodeModel).toBe("new-model")
101+
})
102+
})
103+
104+
describe("KILO_ORG_ID override", () => {
105+
it("should override kilocodeOrganizationId for kilocode provider", () => {
106+
process.env[ENV_OVERRIDES.ORG_ID] = "new-org-id"
107+
108+
const result = applyEnvOverrides(testConfig)
109+
110+
const provider = result.providers.find((p) => p.id === "default")
111+
expect(provider?.kilocodeOrganizationId).toBe("new-org-id")
112+
})
113+
114+
it("should not override organizationId for non-kilocode provider", () => {
115+
testConfig.provider = "anthropic-provider"
116+
process.env[ENV_OVERRIDES.ORG_ID] = "new-org-id"
117+
118+
const result = applyEnvOverrides(testConfig)
119+
120+
const provider = result.providers.find((p) => p.id === "anthropic-provider")
121+
expect(provider?.kilocodeOrganizationId).toBeUndefined()
122+
})
123+
124+
it("should add kilocodeOrganizationId if not present in config", () => {
125+
// Remove organizationId from config
126+
const providerIndex = testConfig.providers.findIndex((p) => p.id === "default")
127+
delete (testConfig.providers[providerIndex] as any).kilocodeOrganizationId
128+
129+
process.env[ENV_OVERRIDES.ORG_ID] = "new-org-id"
130+
131+
const result = applyEnvOverrides(testConfig)
132+
133+
const provider = result.providers.find((p) => p.id === "default")
134+
expect(provider?.kilocodeOrganizationId).toBe("new-org-id")
135+
})
136+
})
137+
138+
describe("Multiple overrides", () => {
139+
it("should apply all overrides when multiple env vars are set", () => {
140+
process.env[ENV_OVERRIDES.PROVIDER] = "default"
141+
process.env[ENV_OVERRIDES.MODEL] = "anthropic/claude-opus-4.0"
142+
process.env[ENV_OVERRIDES.ORG_ID] = "new-org-id"
143+
144+
const result = applyEnvOverrides(testConfig)
145+
146+
expect(result.provider).toBe("default")
147+
const provider = result.providers.find((p) => p.id === "default")
148+
expect(provider?.kilocodeModel).toBe("anthropic/claude-opus-4.0")
149+
expect(provider?.kilocodeOrganizationId).toBe("new-org-id")
150+
})
151+
152+
it("should handle provider switch with model override", () => {
153+
process.env[ENV_OVERRIDES.PROVIDER] = "anthropic-provider"
154+
process.env[ENV_OVERRIDES.MODEL] = "claude-3-opus-20240229"
155+
156+
const result = applyEnvOverrides(testConfig)
157+
158+
expect(result.provider).toBe("anthropic-provider")
159+
const provider = result.providers.find((p) => p.id === "anthropic-provider")
160+
expect(provider?.apiModelId).toBe("claude-3-opus-20240229")
161+
})
162+
})
163+
164+
describe("Edge cases", () => {
165+
it("should handle empty config providers array", () => {
166+
testConfig.providers = []
167+
168+
const result = applyEnvOverrides(testConfig)
169+
170+
expect(result.providers).toEqual([])
171+
})
172+
173+
it("should handle config with no current provider", () => {
174+
testConfig.provider = "nonexistent"
175+
176+
const result = applyEnvOverrides(testConfig)
177+
178+
expect(result).toEqual(testConfig)
179+
})
180+
181+
it("should handle empty string env variables", () => {
182+
process.env[ENV_OVERRIDES.PROVIDER] = ""
183+
process.env[ENV_OVERRIDES.MODEL] = ""
184+
process.env[ENV_OVERRIDES.ORG_ID] = ""
185+
186+
const result = applyEnvOverrides(testConfig)
187+
188+
// Empty strings should not trigger overrides
189+
expect(result.provider).toBe("default")
190+
})
191+
})
192+
193+
describe("Provider-specific model fields", () => {
194+
it("should use correct model field for different providers", () => {
195+
const providers = [
196+
{ id: "ollama-test", provider: "ollama" as const, ollamaModelId: "llama2" },
197+
{ id: "openrouter-test", provider: "openrouter" as const, openRouterModelId: "anthropic/claude" },
198+
{ id: "lmstudio-test", provider: "lmstudio" as const, lmStudioModelId: "local-model" },
199+
]
200+
201+
testConfig.providers = [...testConfig.providers, ...providers]
202+
203+
// Test ollama
204+
testConfig.provider = "ollama-test"
205+
process.env[ENV_OVERRIDES.MODEL] = "llama3"
206+
let result = applyEnvOverrides(testConfig)
207+
let provider = result.providers.find((p) => p.id === "ollama-test")
208+
expect(provider?.ollamaModelId).toBe("llama3")
209+
210+
// Test openrouter
211+
testConfig.provider = "openrouter-test"
212+
process.env[ENV_OVERRIDES.MODEL] = "openai/gpt-4"
213+
result = applyEnvOverrides(testConfig)
214+
provider = result.providers.find((p) => p.id === "openrouter-test")
215+
expect(provider?.openRouterModelId).toBe("openai/gpt-4")
216+
217+
// Test lmstudio
218+
testConfig.provider = "lmstudio-test"
219+
process.env[ENV_OVERRIDES.MODEL] = "codellama"
220+
result = applyEnvOverrides(testConfig)
221+
provider = result.providers.find((p) => p.id === "lmstudio-test")
222+
expect(provider?.lmStudioModelId).toBe("codellama")
223+
})
224+
})
225+
})

cli/src/config/env-overrides.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import type { CLIConfig } from "./types.js"
2+
import { logs } from "../services/logs.js"
3+
4+
/**
5+
* Environment variable names for config overrides
6+
*/
7+
export const ENV_OVERRIDES = {
8+
PROVIDER: "KILO_PROVIDER",
9+
MODEL: "KILO_MODEL",
10+
ORG_ID: "KILO_ORG_ID",
11+
} as const
12+
13+
/**
14+
* Apply environment variable overrides to the config
15+
* Overrides the current provider's settings based on environment variables
16+
*
17+
* Environment variables:
18+
* - KILO_PROVIDER: Override the active provider ID
19+
* - KILO_MODEL: Override the model for the current provider
20+
* - KILO_ORG_ID: Override the organization ID (for kilocode provider)
21+
*
22+
* @param config The config to apply overrides to
23+
* @returns The config with environment variable overrides applied
24+
*/
25+
export function applyEnvOverrides(config: CLIConfig): CLIConfig {
26+
const overriddenConfig = { ...config }
27+
28+
// Override provider if KILO_PROVIDER is set
29+
const envProvider = process.env[ENV_OVERRIDES.PROVIDER]
30+
31+
if (envProvider) {
32+
// Check if the provider exists in the config
33+
const providerExists = config.providers.some((p) => p.id === envProvider)
34+
35+
if (providerExists) {
36+
overriddenConfig.provider = envProvider
37+
38+
logs.info(
39+
`Config override: provider set to "${envProvider}" from ${ENV_OVERRIDES.PROVIDER}`,
40+
"EnvOverrides",
41+
)
42+
} else {
43+
logs.warn(
44+
`Config override ignored: provider "${envProvider}" from ${ENV_OVERRIDES.PROVIDER} not found in config`,
45+
"EnvOverrides",
46+
)
47+
}
48+
}
49+
50+
// Get the current provider (after potential provider override)
51+
const currentProvider = overriddenConfig.providers.find((p) => p.id === overriddenConfig.provider)
52+
53+
if (!currentProvider) {
54+
// No valid provider, return config as-is
55+
return overriddenConfig
56+
}
57+
58+
// Override model if KILO_MODEL is set
59+
const envModel = process.env[ENV_OVERRIDES.MODEL]
60+
61+
if (envModel) {
62+
// Apply model override based on provider type
63+
const modelField = getModelFieldForProvider(currentProvider.provider)
64+
65+
if (modelField) {
66+
// Create a new providers array with the updated provider
67+
overriddenConfig.providers = overriddenConfig.providers.map((p) => {
68+
if (p.id === currentProvider.id) {
69+
return {
70+
...p,
71+
[modelField]: envModel,
72+
}
73+
}
74+
75+
return p
76+
})
77+
78+
logs.info(
79+
`Config override: ${modelField} set to "${envModel}" from ${ENV_OVERRIDES.MODEL} for provider "${currentProvider.id}"`,
80+
"EnvOverrides",
81+
)
82+
}
83+
}
84+
85+
// Override organization ID if KILO_ORG_ID is set (only for kilocode provider)
86+
const envOrgId = process.env[ENV_OVERRIDES.ORG_ID]
87+
88+
if (envOrgId && currentProvider.provider === "kilocode") {
89+
// Create a new providers array with the updated provider
90+
overriddenConfig.providers = overriddenConfig.providers.map((p) => {
91+
if (p.id === currentProvider.id) {
92+
return {
93+
...p,
94+
kilocodeOrganizationId: envOrgId,
95+
}
96+
}
97+
98+
return p
99+
})
100+
101+
logs.info(
102+
`Config override: kilocodeOrganizationId set to "${envOrgId}" from ${ENV_OVERRIDES.ORG_ID} for provider "${currentProvider.id}"`,
103+
"EnvOverrides",
104+
)
105+
} else if (envOrgId && currentProvider.provider !== "kilocode") {
106+
logs.warn(
107+
`Config override ignored: ${ENV_OVERRIDES.ORG_ID} is only applicable for kilocode provider, current provider is "${currentProvider.provider}"`,
108+
"EnvOverrides",
109+
)
110+
}
111+
112+
return overriddenConfig
113+
}
114+
115+
/**
116+
* Get the model field name for a given provider
117+
* Different providers use different field names for their model ID
118+
*/
119+
function getModelFieldForProvider(provider: string): string | null {
120+
switch (provider) {
121+
case "kilocode":
122+
return "kilocodeModel"
123+
124+
case "anthropic":
125+
return "apiModelId"
126+
127+
case "openai-native":
128+
return "apiModelId"
129+
130+
case "openrouter":
131+
return "openRouterModelId"
132+
133+
case "ollama":
134+
return "ollamaModelId"
135+
136+
case "lmstudio":
137+
return "lmStudioModelId"
138+
139+
case "openai":
140+
return "apiModelId"
141+
142+
case "glama":
143+
return "glamaModelId"
144+
145+
case "litellm":
146+
return "litellmModelId"
147+
148+
case "deepinfra":
149+
return "deepInfraModelId"
150+
151+
case "unbound":
152+
return "unboundModelId"
153+
154+
case "requesty":
155+
return "requestyModelId"
156+
157+
case "vercel-ai-gateway":
158+
return "vercelAiGatewayModelId"
159+
160+
case "io-intelligence":
161+
return "ioIntelligenceModelId"
162+
163+
case "huggingface":
164+
return "huggingFaceModelId"
165+
166+
default:
167+
// For most other providers, use apiModelId as fallback
168+
return "apiModelId"
169+
}
170+
}

0 commit comments

Comments
 (0)