Skip to content

Ambient OPENAI_* env vars silently override config.json provider settings (401 with OpenAI-compatible endpoints) #42

Description

@StarChen-Cycler

Environment

  • @liustack/modlens 3.16.4, installed as the DeepSeek Harness (dsh) plugin on Windows (dsh plugin --profile web add @liustack/modlens@3.16.4)
  • Engine: an OpenAI-compatible endpoint (Moonshot/Kimi, https://api.moonshot.cn/v1) configured in ~/.modlens/config.json

Problem

resolveProviderSettings applies the ENV_BINDINGS after the file values and overrides them unconditionally:

for (const [field, envName] of Object.entries(bindings)) {
  const value = env[envName]?.trim();
  if (value) {
    settings[field] = value;
  }
}

So any ambient OPENAI_API_KEY / OPENAI_BASE_URL in the process environment silently replaces the engine explicitly configured in config.json. This is easy to hit on Windows (user-level env vars set for other OpenAI-compatible tools) and under the dsh plugin in particular, because the plugin spawns the CLI with the full host process environment.

Result: a key meant for one endpoint is sent to the configured endpoint, and every read fails with:

openai: OpenAI-compatible API error 401: {"error":{"message":"Invalid Authentication","type":"invalid_authentication_error"}}

There is no opt-out, and the error gives no hint that the key came from the environment rather than the config file. (Same failure class as the ANTHROPIC_BASE_URL trap already documented in configure.md.)

Repro (verified)

  1. Configure providers.openai in ~/.modlens/config.json with a working key for an OpenAI-compatible endpoint (e.g. Kimi).
  2. Set an unrelated OPENAI_API_KEY in the process environment.
  3. Run a read → 401 (wrong key sent to the configured endpoint). Unset the env var → the same config succeeds. Reproduced deterministically in both directions.

Proposed fix (verified locally)

Let an explicitly configured file value win; env only fills gaps:

for (const [field, envName] of Object.entries(bindings)) {
  const value = env[envName]?.trim();
  if (value && !settings[field]?.trim()) {
    settings[field] = value;
  }
}

Alternative/additional: an opt-out such as providers.openai.ignoreEnv: true for users who want the file to be the single source of truth.

As a second line of defense on the dsh side I also patched the plugin's spawn to not forward ambient credential env vars (OPENAI_*, GEMINI_API_KEY, ANTHROPIC_*) to the CLI, so config.json stays authoritative for dsh users regardless.

Test results (after the dist/main.js patch)

  • CLI reads: 4/4 success against the configured endpoint (previously failed with 401 whenever the ambient var was present).
  • dsh plugin tool path (modlens_read_image): success.
  • doctor continues to report the config file as the effective source.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions