def _get_composed_model(
        onnx_model_paths: list,
        output_model_path: Union[str, Path],
        external_config: dict,
        saved_cb_files: Optional[dict] = None,
        as_model_dir: bool = False,
    ) -> ONNXModelHandler:
        """Compose multiple ONNX models into a single model.

        :param onnx_model_paths: List of ONNX model paths.
        :param output_model_path: Path to save the composed ONNX model.
        :param external_config: Configuration for external data.
        :param saved_cb_files: Dictionary of saved context binary files.
        :param as_model_dir: Use model parent directory as output model_path.
        :return: Composed ONNX model.
        """
        dags = [OnnxDAG.from_model_path(path) for path in onnx_model_paths]

        seen_inputs = set()
        seen_outputs = set()
        for dag in dags:
            dag_inputs = set(dag.get_input_names())
            dag_outputs = set(dag.get_output_names())
            # avoid circular connection, model_2 output cannot be model_1 input
            assert dag_outputs.isdisjoint(seen_inputs), (
                f"Output names {dag_outputs.intersection(seen_inputs)} are already used as input names."
            )
            # avoid reused output name
            assert dag_outputs.isdisjoint(seen_outputs), (
                f"Output names {dag_outputs.intersection(seen_outputs)} are already used as output names."
            )

            # update seen inputs and outputs
            seen_inputs.update(dag_inputs)
            seen_outputs.update(dag_outputs)

        # will only keep the unused outputs
        # inputs will be automatically taken care of during compose
        final_outputs = seen_outputs - seen_inputs

        # compose
        composed_dag = dags.pop(0)
        while dags:
            dag = dags.pop(0)

            cd_input_names = set(composed_dag.get_input_names())
            cd_output_names = set(composed_dag.get_output_names())
            cd_initializer_names = set(composed_dag.get_initializer_names())
            for input_name in dag.get_input_names():
                if input_name in cd_input_names | cd_output_names:
                    assert dag.get_io_shape(input_name) == composed_dag.get_io_shape(input_name), (
                        f"Input shape mismatch: {input_name}"
                    )
                    assert dag.get_io_dtype(input_name) == composed_dag.get_io_dtype(input_name), (
                        f"Input dtype mismatch: {input_name}"
                    )
                    continue

                # will add to graph 0 for now
                # this will fail if the connection already exists in the graph = expected behavior.
                composed_dag.add_input(dag.get_input_proto(input_name), 0)

            for output_name in dag.get_output_names():
                composed_dag.add_output(dag.get_output_proto(output_name), 0)

            for init_name in dag.get_initializer_names():
                if init_name in cd_initializer_names:
                    (
                        np.testing.assert_array_equal(
                            dag.get_initializer_np_array(init_name), composed_dag.get_initializer_np_array(init_name)
                        ),
                        f"Initializer mismatch: {init_name}",
                    )
                    continue

                composed_dag.add_initializer(dag.get_initializer_proto(init_name), 0)

            for intermediate_name in dag.get_intermediate_names():
                if value_info := dag.get_value_info_proto(intermediate_name):
                    composed_dag.add_value_info(value_info, 0)

            for node_name in dag.topological_sort():
                node = dag.get_node_proto(node_name)
                if composed_dag.has_node(node_name):
                    # there might be some dq nodes for initializers that are common between models
                    # since split model keeps dq with the consumer op
                    assert composed_dag.get_node_proto(node_name) == node, f"Node mismatch: {node_name}"
                    continue

                composed_dag.add_node(node, 0)

        for output_name in composed_dag.get_output_names():
            if output_name not in final_outputs:
                composed_dag.remove_output(output_name)

        # update the model graph
        composed_dag.update()

        # save the composed model
        output_model_path = Path(output_model_path)
        has_external_data = model_proto_to_file(composed_dag.model, output_model_path, **external_config)

        # copy over context binary files if any
        saved_cb_files = saved_cb_files if saved_cb_files is not None else {}
        for path in onnx_model_paths:
            has_external_data |= copy_context_bin_files(path, output_model_path.parent, saved_cb_files=saved_cb_files)

        return ONNXModelHandler(
            output_model_path.parent if (has_external_data or as_model_dir) else output_model_path,
            onnx_file_name=output_model_path.name if (has_external_data or as_model_dir) else None,
        )
Compose multiple ONNX models into a single model.

        :param onnx_model_paths: List of ONNX model paths.
        :param output_model_path: Path to save the composed ONNX model.
        :param external_config: Configuration for external data.
        :param saved_cb_files: Dictionary of saved context binary files.
        :param as_model_dir: Use model parent directory as output model_path.
        :return: Composed ONNX model.
        
olive/passes/onnx/compose.py