Implement extensions/vision/src/config.ts (env resolution + defaults) #254

Closed
opened 2026-09-17 03:32:49 +00:00 by david · 1 comment
Owner

Summary

Create extensions/vision/src/config.ts: load the project .env without touching process.env, merge it with the real environment, and resolve the vision extension's configuration — returning null when DEEPSEEK_API_KEY is absent so the extension can register zero tools.

Background

Depends on: #253

The vision extension is unconfigured-safe: with no DEEPSEEK_API_KEY it must register no tool and boot pi cleanly (the same shape extensions/mongodb and extensions/postgres use for an unset MONGODB_URI). When keyed, it needs a resolved config object that every later module consumes — the HTTP client reads apiKey/baseUrl/timeoutMs/model, the tool reads maxTokens/defaultThinking.

The .env handling deliberately mirrors extensions/mongodb/src/env.ts: dotenv.parse on the file contents (never dotenv.config, which would mutate process.env), with the real process.env winning on conflicts.

Invalid numeric settings must fail loudly at load time with a ToolError (config category) rather than silently falling back to a default — a typo in VISION_MAX_TOKENS should be visible immediately.

Documentation Required

A separate process downloads these into the listed folders before this issue is implemented. Check the folder for the actual reference material before starting.

docs/reference/dotenv/

docs/reference/nodejs/

docs/reference/deepseek-api/

Implementation Details

Create extensions/vision/src/config.ts with this public surface:

export interface VisionConfig {
  apiKey: string;
  model: string;
  baseUrl: string;
  maxTokens: number;
  timeoutMs: number;
  defaultThinking: boolean;
}

/** Returns null when DEEPSEEK_API_KEY is absent or empty. */
export function resolveConfig(env: Record<string, string | undefined>): VisionConfig | null;

/** Parse the repo-root `.env` without mutating `process.env`. */
export function loadEnvFile(): Record<string, string>;

loadEnvFile copies extensions/mongodb/src/env.ts's loadEnvFile (fs read of path.join(process.cwd(), ".env"), dotenv.parse, return {} on any error). The factory calls it and merges as { ...loadEnvFile(), ...process.env } so process.env wins.

Variables and defaults (all optional except the key):

Variable Required Default Notes
DEEPSEEK_API_KEY yes Absent or emptynull
VISION_MODEL no deepseek-flash sent verbatim
VISION_BASE_URL no https://api.deepseek.com strip trailing slashes; endpoint is ${base}/chat/completions
VISION_MAX_TOKENS no 8000 must parse as a number
VISION_TIMEOUT no 120000 ms; must parse as a number
VISION_THINKING no false accept true/false/1/0

Details:

  • An unset or empty-string variable falls back to its default; whitespace-only values count as empty.
  • Trailing-slash normalisation: https://api.deepseek.com///https://api.deepseek.com.
  • A value that does not parse as a finite number for VISION_MAX_TOKENS/VISION_TIMEOUT, or a VISION_THINKING value outside the four accepted forms, throws new ToolError(..., "config") with a message naming the variable and the bad value (e.g. `VISION_MAX_TOKENS must be a number, got "abc"`). Numeric values must also be > 0.
  • Never mutate process.env.
  • Import ToolError from ./errors.

Test file extensions/vision/src/config.test.ts (TDD, no filesystem or network needed except where a temp .env is written):

  • defaults for every variable when only DEEPSEEK_API_KEY is set;
  • process.env value beats .env value (merged-object precedence);
  • missing and empty key ⇒ null;
  • trailing-slash normalisation;
  • invalid VISION_MAX_TOKENS, invalid VISION_TIMEOUT, non-positive numbers, and bad VISION_THINKINGToolError with category === "config";
  • boolean parsing for true/false/1/0.

Acceptance Criteria

  • resolveConfig returns null when DEEPSEEK_API_KEY is missing or empty, and a fully-populated VisionConfig otherwise.
  • All documented defaults are applied, and VISION_BASE_URL trailing slashes are stripped.
  • VISION_THINKING accepts exactly true/false/1/0 and defaults to false.
  • Invalid numerics and invalid booleans throw ToolError with category === "config", and the message names the offending variable.
  • loadEnvFile() parses .env via dotenv.parse and never mutates process.env.
  • extensions/vision/src/config.test.ts covers every row above and node --test extensions/vision/src/config.test.ts passes.

Test Plan

cd /Users/david/Projects/pi-extensions-and-skills
node --test extensions/vision/src/config.test.ts

Expected: all tests pass with no network access; the only filesystem access is a temp .env fixture.

Manual unconfigured check (no .env, no key):

node --experimental-strip-types -e "import('./extensions/vision/src/config.ts').then(m => console.log(m.resolveConfig({})))"

Expected: null.

## Summary Create `extensions/vision/src/config.ts`: load the project `.env` without touching `process.env`, merge it with the real environment, and resolve the `vision` extension's configuration — returning `null` when `DEEPSEEK_API_KEY` is absent so the extension can register zero tools. ## Background **Depends on:** #253 The `vision` extension is unconfigured-safe: with no `DEEPSEEK_API_KEY` it must register no tool and boot pi cleanly (the same shape `extensions/mongodb` and `extensions/postgres` use for an unset `MONGODB_URI`). When keyed, it needs a resolved config object that every later module consumes — the HTTP client reads `apiKey`/`baseUrl`/`timeoutMs`/`model`, the tool reads `maxTokens`/`defaultThinking`. The `.env` handling deliberately mirrors `extensions/mongodb/src/env.ts`: `dotenv.parse` on the file contents (never `dotenv.config`, which would mutate `process.env`), with the real `process.env` winning on conflicts. Invalid numeric settings must fail loudly at load time with a `ToolError` (`config` category) rather than silently falling back to a default — a typo in `VISION_MAX_TOKENS` should be visible immediately. ## Documentation Required A separate process downloads these into the listed folders before this issue is implemented. Check the folder for the actual reference material before starting. **`docs/reference/dotenv/`** - https://github.com/motdotla/dotenv#readme — `dotenv.parse()` behaviour (returns a record, does not mutate `process.env`), which is the whole point of using it over `dotenv.config`. - https://github.com/motdotla/dotenv/blob/master/lib/main.d.ts — the exact `parse` signature for typing. **`docs/reference/nodejs/`** - https://nodejs.org/api/process.html#processenv — `process.env` semantics (values are `string | undefined`). **`docs/reference/deepseek-api/`** - https://api-docs.deepseek.com/quick_start/pricing/ — confirms `deepseek-flash` and `https://api.deepseek.com` as the model/base URL defaults. ## Implementation Details Create `extensions/vision/src/config.ts` with this public surface: ```ts export interface VisionConfig { apiKey: string; model: string; baseUrl: string; maxTokens: number; timeoutMs: number; defaultThinking: boolean; } /** Returns null when DEEPSEEK_API_KEY is absent or empty. */ export function resolveConfig(env: Record<string, string | undefined>): VisionConfig | null; /** Parse the repo-root `.env` without mutating `process.env`. */ export function loadEnvFile(): Record<string, string>; ``` `loadEnvFile` copies `extensions/mongodb/src/env.ts`'s `loadEnvFile` (fs read of `path.join(process.cwd(), ".env")`, `dotenv.parse`, return `{}` on any error). The factory calls it and merges as `{ ...loadEnvFile(), ...process.env }` so `process.env` wins. Variables and defaults (all optional except the key): | Variable | Required | Default | Notes | |---|---|---|---| | `DEEPSEEK_API_KEY` | yes | — | Absent **or empty** ⇒ `null` | | `VISION_MODEL` | no | `deepseek-flash` | sent verbatim | | `VISION_BASE_URL` | no | `https://api.deepseek.com` | strip trailing slashes; endpoint is `${base}/chat/completions` | | `VISION_MAX_TOKENS` | no | `8000` | must parse as a number | | `VISION_TIMEOUT` | no | `120000` | ms; must parse as a number | | `VISION_THINKING` | no | `false` | accept `true`/`false`/`1`/`0` | Details: - An unset or empty-string variable falls back to its default; whitespace-only values count as empty. - Trailing-slash normalisation: `https://api.deepseek.com///` ⇒ `https://api.deepseek.com`. - A value that does not parse as a finite number for `VISION_MAX_TOKENS`/`VISION_TIMEOUT`, or a `VISION_THINKING` value outside the four accepted forms, throws `new ToolError(..., "config")` with a message naming the variable and the bad value (e.g. `` `VISION_MAX_TOKENS must be a number, got "abc"` ``). Numeric values must also be > 0. - Never mutate `process.env`. - Import `ToolError` from `./errors`. Test file `extensions/vision/src/config.test.ts` (TDD, no filesystem or network needed except where a temp `.env` is written): - defaults for every variable when only `DEEPSEEK_API_KEY` is set; - `process.env` value beats `.env` value (merged-object precedence); - missing and empty key ⇒ `null`; - trailing-slash normalisation; - invalid `VISION_MAX_TOKENS`, invalid `VISION_TIMEOUT`, non-positive numbers, and bad `VISION_THINKING` ⇒ `ToolError` with `category === "config"`; - boolean parsing for `true`/`false`/`1`/`0`. ## Acceptance Criteria - [ ] `resolveConfig` returns `null` when `DEEPSEEK_API_KEY` is missing or empty, and a fully-populated `VisionConfig` otherwise. - [ ] All documented defaults are applied, and `VISION_BASE_URL` trailing slashes are stripped. - [ ] `VISION_THINKING` accepts exactly `true`/`false`/`1`/`0` and defaults to `false`. - [ ] Invalid numerics and invalid booleans throw `ToolError` with `category === "config"`, and the message names the offending variable. - [ ] `loadEnvFile()` parses `.env` via `dotenv.parse` and never mutates `process.env`. - [ ] `extensions/vision/src/config.test.ts` covers every row above and `node --test extensions/vision/src/config.test.ts` passes. ## Test Plan ```bash cd /Users/david/Projects/pi-extensions-and-skills node --test extensions/vision/src/config.test.ts ``` Expected: all tests pass with no network access; the only filesystem access is a temp `.env` fixture. Manual unconfigured check (no `.env`, no key): ```bash node --experimental-strip-types -e "import('./extensions/vision/src/config.ts').then(m => console.log(m.resolveConfig({})))" ``` Expected: `null`.
david self-assigned this 2026-09-17 03:32:49 +00:00
david closed this issue 2026-09-17 08:23:06 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #268

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/268
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills#254
No description provided.