ObolNetwork / ObolNetwork/obol-stack

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

Open
#746 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
11
Forks
1
PR merge metrics
No merged PRs in 30d

Description

Problem

The /v1-suffix bug family (#745, ffa7cf49, 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: https://github.com/ObolNetwork/obol-stack/pull/745#issuecomment-4966292476

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 #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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by auditing internal/model and the buildCustomEndpointEntryWithOptions call path, using buyprompts.ChatCompletionsURL as the existing ServiceRoot convention. Then trace the config-drift checker through reconcile and its live ConfigMap entries. Done means the proposed types reject ambiguous inputs, consumers use the appropriate type, and reconciliation asserts that every openai/ api_base ends in /v1.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, kubernetes
Domain
backend, devops
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.