Skip to content

[RFC]: A capability-based Model IR — decouple model access from the OpenAI protocol #25

Description

@ethan-scitix

Motivation

Model's access interface is overfit to the OpenAI /v1/completions protocol, and
that assumption is about to break in three directions at once: RL/logprobs
consumers, the "big three" latest APIs (OpenAI Responses, Anthropic Messages,
Google genai / AI Studio), and richer eval logic (reasoning, multi-turn, continuation).

Evidence the current abstraction leaks:

  • Model.alogprobs(prompt, *, max_tokens, logprobs: int, echo, temperature) is
    completions vocabulary. ChatModel._alogprobs_impl accepts echo and silently
    ignores it
    — a phantom param on the universal interface.
  • Every non-completions backend must translate: echo -> logprob_start_len,
    logprobs: int -> top_logprobs_num, max_tokens -> max_new_tokens.
  • echo-based prompt scoring is itself an OpenAI workaround for the lack of a
    native scoring endpoint; sglang /generate has return_logprob and no echo
    concept. The base models a workaround as if it were the universal primitive.
  • The output shape (top_logprobs: list[dict[str, float]]) cannot carry token ids
    or a reasoning channel — both required by real consumers (RL, reasoning eval).

The interface was only ever validated by one protocol. The incoming sglang native
backend, plus the big-three protocols, are the "second implementors" that expose the
boundary. Rather than keep bolting workarounds on, define a provider-agnostic
IR for model capabilities and make every provider an adapter onto it.

Proposed Change

Core idea — one Model IR + transports as frontends

  • One Model IR, two message shapes: Request and Response (the existing
    ModelOutput is the Response side; rename or alias).
  • Transport = provider frontend (strategy, composed into Model, not a base
    class): it lowers a Request into its native wire and lifts the native
    response back into a Response. OpenAI completions / OpenAI chat / OpenAI
    Responses / Anthropic Messages / Google genai / sglang native / vLLM are all just
    frontends.
  • Capabilities = which IR features a frontend can honor, declared per transport
    as optional Protocols. The catalog is open: adding a capability later =
    add a Protocol + optional Request/Response fields (additive, not a redesign).
  • Validation = a Request using a feature the transport lacks is rejected at
    setup
    , never silently ignored (kills the echo-on-chat class of bug).

Composition (Model)

Model = { resource pool / limiter (explicit collaborator) } + { Transport (frontend) }
  async def arun(req: Request) -> Response          # the one primitive (acquires limiters)
  # capability-gated sugar (thin wrappers over arun, present only when supported):
  #   agenerate(...)      [any]
  #   achat(...)          [Chat]
  #   ascore_input(...)   [InputScoring]
  • Drop as_type (cross-kind base<->inst conversion): its only use case
    (mixing base and inst on one endpoint in a single eval) has no real consumer.
  • with_args decomposes: per-task infer_args become plain Request fields
    (no derived model object); concurrency_limit sub-quota becomes an explicit
    resource-pool collaborator. No "conversion capability" axis survives.

Consumer -> capability matrix

consumer capabilities
1. RL / agentic Chat + Tools + SampledLogprobs(+token_ids) + (Reasoning)
2. reasoning eval Chat + Reasoning + big-three adapters (Responses/Messages/genai)
3. multi-turn Chat
4. base ppl / bpb Completion + InputScoring(+byte/char accounting) + TopKLogprobs
5. special (budget/truncate/continuation) Reasoning(controls) + Prefill / FIM

Carved OUT of the model interface (task layer): fallback-extract, LLM-judge
orchestration, thinking-truncation post-processing. (judge = a Chat call + task scoring.)

Provider -> capability matrix

transport capabilities
OpenAI completions Completion, InputScoring(echo), TopK
OpenAI chat Chat, Tools, TopK, Structured
OpenAI Responses Chat, Tools, Reasoning, Structured
Anthropic Messages Chat, Tools, Reasoning(+budget), Prefill
Google genai / AI Studio Chat, Tools, Reasoning
sglang /generate Completion, InputScoring, SampledLogprobs, TopK, Prefill
vLLM (oai-compat) ~OpenAI + prompt_logprobs

IR shapes + capability catalog

Request:  input: str | list[Message]            # modality (rides with transport)
          sampling (temp/top_p/max_tokens/stop/seed)
          tools? ; reasoning?(budget/effort) ; response_format?
          prefix? / suffix?                      # constrained-context corner (Prefill/FIM)
          return_logprobs? / score_input? / top_k?

Response: texts
          reasoning?: { text, opaque_roundtrip } # opaque needed for AnthropIc/Google multi-turn signatures
          tool_calls?
          logprobs?(+token_ids) ; top_logprobs?
          input_scoring?(byte/char)              # for BPB/CLP
          usage

Capabilities: Completion | Chat | Tools | Reasoning | Prefill | FIM |
              InputScoring | SampledLogprobs | TopKLogprobs | StructuredOutput

Feedback Period

~1-2 weeks (design-heavy).

Any Other Things

  • Lock with RFC [RFC]: Resume version-compatibility gate + per-record version provenance #24. The Response IR is the persisted record schema
    (@sieval_record). So IR evolution is exactly what [RFC]: Resume version-compatibility gate + per-record version provenance #24's resume version gate
    protects: a breaking IR change ships as a >= minor bump and the gate refuses
    cross-series resume. The two RFCs are coupled by design.
  • Non-goals.
    • Environment / sandbox execution (e2b-style RPC) is a sibling abstraction,
      not a model transport. The Model IR only needs to represent tool_call /
      tool_result; who executes them (e2b, local) is a separate Executor IR /
      future RFC. Folding sandbox RPC into the model transport repeats infra-layer
      role confusion.
    • Task-layer eval logic (fallback-extract, judge orchestration, truncation).
    • Structured-FIM wiring until a FIM benchmark exists (OSS FIM is usually
      Completion + a task-built template, not a transport feature).
    • as_type / cross-kind model conversion.
  • Sequencing. The sglang native backend (feat(models): add SglangGenModel for echoed-input logprobs via sglang /generate #21 + follow-up: SglangGenModel
    onto Model[str] + native /generate) lands first under the current interface,
    translating params internally. This RFC is the eventual clean target and does not
    block that work.
  • Dependencies. Big-three adapters mean multiple SDKs -> optional-dependency
    groups + lazy transport loading.
  • Capability requirement declaration. Extend @sieval_task to declare required
    capabilities (today it only declares model_type); assert at setup. Replaces the
    current type: chat | gen with a richer capability requirement.

Before submitting a new issue

  • Make sure you already searched for relevant issues and documentation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCRequest for comments on architectural/design changes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions