Skip to content

[api][java][python] Name the model the capability predicate judges - #1128

Open
weiqingy wants to merge 2 commits into
apache:mainfrom
weiqingy:912-finalization
Open

weiqingy wants to merge 2 commits into
apache:mainfrom
weiqingy:912-finalization

Conversation

@weiqingy

Copy link
Copy Markdown
Collaborator

Linked issue: #912

Purpose of change

Nothing changes for a caller: no production code calls what this adds. It is the first step of wiring an agent output_schema through to the providers that can enforce it, and it removes a trap that step would otherwise fall into.

A chat model connection resolves the model it calls from the request parameters, and some connections judge a different name than the one they send. An Azure deployment is a name the user chooses, while the capability predicate has to see the model behind it. Several connections fall back to their own default when the caller omits the model. Until now the predicate's contract said its argument was "the model actually being called", which would walk any caller outside the connection into asking about the wrong name.

Runtime flow

effectiveModelFor(modelParams) in Java and effective_model_for(model_kwargs) in Python answer which model the capability predicate judges for a given parameter map. The base returns the plain model entry. A connection overrides it wherever its own request builder resolves that name differently: Azure returns the backing model rather than the deployment, and the connections with a default model return that default when the key is absent or blank.

Key decisions

The fallback lives in each override, not in the base. In both languages the default model is private to the subclass, so the base cannot reach it. That is also what normalizes the two languages, which previously disagreed about what an omitted model means.

The hook sits on the connection. Only the connection knows how its own request builder resolves the model, and keeping the two together is what the tests pin.

The contract no longer promises a universal answer for an unknown model. Connections whose capability belongs to the endpoint rather than the model report support for names they have never seen, so the contract now separates them from the connections that classify by name.

Behavioral Semantics

Interaction decisions

Connection Model it sends Model the predicate judges Override
Azure OpenAI the deployment the backing model yes
OpenAI, OpenAI Responses, Anthropic, Gemini, Bedrock (Java) model, else its default same yes
Tongyi, watsonx (Python) model, else its default same yes
Ollama, watsonx (Java), OpenAI and Anthropic (Python) model same no, the base answer is already right
vLLM inherits the OpenAI connection same no

Behavioral contracts

  1. The hook returns the model the connection's own capability predicate judges for a given parameter map.
  2. It accepts an absent, null or blank model and returns null rather than raising, so a caller can ask speculatively.
  3. It reads the parameter map without consuming it, so asking does not change what the request would send.
  4. A predicate must accept a null model without raising. A connection that classifies by model name reports such a model not capable; one whose capability belongs to the endpoint may report a name it has never seen as capable.

Failure behavior

No new failure path. Nothing calls the hook, a missing model is an answer rather than an error, and the connections that raise on an unresolvable model during request building continue to do so unchanged.

Tests

Contract Tests
1 effectiveModelForNamesTheModelTheBuilderJudges on Azure, OpenAI, Anthropic, Bedrock and Gemini; the Python binding tests on Azure, Tongyi, watsonx, OpenAI, Anthropic and Ollama
2 the base-default and fallback tests in BaseChatModelTest and test_chat_model_base.py, over explicit, blank and absent models
3 testEffectiveModelForDoesNotConsumeTheBackingModel
4 the existing capability predicate tests in both languages, unchanged

Coverage by risk. The failure that matters is a hook that answers about a model the request will not use, which no assertion against a literal can catch. The binding tests capture the model the request builder actually feeds the predicate and compare it with the hook, so a builder that later gains or loses a fallback without a matching override fails them. That was verified by mutation: changing a builder alone leaves every other test green.

Not verified. No live provider was called. There is no end-to-end evidence that a gate reads the right model, because the gate arrives in a later step; what is proven here is agreement between each hook and its own request builder.

API

New protected method on the Java connection base and its public Python counterpart, both on a beta base class, plus overrides. No user-facing API changes and no behavior change for an existing caller: the hook has no production caller in this PR.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

Was this patch authored or co-authored using generative AI tooling?

  • Yes
  • No

Generated-by: Claude Code 2.1.272 (Claude Opus 5)

A connection resolves the model it calls from the request parameters, and
some connections judge a different name than the one they send: an Azure
deployment is a user-chosen name, while the capability predicate has to
see the model behind it. The predicate's contract said its argument was
the model actually being called, which would walk any caller outside the
connection into asking about the wrong name.

Add effectiveModelFor, which answers which model the predicate judges for
a given parameter map, and override it wherever a connection resolves that
name differently from reading the model parameter: the default model
fallbacks, and Azure's backing model. Reword the contract so a caller asks
that hook rather than substituting its own resolution, and so it no longer
claims an unrecognized model reports false, which is untrue of the
connections whose capability belongs to the endpoint.

Nothing calls the hook yet, so behavior is unchanged. The tests pin the
hook against the model each request builder actually feeds the predicate,
which a hook-side assertion cannot catch.

Generated-by: Claude Code 2.1.272 (Claude Opus 5)
Mirror the Java hook on the Python connection base. A connection resolves
the model it calls from the chat kwargs, and some connections judge a
different name than the one they send: an Azure deployment is a
user-chosen name, while the capability predicate has to see the model
behind it, and Tongyi and watsonx fall back to their own default when the
caller omits the key.

Add effective_model_for, which answers which model the predicate judges
for a given kwargs mapping, and override it on those three connections.
Reword the capability docstring to the contract the Java side now states:
a caller outside the connection asks that hook rather than resolving the
model itself, the predicate accepts a missing model without raising, and a
connection whose capability belongs to the endpoint may report support for
a name it has never seen.

Nothing calls the hook yet, so behavior is unchanged. The tests pin each
hook against the model its own request path feeds the predicate, which a
hook-side assertion cannot catch.

Generated-by: Claude Code 2.1.272 (Claude Opus 5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant