You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* Add tool_choice + ForceTool (proposal 0025)
Provider.complete() gains an optional tool_choice parameter — one of
"auto", "required", "none", or a ForceTool record — constraining the
model's tool-calling behavior. Pre-send validation routes the three
§5 failure modes through ProviderInvalidRequest (§7's existing
category; no new category per the proposal's framing).
ForceTool is a frozen Pydantic model with type: Literal["tool"]
matching the spec discriminator. The OpenAI wire mapping in
_build_request_body translates the spec shape to OpenAI's body per
§8.1.1: string literals pass through verbatim; ForceTool renames
type to "function" and nests the name under a function sub-object.
None / omit preserves pre-0025 behavior — the field is absent on
the wire and the provider's own default applies.
15 unit tests cover the three validation rules, ForceTool shape
constraints (frozen, extras-forbid, Literal type), and the wire
mapping rows from §8.1.1.
* Extend conformance harness for tool_choice fixtures
Adds assert_tool_choice_absent matcher mirroring the existing
response_format_absent pattern, _build_tool_choice parser handling
both YAML shapes (string for the three modes, dict for the
ForceTool record form), and tool_choice passthrough on both call
sites (raises path and success path).
The expected_wire_request_checks dispatcher gains a
tool_choice_absent key for fixture 029's default case, where the
wire body MUST omit the field entirely (preserves pre-0025 caller
behavior — the OpenAI provider's own default applies).
LlmCallSpec uses _AllowExtras so tool_choice parses without an
explicit pydantic field; no fixture-parsing model changes needed.
* Bump spec to v0.20.1; refresh docs and CHANGELOG
Submodule pin advances from v0.19.0 to v0.20.1, absorbing proposal
0025 (tool_choice, v0.20.0) and proposal 0026 (§8.X wire-format
mapping subsection template, v0.20.1). 0026 is purely textual; the
existing OpenAI §8.1 mapping is the template's reference shape so
no python module-level work is needed. Runtime spec_version pins in
pyproject and __init__ updated to match; smoke test asserts v0.20.1.
CHANGELOG Unreleased section gains tool_choice + ForceTool +
ToolChoice + validate_tool_choice entries under Added; the
Provider.complete() signature extension noted under Changed;
cumulative pin-bump summary updated to v0.17.0 -> v0.20.1 across
six spec versions absorbed this cycle.
docs/concepts/llms.md gains a "Controlling tool-call behavior with
tool_choice" subsection under Tool calling, covering the four modes,
the three pre-send validation rules, and the cross-provider caveat
(not all providers honor tool_choice).
* Drop speculative future-providers reference from CHANGELOG
The "future Anthropic / Gemini providers will follow the §8.X
template" framing references unproposed spec work. Out-of-scope
items belong in PR descriptions scoped to this PR; speculative
future capabilities and proposal numbers don't belong in the
release-notes record.
* Address PR #70 review: validate tool_choice strings + reword CHANGELOG
validate_tool_choice now rejects unknown string literals at the API
boundary. Pyright catches the well-formed path via the ToolChoice
Literal alias, but Python doesn't enforce Literal at runtime — an
untyped caller passing `tool_choice="bogus"` previously fell through
to the wire and yielded a hard-to-debug provider-side 4xx. Now
surfaces as ProviderInvalidRequest with the allowed values listed.
CHANGELOG entry on the Provider.complete() signature change reworded
to be technically accurate: third-party Provider implementations
MUST accept the new parameter for Protocol conformance under strict
typing (and to avoid TypeError at the call site); they MAY ignore it
in their wire-body emission, which is how "provider doesn't honor
tool_choice" looks at the impl level. The "silently drops" framing
was wrong — accept-and-ignore is the correct pattern.
* Address PR #70 round 2: reject non-str / non-ForceTool tool_choice
validate_tool_choice now rejects values that are neither a string
nor a ForceTool instance at the API boundary. A caller hand-building
a dict like {"type": "tool", "name": X} (instead of constructing a
ForceTool) previously fell through validation and the raw dict
ended up on the wire — the spec→wire rename only runs on actual
ForceTool instances, so OpenAI got the wrong request shape and
rejected with a hard-to-debug 4xx.
Surfaces at the boundary with the expected type listed:
ProviderInvalidRequest('tool_choice must be one of "auto" /
"required" / "none" or a ForceTool instance, got <type>').
Adds a unit test covering dict, int, and list inputs. The
reportUnnecessaryIsInstance suppression on the ForceTool isinstance
check is load-bearing — pyright narrows the type via the alias,
but the entire point of this validator is to defend against
untyped callers that bypass static narrowing at runtime.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,6 +8,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
8
8
9
9
### Added
10
10
11
+
-**`tool_choice` parameter on `Provider.complete()`** (proposal 0025, accepted in spec v0.20.0). Optional discriminated-union value constraining the model's tool-calling behavior — one of `"auto"`, `"required"`, `"none"`, or a `ForceTool(name=...)` record. Validation runs pre-send: `"required"` and `ForceTool` both demand non-empty `tools`, and `ForceTool.name` must appear in the supplied list; violations raise `ProviderInvalidRequest` (§7's existing category — no new error category). When `tool_choice` is `None` (the default) the wire field is omitted and the provider's own default applies, preserving pre-0025 behavior exactly. The `OpenAIProvider` maps the spec shape onto OpenAI's wire shape per §8.1.1 (the `ForceTool.type="tool"` renames to wire `type="function"`).
12
+
-**`ForceTool` and `ToolChoice` public types** at `openarmature.llm.ForceTool` / `openarmature.llm.ToolChoice`. `ForceTool` is a frozen Pydantic model with `type: Literal["tool"] = "tool"` and `name: str`; `ToolChoice = Literal["auto", "required", "none"] | ForceTool` is the type alias used in `Provider.complete()`'s signature.
13
+
-**`validate_tool_choice` public validator** at `openarmature.llm.validate_tool_choice`. Standalone validator covering the three §5 pre-send rules; useful for third-party `Provider` implementations that want to reuse the canonical validation logic.
11
14
-**Bounded drain timeout on `CompiledGraph.drain()`** (proposal 0010, accepted in spec v0.19.0). `drain()` accepts an optional `timeout: float | None = None` parameter (non-negative seconds). When supplied, drain returns no later than the deadline; any observer events still queued or in-flight are reported as undelivered. Workers are cancelled cleanly so the compiled graph remains usable for subsequent invocations — partial delivery state from one drain does NOT leak into the next. Solves the "slow / hung / misbehaving observer blocks process exit" footgun for short-lived processes (CLIs, scripts, serverless functions). Observers SHOULD be cancellation-safe (idempotent writes, `try/finally` cleanup); the spec doesn't mandate it but the docs recommend it.
12
15
-**`DrainSummary` frozen dataclass** at `openarmature.graph.DrainSummary`. Returned from every `drain()` call (with or without `timeout`). Fields: `undelivered_count: int`, `timeout_reached: bool`. The shape is consistent across timed and untimed drains — callers receive the same dataclass whether the timeout was supplied or not. Per the v0.19.0 contract the two declared fields are the spec-mandated minimum; richer diagnostic detail (per-observer counts, sampled event metadata) is reserved for follow-on PRs.
13
16
-**Per-instance fan-out resume contract** (proposal 0009, accepted in spec v0.18.0). The engine now writes a checkpoint record at every `completed` event inside a fan-out instance (in addition to the existing outermost-graph + subgraph-internal + fan-out node completion saves). On resume the engine consults the saved record's `fan_out_progress` field and treats each instance as `completed` (skip, contribution rolls forward), `in_flight` (re-run from subgraph entry), or `not_started` (dispatch normally). The `append` reducer's no-double-merge guarantee holds across resume because `completed` is a one-shot accumulator state.
@@ -17,13 +20,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
17
20
18
21
### Changed
19
22
23
+
-**`Provider.complete()` signature** extended with an optional `tool_choice: ToolChoice | None = None` parameter (per proposal 0025 v0.20.0). Backward-compatible: callers that omit the new argument see no wire-shape change. Third-party `Provider` implementations MUST add the parameter to remain Protocol-conformant under strict type checking (and to accept calls that pass `tool_choice` without raising `TypeError`); they MAY ignore it in their wire-body emission, which is how "provider doesn't honor tool_choice" looks at the impl level. The `OpenAIProvider` wire mapping is implemented per §8.1.1.
20
24
-**`CompiledGraph.drain()` return type** changed from `None` to `DrainSummary` (pre-1.0; per proposal 0010 v0.19.0 contract). Callers that ignored the return are unaffected — `await graph.drain()` discards the returned dataclass exactly as before. Callers that explicitly typed the return as `None` will need to update their annotation.
21
25
-**Fan-out resume behavior** flipped from atomic restart (0008's v1 contract) to per-instance resume. A crash mid-fan-out used to re-run the entire fan-out on resume; now only the instances that did not complete-and-record their contribution re-run. The economics matter for large fan-outs of expensive work (LLM calls, long extractions): an 80% complete fan-out crash now restores 80% of its results rather than discarding them.
22
26
-**`SQLiteCheckpointer` schema** picks up a new `fan_out_progress_blob` column (added via `ALTER TABLE` for backward compatibility with pre-0009 databases). Pre-0009 rows back-fill as NULL on load and round-trip as the empty-tuple default. Both `pickle` and `json` serialization modes round-trip the new field.
23
27
24
28
### Notes
25
29
26
-
- **Pinned spec version bumped from v0.17.0 to v0.19.0 over this Unreleased cycle.** Four spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension; purely textual reframe of llm-provider §8 as a catalog of wire-format mappings, OpenAI-compatible body nested under §8.1, code references updated to §8.1 / §8.1.1 / §8.1.2 / §8.1.3 / §8.1.5.1 / §8.1.1.1), v0.18.0 (proposal 0009, per-instance fan-out resume; pipeline-utilities §10.3 / §10.7 revised, §10.11 added with per-instance state machine plus composition rules plus configurable batching; the `append` reducer no-double-merge invariant from §10.11.1 is the load-bearing correctness story; see Added / Changed above), v0.18.1 (fixture-only patch on `release/v0.18.1` correcting an off-by-one literal in fixture 052's expected `results`), and v0.19.0 (proposal 0010, bounded drain timeout; graph-engine §6 amended with the `timeout` parameter and `DrainSummary` return contract; see Added / Changed above). All existing conformance fixtures continue to pass.
30
+
- **Pinned spec version bumped from v0.17.0 to v0.20.1 over this Unreleased cycle.** Six spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension — purely textual reframe of llm-provider §8 as a catalog of wire-format mappings; OpenAI-compatible body nested under §8.1), v0.18.0 (proposal 0009, per-instance fan-out resume — pipeline-utilities §10.3 / §10.7 revised, §10.11 added; the `append` reducer no-double-merge invariant is the load-bearing correctness story), v0.18.1 (fixture-only patch correcting an off-by-one literal in fixture 052's expected `results`), v0.19.0 (proposal 0010, bounded drain timeout — graph-engine §6 amended with the `timeout` parameter and `DrainSummary` return contract), v0.20.0 (proposal 0025, llm-provider `tool_choice` — §5 / §7 / §8.1.1 amended; see Added / Changed above), and v0.20.1 (proposal 0026, llm-provider §8.X wire-format mapping subsection template — purely textual §8 framing paragraph; the existing OpenAI §8.1 mapping is the template's reference shape so no python module-level work was needed). All existing conformance fixtures continue to pass.
0 commit comments