Skip to content

Commit 5ff5443

Browse files
jsondaicopybara-github
authored andcommitted
chore: GenAI Client(evals) - normalize inference model names in create_evaluation_run
The Evaluation Service routes server-side inference by parsing the EvaluationRunInferenceConfig.model string into a publisher-model or endpoint resource name, from which it extracts the serving location. Names that are not fully qualified fail server-side in two ways: - A short name (e.g. `gemini-2.5-flash`) is rejected by API validation with a clear 400 INVALID_ARGUMENT error. - A location-less path (e.g. `publishers/google/models/gemini-2.5-flash`) passes validation but has no location for the server to route on, so it fails later with an opaque `INTERNAL: Internal error occurred.` that is impossible to debug from the client. The SDK previously passed `model` through verbatim. This change makes the SDK guard the input: model names in inference_configs are normalized to a fully-qualified `projects/{project}/locations/{location}/...` resource name using the client's project and location before the request is sent. Short Gemini names, location-less publisher/endpoint/tuned-model paths, and `models/{id}` are expanded; already fully-qualified names pass through. Unrecognized inputs and a client missing project/location raise a clear client-side ValueError. Also corrects the create_evaluation_run docstring and the EvaluationRunInferenceConfig.model field doc, which incorrectly showed a short model name as a working example. PiperOrigin-RevId: 926248584
1 parent 2faf725 commit 5ff5443

7 files changed

Lines changed: 299 additions & 6 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,9 +513,22 @@ def _resolve_inference_configs(
513513
else:
514514
config.agent_configs = parsed_agent_info.agents
515515

516-
# Resolve prompt template data
516+
# Normalize model names and resolve prompt template data.
517517
if inference_configs:
518518
for inference_config in inference_configs.values():
519+
model_val = (
520+
inference_config.get("model")
521+
if isinstance(inference_config, dict)
522+
else inference_config.model
523+
)
524+
if model_val:
525+
normalized_model = _normalize_inference_model_name(
526+
model_val, api_client
527+
)
528+
if isinstance(inference_config, dict):
529+
inference_config["model"] = normalized_model
530+
else:
531+
inference_config.model = normalized_model
519532
prompt_template_val = (
520533
inference_config.get("prompt_template")
521534
if isinstance(inference_config, dict)
@@ -912,6 +925,92 @@ def _is_gemini_model(model: str) -> bool:
912925
)
913926

914927

928+
def _normalize_inference_model_name(model: str, api_client: BaseApiClient) -> str:
929+
"""Returns a fully-qualified model resource name for server-side inference.
930+
931+
The Evaluation Service routes inference by parsing the model string into a
932+
publisher-model or endpoint resource name, from which it extracts the
933+
serving location. Names that are not fully qualified fail server-side in two
934+
ways:
935+
936+
- A short name (e.g. ``gemini-2.5-flash``) is rejected by API validation
937+
with a clear 400 ``INVALID_ARGUMENT`` error.
938+
- A location-less path (e.g. ``publishers/google/models/gemini-2.5-flash``)
939+
passes validation but has no location for the server to route on, so it
940+
fails later with an opaque ``INTERNAL`` error that is impossible to debug
941+
from the client.
942+
943+
This helper expands both forms to the fully-qualified resource name using
944+
the client's project and location so the value sent to the server is always
945+
routable.
946+
947+
Args:
948+
model: The model name from an ``EvaluationRunInferenceConfig``. May be a
949+
short Gemini name, a location-less publisher/endpoint/tuned-model
950+
path, or an already fully-qualified ``projects/.../locations/...``
951+
resource name.
952+
api_client: The API client, used to source the project and location for
953+
expansion.
954+
955+
Returns:
956+
A fully-qualified model resource name.
957+
958+
Raises:
959+
ValueError: If ``model`` needs expansion but the client is missing a
960+
project or location, or if ``model`` is not a recognized Vertex model
961+
form.
962+
"""
963+
if not model:
964+
return model
965+
966+
# Already fully-qualified; nothing to do.
967+
if model.startswith("projects/"):
968+
return model
969+
970+
project = getattr(api_client, "project", None)
971+
location = getattr(api_client, "location", None)
972+
prefix = f"projects/{project}/locations/{location}/"
973+
974+
def _require_project_location() -> None:
975+
if not project or not location:
976+
raise ValueError(
977+
f"Cannot expand model name '{model}' to a fully-qualified"
978+
" resource name because the client is missing a project or"
979+
" location. Set project and location on the client, or pass a"
980+
" fully-qualified"
981+
" 'projects/{project}/locations/{location}/publishers/google/models/{model}'"
982+
" resource name."
983+
)
984+
985+
# Location-less Vertex resource paths: prepend project/location.
986+
if (
987+
model.startswith("publishers/")
988+
or model.startswith("endpoints/")
989+
or model.startswith("tunedModels/")
990+
):
991+
_require_project_location()
992+
return f"{prefix}{model}"
993+
994+
# Bare 'models/{id}' canonicalizes to the publisher path.
995+
if model.startswith("models/"):
996+
_require_project_location()
997+
return f"{prefix}publishers/google/{model}"
998+
999+
# Bare short Gemini name, e.g. 'gemini-2.5-flash'.
1000+
if "/" not in model and _is_gemini_model(model):
1001+
_require_project_location()
1002+
return f"{prefix}publishers/google/models/{model}"
1003+
1004+
raise ValueError(
1005+
f"Unrecognized model name '{model}'. Provide a Gemini model name (e.g."
1006+
" 'gemini-2.5-flash'), or a fully-qualified publisher-model or endpoint"
1007+
" resource name (e.g."
1008+
" 'projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash'"
1009+
" or"
1010+
" 'projects/{project}/locations/{location}/endpoints/{endpoint}')."
1011+
)
1012+
1013+
9151014
def _run_inference_internal(
9161015
api_client: BaseApiClient,
9171016
model: Union[Callable[[Any], Any], str],

agentplatform/_genai/evals.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2659,6 +2659,11 @@ def create_evaluation_run(
26592659
The key is the candidate name, and the value is the inference config.
26602660
If provided, `agent_info` must be None. If omitted and `agent_info` is provided,
26612661
this will be automatically constructed using `agent_info` and `user_simulator_config`.
2662+
The `model` field of an inference config accepts a short Gemini model
2663+
name (e.g. `gemini-2.5-flash`), which is automatically expanded to a
2664+
fully-qualified resource name using the client's project and location,
2665+
or an already fully-qualified publisher-model or endpoint resource
2666+
name.
26622667
Example:
26632668
{"candidate-1": types.EvaluationRunInferenceConfig(model="gemini-2.5-flash")}
26642669
labels: The labels to apply to the evaluation run.
@@ -4448,6 +4453,11 @@ async def create_evaluation_run(
44484453
The key is the candidate name, and the value is the inference config.
44494454
If provided, `agent_info` must be None. If omitted and `agent_info` is provided,
44504455
this will be automatically constructed using `agent_info` and `user_simulator_config`.
4456+
The `model` field of an inference config accepts a short Gemini model
4457+
name (e.g. `gemini-2.5-flash`), which is automatically expanded to a
4458+
fully-qualified resource name using the client's project and location,
4459+
or an already fully-qualified publisher-model or endpoint resource
4460+
name.
44514461
Example:
44524462
{"candidate-1": types.EvaluationRunInferenceConfig(model="gemini-2.5-flash")}
44534463
red_teaming_config: This field is experimental and may change in future

agentplatform/_genai/types/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2586,7 +2586,7 @@ class EvaluationRunInferenceConfig(_common.BaseModel):
25862586
)
25872587
model: Optional[str] = Field(
25882588
default=None,
2589-
description="""The fully qualified name of the publisher model or endpoint to use for inference.""",
2589+
description="""The model to use for inference. Accepts a short Gemini model name (e.g. `gemini-2.5-flash`), which is automatically expanded to a fully-qualified resource name using the client's project and location, or an already fully-qualified publisher-model or endpoint resource name (e.g. `projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash`).""",
25902590
)
25912591
prompt_template: Optional[EvaluationRunPromptTemplate] = Field(
25922592
default=None, description="""The prompt template used for inference."""
@@ -2611,7 +2611,7 @@ class EvaluationRunInferenceConfigDict(TypedDict, total=False):
26112611
"""The agent config."""
26122612

26132613
model: Optional[str]
2614-
"""The fully qualified name of the publisher model or endpoint to use for inference."""
2614+
"""The model to use for inference. Accepts a short Gemini model name (e.g. `gemini-2.5-flash`), which is automatically expanded to a fully-qualified resource name using the client's project and location, or an already fully-qualified publisher-model or endpoint resource name (e.g. `projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash`)."""
26152615

26162616
prompt_template: Optional[EvaluationRunPromptTemplateDict]
26172617
"""The prompt template used for inference."""

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,81 @@ def test_get_api_client_with_none_location(
228228
mock_agentplatform_client.assert_not_called()
229229

230230

231+
class TestNormalizeInferenceModelName:
232+
_FQ_PREFIX = f"projects/{_TEST_PROJECT}/locations/{_TEST_LOCATION}/"
233+
234+
def test_short_gemini_name_expanded(self, mock_api_client_fixture):
235+
result = _evals_common._normalize_inference_model_name(
236+
"gemini-2.5-flash", mock_api_client_fixture
237+
)
238+
assert result == (f"{self._FQ_PREFIX}publishers/google/models/gemini-2.5-flash")
239+
240+
def test_location_less_publisher_path_prepended(self, mock_api_client_fixture):
241+
result = _evals_common._normalize_inference_model_name(
242+
"publishers/google/models/gemini-2.5-flash", mock_api_client_fixture
243+
)
244+
assert result == (f"{self._FQ_PREFIX}publishers/google/models/gemini-2.5-flash")
245+
246+
def test_third_party_publisher_path_prepended(self, mock_api_client_fixture):
247+
result = _evals_common._normalize_inference_model_name(
248+
"publishers/anthropic/models/claude-sonnet", mock_api_client_fixture
249+
)
250+
assert result == (f"{self._FQ_PREFIX}publishers/anthropic/models/claude-sonnet")
251+
252+
def test_endpoint_path_prepended(self, mock_api_client_fixture):
253+
result = _evals_common._normalize_inference_model_name(
254+
"endpoints/123", mock_api_client_fixture
255+
)
256+
assert result == f"{self._FQ_PREFIX}endpoints/123"
257+
258+
def test_models_path_canonicalized(self, mock_api_client_fixture):
259+
result = _evals_common._normalize_inference_model_name(
260+
"models/gemini-2.5-flash", mock_api_client_fixture
261+
)
262+
assert result == (f"{self._FQ_PREFIX}publishers/google/models/gemini-2.5-flash")
263+
264+
def test_fully_qualified_name_unchanged(self, mock_api_client_fixture):
265+
fq = f"{self._FQ_PREFIX}publishers/google/models/gemini-2.5-flash"
266+
assert (
267+
_evals_common._normalize_inference_model_name(fq, mock_api_client_fixture)
268+
== fq
269+
)
270+
271+
def test_empty_model_unchanged(self, mock_api_client_fixture):
272+
assert (
273+
_evals_common._normalize_inference_model_name("", mock_api_client_fixture)
274+
== ""
275+
)
276+
277+
def test_missing_project_or_location_raises(self, mock_api_client_fixture):
278+
mock_api_client_fixture.location = None
279+
with pytest.raises(ValueError, match="missing a project or location"):
280+
_evals_common._normalize_inference_model_name(
281+
"gemini-2.5-flash", mock_api_client_fixture
282+
)
283+
284+
def test_unrecognized_model_raises(self, mock_api_client_fixture):
285+
with pytest.raises(ValueError, match="Unrecognized model name"):
286+
_evals_common._normalize_inference_model_name(
287+
"not/a/valid/model", mock_api_client_fixture
288+
)
289+
290+
def test_resolve_inference_configs_normalizes_model(self, mock_api_client_fixture):
291+
inference_configs = {
292+
"candidate-1": agentplatform_genai_types.EvaluationRunInferenceConfig(
293+
model="gemini-2.5-flash"
294+
)
295+
}
296+
result = _evals_common._resolve_inference_configs(
297+
mock_api_client_fixture,
298+
common_types.EvaluationRunDataSource(),
299+
inference_configs,
300+
)
301+
assert result["candidate-1"].model == (
302+
f"{self._FQ_PREFIX}publishers/google/models/gemini-2.5-flash"
303+
)
304+
305+
231306
class TestTransformers:
232307
"""Unit tests for transformers."""
233308

vertexai/_genai/_evals_common.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,9 +513,22 @@ def _resolve_inference_configs(
513513
else:
514514
config.agent_configs = parsed_agent_info.agents
515515

516-
# Resolve prompt template data
516+
# Normalize model names and resolve prompt template data.
517517
if inference_configs:
518518
for inference_config in inference_configs.values():
519+
model_val = (
520+
inference_config.get("model")
521+
if isinstance(inference_config, dict)
522+
else inference_config.model
523+
)
524+
if model_val:
525+
normalized_model = _normalize_inference_model_name(
526+
model_val, api_client
527+
)
528+
if isinstance(inference_config, dict):
529+
inference_config["model"] = normalized_model
530+
else:
531+
inference_config.model = normalized_model
519532
prompt_template_val = (
520533
inference_config.get("prompt_template")
521534
if isinstance(inference_config, dict)
@@ -912,6 +925,92 @@ def _is_gemini_model(model: str) -> bool:
912925
)
913926

914927

928+
def _normalize_inference_model_name(model: str, api_client: BaseApiClient) -> str:
929+
"""Returns a fully-qualified model resource name for server-side inference.
930+
931+
The Evaluation Service routes inference by parsing the model string into a
932+
publisher-model or endpoint resource name, from which it extracts the
933+
serving location. Names that are not fully qualified fail server-side in two
934+
ways:
935+
936+
- A short name (e.g. ``gemini-2.5-flash``) is rejected by API validation
937+
with a clear 400 ``INVALID_ARGUMENT`` error.
938+
- A location-less path (e.g. ``publishers/google/models/gemini-2.5-flash``)
939+
passes validation but has no location for the server to route on, so it
940+
fails later with an opaque ``INTERNAL`` error that is impossible to debug
941+
from the client.
942+
943+
This helper expands both forms to the fully-qualified resource name using
944+
the client's project and location so the value sent to the server is always
945+
routable.
946+
947+
Args:
948+
model: The model name from an ``EvaluationRunInferenceConfig``. May be a
949+
short Gemini name, a location-less publisher/endpoint/tuned-model
950+
path, or an already fully-qualified ``projects/.../locations/...``
951+
resource name.
952+
api_client: The API client, used to source the project and location for
953+
expansion.
954+
955+
Returns:
956+
A fully-qualified model resource name.
957+
958+
Raises:
959+
ValueError: If ``model`` needs expansion but the client is missing a
960+
project or location, or if ``model`` is not a recognized Vertex model
961+
form.
962+
"""
963+
if not model:
964+
return model
965+
966+
# Already fully-qualified; nothing to do.
967+
if model.startswith("projects/"):
968+
return model
969+
970+
project = getattr(api_client, "project", None)
971+
location = getattr(api_client, "location", None)
972+
prefix = f"projects/{project}/locations/{location}/"
973+
974+
def _require_project_location() -> None:
975+
if not project or not location:
976+
raise ValueError(
977+
f"Cannot expand model name '{model}' to a fully-qualified"
978+
" resource name because the client is missing a project or"
979+
" location. Set project and location on the client, or pass a"
980+
" fully-qualified"
981+
" 'projects/{project}/locations/{location}/publishers/google/models/{model}'"
982+
" resource name."
983+
)
984+
985+
# Location-less Vertex resource paths: prepend project/location.
986+
if (
987+
model.startswith("publishers/")
988+
or model.startswith("endpoints/")
989+
or model.startswith("tunedModels/")
990+
):
991+
_require_project_location()
992+
return f"{prefix}{model}"
993+
994+
# Bare 'models/{id}' canonicalizes to the publisher path.
995+
if model.startswith("models/"):
996+
_require_project_location()
997+
return f"{prefix}publishers/google/{model}"
998+
999+
# Bare short Gemini name, e.g. 'gemini-2.5-flash'.
1000+
if "/" not in model and _is_gemini_model(model):
1001+
_require_project_location()
1002+
return f"{prefix}publishers/google/models/{model}"
1003+
1004+
raise ValueError(
1005+
f"Unrecognized model name '{model}'. Provide a Gemini model name (e.g."
1006+
" 'gemini-2.5-flash'), or a fully-qualified publisher-model or endpoint"
1007+
" resource name (e.g."
1008+
" 'projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash'"
1009+
" or"
1010+
" 'projects/{project}/locations/{location}/endpoints/{endpoint}')."
1011+
)
1012+
1013+
9151014
def _run_inference_internal(
9161015
api_client: BaseApiClient,
9171016
model: Union[Callable[[Any], Any], str],

vertexai/_genai/evals.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2659,6 +2659,11 @@ def create_evaluation_run(
26592659
The key is the candidate name, and the value is the inference config.
26602660
If provided, `agent_info` must be None. If omitted and `agent_info` is provided,
26612661
this will be automatically constructed using `agent_info` and `user_simulator_config`.
2662+
The `model` field of an inference config accepts a short Gemini model
2663+
name (e.g. `gemini-2.5-flash`), which is automatically expanded to a
2664+
fully-qualified resource name using the client's project and location,
2665+
or an already fully-qualified publisher-model or endpoint resource
2666+
name.
26622667
Example:
26632668
{"candidate-1": types.EvaluationRunInferenceConfig(model="gemini-2.5-flash")}
26642669
labels: The labels to apply to the evaluation run.
@@ -4448,6 +4453,11 @@ async def create_evaluation_run(
44484453
The key is the candidate name, and the value is the inference config.
44494454
If provided, `agent_info` must be None. If omitted and `agent_info` is provided,
44504455
this will be automatically constructed using `agent_info` and `user_simulator_config`.
4456+
The `model` field of an inference config accepts a short Gemini model
4457+
name (e.g. `gemini-2.5-flash`), which is automatically expanded to a
4458+
fully-qualified resource name using the client's project and location,
4459+
or an already fully-qualified publisher-model or endpoint resource
4460+
name.
44514461
Example:
44524462
{"candidate-1": types.EvaluationRunInferenceConfig(model="gemini-2.5-flash")}
44534463
red_teaming_config: This field is experimental and may change in future

vertexai/_genai/types/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2549,7 +2549,7 @@ class EvaluationRunInferenceConfig(_common.BaseModel):
25492549
)
25502550
model: Optional[str] = Field(
25512551
default=None,
2552-
description="""The fully qualified name of the publisher model or endpoint to use for inference.""",
2552+
description="""The model to use for inference. Accepts a short Gemini model name (e.g. `gemini-2.5-flash`), which is automatically expanded to a fully-qualified resource name using the client's project and location, or an already fully-qualified publisher-model or endpoint resource name (e.g. `projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash`).""",
25532553
)
25542554
prompt_template: Optional[EvaluationRunPromptTemplate] = Field(
25552555
default=None, description="""The prompt template used for inference."""
@@ -2574,7 +2574,7 @@ class EvaluationRunInferenceConfigDict(TypedDict, total=False):
25742574
"""The agent config."""
25752575

25762576
model: Optional[str]
2577-
"""The fully qualified name of the publisher model or endpoint to use for inference."""
2577+
"""The model to use for inference. Accepts a short Gemini model name (e.g. `gemini-2.5-flash`), which is automatically expanded to a fully-qualified resource name using the client's project and location, or an already fully-qualified publisher-model or endpoint resource name (e.g. `projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash`)."""
25782578

25792579
prompt_template: Optional[EvaluationRunPromptTemplateDict]
25802580
"""The prompt template used for inference."""

0 commit comments

Comments
 (0)