JuliusBrussee/caveman · error

provider %q has no configured upstream URL

Error message

provider %q has no configured upstream URL

What it means

Base.ResolveUpstreamURL determines where to forward a request: it uses route.BaseURL if set, else the adapter's configured BaseURL, and refuses to proceed when the result is empty/whitespace. Without a base URL the proxy has no upstream to dial, so it fails closed rather than guessing a provider default.

Source

Thrown at proxy/providers/adapter.go:408

	for _, route := range b.Routes {
		// Routes ending in '/' are explicit subtree mounts (for example
		// /compat/ and /azure/). Every other route is exact. Treating all routes
		// as prefixes made /v1/responses-anything and /v1/messages/unknown valid
		// proxy surfaces, violating the gateway's closed route allowlist.
		if path == route || strings.HasSuffix(route, "/") && strings.HasPrefix(path, route) {
			return true
		}
	}
	return false
}

func (b Base) ResolveUpstreamURL(ctx context.Context, req *http.Request, route RouteContext) (*url.URL, error) {
	baseURL := b.BaseURL
	if route.BaseURL != "" {
		baseURL = route.BaseURL
	}
	if strings.TrimSpace(baseURL) == "" {
		return nil, fmt.Errorf("provider %q has no configured upstream URL", b.Provider)
	}
	base, err := url.Parse(baseURL)
	if err != nil {
		return nil, err
	}
	if !base.IsAbs() || base.Hostname() == "" {
		return nil, fmt.Errorf("provider %q upstream URL must be absolute", b.Provider)
	}
	path := req.URL.Path
	for _, prefix := range []string{"/openai", "/anthropic", "/gemini", "/compat/stub", "/compat/openai-compatible"} {
		path = strings.TrimPrefix(path, prefix)
	}
	if strings.HasPrefix(req.URL.Path, "/azure/") {
		path = strings.TrimPrefix(req.URL.Path, "/azure")
	}
	base.Path = strings.TrimRight(base.Path, "/") + path
	base.RawQuery = req.URL.RawQuery
	return base, nil

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set the provider's base URL in config (e.g. base_url: https://api.openai.com) or the corresponding env var, then restart.
  2. If overriding per route, confirm the route actually populates BaseURL — empty string falls back to the adapter config, which must then be non-empty.
  3. Print the effective config at startup or add a config lint that fails when a enabled provider lacks a base URL.

Example fix

# before (caveman.yaml)
providers:
  openai-compat:
    api_key: sk-...
    # base_url missing -> error on first request

# after
providers:
  openai-compat:
    api_key: sk-...
    base_url: https://api.mylab.local/v1
Defensive patterns

Strategy: validation

Validate before calling

func hasUpstreamBaseURL(routeBase, adapterBase string) bool {
    effective := routeBase
    if strings.TrimSpace(effective) == "" {
        effective = adapterBase
    }
    return strings.TrimSpace(effective) != ""
}

if !hasUpstreamBaseURL(route.BaseURL, adapter.BaseURL) {
    return fmt.Errorf("provider %s: configure base_url before routing traffic", name)
}

Try / catch

if _, err := adapter.ResolveUpstreamURL(ctx, req, route); err != nil {
    if strings.Contains(err.Error(), "has no configured upstream URL") {
        http.Error(w, "provider not configured with an upstream base URL", http.StatusBadGateway)
        return
    }
    http.Error(w, err.Error(), http.StatusBadRequest)
}

Prevention

When it happens

Trigger: A provider entry (e.g. in caveman.yaml or the managed route config) was created with credentials but no base_url; RouteContext carried an empty BaseURL and the adapter default was never set; the env var backing the adapter's base URL was unset in the deployment unit.

Common situations: Adding a new openai-compatible provider and forgetting the base_url field; base URL configured only for a different environment/profile; YAML indentation puts base_url under the wrong provider block so it parses as absent.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/292befc3ef0991a9. Report an issue: GitHub.