Skip to content
Merged
21 changes: 20 additions & 1 deletion src/opendox/cli_model_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ def _declared_binding(args: argparse.Namespace) -> "binding_mod.ModelProviderBin
id=args.id, label=args.label, provider=args.provider,
credential_ref=args.credential_ref, auth_kind=args.auth_kind,
approved_by=args.approved_by, endpoint=args.endpoint,
dialect=args.dialect, broker_argv=tuple(args.broker_argv))
dialect=args.dialect, model=args.model,
broker_argv=tuple(args.broker_argv))


#: What `list` prints for a binding that declares no model (#1144 box 16.2).
#: The request then names the binding's id, as every request did before the
#: field existed, and the operator reading the list should see that.
NO_MODEL_DECLARED = "(none declared: the request names this binding's id)"


def cmd_model_binding_list(args: argparse.Namespace) -> int:
Expand All @@ -77,6 +84,9 @@ def cmd_model_binding_list(args: argparse.Namespace) -> int:
print(f" credential ref {record['credential_ref']}")
print(f" endpoint {record['endpoint']}")
print(f" dialect {record['dialect']}")
model = record["model"]
print(f" model "
f"{model if model is not None else NO_MODEL_DECLARED}")
print(f" broker argv {record['broker_argv']}")
print(f" custody {record['credential_custody']}")
return 0
Expand Down Expand Up @@ -203,6 +213,15 @@ def _add_binding_declaration_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--dialect", required=True,
choices=list(binding_mod.DIALECTS),
help="the request grammar that endpoint speaks")
# THE MODEL THE PROVIDER RECEIVES (#1144 box 16.2), the route's third fact.
# OPTIONAL, and that keeps a binding declared without it meaning what it
# always meant: the request names the binding's id. `edit` replaces the
# whole binding, as it always has, so an edit that omits `--model` declares
# none.
parser.add_argument("--model", default=None,
help="the model name the provider receives in each "
"request (default: none declared, and the "
"request names this binding's id)")
# A POSITIONAL, taken after a bare `--`, and that is the fix for a real
# trap rather than a style choice: a broker invocation is full of
# option-shaped members (`--binding`, `--ref`), and as a flag's value they
Expand Down
47 changes: 39 additions & 8 deletions src/opendox/doxbench_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
would then be accountable for. So provider routing is the CONSUMER's fact, and
the consumer's declared record is where a fact the consumer owns belongs.
Declaring a route is not holding a transport: nothing here opens a socket, and
the module still names no provider host of its own.
the module still names no provider host of its own. #1144 box 16.2 gave the
route a third fact, `model`: the model name the provider receives. It is the
consumer's fact for the same reason.

THE BROKER INVOCATION IS DECLARED, NOT WRITTEN INTO CODE — the program and its
fixed leading arguments. `broker_argv` is the BASE invocation and names no
Expand Down Expand Up @@ -133,9 +135,14 @@
#: `provider` and `approved_by` are REQUIRED flags of the declared `intake`
#: (`--provider`, `--approved-by`; the second because `credential-contracts`
#: holds that a grant without an approver is invalid), and `endpoint`/`dialect`
#: are the provider route the mint answer deliberately does not carry. STILL NO
#: SECRET FIELD: nine fields, and the absence of a tenth is the same point the
#: absence of a sixth was.
#: are the provider route the mint answer deliberately does not carry.
#:
#: AND FROM NINE TO TEN BY #1144 box 16.2 (plan 034 T079): `model`, the model
#: name the provider receives as the request's model. It sits with the route
#: it belongs to, after `dialect`. Before it, the request named the catalog
#: handle, which is this binding's `id`, so no provider model could be named.
#: STILL NO SECRET FIELD: ten fields, and the absence of an eleventh is the
#: same point the absence of a sixth was.
BINDING_FIELDS: tuple[str, ...] = (
"id",
"label",
Expand All @@ -145,16 +152,25 @@
"approved_by",
"endpoint",
"dialect",
"model",
"broker_argv",
)

#: The one field a stored record may leave out. A record without `model` was
#: declared before the field existed, and it keeps the meaning it had: the
#: request names the catalog handle, this binding's `id`. Every other field is
#: required, as it always was.
OPTIONAL_BINDING_FIELDS: tuple[str, ...] = ("model",)

#: The CLOSED placeholder vocabulary an argv template may name. Every member is
#: a field of the binding itself, which is the property that matters: a template
#: can only ever be filled with facts the binding already discloses, so no
#: substitution can smuggle a value the record does not carry. A template naming
#: anything outside this set is refused at construction rather than at
#: execution — an operator finds out when they declare the binding, not when a
#: turn fails.
#: turn fails. `model` is not a member: a broker's invocation is about custody,
#: never about which model a turn asks for, and an undeclared model has no
#: value to fill a placeholder with.
ARGV_PLACEHOLDERS: tuple[str, ...] = (
"binding_id", "label", "provider", "credential_ref", "auth_kind",
"approved_by", "endpoint", "dialect")
Expand Down Expand Up @@ -195,12 +211,19 @@ def _require_non_blank_str(field: str, value: object) -> str:
class ModelProviderBinding:
"""ONE model provider, as settings hold it.

Nine fields, and the absence of a tenth is the point (see the module
Ten fields, and the absence of an eleventh is the point (see the module
docstring). `broker_argv` is the DECLARED BASE invocation as a tuple of argv
members — argv, never a shell string, so no operator's label and no
credential reference can ever be read as shell syntax. It names the program
and its fixed leading arguments and NOT the operation: the operation is a
declared subcommand `doxbench_provider` appends.

`model` (#1144 box 16.2) is the model name the provider receives as the
request's model. It is KEYWORD-ONLY and defaults to None, so every
construction written before it existed still builds the binding it built.
That binding keeps its old meaning: with no model declared, the request
names the catalog handle, which is the binding's `id`, exactly as before.
A declared model is a non-blank string.
"""

id: str
Expand All @@ -211,12 +234,15 @@ class ModelProviderBinding:
approved_by: str
endpoint: str
dialect: str
model: str | None = dataclasses.field(default=None, kw_only=True)
broker_argv: tuple[str, ...]

def __post_init__(self) -> None:
for field in ("id", "label", "provider", "credential_ref", "auth_kind",
"approved_by", "endpoint", "dialect"):
_require_non_blank_str(field, getattr(self, field))
if self.model is not None:
_require_non_blank_str("model", self.model)
if self.auth_kind not in AUTH_KINDS:
raise BindingRefused(
f"auth_kind {self.auth_kind!r} is outside the closed "
Expand Down Expand Up @@ -259,7 +285,8 @@ def __post_init__(self) -> None:
def as_record(self) -> dict:
"""The STORED record: the record kind, then exactly ``BINDING_FIELDS``
in order. `broker_argv` becomes a list because that is what YAML round
trips; nothing else changes shape."""
trips. An undeclared `model` is written as null, so every stored
record carries all ten keys; nothing else changes shape."""
return {
"kind": BINDING_KIND,
"id": self.id,
Expand All @@ -270,6 +297,7 @@ def as_record(self) -> dict:
"approved_by": self.approved_by,
"endpoint": self.endpoint,
"dialect": self.dialect,
"model": self.model,
"broker_argv": list(self.broker_argv),
}

Expand Down Expand Up @@ -334,7 +362,9 @@ def from_record(cls, record: object) -> "ModelProviderBinding":
raise BindingRefused(
f"a binding record declares kind {declared_kind!r}, not "
f"{BINDING_KIND!r}")
missing = [field for field in BINDING_FIELDS if field not in record]
missing = [field for field in BINDING_FIELDS
if field not in record
and field not in OPTIONAL_BINDING_FIELDS]
if missing:
raise BindingRefused(
f"a binding record is missing {missing}")
Expand All @@ -347,6 +377,7 @@ def from_record(cls, record: object) -> "ModelProviderBinding":
approved_by=record["approved_by"],
endpoint=record["endpoint"],
dialect=record["dialect"],
model=record.get("model"),
broker_argv=record["broker_argv"],
)

Expand Down
19 changes: 10 additions & 9 deletions src/opendox/doxbench_intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@
never sees a credential — which is why it is safe for it to be the surface a
browser talks to.

WHAT IT DOES NOT WIDEN, also deliberately: the BINDING's closed nine-field
record (`doxbench_binding.BINDING_FIELDS`) and the CATALOG entry's closed public
shape (`doxbench_model.DECLARABLE_ENTRY_FIELDS`). The count is named by that
tuple rather than restated here, because the shape has grown twice by governing
release since this module was written — the routing declaration at
contract-v1.38 and the input-modality declaration at contract-v2.2 — and this
paragraph's claim is that THIS module widens nothing, which is unchanged by
either. Proposed-versus-
WHAT IT DOES NOT WIDEN, also deliberately: the BINDING's closed record
(`doxbench_binding.BINDING_FIELDS`) and the CATALOG entry's closed public
shape (`doxbench_model.DECLARABLE_ENTRY_FIELDS`). Each count is named by its
tuple rather than restated here, because both shapes have grown since this
module was written. The catalog entry's shape grew twice by governing release
(the routing declaration at contract-v1.38 and the input-modality declaration
at contract-v2.2), and the binding's record grew once, by `model` (#1144 box
16.2, plan 034 T079). This paragraph's claim is that THIS module widens
nothing, which is unchanged by any of them. Proposed-versus-
approved is a SERVER-SIDE distinction and a pending declaration is simply not in
the catalog, so NEITHER SHAPE GAINS A FIELD FROM THIS MODULE and this module
needs no release act. (Both statements are scoped to this module deliberately.
The catalog shape HAS gained fields — by the governing releases named above —
and each of those was a release act; what has never happened, and is what this
paragraph promises, is this module widening either shape.) The declaration is a
SECOND record beside the binding, not a tenth field on it.
SECOND record beside the binding, not a field on it.

WHY PENDING-NESS IS A DECLARED FACT AND NOT A DEFAULT. A binding this document
says nothing about is UNAFFECTED: it resolves exactly as it resolved before this
Expand Down
26 changes: 18 additions & 8 deletions src/opendox/doxbench_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,14 @@ def _chat_answer(document: dict) -> str:
}


def _post_to_provider(token: MintedToken, *, model_id: str, prompt: str,
def _post_to_provider(token: MintedToken, *, model: str, prompt: str,
timeout: float, opener) -> str:
"""The ONE place a provider is contacted. Returns the assistant prose.

`model` is the model name the request carries, which the port chose (the
binding's declared `model`, or the catalog handle for a binding that
declares none).

The token travels in the request's authorization header and nowhere else;
it is not in the URL (which a proxy logs), not in the body (which an error
handler might echo), and not in this function's return value.
Expand All @@ -731,7 +735,7 @@ def _post_to_provider(token: MintedToken, *, model_id: str, prompt: str,
f"{DIALECTS}; the binding refuses it at declaration, so no turn "
"can carry one")
build_request, read_answer = arm
body = json.dumps(build_request(model_id, prompt)).encode("utf-8")
body = json.dumps(build_request(model, prompt)).encode("utf-8")
request = urllib.request.Request( # noqa: S310 - endpoint declared on the binding by its operator, carried on the minted token
token.endpoint, data=body, method="POST")
request.add_header("Content-Type", "application/json")
Expand Down Expand Up @@ -909,15 +913,21 @@ def dispatch(self, prompt_envelope: object) -> object:
unrelated issuances. The expired mint's reference is read off the
token this turn is holding and lives no longer than the turn;
* a SECOND expiry inside the same turn raises the standard refusal.
No third call is bought."""
model_id = getattr(prompt_envelope, "model_id", None)
if not isinstance(model_id, str) or not model_id:
No third call is bought.

THE REQUEST'S MODEL IS THE BINDING'S DECLARED `model` (#1144 box
16.2). A binding that declares none sends the catalog handle, which is
what every request sent before the field existed, byte for byte."""
handle = getattr(prompt_envelope, "model_id", None)
if not isinstance(handle, str) or not handle:
entries = self._declared_catalog.entries
model_id = entries[0].model_id if entries else ""
handle = entries[0].model_id if entries else ""
declared_model = self._binding.model
model = declared_model if declared_model is not None else handle
prompt = bridge_mod.render_prompt_message(prompt_envelope)
token = self._current_token(REASON_FIRST_MINT)
try:
prose = _post_to_provider(token, model_id=model_id, prompt=prompt,
prose = _post_to_provider(token, model=model, prompt=prompt,
timeout=self._timeout_seconds,
opener=self._opener)
except _TokenExpired:
Expand All @@ -932,7 +942,7 @@ def dispatch(self, prompt_envelope: object) -> object:
self._record(REASON_PAID_RETRY)
try:
prose = _post_to_provider(
token, model_id=model_id, prompt=prompt,
token, model=model, prompt=prompt,
timeout=self._timeout_seconds, opener=self._opener)
except _TokenExpired:
self._forget_token()
Expand Down
Loading
Loading