Env-sourced endpoints are not trimmed: a trailing CR in ANTHROPIC_BASE_URL fails every file with an opaque URL-parse error

Author: seyeeLCreated Sep 18, 2026Updated Sep 18, 2026

OpenCodeReview Version

open-code-review dev windows/amd64 — built from source at commit 089b68b (v1.12.4-14-g089b68b, 2026-09-17). Re-checked against origin/main 189be5b (2026-09-17 20:59): both functions below still read the raw environment values there.

Operating System

Windows (Windows 11, 10.0.22631), Git Bash

Installation Method

Built from source (go build -o dist/ocr.exe ./cmd/opencodereview)

LLM Provider

Anthropic-compatible gateway, reached through the Claude Code environment resolution path (ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL)

Bug Description

Endpoint resolution normalizes its inputs on some paths but not others:

resolution path trims the value?
parseShellRC (shell rc file) yes — line and value (internal/llm/resolver.go ~767, ~780)
parseTimeoutEnv yes (~204)
NormalizeAuthHeader yes (~827)
provider api_key yes (~426)
tryOCREnvOCR_LLM_URL/TOKEN/MODEL no (~251-254)
tryCCEnvANTHROPIC_BASE_URL/AUTH_TOKEN/MODEL no (~702-705)

So a value that the shell-rc path tolerates is fatal on the environment path. OCR_LLM_PROTOCOL, read a few lines below tryOCREnv's untrimmed reads, is wrapped in strings.TrimSpace, which makes this look like an oversight rather than intent.

Two consequences:

  1. Opaque failure. The Go SDK rejects the value before any request is sent: requestoption: WithBaseURL failed to parse url parse "...\r": net/url: invalid control character in URL. Nothing in the message points at the environment, or at which variable was dirty. In my case this failed all 36 files of a review run (36 llm_error records with an identical message in the session JSONL), and the value only had a trailing \r — exactly what a CRLF-generated environment produces on Windows.
  2. The session log can't be triaged. llm_error records carry only the raw error string, not the resolved Source, so a reader cannot tell whether the bad endpoint came from the environment, the config file, or the rc file.

Steps to Reproduce

A dummy token is enough: the failure happens while parsing the URL, before any request, so nothing is spent.

bash
export ANTHROPIC_BASE_URL=$'https://<host>/api/anthropic\r'   # trailing CR, e.g. CRLF-generated env
export ANTHROPIC_AUTH_TOKEN=dummy
export ANTHROPIC_MODEL=<model>
ocr llm test

export ANTHROPIC_BASE_URL="https://<host>/api/anthropic"      # same value, trimmed
ocr llm test

The same applies to OCR_LLM_URL / OCR_LLM_TOKEN / OCR_LLM_MODEL.

Expected Behavior

  • OCR_LLM_* and ANTHROPIC_* values are trimmed before use — or normalized once in finalizeResolvedEndpoint, which already normalizes the model suffix — so every resolution path behaves identically.
  • When a value can't be used, the error names the source and the variable, e.g. Claude Code environment: invalid ANTHROPIC_BASE_URL (control character in URL), and the session record preserves which source was resolved.

Logs / Error Output

bash
$ ANTHROPIC_BASE_URL=$'https://zenmux.ai/api/anthropic\r' ANTHROPIC_AUTH_TOKEN=dummy ANTHROPIC_MODEL=<model> ocr llm test
Error: llm request failed: requestoption: WithBaseURL failed to parse url parse "https://zenmux.ai/api/anthropic\r": net/url: invalid control character in URL

$ ANTHROPIC_BASE_URL="https://zenmux.ai/api/anthropic" ANTHROPIC_AUTH_TOKEN=dummy ANTHROPIC_MODEL=<model> ocr llm test
Error: llm request failed: POST "https://zenmux.ai/api/anthropic/v1/messages": 403 Forbidden {"error":{"code":"403","type":"access_denied",...}}

The second command reaches the request, i.e. URL construction succeeded once the CR was gone (the token is a dummy, so the gateway rejects the auth — nothing was consumed).

Session JSONL from the failing review run, 36 records of this shape:

json
{"duration_ms":0,"error":"requestoption: WithBaseURL failed to parse url parse \"https://<host>/api/anthropic\\r\": net/url: invalid control character in URL","filePath":"<redacted>","request_no":1,"taskType":"main_task","type":"llm_error"}

AI/LLM Disclosure

The reproduction and verification above were run by me. This write-up was drafted with an AI coding agent (OMP, model deepseek/deepseek-v4.1-flash, via a gateway), and I checked every command and log line myself.

Additional Context

Related, on the same fallback path — disclosing the resolved endpoint:

The Claude Code environment fallback is documented and covered by tests (resolver_test.go:55), and honoring ambient ANTHROPIC_* is reasonable for zero-config use. What's missing is disclosure: the resolved source is printed only by ocr llm test (cmd/opencodereview/llm_cmd.go:110), while ocr review prints nothing about it and --preview lists files only. A review launched by a wrapper script, another agent, or CI therefore silently inherits whatever endpoint and model the ambient environment carries — and the price spread between tiers is large (roughly 8x between the cheap and expensive tier on my gateway). One 36-file review inherited an expensive tier model and consumed ~1.42M tokens, about 90% of that day's gateway spend, which I only noticed on the bill.

Suggestions, in descending order of how much they would have helped:

  1. Print one line at review start, on stderr so --format json|sarif stdout stays clean: [ocr] llm: <model> via <source>.
  2. When the only resolved source is an ambient one (Claude Code environment / Shell rc file), say so explicitly, and consider requiring confirmation (--yes) or offering an escape hatch such as --no-ambient-endpoint.
  3. Let ocr llm test accept --model the way ocr review does, so a run with a specific model can be preflighted. My preflight failed with unknown flag: --model and had to be rewritten to set ANTHROPIC_MODEL instead.

Minor note from the same run: the review exited 0 with 22 of 36 items covered after the gateway answered 402 quote_exceeded for the rest. That matches the documented contract (cmd/opencodereview/review_cmd.go:330-339 — non-zero only when every selected item failed) and is the right behavior for a self-imposed budget cap, but for a provider-side auth/quota failure CI would read a half-finished review as success.

Source: alibaba/open-code-review