Skip to content

Enforce the /v1 api_base invariant with types + a drift-checker assertion #746

Description

@bussyjd

Problem

The /v1-suffix bug family (#745, ffa7cf4, the deleted WarnAndStripV1Suffix fossil) is a recurring type error: "service root without /v1" and "OpenAI base with /v1" are two different types carried in one string. The invariant is currently documentation-enforced (CLAUDE.md pitfall 6, a doc comment on buildCustomEndpointEntryWithOptions) — and documentation-enforced invariants are exactly what decayed into this incident: the codebase carried two contradictory theories of who appends /v1 for months, with no test or build failure.

Full audit of every producer/consumer: #745 (comment)

Proposal (~40 lines, one package)

  1. Newtypes in internal/model:
type ServiceRoot string  // scheme://host:port — never a path
type OpenAIBase string   // ServiceRoot + "/v1" — what a LiteLLM openai/ api_base must be

func NewServiceRoot(raw string) (ServiceRoot, error)  // trims trailing "/"; REJECTS a trailing /v1 with an error — never silently strips
func (r ServiceRoot) OpenAI() OpenAIBase
func (r ServiceRoot) ChatCompletions() string

buildCustomEndpointEntryWithOptions takes OpenAIBase; the discovery bug class becomes a compile error. Buyer-side code keeps ServiceRoot and derives full URLs at the edge (it already follows this convention — buyprompts.ChatCompletionsURL).

  1. Runtime tripwire for what types can't reach (the live ConfigMap): the config-drift checker compares model names only, so a same-name entry with a divergent api_base — exactly the poisoned model group in fix(model): register discovered endpoints with /v1 api_base #745's incident — is invisible to it. Add an assertion at reconcile: every openai/ entry's api_base ends in /v1. This alone would have caught the incident in production.

  2. Rejection over rewriting: constructors error on ambiguous input. Silent normalization is how the WarnAndStripV1Suffix disaster happened — a helpful auto-fix encoding a wrong theory.

Non-goals

No behavior change; pure enforcement of the invariant #745 documented. Not release-blocking for v0.13.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions