Post-v1: per-RFC model availability (UX half) folded into §6.6
First §19.2 candidate settled after v1. The heavier per-RFC-model
topic subdivided into UX (this) and credential delegation + funder
role (still §19.2). New §6.6 carries the rule: an optional `models:`
frontmatter field on the meta-repo RFC entry; absent inherits the
operator universe, populated narrows the picker to the intersection
with provisioned providers, `[]` opts the RFC out of AI entirely.
The first resolved entry is the RFC default. §18's ENABLED_MODELS is
reframed as the operator universe.
Code: migration 009 adds nullable cached_rfcs.models_json (NULL ≠ []
is load-bearing); entry.py grows the optional field with absent-vs-
empty round-tripping in parse/serialize; new models_resolver module
holds the rule; api_branches replaces /api/models with the slug-aware
/api/rfcs/{slug}/models and threads the chat + reask paths through
the resolver; api_prs §10.2 uses the resolver and extends the stub
fallback to the opt-out case; frontend passes slug to listModels.
Tests 106/106 green (96 prior + 10 in test_per_rfc_models.py). No
behavioral change for entries without `models:` — operator universe
preserved as default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""§6.6 per-RFC model availability — the resolver.
|
||||
|
||||
The meta-repo entry's optional `models:` frontmatter and the operator's
|
||||
provisioned providers (the §18 `ENABLED_MODELS` universe) combine into a
|
||||
single resolved list per RFC. Every AI surface picks against that list:
|
||||
the §8.12 chat picker, the §10.2 PR-draft, the §8.13 flag-resolution
|
||||
invocation, the §9.1 tag suggestions when graduation is in scope.
|
||||
|
||||
Rule, in one place:
|
||||
|
||||
- Cache row's `models_json` is NULL → the field is absent on the entry.
|
||||
Resolved list = the operator's provisioned universe.
|
||||
- Cache row's `models_json` is a JSON array → the field is set on the
|
||||
entry. Resolved list = intersection of the array with the operator's
|
||||
provisioned universe, preserving the entry's stated order.
|
||||
|
||||
The empty case folds in naturally: an entry with `models: []` yields an
|
||||
empty intersection, and an entry whose listed models are no longer
|
||||
provisioned by the operator also yields an empty intersection. Callers
|
||||
treat both the same — surfaces refuse cleanly per §6.6.
|
||||
|
||||
The function is slug-aware and provider-aware; it does not depend on
|
||||
the FastAPI request lifecycle, which keeps it cheap to call inside any
|
||||
endpoint that knows the slug and has the providers dict in scope.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from . import db
|
||||
from .providers import BaseProvider
|
||||
|
||||
|
||||
def resolve_models_for_rfc(
|
||||
slug: str, providers: dict[str, BaseProvider]
|
||||
) -> list[str]:
|
||||
"""Return the per-RFC resolved model keys per §6.6.
|
||||
|
||||
The first entry is the RFC's default model. An empty list means
|
||||
AI is unavailable on this RFC and callers refuse the AI surface.
|
||||
"""
|
||||
universe = list(providers.keys())
|
||||
row = db.conn().execute(
|
||||
"SELECT models_json FROM cached_rfcs WHERE slug = ?",
|
||||
(slug,),
|
||||
).fetchone()
|
||||
if row is None or row["models_json"] is None:
|
||||
return universe
|
||||
try:
|
||||
listed = [str(m) for m in json.loads(row["models_json"])]
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return universe
|
||||
if not listed:
|
||||
return []
|
||||
return [m for m in listed if m in providers]
|
||||
|
||||
|
||||
def default_model_for_rfc(
|
||||
slug: str, providers: dict[str, BaseProvider]
|
||||
) -> str:
|
||||
"""The first entry in the resolved list, or empty string if none.
|
||||
|
||||
Callers that need a deterministic single choice — §10.2's draft,
|
||||
§9.1's tag suggestions — use this. An empty return signals the
|
||||
refusal path per §6.6.
|
||||
"""
|
||||
resolved = resolve_models_for_rfc(slug, providers)
|
||||
return resolved[0] if resolved else ""
|
||||
Reference in New Issue
Block a user