Skip to content

fix(cli): implement documented --base-url flag with a generic custom OpenAI-compatible provider#82

Open
LivinTribunal wants to merge 2 commits into
mainfrom
fix/base-url-flag
Open

fix(cli): implement documented --base-url flag with a generic custom OpenAI-compatible provider#82
LivinTribunal wants to merge 2 commits into
mainfrom
fix/base-url-flag

Conversation

@LivinTribunal

@LivinTribunal LivinTribunal commented Jul 6, 2026

Copy link
Copy Markdown

Fixes #80.

--base-url was documented at harnext.dev but never implemented: parseArgs silently dropped the flag token and its URL value fell into args.messages, i.e. it was sent to the model as literal prompt text. There was also no way to reach a self-hosted OpenAI-compatible endpoint, since resolveModel only special-cased ollama/nvidia/openrouter.

What this does

  • --base-url <url> — real flag, parsed in args.ts, documented in --help. Implies --provider custom when no --provider is given (matching the documented example harnext --base-url http://localhost:8000/v1 --model my-finetune). Also honored for --provider ollama (overrides the stored Ollama URL). Any other explicit provider errors clearly — registry providers use non-OpenAI APIs where a URL swap would half-work at best.
  • --api-key <key> — generic per-run key for whatever provider is resolved (env-var injection for registry providers, threaded into the stream call for custom/ollama/nvidia). Never persisted. Interplay with --fallback-model (feat: request-time model fallback at streamFn — honor --fallback-model (#51) #65): the key seeds only the primary provider; a fallback on a different provider resolves its own env key, so the primary key never reaches the fallback endpoint.
  • New custom provider (packages/core/src/custom.ts) — generalizes the Ollama builder to any OpenAI-compatible endpoint (vLLM, llama.cpp, LiteLLM, LM Studio). /v1 is appended only when the URL has no path, so http://host:8000, http://host:8000/v1, and non-standard mounts all work.
  • SDK: createAgentSession gains baseUrl/apiKey options; a stored auth.json config (getProviderConfig('custom')?.baseUrl) works as fallback for SDK/heartbeat callers.

Semantics worth noting

  • Ephemeral: CLI-passed baseUrl/apiKey are never written to auth.json or preferences, consistent with -m/--provider not persisting. ensureAuth short-circuits on CLI overrides, so a fresh machine never sees the onboarding prompt in headless runs.
  • Keyless endpoints get a placeholder key in the stream call: pi-ai's OpenAI-compatible client throws on an empty key and would otherwise silently fall back to $OPENAI_API_KEY — leaking the user's real OpenAI key to an arbitrary URL. The placeholder is both a correctness and a security fix.
  • custom is hidden from the interactive pickers (new hidden flag on ProviderInfo): an unbranched menu entry would crash on the registry lookup. Full picker integration (base-URL prompt + /v1/models listing, mirroring the NVIDIA/Ollama flows) is a clean follow-up.
  • Behavior change: resuming a session with --base-url no longer silently reuses the resumed session's provider/model — the old model id (e.g. an Anthropic id) would be meaningless against the new endpoint, so the user gets the explicit "requires --model" error instead.

Validation errors (all exit 1 with a clear message)

  • --base-url with a non-custom/non-ollama provider
  • malformed --base-url
  • provider custom without a model id
  • provider custom without any base URL (flag or stored config)

Tests

Four new files (21 cases): builder/URL-normalization + registry assertions (custom.test.ts), offline SDK model resolution incl. stored-config fallback and the Ollama override (sdk-model-resolution.test.ts), the args regression — documented example parses with empty messages (args-base-url.test.ts), and ensureAuth override short-circuit with nothing persisted (ensure-auth-overrides.test.ts). Full suite: 76 files / 649 tests green; typecheck + lint clean.

E2E verified on a clean HARNEXT_HOME: the documented command goes straight to the endpoint (connection error against a dead port, no onboarding, no "Unknown model"), and each error path prints its message.

Updated after merging main: the custom-provider resolution now lives in the shared resolveModel() (new optional baseUrl param, 2-arg callers unchanged) and the key injection in withProviderAuth(), both introduced by the #65 fallback refactor.

…tible provider

--base-url was documented but unrecognized: parseArgs silently dropped the
flag and its URL value leaked into args.messages as prompt text (#80).

- parse --base-url / --api-key in args.ts and document them in --help
- new custom.ts builder generalizing the ollama pattern to any
  OpenAI-compatible endpoint; /v1 appended only when the URL has no path
- sdk: baseUrl/apiKey session options, custom model-resolution branch,
  ollama baseUrl override, streamFn seeds the caller apiKey for every
  provider and injects a placeholder key for keyless custom endpoints
  (prevents pi-ai falling back to $OPENAI_API_KEY against arbitrary URLs)
- --base-url implies --provider custom; other providers error clearly
- ensureAuth short-circuits on CLI overrides: never prompts, never persists
- custom provider hidden from interactive pickers for now
@LivinTribunal LivinTribunal requested a review from yasha-dev1 July 6, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--base-url flag documented but not implemented — silently dropped (and its value leaks into the prompt)

1 participant