Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions verifiers/v1/clients/config.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,59 @@
"""Client configs: describe an OpenAI-compatible endpoint and resolve it to a Client.

A `BaseClientConfig` is an OpenAI-compatible endpoint (base_url + API-key env var
+ extra headers) that `resolve_client` turns into a `Client`. Prime team-billing
is baked in via a validator, so it's handled in one place. Both the eval entrypoint
(its model client) and in-env LLM calls (e.g. a judge reward) build clients from
these — inherit `BaseClientConfig` to get the endpoint/header handling for free.
+ extra headers) that `resolve_client` turns into a `Client`. The default Prime
endpoint, API key, and team fall back to the active Prime CLI config, so direct
`uv run eval` calls behave like `prime eval`. Both the eval entrypoint (its model
client) and in-env LLM calls (e.g. a judge reward) build clients from these.
`ClientConfig` is the CLI-selectable discriminated union (eval | train).
"""

import os
from typing import Annotated, Literal
from urllib.parse import urlparse

from openai import AsyncOpenAI
from pydantic import Field, model_validator
from pydantic_config import BaseConfig
from renderers import RendererConfig

from verifiers.utils.client_utils import load_prime_config
from verifiers.v1.clients.client import Client
from verifiers.v1.clients.eval import EvalClient
from verifiers.v1.clients.train import TrainClient

DEFAULT_PRIME_INFERENCE_URL = "https://api.pinference.ai/api/v1"
PRIME_INFERENCE_HOST = "pinference.ai"
PRIME_TEAM_ID_HEADER = "X-Prime-Team-ID"


class BaseClientConfig(BaseConfig):
"""An OpenAI-compatible endpoint. The API key is read from an env var."""

base_url: str = "https://api.pinference.ai/api/v1"
base_url: str = DEFAULT_PRIME_INFERENCE_URL
api_key_var: str = "PRIME_API_KEY"
headers: dict[str, str] = Field(default_factory=dict)
"""Extra HTTP headers sent on every request."""

@model_validator(mode="after")
def add_prime_team_id(self) -> "BaseClientConfig":
# Prime inference bills the personal balance unless a team is named; on
# that endpoint, route billing to PRIME_TEAM_ID when set (explicit wins).
team_id = os.environ.get("PRIME_TEAM_ID")
if PRIME_INFERENCE_HOST in self.base_url and team_id:
def apply_prime_config(self) -> "BaseClientConfig":
if self.api_key_var != "PRIME_API_KEY":
return self
prime_config = load_prime_config()
prime_base_url = (
os.environ.get("PRIME_INFERENCE_URL")
or prime_config.get("inference_url")
or DEFAULT_PRIME_INFERENCE_URL
)
if "base_url" not in self.model_fields_set:
self.base_url = prime_base_url
Comment on lines +48 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve subclass endpoint defaults

model_fields_set only contains values supplied by the caller, not defaults declared on a BaseClientConfig subclass. With an environment config such as class StagingJudge(BaseClientConfig): base_url = "https://staging.pinference.ai/api/v1" that intentionally keeps api_key_var="PRIME_API_KEY", this branch treats the subclass default as missing and overwrites it with the active Prime/default URL, so the environment silently calls the wrong endpoint unless every caller explicitly passes base_url.

Useful? React with 👍 / 👎.

host = urlparse(self.base_url).hostname or ""
if host != PRIME_INFERENCE_HOST and not host.endswith(
f".{PRIME_INFERENCE_HOST}"
):
return self
Comment thread
cursor[bot] marked this conversation as resolved.
team_id = os.environ.get("PRIME_TEAM_ID") or prime_config.get("team_id")
if team_id:
self.headers.setdefault(PRIME_TEAM_ID_HEADER, team_id)
return self

Expand Down Expand Up @@ -73,7 +89,15 @@ class TrainClientConfig(BaseClientConfig):


def resolve_client(config: BaseClientConfig) -> Client:
api_key = os.environ.get(config.api_key_var, "EMPTY")
api_key = os.environ.get(config.api_key_var)
host = urlparse(config.base_url).hostname or ""
if (
not api_key
and config.api_key_var == "PRIME_API_KEY"
and (host == PRIME_INFERENCE_HOST or host.endswith(f".{PRIME_INFERENCE_HOST}"))
):
api_key = load_prime_config().get("api_key")
Comment on lines +93 to +99

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arfh, i hate this custom logic here. getting auth smell. how come no other provider requires this?

api_key = api_key or "EMPTY"
if isinstance(config, TrainClientConfig):
# The renderer calls a vLLM `/inference/v1/generate` engine through the OpenAI SDK.
openai = AsyncOpenAI(
Expand Down
Loading