diff --git a/src/together/lib/cli/api/beta/endpoints/_utils/_resolve_model.py b/src/together/lib/cli/api/beta/endpoints/_utils/_resolve_model.py index 5d78dc23..e8f7022d 100644 --- a/src/together/lib/cli/api/beta/endpoints/_utils/_resolve_model.py +++ b/src/together/lib/cli/api/beta/endpoints/_utils/_resolve_model.py @@ -1,8 +1,9 @@ from __future__ import annotations import re +from typing import NamedTuple -from together import APIError +from together import NotFoundError from together.types.beta import Model, Endpoint from together.lib.cli.utils.config import CLIConfigParameter from together.lib.cli.utils._console import console @@ -18,9 +19,12 @@ # Logic for resolving a model + config from a user input string # # 1. Raw model id (e.g. ml_...) -# → GET /configs?referenceModelId=... for the config and model path +# → retrieve from --project when possible; config via baseModelId / id +# → else GET /configs?referenceModelId=... (public / reference models) # 2. Full model path (projects/.../models/...) -# → parse the model id, then same as (1) +# → retrieve that model (keep it as the deploy target), resolve config via +# baseModelId (or the model id when it is itself a reference model). +# Preserve an optional /revisions/... pin on the deploy path. # 3. Named model (prefix/model-name) # a. prefix == project slug → list private models by name, resolve config via # baseModelId (path 1), but deploy the custom model path @@ -31,7 +35,13 @@ # - one profile + no --config → use that profile's config # - --config given → use the profile whose config matches # After either path, re-validate against the user's --config when provided. -MODEL_PATH_RE = re.compile(r"^projects/([^/]+)/models/([^/]+)(?:/revisions/[^/]+)?$") +MODEL_PATH_RE = re.compile(r"^projects/([^/]+)/models/([^/]+)(?:/revisions/([^/]+))?$") + + +class ResolvedModelAndConfig(NamedTuple): + model: Model + config: Config + revision_id: str | None = None async def resolve_model( @@ -40,8 +50,8 @@ async def resolve_model( *, config_id: str | None = None, ) -> Model: - model, _config = await resolve_model_and_config(config, model_input, config_id=config_id) - return model + resolved = await resolve_model_and_config(config, model_input, config_id=config_id) + return resolved.model async def resolve_model_and_config( @@ -49,15 +59,38 @@ async def resolve_model_and_config( model_input: str, *, config_id: str | None = None, -) -> tuple[Model, Config]: +) -> ResolvedModelAndConfig: """Resolve a deployable model and the config revision to pair with it.""" - # 1 / 2. Full model path or raw model id → configs list by referenceModelId + # 2. Full model path → keep the user's model; config from its base/reference. path_match = MODEL_PATH_RE.match(model_input) if path_match: - _project_id, model_id = path_match.groups() - return await _resolve_via_configs(config, model_id, config_id=config_id, model_input=model_input) + project_id, model_id, revision_id = path_match.group(1), path_match.group(2), path_match.group(3) + return await _resolve_explicit_model( + config, + model_id=model_id, + project_id=project_id, + config_id=config_id, + model_input=model_input, + revision_id=revision_id, + ) + # 1. Raw model id if "/" not in model_input: + if config.project_id: + try: + model = await config.client.beta.models.retrieve(id=model_input, project_id=config.project_id) + except NotFoundError: + pass + else: + reference_model_id = model.base_model_id or model.id + assert reference_model_id is not None + return await _resolve_config_for_model( + config, + model, + reference_model_id=reference_model_id, + config_id=config_id, + model_input=model_input, + ) return await _resolve_via_configs(config, model_input, config_id=config_id, model_input=model_input) # 3. Named model (prefix/model-name) @@ -70,24 +103,74 @@ async def resolve_model_and_config( reference_model_id = model.base_model_id or model.id assert reference_model_id is not None # Config comes from the base/reference model; deploy path stays the custom model. - _base_model, selected_config = await _resolve_via_configs( + return await _resolve_config_for_model( config, - reference_model_id, + model, + reference_model_id=reference_model_id, config_id=config_id, model_input=model_input, ) - return model, selected_config return await _resolve_public_model_and_config(config, model_input, config_id=config_id) +async def _resolve_explicit_model( + config: CLIConfigParameter, + *, + model_id: str, + project_id: str, + config_id: str | None, + model_input: str, + revision_id: str | None = None, +) -> ResolvedModelAndConfig: + """Load the user-specified model and pair it with a compatible config.""" + try: + model = await config.client.beta.models.retrieve(id=model_id, project_id=project_id) + except NotFoundError: + raise ValueError(f"Model {model_input} not found.") from None + + reference_model_id = model.base_model_id or model.id + assert reference_model_id is not None + return await _resolve_config_for_model( + config, + model, + reference_model_id=reference_model_id, + config_id=config_id, + model_input=model_input, + revision_id=revision_id, + ) + + +async def _resolve_config_for_model( + config: CLIConfigParameter, + model: Model, + *, + reference_model_id: str, + config_id: str | None, + model_input: str, + revision_id: str | None = None, +) -> ResolvedModelAndConfig: + selected = resolve_config( + await resolve_configs(config, reference_model_id), + config_id, + model=model_input, + ) + selected = validate_requested_config(selected, config_id, model=model_input) + return ResolvedModelAndConfig(model=model, config=selected, revision_id=revision_id) + + async def _resolve_via_configs( config: CLIConfigParameter, reference_model_id: str, *, config_id: str | None, model_input: str, -) -> tuple[Model, Config]: +) -> ResolvedModelAndConfig: + """Resolve a public/reference model id through the configs API. + + The deploy target is the config's reference model — correct when the user + passed a bare reference-model id that is not retrievable under --project. + """ selected = resolve_config( await resolve_configs(config, reference_model_id), config_id, @@ -95,7 +178,7 @@ async def _resolve_via_configs( ) selected = validate_requested_config(selected, config_id, model=model_input) model = await _retrieve_model_from_reference(config, selected, model_input=model_input) - return model, selected + return ResolvedModelAndConfig(model=model, config=selected) async def _retrieve_model_from_reference( @@ -108,7 +191,7 @@ async def _retrieve_model_from_reference( path = selected.reference_model or "" match = MODEL_PATH_RE.match(path) if match: - project_id, model_id = match.groups() + project_id, model_id = match.group(1), match.group(2) elif selected.reference_model_id and selected.project_id: project_id, model_id = selected.project_id, selected.reference_model_id else: @@ -116,10 +199,8 @@ async def _retrieve_model_from_reference( try: return await config.client.beta.models.retrieve(id=model_id, project_id=project_id) - except APIError as e: - if "not found" in e.message.lower(): - raise ValueError(f"Model {model_input} not found.") from None - raise + except NotFoundError: + raise ValueError(f"Model {model_input} not found.") from None async def _find_private_model_by_name(config: CLIConfigParameter, name: str) -> Model: @@ -236,7 +317,7 @@ async def _resolve_public_model_and_config( model_input: str, *, config_id: str | None = None, -) -> tuple[Model, Config]: +) -> ResolvedModelAndConfig: supported_models = await config.client.beta.models.list_supported(search=model_input) if not supported_models.data: raise ValueError(f"Model {model_input} not found.") @@ -255,7 +336,7 @@ async def _resolve_public_model_and_config( match = MODEL_PATH_RE.match(profile.model or "") if not match: raise ValueError(f"Invalid model path: {profile.model}") - project_id, model_id = match.groups() + project_id, model_id, revision_id = match.group(1), match.group(2), match.group(3) selected_config = validate_requested_config( config_from_profile(profile), @@ -263,11 +344,14 @@ async def _resolve_public_model_and_config( model=model_input, ) model = Model.construct(id=model_id, projectId=project_id, name=public_model.name or model_id) - return model, selected_config + return ResolvedModelAndConfig(model=model, config=selected_config, revision_id=revision_id) -def construct_model_path(model: Model) -> str: - return f"projects/{model.project_id}/models/{model.id}" +def construct_model_path(model: Model, revision_id: str | None = None) -> str: + path = f"projects/{model.project_id}/models/{model.id}" + if revision_id: + return f"{path}/revisions/{revision_id}" + return path async def resolve_endpoint(config: CLIConfigParameter, endpoint_id_or_name: str) -> Endpoint: diff --git a/src/together/lib/cli/api/beta/endpoints/ab.py b/src/together/lib/cli/api/beta/endpoints/ab.py index 3b75c59c..040fdb37 100644 --- a/src/together/lib/cli/api/beta/endpoints/ab.py +++ b/src/together/lib/cli/api/beta/endpoints/ab.py @@ -74,7 +74,8 @@ async def ab( ) verify_control_receiving_traffic(endpoint, control) - resolved_model, config_value = await resolve_model_and_config(config, model, config_id=config_id) + resolved = await resolve_model_and_config(config, model, config_id=config_id) + resolved_model, config_value = resolved.model, resolved.config autoscaling = build_autoscaling( min_replicas=1, @@ -103,7 +104,7 @@ async def ab( endpoint_id=endpoint.id, enable_lora=enable_lora, name=name, - model=construct_model_path(resolved_model), + model=construct_model_path(resolved_model, resolved.revision_id), config=construct_config_path(config_value), autoscaling=autoscaling, ), diff --git a/src/together/lib/cli/api/beta/endpoints/deploy.py b/src/together/lib/cli/api/beta/endpoints/deploy.py index 454c460c..a314ba25 100644 --- a/src/together/lib/cli/api/beta/endpoints/deploy.py +++ b/src/together/lib/cli/api/beta/endpoints/deploy.py @@ -30,6 +30,7 @@ PlacementProfile, ) from together.lib.cli.api.beta.endpoints._utils._resolve_model import ( + MODEL_PATH_RE, construct_model_path, resolve_model_and_config, ) @@ -174,7 +175,18 @@ async def deploy( config: CLIConfigParameter, ) -> None: """Create a deployment on a new or existing dedicated inference endpoint.""" - resolved_model, config_value = await resolve_model_and_config(config, model, config_id=config_id) + model_path_match = MODEL_PATH_RE.match(model) + if model_revision is not None and model_path_match is not None and model_path_match.group(3) is not None: + raise ValueError( + "Do not pass --model-revision when --model already includes a revision. " + "Specify the revision only in the fully qualified --model path." + ) + + resolved = await resolve_model_and_config(config, model, config_id=config_id) + resolved_model, config_value = resolved.model, resolved.config + # Prefer revision pin from a fully-qualified model path; fall back to the + # deprecated --model-revision flag. + resolved_revision = resolved.revision_id or model_revision autoscaling = build_autoscaling( min_replicas=min_replicas, @@ -200,16 +212,18 @@ async def deploy( else: placement_value = placement.to_json() + model_path = construct_model_path(resolved_model, resolved_revision) + if not config.json: _print_deployment_preview( endpoint=endpoint_name_or_id, deployment_name=deployment_name, model=resolved_model, + model_path=model_path, config_value=config_value, autoscaling=autoscaling, placement=placement_value, enable_lora=enable_lora, - model_revision=model_revision, traffic_weight=traffic_weight, ) await assert_explicit_project_id(config) @@ -222,11 +236,12 @@ async def deploy( config.client.beta.endpoints.deployments.create( endpoint.id, name=deployment_name, - model=construct_model_path(resolved_model), + model=model_path, config=construct_config_path(config_value), autoscaling=autoscaling, enable_lora=enable_lora if enable_lora is not None else omit, - model_revision_id=model_revision or omit, + # Revision is already embedded in model_path when present. + model_revision_id=omit, placement=placement_value or omit, ), ) @@ -267,11 +282,11 @@ def _print_deployment_preview( endpoint: str, deployment_name: str, model: Model, + model_path: str, config_value: Config, autoscaling: DeploymentAutoscalingParam, placement: Placement | None, enable_lora: bool | None, - model_revision: str | None, traffic_weight: float | None, ) -> None: table = Table(expand=True, show_header=False, show_edge=False, show_lines=False, box=None, pad_edge=False) @@ -302,9 +317,6 @@ def add_row(flag: str, value: str) -> None: if percentile := metric.get("percentile"): add_row("--scaling-percentile", percentile) - if model_revision: - add_row("--model-revision", model_revision) - if placement is not None: if "profile" in placement: add_row("--placement", placement["profile"]) # type: ignore[typeddict-item] @@ -324,7 +336,7 @@ def add_row(flag: str, value: str) -> None: add_row("--enable-lora", "true" if enable_lora else "false") if traffic_weight is not None: add_row("--traffic-weight", str(traffic_weight)) - add_row("--model", model.name) + add_row("--model", f"{model.name} ({model_path})") add_row("--config", config_value.id) # type: ignore table.add_row("\n".join(args)) diff --git a/src/together/lib/cli/api/beta/endpoints/shadow.py b/src/together/lib/cli/api/beta/endpoints/shadow.py index a93b3ff3..e64d58f6 100644 --- a/src/together/lib/cli/api/beta/endpoints/shadow.py +++ b/src/together/lib/cli/api/beta/endpoints/shadow.py @@ -92,7 +92,8 @@ async def shadow( rate, target_qps = await resolve_rate_or_target_qps(rate, target_qps, config=config) endpoint_id = (await resolve_endpoint(config, endpoint_id_or_name)).id - resolved_model, config_value = await resolve_model_and_config(config, model, config_id=config_id) + resolved = await resolve_model_and_config(config, model, config_id=config_id) + resolved_model, config_value = resolved.model, resolved.config autoscaling = build_autoscaling( min_replicas=1, @@ -122,7 +123,7 @@ async def shadow( endpoint_id=endpoint_id, name=name, enable_lora=enable_lora, - model=construct_model_path(resolved_model), + model=construct_model_path(resolved_model, resolved.revision_id), config=construct_config_path(config_value), autoscaling=autoscaling, ), diff --git a/src/together/lib/cli/api/beta/models/create.py b/src/together/lib/cli/api/beta/models/create.py index 94b3e188..01a00f3b 100644 --- a/src/together/lib/cli/api/beta/models/create.py +++ b/src/together/lib/cli/api/beta/models/create.py @@ -30,7 +30,7 @@ async def preprompt(self, config: CLIConfig) -> None: for profile in model.deployment_profiles: match = MODEL_PATH_RE.match(profile.model) if match: - _, model_id = match.groups() + model_id = match.group(2) self.choices.append((f"{model.name} ({profile.quantization})", model_id)) diff --git a/tests/cli/test_beta_endpoints.py b/tests/cli/test_beta_endpoints.py index bcc3908f..ec7f2770 100644 --- a/tests/cli/test_beta_endpoints.py +++ b/tests/cli/test_beta_endpoints.py @@ -107,6 +107,34 @@ def _mock_model_and_config(respx_mock: MockRouter) -> None: class TestBetaEndpointsDeploy: + @pytest.mark.parametrize("model_revision", ["rev_in_path", "rev_from_flag"]) + def test_deploy_rejects_model_path_and_revision_flag( + self, + cli_runner: CliRunner, + model_revision: str, + ) -> None: + result = cli_runner.invoke( + [ + "beta", + "endpoints", + "deploy", + "--project", + "proj", + "--endpoint", + "ep_1", + "--model", + "projects/proj/models/ml_1/revisions/rev_in_path", + "--model-revision", + model_revision, + "--config", + "cr_1", + "--json", + ] + ) + + assert result.exit_code != 0 + assert "Do not pass --model-revision when --model already includes a revision" in result.output + @pytest.mark.respx(base_url=base_url) def test_deploy_creates_endpoint_deployment_and_traffic_split( self, diff --git a/tests/cli/test_beta_endpoints_shadow.py b/tests/cli/test_beta_endpoints_shadow.py index 8fc80d94..82189f57 100644 --- a/tests/cli/test_beta_endpoints_shadow.py +++ b/tests/cli/test_beta_endpoints_shadow.py @@ -404,10 +404,12 @@ def test_shadow_resolves_single_config( assert result.exit_code == 0, result.output url = str(cast(Call, configs_route.calls[0]).request.url) - assert "referenceModelId=ml_1" in url + # Configs are resolved via the model's baseModelId; deploy target stays ml_1. + assert "referenceModelId=ml_base" in url deployment_body = json.loads(cast(Call, create_deployment_route.calls[0]).request.content.decode()) assert deployment_body["config"] == "projects/proj/configs/cr_1" + assert deployment_body["model"] == "projects/proj/models/ml_1" @pytest.mark.respx(base_url=base_url) def test_shadow_errors_on_multiple_configs( @@ -416,6 +418,7 @@ def test_shadow_errors_on_multiple_configs( cli_runner: CliRunner, ) -> None: _mock_endpoint(respx_mock) + respx_mock.get("/projects/proj/models/ml_1").mock(return_value=httpx.Response(200, json=_model_body())) respx_mock.get("/projects/proj/configs").mock( return_value=httpx.Response( 200, diff --git a/tests/cli/test_resolve_model.py b/tests/cli/test_resolve_model.py index dc65052a..fc4fbd7a 100644 --- a/tests/cli/test_resolve_model.py +++ b/tests/cli/test_resolve_model.py @@ -5,6 +5,7 @@ import pytest +from together import NotFoundError from together.types.beta import Model from together.lib.cli.utils.config import CLIConfig from together.types.beta.models.config import Config @@ -96,43 +97,109 @@ async def test_raw_model_id_resolves_via_configs() -> None: baseModelId=None, ) client = MagicMock() + # Not found under --project → fall back to configs reference-model lookup. + client.beta.models.retrieve = AsyncMock( + side_effect=[ + NotFoundError(message="Model not found", response=MagicMock(), body=None), + retrieved, + ] + ) client.beta.models.configs.list = AsyncMock( return_value=MagicMock(data=[_config()]), ) - client.beta.models.retrieve = AsyncMock(return_value=retrieved) client.whoami = AsyncMock() - model, config = await resolve_model_and_config(_cli_config(client), "ml_base", config_id=None) + resolved = await resolve_model_and_config(_cli_config(client), "ml_base", config_id=None) client.beta.models.configs.list.assert_awaited_once_with(reference_model_id="ml_base") - client.beta.models.retrieve.assert_awaited_once_with(id="ml_base", project_id="proj_public") + assert client.beta.models.retrieve.await_args_list[0].kwargs == { + "id": "ml_base", + "project_id": "proj_mine", + } + assert client.beta.models.retrieve.await_args_list[1].kwargs == { + "id": "ml_base", + "project_id": "proj_public", + } client.whoami.assert_not_awaited() - assert model.name == "together/some-named-model" - assert construct_model_path(model) == "projects/proj_public/models/ml_base" - assert construct_config_path(config) == "projects/proj_public/configs/cr_1" + assert resolved.model.name == "together/some-named-model" + assert construct_model_path(resolved.model, resolved.revision_id) == "projects/proj_public/models/ml_base" + assert construct_config_path(resolved.config) == "projects/proj_public/configs/cr_1" @pytest.mark.asyncio -async def test_full_model_path_parses_id_then_configs() -> None: +async def test_full_model_path_keeps_model_and_revision_pin() -> None: client = MagicMock() + client.beta.models.retrieve = AsyncMock( + return_value=_private_model(id="ml_base", projectId="proj_public", name="together/base", baseModelId=None), + ) client.beta.models.configs.list = AsyncMock( return_value=MagicMock(data=[_config()]), ) - client.beta.models.retrieve = AsyncMock( - return_value=_private_model(id="ml_base", projectId="proj_public", name="together/base"), - ) - model, config = await resolve_model_and_config( + resolved = await resolve_model_and_config( _cli_config(client), "projects/proj_public/models/ml_base/revisions/rev_9", config_id="cr_1", ) - client.beta.models.configs.list.assert_awaited_once_with(reference_model_id="ml_base") client.beta.models.retrieve.assert_awaited_once_with(id="ml_base", project_id="proj_public") - assert model.id == "ml_base" - assert model.name == "together/base" - assert config.id == "cr_1" + client.beta.models.configs.list.assert_awaited_once_with(reference_model_id="ml_base") + assert resolved.model.id == "ml_base" + assert resolved.model.name == "together/base" + assert resolved.revision_id == "rev_9" + assert resolved.config.id == "cr_1" + assert ( + construct_model_path(resolved.model, resolved.revision_id) + == "projects/proj_public/models/ml_base/revisions/rev_9" + ) + + +@pytest.mark.asyncio +async def test_finetuned_resource_path_keeps_custom_model_not_config_base() -> None: + """Regression: resource-path input must not silently deploy config.referenceModel.""" + ft = _private_model( + id="ml_ft", + projectId="proj_mine", + name="my-slug/ft-model", + baseModelId="ml_base", + ) + client = MagicMock() + client.beta.models.retrieve = AsyncMock(return_value=ft) + client.beta.models.configs.list = AsyncMock( + return_value=MagicMock(data=[_config()]), + ) + + resolved = await resolve_model_and_config( + _cli_config(client), + "projects/proj_mine/models/ml_ft/revisions/rv_pin", + config_id="cr_1", + ) + + client.beta.models.retrieve.assert_awaited_once_with(id="ml_ft", project_id="proj_mine") + client.beta.models.configs.list.assert_awaited_once_with(reference_model_id="ml_base") + assert resolved.model.id == "ml_ft" + assert resolved.revision_id == "rv_pin" + assert ( + construct_model_path(resolved.model, resolved.revision_id) == "projects/proj_mine/models/ml_ft/revisions/rv_pin" + ) + assert construct_config_path(resolved.config) == "projects/proj_public/configs/cr_1" + + +@pytest.mark.asyncio +async def test_raw_finetuned_model_id_under_project_keeps_custom_model() -> None: + ft = _private_model() + client = MagicMock() + client.beta.models.retrieve = AsyncMock(return_value=ft) + client.beta.models.configs.list = AsyncMock( + return_value=MagicMock(data=[_config()]), + ) + + resolved = await resolve_model_and_config(_cli_config(client), "ml_custom", config_id=None) + + client.beta.models.retrieve.assert_awaited_once_with(id="ml_custom", project_id="proj_mine") + client.beta.models.configs.list.assert_awaited_once_with(reference_model_id="ml_base") + assert construct_model_path(resolved.model, resolved.revision_id) == "projects/proj_mine/models/ml_custom" + assert construct_config_path(resolved.config) == "projects/proj_public/configs/cr_1" @pytest.mark.asyncio @@ -148,21 +215,19 @@ async def _list_models() -> AsyncIterator[Model]: client.beta.models.configs.list = AsyncMock( return_value=MagicMock(data=[_config()]), ) - client.beta.models.retrieve = AsyncMock( - return_value=_private_model(id="ml_base", projectId="proj_public", name="together/base"), - ) + client.beta.models.retrieve = AsyncMock() - model, config = await resolve_model_and_config( + resolved = await resolve_model_and_config( _cli_config(client), "my-slug/custom-model", config_id=None, ) client.beta.models.configs.list.assert_awaited_once_with(reference_model_id="ml_base") - client.beta.models.retrieve.assert_awaited_once_with(id="ml_base", project_id="proj_public") - assert construct_model_path(model) == "projects/proj_mine/models/ml_custom" - assert construct_config_path(config) == "projects/proj_public/configs/cr_1" - assert model.name == "my-slug/custom-model" + client.beta.models.retrieve.assert_not_awaited() + assert construct_model_path(resolved.model, resolved.revision_id) == "projects/proj_mine/models/ml_custom" + assert construct_config_path(resolved.config) == "projects/proj_public/configs/cr_1" + assert resolved.model.name == "my-slug/custom-model" @pytest.mark.asyncio @@ -173,16 +238,16 @@ async def test_public_named_model_uses_deployment_profile() -> None: return_value=MagicMock(data=[_supported_model()]), ) - model, config = await resolve_model_and_config( + resolved = await resolve_model_and_config( _cli_config(client), "meta-llama/Llama-3-8b", config_id=None, ) client.beta.models.list_supported.assert_awaited_once_with(search="meta-llama/Llama-3-8b") - assert construct_model_path(model) == "projects/proj_public/models/ml_pub" - assert construct_config_path(config) == "projects/proj_public/configs/cr_pub" - assert model.name == "meta-llama/Llama-3-8b" + assert construct_model_path(resolved.model, resolved.revision_id) == "projects/proj_public/models/ml_pub" + assert construct_config_path(resolved.config) == "projects/proj_public/configs/cr_pub" + assert resolved.model.name == "meta-llama/Llama-3-8b" @pytest.mark.asyncio @@ -214,14 +279,14 @@ async def test_public_model_selects_profile_by_config_id() -> None: return_value=MagicMock(data=[_supported_model(deploymentProfiles=profiles)]), ) - model, config = await resolve_model_and_config( + resolved = await resolve_model_and_config( _cli_config(client), "meta-llama/Llama-3-8b", config_id="cr_b", ) - assert config.id == "cr_b" - assert construct_model_path(model) == "projects/proj_public/models/ml_pub_b" + assert resolved.config.id == "cr_b" + assert construct_model_path(resolved.model, resolved.revision_id) == "projects/proj_public/models/ml_pub_b" @pytest.mark.asyncio @@ -263,6 +328,9 @@ async def test_public_model_multiple_profiles_requires_flags(capsys: pytest.Capt @pytest.mark.asyncio async def test_raw_model_rejects_mismatched_config_id() -> None: client = MagicMock() + client.beta.models.retrieve = AsyncMock( + side_effect=NotFoundError(message="Model not found", response=MagicMock(), body=None), + ) client.beta.models.configs.list = AsyncMock( return_value=MagicMock(data=[_config()]), )