JuliusBrussee/caveman · error

compat upstream base_url

Error message

compat upstream %q base_url: %w

What it means

NewNamedWithWireDialect validates the mount's base_url with ValidateBaseURL and wraps any failure as "compat upstream %q base_url: %w", naming the mount. A malformed base URL prevents constructing the adapter so routing to that upstream is refused at startup.

Solutions

  1. Set base_url to a full absolute URL including scheme, e.g. https://api.example.com.
  2. Trim stray whitespace/quotes from the config value or env interpolation.
  3. Check the message's mount name and fix that specific upstream's base_url entry.
  4. Validate the URL (net/url.Parse + scheme check) before writing it into config.

Example fix

# before
base_url = "localhost:8080/v1"
# after
base_url = "https://localhost:8080/v1"
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(strings.TrimSpace(rawBaseURL))
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
    return fmt.Errorf("base_url must be absolute http(s) URL, got %q", rawBaseURL)
}

Try / catch

adapter, err := openaicompat.NewNamedWithWireDialect(name, baseURL, dialect)
if err != nil {
    if strings.Contains(err.Error(), "base_url") {
        return nil, fmt.Errorf("fix base_url for %s: %w", name, err)
    }
    return nil, err
}

Prevention

When it happens

Trigger: baseURL after TrimSpace fails ValidateBaseURL: empty string, missing scheme (no http:// or https://), non-URL text, or a URL with disallowed components — passed via NewNamedWithWireDialect, NewNamed, or buildAdapters from config.

Common situations: Config with base_url = "localhost:8080" (no scheme); trailing whitespace or quotes left in the YAML/env value; placeholder URLs like "https://YOUR_API_HOST" not substituted; empty base_url for an optional mount.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/88f8e3f3825319c0. Report an issue: GitHub.

Appendix: source

Thrown at proxy/providers/openaicompat/openaicompat.go:378

//     the OpenAI-shape contradiction check reads a warm cache as malformed and
//     drops every such row from token accounting (issue #1026).
//   - COMPRESSION ZONES follow the Anthropic Messages grammar on Messages-path
//     requests and keep the OpenAI grammar on every other path (see
//     anthropicWireZones): the Anthropic extractor keys the live/frozen
//     boundary on the request's own cache_control breakpoints.
//
// Routing, header mapping, telemetry provider, and pricing keep the mount's
// openai_compatible identity in every dialect.
func NewNamedWithWireDialect(name, baseURL, wireDialect string, forwardHeaders ...string) (providers.Adapter, error) {
	if err := ValidateName(name); err != nil {
		return nil, err
	}
	if err := ValidateWireDialect(wireDialect); err != nil {
		return nil, fmt.Errorf("compat upstream %q: %w", name, err)
	}
	baseURL = strings.TrimSpace(baseURL)
	if err := ValidateBaseURL(baseURL); err != nil {
		return nil, fmt.Errorf("compat upstream %q base_url: %w", name, err)
	}
	if err := ValidateForwardHeaders(forwardHeaders); err != nil {
		return nil, fmt.Errorf("compat upstream %q forward_headers: %w", name, err)
	}
	prefix := "/compat/" + name
	return namedAdapter{
		Base: providers.Base{
			Provider:      "openai_compatible",
			BaseURL:       baseURL,
			Routes:        []string{prefix + "/"},
			UsageProvider: wireDialectUsageProvider[wireDialect],
		},
		prefix:         prefix,
		forwardHeaders: append([]string(nil), forwardHeaders...),
		wireDialect:    wireDialect,
	}, nil
}

View on GitHub (pinned to 3ee70a1026)